From 046c72c83f5390a776e12f48d2f45279595593d0 Mon Sep 17 00:00:00 2001 From: Christian Femers Date: Sun, 28 Feb 2021 13:16:22 +0100 Subject: [PATCH 1/8] Add test run with Java 16 pre-release build --- .github/workflows/maven.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 940ca2d0..7eec1398 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [11, 12, 13, 14, 15] + java: [11, 12, 13, 14, 15, 16-ea] name: Java ${{ matrix.java }} Run steps: - uses: actions/checkout@v2 From d5e66a67a51eda19f4f1b64b7e0b36d52313a6d8 Mon Sep 17 00:00:00 2001 From: Christian Femers Date: Fri, 19 Mar 2021 08:37:17 +0100 Subject: [PATCH 2/8] Remove abstract Throwables in the safe type set --- .../internal/sanitization/ThrowableSets.java | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableSets.java b/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableSets.java index 07d653a0..22e3ca2c 100644 --- a/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableSets.java +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableSets.java @@ -98,35 +98,34 @@ private Java() { java.io.EOFException.class, java.io.FileNotFoundException.class, java.io.IOError.class, java.io.IOException.class, java.io.InterruptedIOException.class, java.io.InvalidClassException.class, java.io.InvalidObjectException.class, java.io.NotActiveException.class, - java.io.NotSerializableException.class, java.io.ObjectStreamException.class, - java.io.OptionalDataException.class, java.io.StreamCorruptedException.class, - java.io.SyncFailedException.class, java.io.UTFDataFormatException.class, - java.io.UncheckedIOException.class, java.io.UnsupportedEncodingException.class, - java.lang.AbstractMethodError.class, java.lang.ArithmeticException.class, - java.lang.ArrayIndexOutOfBoundsException.class, java.lang.ArrayStoreException.class, - java.lang.AssertionError.class, java.lang.BootstrapMethodError.class, - java.lang.ClassCastException.class, java.lang.ClassCircularityError.class, - java.lang.ClassFormatError.class, java.lang.CloneNotSupportedException.class, - java.lang.EnumConstantNotPresentException.class, java.lang.Error.class, java.lang.Exception.class, - java.lang.IllegalAccessError.class, java.lang.IllegalAccessException.class, - java.lang.IllegalArgumentException.class, java.lang.IllegalCallerException.class, - java.lang.IllegalMonitorStateException.class, java.lang.IllegalStateException.class, - java.lang.IllegalThreadStateException.class, java.lang.IncompatibleClassChangeError.class, - java.lang.IndexOutOfBoundsException.class, java.lang.InstantiationError.class, - java.lang.InstantiationException.class, java.lang.InternalError.class, - java.lang.InterruptedException.class, java.lang.LayerInstantiationException.class, - java.lang.LinkageError.class, java.lang.NegativeArraySizeException.class, - java.lang.NoClassDefFoundError.class, java.lang.NoSuchFieldError.class, - java.lang.NoSuchFieldException.class, java.lang.NoSuchMethodError.class, - java.lang.NoSuchMethodException.class, java.lang.NullPointerException.class, - java.lang.NumberFormatException.class, java.lang.OutOfMemoryError.class, - java.lang.ReflectiveOperationException.class, java.lang.RuntimeException.class, - java.lang.SecurityException.class, java.lang.StackOverflowError.class, + java.io.NotSerializableException.class, java.io.OptionalDataException.class, + java.io.StreamCorruptedException.class, java.io.SyncFailedException.class, + java.io.UTFDataFormatException.class, java.io.UncheckedIOException.class, + java.io.UnsupportedEncodingException.class, java.lang.AbstractMethodError.class, + java.lang.ArithmeticException.class, java.lang.ArrayIndexOutOfBoundsException.class, + java.lang.ArrayStoreException.class, java.lang.AssertionError.class, + java.lang.BootstrapMethodError.class, java.lang.ClassCastException.class, + java.lang.ClassCircularityError.class, java.lang.ClassFormatError.class, + java.lang.CloneNotSupportedException.class, java.lang.EnumConstantNotPresentException.class, + java.lang.Error.class, java.lang.Exception.class, java.lang.IllegalAccessError.class, + java.lang.IllegalAccessException.class, java.lang.IllegalArgumentException.class, + java.lang.IllegalCallerException.class, java.lang.IllegalMonitorStateException.class, + java.lang.IllegalStateException.class, java.lang.IllegalThreadStateException.class, + java.lang.IncompatibleClassChangeError.class, java.lang.IndexOutOfBoundsException.class, + java.lang.InstantiationError.class, java.lang.InstantiationException.class, + java.lang.InternalError.class, java.lang.InterruptedException.class, + java.lang.LayerInstantiationException.class, java.lang.LinkageError.class, + java.lang.NegativeArraySizeException.class, java.lang.NoClassDefFoundError.class, + java.lang.NoSuchFieldError.class, java.lang.NoSuchFieldException.class, + java.lang.NoSuchMethodError.class, java.lang.NoSuchMethodException.class, + java.lang.NullPointerException.class, java.lang.NumberFormatException.class, + java.lang.OutOfMemoryError.class, java.lang.ReflectiveOperationException.class, + java.lang.RuntimeException.class, java.lang.SecurityException.class, java.lang.StackOverflowError.class, java.lang.StringIndexOutOfBoundsException.class, java.lang.ThreadDeath.class, java.lang.Throwable.class, java.lang.TypeNotPresentException.class, java.lang.UnknownError.class, java.lang.UnsatisfiedLinkError.class, java.lang.UnsupportedClassVersionError.class, java.lang.UnsupportedOperationException.class, java.lang.VerifyError.class, - java.lang.VirtualMachineError.class, java.lang.annotation.AnnotationFormatError.class, + java.lang.annotation.AnnotationFormatError.class, java.lang.annotation.AnnotationTypeMismatchException.class, java.lang.annotation.IncompleteAnnotationException.class, java.lang.instrument.IllegalClassFormatException.class, From 19cce5663a2a568122109fd438acbe73e1098425 Mon Sep 17 00:00:00 2001 From: Christian Femers Date: Fri, 19 Mar 2021 16:26:32 +0100 Subject: [PATCH 3/8] Rework Throwable post processing for Java 16 and JEP 396 --- .../in/test/api/internal/ReportingUtils.java | 49 ++-- .../in/test/api/internal/ThrowableUtils.java | 84 ------- .../ArbitraryThrowableSanitizer.java | 22 ++ .../AssertionFailedErrorSanitizer.java | 14 +- .../ExceptionInInitializerErrorSanitizer.java | 56 ++--- .../sanitization/MessageTransformer.java | 13 + .../MultipleAssertionsErrorSanitizer.java | 31 ++- .../MultipleFailuresErrorSanitizer.java | 38 ++- .../PrivilegedExceptionSanitizer.java | 4 +- .../SafeTypeThrowableSanitizer.java | 196 +++++++++++++++ .../sanitization/SanitizationException.java | 2 +- .../sanitization/SanitizationUtils.java | 65 +++-- .../SimpleThrowableSanitizer.java | 16 -- .../SoftAssertionErrorSanitizer.java | 10 +- .../SpecificThrowableSanitizer.java | 6 +- .../sanitization/ThrowableCreator.java | 7 + .../internal/sanitization/ThrowableInfo.java | 235 ++++++++++++++++++ .../sanitization/ThrowableSanitizer.java | 10 +- .../internal/sanitization/ThrowableUtils.java | 170 +++++++++++++ .../api/security/FixSystemErrAppender.java | 1 - .../api/util/UnexpectedExceptionError.java | 26 +- .../tum/in/test/api/ExceptionFailureTest.java | 9 +- .../test/api/internal/ThrowableUtilsTest.java | 77 ------ .../sanitization/ThrowableUtilsTest.java | 174 +++++++++++++ .../tum/in/testuser/ExceptionFailureUser.java | 2 +- 25 files changed, 990 insertions(+), 327 deletions(-) delete mode 100644 src/main/java/de/tum/in/test/api/internal/ThrowableUtils.java create mode 100644 src/main/java/de/tum/in/test/api/internal/sanitization/ArbitraryThrowableSanitizer.java create mode 100644 src/main/java/de/tum/in/test/api/internal/sanitization/MessageTransformer.java create mode 100644 src/main/java/de/tum/in/test/api/internal/sanitization/SafeTypeThrowableSanitizer.java delete mode 100644 src/main/java/de/tum/in/test/api/internal/sanitization/SimpleThrowableSanitizer.java create mode 100644 src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableCreator.java create mode 100644 src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableInfo.java create mode 100644 src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableUtils.java delete mode 100644 src/test/java/de/tum/in/test/api/internal/ThrowableUtilsTest.java create mode 100644 src/test/java/de/tum/in/test/api/internal/sanitization/ThrowableUtilsTest.java diff --git a/src/main/java/de/tum/in/test/api/internal/ReportingUtils.java b/src/main/java/de/tum/in/test/api/internal/ReportingUtils.java index 2b731acf..dae00098 100644 --- a/src/main/java/de/tum/in/test/api/internal/ReportingUtils.java +++ b/src/main/java/de/tum/in/test/api/internal/ReportingUtils.java @@ -2,12 +2,13 @@ import java.util.Objects; import java.util.Optional; -import java.util.function.UnaryOperator; import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import de.tum.in.test.api.internal.sanitization.MessageTransformer; +import de.tum.in.test.api.internal.sanitization.ThrowableInfo; import de.tum.in.test.api.internal.sanitization.ThrowableSanitizer; import de.tum.in.test.api.localization.Messages; import de.tum.in.test.api.security.ArtemisSecurityManager; @@ -47,34 +48,29 @@ public static Throwable processThrowable(Throwable t, TestContext context) { } private static Throwable processThrowableRegularly(Throwable t) { - Throwable newT = trySanitizeThrowable(t); - tryPostProcessMessageOrAddSuppressed(newT, ReportingUtils::postProcessMessage); - if (!(newT instanceof AssertionError)) { - addStackframeInfoToMessage(newT); - } - return newT; + return trySanitizeThrowable(t, ReportingUtils::transformMessage); } private static Throwable processThrowablePrivilegedOnly(Throwable t, String nonprivilegedFailureMessage) { - Throwable newT; if (t instanceof PrivilegedException) - newT = trySanitizeThrowable(t); - else - newT = new AssertionError(nonprivilegedFailureMessage); - tryPostProcessMessageOrAddSuppressed(t, ReportingUtils::postProcessMessage); - return newT; + return trySanitizeThrowable(t, ReportingUtils::transformMessage); + return new AssertionError(nonprivilegedFailureMessage); } - private static Throwable trySanitizeThrowable(Throwable t) { + private static Throwable trySanitizeThrowable(Throwable t, MessageTransformer messageTransformer) { String name = "unknown"; - Throwable newT; try { name = t.getClass().getName(); - newT = ThrowableSanitizer.sanitize(t); + return ThrowableSanitizer.sanitize(t, messageTransformer); } catch (Throwable sanitizationError) { return handleSanitizationFailure(name, sanitizationError); } - return newT; + } + + private static String transformMessage(ThrowableInfo info) { + if (!AssertionError.class.isAssignableFrom(info.getClass())) + addStackframeInfoToMessage(info); + return info.getMessage(); } private static Throwable handleSanitizationFailure(String name, Throwable error) { @@ -83,26 +79,13 @@ private static Throwable handleSanitizationFailure(String name, Throwable error) return new SecurityException(name + " thrown, but cannot be displayed: " + info + ""); } - private static void addStackframeInfoToMessage(Throwable newT) { - StackTraceElement[] stackTrace = BlacklistedInvoker.invoke(newT::getStackTrace); + private static void addStackframeInfoToMessage(ThrowableInfo info) { + StackTraceElement[] stackTrace = info.getStackTrace(); var first = ArtemisSecurityManager.firstNonWhitelisted(stackTrace); if (first.isPresent()) { String call = first.get().toString(); - tryPostProcessMessageOrAddSuppressed(newT, old -> Objects.toString(old, "") + "\n" + info.setMessage(Objects.toString(info.getMessage(), "") + "\n" + Messages.formatLocalized("reporting.problem_location_hint", call)); } } - - private static String tryPostProcessMessageOrAddSuppressed(Throwable t, UnaryOperator transform) { - String newMessage = transform.apply(ThrowableUtils.getDetailMessage(t)); - ThrowableUtils.setDetailMessage(t, newMessage); - return newMessage; - } - - /** - * This is currently just returning the message with normalized newlines - */ - private static String postProcessMessage(String message) { - return message == null ? null : message.replace("\r", ""); - } } diff --git a/src/main/java/de/tum/in/test/api/internal/ThrowableUtils.java b/src/main/java/de/tum/in/test/api/internal/ThrowableUtils.java deleted file mode 100644 index b8880278..00000000 --- a/src/main/java/de/tum/in/test/api/internal/ThrowableUtils.java +++ /dev/null @@ -1,84 +0,0 @@ -package de.tum.in.test.api.internal; - -import java.lang.invoke.MethodHandles; -import java.lang.invoke.VarHandle; -import java.util.List; - -import org.apiguardian.api.API; -import org.apiguardian.api.API.Status; - -@API(status = Status.INTERNAL) -public final class ThrowableUtils { - - private static final String DETAIL_MESSAGE_NAME = "detailMessage"; - private static final String CAUSE_NAME = "cause"; - private static final String STACK_TRACE_NAME = "stackTrace"; - private static final String SUPPRESSED_EXCEPTIONS_NAME = "suppressedExceptions"; - - private static final VarHandle DETAIL_MESSAGE; - private static final VarHandle CAUSE; - private static final VarHandle STACK_TRACE; - private static final VarHandle SUPPRESSED_EXCEPTIONS; - - static { - try { - var lookup = MethodHandles.privateLookupIn(Throwable.class, MethodHandles.lookup()); - DETAIL_MESSAGE = lookup.findVarHandle(Throwable.class, DETAIL_MESSAGE_NAME, String.class); - CAUSE = lookup.findVarHandle(Throwable.class, CAUSE_NAME, Throwable.class); - STACK_TRACE = lookup.findVarHandle(Throwable.class, STACK_TRACE_NAME, StackTraceElement[].class); - SUPPRESSED_EXCEPTIONS = lookup.findVarHandle(Throwable.class, SUPPRESSED_EXCEPTIONS_NAME, List.class); - } catch (NoSuchFieldException | IllegalAccessException e) { - throw new ExceptionInInitializerError(e); - } - } - - public static String getDetailMessage(Throwable target) { - checkAccess(); - return (String) DETAIL_MESSAGE.getVolatile(target); - } - - public static Throwable getCause(Throwable target) { - checkAccess(); - return (Throwable) CAUSE.getVolatile(target); - } - - public static StackTraceElement[] getStackTrace(Throwable target) { - checkAccess(); - return (StackTraceElement[]) STACK_TRACE.getVolatile(target); - } - - public static List getSuppressedExceptions(Throwable target) { - checkAccess(); - return (List) SUPPRESSED_EXCEPTIONS.getVolatile(target); - } - - public static void setDetailMessage(Throwable target, String newValue) { - checkAccess(); - DETAIL_MESSAGE.setVolatile(target, newValue); - } - - public static void setCause(Throwable target, Throwable newValue) { - checkAccess(); - CAUSE.setVolatile(target, newValue); - } - - public static void setStackTrace(Throwable target, StackTraceElement... newValue) { - checkAccess(); - STACK_TRACE.setVolatile(target, newValue); - } - - public static void setSuppressedException(Throwable target, List newValue) { - checkAccess(); - SUPPRESSED_EXCEPTIONS.setVolatile(target, newValue); - } - - private static void checkAccess() { - SecurityManager sm = System.getSecurityManager(); - if (sm != null) - sm.checkPackageAccess(ThrowableUtils.class.getPackageName()); - } - - private ThrowableUtils() { - - } -} diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/ArbitraryThrowableSanitizer.java b/src/main/java/de/tum/in/test/api/internal/sanitization/ArbitraryThrowableSanitizer.java new file mode 100644 index 00000000..e11e84e6 --- /dev/null +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/ArbitraryThrowableSanitizer.java @@ -0,0 +1,22 @@ +package de.tum.in.test.api.internal.sanitization; + +import de.tum.in.test.api.util.UnexpectedExceptionError; + +enum ArbitraryThrowableSanitizer implements SpecificThrowableSanitizer { + INSTANCE; + + @Override + public boolean canSanitize(Throwable t) { + return true; + } + + @Override + public Throwable sanitize(Throwable t, MessageTransformer messageTransformer) { + ThrowableInfo info = ThrowableInfo.getEssentialInfosSafeFrom(t); + String className = t.getClass().getName(); + String message = messageTransformer.apply(info); + String combinedMessage = message == null ? className : className + ": " + message; + return UnexpectedExceptionError.create(t.getClass(), combinedMessage, info.getCause(), info.getStackTrace(), + info.getSuppressed()); + } +} diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/AssertionFailedErrorSanitizer.java b/src/main/java/de/tum/in/test/api/internal/sanitization/AssertionFailedErrorSanitizer.java index d103c51b..c1750af1 100644 --- a/src/main/java/de/tum/in/test/api/internal/sanitization/AssertionFailedErrorSanitizer.java +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/AssertionFailedErrorSanitizer.java @@ -18,15 +18,15 @@ public boolean canSanitize(Throwable t) { } @Override - public Throwable sanitize(Throwable t) { + public Throwable sanitize(Throwable t, MessageTransformer messageTransformer) { AssertionFailedError afe = (AssertionFailedError) t; - ValueWrapper expected = invoke(afe::getExpected); - ValueWrapper actual = invoke(afe::getActual); - if (expected == null && actual == null) - return SimpleThrowableSanitizer.INSTANCE.sanitize(afe); - AssertionFailedError newAfe = new AssertionFailedError(invoke(afe::getMessage), sanitizeValue(expected), + ValueWrapper expected = afe.getExpected(); + ValueWrapper actual = afe.getExpected(); + ThrowableInfo info = ThrowableInfo.getEssentialInfosSafeFrom(t).sanitize(); + String newMessage = messageTransformer.apply(info); + AssertionFailedError newAfe = new AssertionFailedError(newMessage, sanitizeValue(expected), sanitizeValue(actual)); - SanitizationUtils.copyThrowableInfoSafe(afe, newAfe); + SanitizationUtils.copyThrowableInfoSafe(info, newAfe); return newAfe; } diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/ExceptionInInitializerErrorSanitizer.java b/src/main/java/de/tum/in/test/api/internal/sanitization/ExceptionInInitializerErrorSanitizer.java index 23c8ae6e..8d7d500b 100644 --- a/src/main/java/de/tum/in/test/api/internal/sanitization/ExceptionInInitializerErrorSanitizer.java +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/ExceptionInInitializerErrorSanitizer.java @@ -1,49 +1,41 @@ package de.tum.in.test.api.internal.sanitization; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.VarHandle; -import java.util.Optional; import java.util.Set; enum ExceptionInInitializerErrorSanitizer implements SpecificThrowableSanitizer { INSTANCE; - private static final String EXCEPTION_NAME = "exception"; - private final Set> types = Set.of(ExceptionInInitializerError.class); - /** - * Since Java 12 the exception member has been removed, so it is possibly not - * present and cannot be sanitized. - */ - private static final Optional EXCEPTION; - - static { - VarHandle exceptionHandle; - try { - var lookup = MethodHandles.privateLookupIn(ExceptionInInitializerError.class, MethodHandles.lookup()); - exceptionHandle = lookup.findVarHandle(ExceptionInInitializerError.class, EXCEPTION_NAME, Throwable.class); - } catch (@SuppressWarnings("unused") NoSuchFieldException e) { - // expected for some Java versions - exceptionHandle = null; - } catch (SecurityException | IllegalAccessException e) { - throw new ExceptionInInitializerError(e); - } - EXCEPTION = Optional.ofNullable(exceptionHandle); - } - @Override public boolean canSanitize(Throwable t) { return types.contains(t.getClass()); } @Override - public Throwable sanitize(Throwable t) { - if (EXCEPTION.isPresent()) { - Throwable ex = (Throwable) EXCEPTION.get().getVolatile(t); - EXCEPTION.get().setVolatile(t, ThrowableSanitizer.sanitize(ex)); - } - SimpleThrowableSanitizer.INSTANCE.sanitize(t); - return t; + public Throwable sanitize(Throwable t, MessageTransformer messageTransformer) { + ExceptionInInitializerError eiie = (ExceptionInInitializerError) t; + Throwable exception = eiie.getException(); + ThrowableInfo info = ThrowableInfo.getEssentialInfosSafeFrom(t); + info.setCause(exception); + info.sanitize(); + String newMessage = messageTransformer.apply(info); + ExceptionInInitializerError newEiie; + /* + * Prioritize the message and add cause as suppressed if present. Normally both + * are mutually exclusive, but our messageTransformer might generate a message + * even if there was none before. Because we don't want wo loose the cause, we + * add it as suppressed. This, however, will be automatically done by + * SanitizationUtils.copyThrowableInfoSafe a cause is present and its call to + * initCause fails + */ + if (newMessage != null && !newMessage.isEmpty()) + newEiie = new ExceptionInInitializerError(newMessage); + else if (exception != null) + newEiie = new ExceptionInInitializerError(info.getCause()); + else + newEiie = new ExceptionInInitializerError(); + SanitizationUtils.copyThrowableInfoSafe(info, newEiie); + return newEiie; } } diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/MessageTransformer.java b/src/main/java/de/tum/in/test/api/internal/sanitization/MessageTransformer.java new file mode 100644 index 00000000..cbaec3a6 --- /dev/null +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/MessageTransformer.java @@ -0,0 +1,13 @@ +package de.tum.in.test.api.internal.sanitization; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +@API(status = Status.INTERNAL) +@FunctionalInterface +public interface MessageTransformer { + + MessageTransformer IDENTITY = ThrowableInfo::getMessage; + + String apply(ThrowableInfo throwableInfo); +} \ No newline at end of file diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/MultipleAssertionsErrorSanitizer.java b/src/main/java/de/tum/in/test/api/internal/sanitization/MultipleAssertionsErrorSanitizer.java index 7a9bb24d..4e0a8b60 100644 --- a/src/main/java/de/tum/in/test/api/internal/sanitization/MultipleAssertionsErrorSanitizer.java +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/MultipleAssertionsErrorSanitizer.java @@ -6,6 +6,7 @@ import java.util.Set; import java.util.stream.Collectors; +import org.assertj.core.description.TextDescription; import org.assertj.core.error.MultipleAssertionsError; enum MultipleAssertionsErrorSanitizer implements SpecificThrowableSanitizer { @@ -19,13 +20,29 @@ public boolean canSanitize(Throwable t) { } @Override - public Throwable sanitize(Throwable t) { + public Throwable sanitize(Throwable t, MessageTransformer messageTransformer) { + MultipleAssertionsError mae = (MultipleAssertionsError) t; + ThrowableInfo info = ThrowableInfo.getEssentialInfosSafeFrom(mae).sanitize(); // the list is not safe here, it is simply set in the constructor - MultipleAssertionsError mae = new MultipleAssertionsError( - invoke(() -> List.copyOf(((MultipleAssertionsError) t).getErrors())).stream() - .map(ThrowableSanitizer::sanitize).map(ae -> (AssertionError) ae) - .collect(Collectors.toUnmodifiableList())); - SanitizationUtils.copyThrowableInfoSafe(t, mae); - return mae; + List errors = invoke(() -> List.copyOf(mae.getErrors())).stream() + .map(ThrowableSanitizer::sanitize).map(AssertionError.class::cast) + .collect(Collectors.toUnmodifiableList()); + String description = ""; + if (info.getMessage().startsWith("[")) { + // has a description, that we now have to get somehow + String messageWithoutDecscription = invoke(() -> new MultipleAssertionsError(mae.getErrors()).getMessage()); + String start = SanitizationUtils.removeSuffixMatching(info.getMessage(), messageWithoutDecscription); + if (start != null) + description = start.substring(1, start.length() - 2); + } + /* + * note that this will only affect the description, not the whole message (this + * is not possible) + */ + info.setMessage(description); + description = messageTransformer.apply(info); + MultipleAssertionsError newMae = new MultipleAssertionsError(new TextDescription(description), errors); + SanitizationUtils.copyThrowableInfoSafe(info, newMae); + return newMae; } } diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/MultipleFailuresErrorSanitizer.java b/src/main/java/de/tum/in/test/api/internal/sanitization/MultipleFailuresErrorSanitizer.java index 985af1fa..9a8b4ff9 100644 --- a/src/main/java/de/tum/in/test/api/internal/sanitization/MultipleFailuresErrorSanitizer.java +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/MultipleFailuresErrorSanitizer.java @@ -1,7 +1,5 @@ package de.tum.in.test.api.internal.sanitization; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.VarHandle; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -12,36 +10,32 @@ enum MultipleFailuresErrorSanitizer implements SpecificThrowableSanitizer { INSTANCE; - private static final String HEADING_NAME = "heading"; - private final Set> types = Set.of(MultipleFailuresError.class, AssertJMultipleFailuresError.class); - private static final VarHandle HEADING; - - static { - try { - var lookup = MethodHandles.privateLookupIn(MultipleFailuresError.class, MethodHandles.lookup()); - HEADING = lookup.findVarHandle(MultipleFailuresError.class, HEADING_NAME, String.class); - } catch (IllegalAccessException | NoSuchFieldException e) { - throw new ExceptionInInitializerError(e); - } - } - @Override public boolean canSanitize(Throwable t) { return types.contains(t.getClass()); } @Override - public Throwable sanitize(Throwable t) { - String heading = (String) HEADING.get(t); + public Throwable sanitize(Throwable t, MessageTransformer messageTransformer) { + MultipleFailuresError mfe = (MultipleFailuresError) t; + ThrowableInfo info = ThrowableInfo.getEssentialInfosSafeFrom(mfe).sanitize(); // list is safe here because of defensive copying in MultipleFailuresError - List failures = ((MultipleFailuresError) t).getFailures().stream().map(ThrowableSanitizer::sanitize) - .collect(Collectors.toList()); - MultipleFailuresError mfe = createNewInstance(t, heading, failures); - SanitizationUtils.copyThrowableInfoSafe(t, mfe); - return mfe; + List failures = mfe.getFailures().stream().map(ThrowableSanitizer::sanitize) + .collect(Collectors.toUnmodifiableList()); + /* + * Message already contains the failures and separating both is more difficult. + * So we just pass the message constructed be the old object, as the new object + * will in absence of failures only return the heading. + */ + MultipleFailuresError newMfe = createNewInstance(mfe, info.getMessage(), List.of()); + SanitizationUtils.copyThrowableInfoSafe(info, newMfe); + // Retain failure information in the suppressed Throwables + for (Throwable failure : failures) + newMfe.addSuppressed(failure); + return newMfe; } private static MultipleFailuresError createNewInstance(Throwable t, String heading, List failures) { diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/PrivilegedExceptionSanitizer.java b/src/main/java/de/tum/in/test/api/internal/sanitization/PrivilegedExceptionSanitizer.java index 62d865dd..2784224a 100644 --- a/src/main/java/de/tum/in/test/api/internal/sanitization/PrivilegedExceptionSanitizer.java +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/PrivilegedExceptionSanitizer.java @@ -11,8 +11,8 @@ public boolean canSanitize(Throwable t) { } @Override - public Throwable sanitize(Throwable t) { + public Throwable sanitize(Throwable t, MessageTransformer messageTransformer) { // returning only the content of the privileged exception is the purpose here - return ThrowableSanitizer.sanitize(((PrivilegedException) t).getPriviledgedThrowable()); + return ThrowableSanitizer.sanitize(((PrivilegedException) t).getPriviledgedThrowable(), messageTransformer); } } diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/SafeTypeThrowableSanitizer.java b/src/main/java/de/tum/in/test/api/internal/sanitization/SafeTypeThrowableSanitizer.java new file mode 100644 index 00000000..1176b738 --- /dev/null +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/SafeTypeThrowableSanitizer.java @@ -0,0 +1,196 @@ +package de.tum.in.test.api.internal.sanitization; + +import static org.assertj.core.api.Assertions.entry; + +import java.net.HttpRetryException; +import java.time.format.DateTimeParseException; +import java.util.Collections; +import java.util.IllformedLocaleException; +import java.util.Map; +import java.util.MissingResourceException; +import java.util.Set; +import java.util.function.Supplier; + +import org.junit.experimental.theories.internal.ParameterizedAssertionError; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import de.tum.in.test.api.internal.sanitization.ThrowableInfo.ProperyKey; +import de.tum.in.test.api.util.LruCache; + +import junit.framework.ComparisonCompactor; +import junit.framework.ComparisonFailure; + +enum SafeTypeThrowableSanitizer implements SpecificThrowableSanitizer { + INSTANCE; + + private static final Logger LOG = LoggerFactory.getLogger(SafeTypeThrowableSanitizer.class); + + static final Set NON_DUPLICATABLE_SAFE_TYPES = Set.of( // + "java.io.OptionalDataException", // + "java.util.IllegalFormatException", // + "net.jqwik.api.configurators.ArbitraryConfigurationException", // + "net.jqwik.api.lifecycle.CannotFindStoreException", // + "net.jqwik.api.lifecycle.CannotResolveParameterException", // + "net.jqwik.engine.execution.pipeline.DuplicateExecutionTaskException", // + "net.jqwik.engine.execution.pipeline.PredecessorNotSubmittedException", // + "net.jqwik.engine.properties.arbitraries.NotAFunctionalTypeException", // + "org.junit.Test$None", // + "org.junit.internal.ArrayComparisonFailure", // + "org.junit.runner.FilterFactory$FilterNotCreatedException" // + ); + + static final Map SPECIFIC_CREATORS = Map.ofEntries( // + entry("java.net.HttpRetryException", new HttpRetryExceptionCreator()), // + entry("java.time.format.DateTimeParseException", new DateTimeParseExceptionCreator()), // + entry("java.util.IllformedLocaleException", new IllformedLocaleExceptionCreator()), // + entry("java.util.MissingResourceException", new MissingResourceExceptionCreator()), // + entry("junit.framework.ComparisonFailure", new ThrowableCreatorWrapper(ComparisonFailureCreator::new)), // + entry("org.junit.ComparisonFailure", new ThrowableCreatorWrapper(ComparisonFailureCreator::new)), // + entry("org.junit.experimental.theories.internal.ParameterizedAssertionError", + new ThrowableCreatorWrapper(ParameterizedAssertionErrorCreator::new)) // + ); + + final Map, ThrowableCreator> cachedThrowableCreators = Collections + .synchronizedMap(new LruCache<>(1000)); + + @Override + public boolean canSanitize(Throwable t) { + Class type = t.getClass(); + boolean isSafe = ThrowableSets.SAFE_TYPES.contains(type); + boolean isDuplicatable = !NON_DUPLICATABLE_SAFE_TYPES.contains(type.getName()); + return isSafe && isDuplicatable; + } + + @Override + public Throwable sanitize(Throwable t, MessageTransformer messageTransformer) { + Class type = t.getClass(); + // this is OK because we are only dealing with safe types here + ThrowableInfo info = ThrowableInfo.of(type, ThrowableUtils.retrievePropertyValues(t)); + info.sanitize(ThrowableUtils.PROPERTY_SANITIZER); + var throwableCreator = cachedThrowableCreators.computeIfAbsent(type, this::findThrowableCreator); + try { + Throwable newInstance = throwableCreator.create(info); + SanitizationUtils.copyThrowableInfoSafe(info, newInstance); + return newInstance; + } catch (RuntimeException e) { + LOG.warn("Failed to sanitize an exception of type {}.", type, e); + return ArbitraryThrowableSanitizer.INSTANCE.sanitize(t, messageTransformer); + } + } + + private ThrowableCreator findThrowableCreator(Class type) { + var throwableCreator = SPECIFIC_CREATORS.get(type.getName()); + if (throwableCreator == null) + throwableCreator = ThrowableUtils.getThrowableCreatorFor(type); + return throwableCreator; + } + + private static class ThrowableCreatorWrapper implements ThrowableCreator { + private final Supplier throwableCreatorSupplier; + private ThrowableCreator throwableCreator; + + private ThrowableCreatorWrapper(Supplier throwableCreatorSupplier) { + this.throwableCreatorSupplier = throwableCreatorSupplier; + } + + @Override + public Throwable create(ThrowableInfo throwableInfo) { + if (throwableCreator == null) + throwableCreator = throwableCreatorSupplier.get(); + return throwableCreator.create(throwableInfo); + } + } + + private static class HttpRetryExceptionCreator implements ThrowableCreator { + + private static final ProperyKey RESPONSE_CODE = new ProperyKey<>(int.class, "responseCode"); + private static final ProperyKey LOCATION = new ProperyKey<>(String.class, "location"); + + @Override + public Throwable create(ThrowableInfo info) { + var detail = info.getMessage(); + var code = info.getProperty(RESPONSE_CODE); + var location = info.getProperty(LOCATION); + return new HttpRetryException(detail, code, location); + } + } + + private static class DateTimeParseExceptionCreator implements ThrowableCreator { + + private static final ProperyKey PARSED_STRING = new ProperyKey<>(String.class, "parsedString"); + private static final ProperyKey ERROR_INDEX = new ProperyKey<>(int.class, "errorIndex"); + + @Override + public Throwable create(ThrowableInfo info) { + var message = info.getMessage(); + var parsedData = info.getProperty(PARSED_STRING); + var errorIndex = info.getProperty(ERROR_INDEX); + return new DateTimeParseException(message, parsedData, errorIndex); + } + } + + private static class IllformedLocaleExceptionCreator implements ThrowableCreator { + @Override + public Throwable create(ThrowableInfo info) { + return new IllformedLocaleException(info.getMessage()); + } + } + + private static class MissingResourceExceptionCreator implements ThrowableCreator { + + private static final ProperyKey CLASS_NAME = new ProperyKey<>(String.class, "className"); + private static final ProperyKey KEY = new ProperyKey<>(String.class, "key"); + + @Override + public Throwable create(ThrowableInfo info) { + var message = info.getMessage(); + var className = info.getProperty(CLASS_NAME); + var key = info.getProperty(KEY); + return new MissingResourceException(message, className, key); + } + } + + private static class ComparisonFailureCreator implements ThrowableCreator { + + private static final int MAX_CONTEXT_LENGTH = 20; + + private static final ProperyKey EXPECTED = new ProperyKey<>(String.class, "expected"); + private static final ProperyKey ACTUAL = new ProperyKey<>(String.class, "actual"); + + @Override + public Throwable create(ThrowableInfo info) { + var message = info.getMessage(); + var expected = info.getProperty(EXPECTED); + var actual = info.getProperty(ACTUAL); + String withoutMessage = new ComparisonCompactor(MAX_CONTEXT_LENGTH, expected, actual).compact(""); + var start = SanitizationUtils.removeSuffixMatching(message, withoutMessage); + if (start == null) + message = ""; + else + message = start.trim(); + if (ComparisonFailure.class.isAssignableFrom(info.getType())) + return new ComparisonFailure(message, expected, actual); + return new org.junit.ComparisonFailure(message, expected, actual); + } + } + + private static class ParameterizedAssertionErrorCreator implements ThrowableCreator { + + @Override + public Throwable create(ThrowableInfo info) { + var targetException = info.getCause(); + var message = info.getMessage(); + var methodName = message; + var params = new Object[0]; + try { + var parts = message.substring(0, message.length() - 1).split("\\(", 2); + methodName = parts[0]; + params = parts[1].split(","); + } catch (@SuppressWarnings("unused") RuntimeException e) { + // ignore + } + return new ParameterizedAssertionError(targetException, methodName, params); + } + } +} diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/SanitizationException.java b/src/main/java/de/tum/in/test/api/internal/sanitization/SanitizationException.java index f87d5de3..2e3faf56 100644 --- a/src/main/java/de/tum/in/test/api/internal/sanitization/SanitizationException.java +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/SanitizationException.java @@ -18,7 +18,7 @@ class SanitizationException extends RuntimeException { public SanitizationException(Class originClass, Throwable cause) { super(generateMessage(originClass, cause)); this.originClass = Objects.requireNonNull(originClass, "sanitization failure origin class must not be null"); - this.unsafeCause = Objects.requireNonNull(cause, "sanitization failure cause must not be null"); + unsafeCause = Objects.requireNonNull(cause, "sanitization failure cause must not be null"); } public Class getOriginClass() { diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/SanitizationUtils.java b/src/main/java/de/tum/in/test/api/internal/sanitization/SanitizationUtils.java index d221795c..3db31897 100644 --- a/src/main/java/de/tum/in/test/api/internal/sanitization/SanitizationUtils.java +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/SanitizationUtils.java @@ -2,13 +2,6 @@ import static de.tum.in.test.api.internal.BlacklistedInvoker.invoke; -import java.util.Arrays; -import java.util.stream.Collectors; - -import de.tum.in.test.api.internal.ThrowableUtils; -import de.tum.in.test.api.security.ArtemisSecurityManager; -import de.tum.in.test.api.util.IgnorantUnmodifiableList; - final class SanitizationUtils { private SanitizationUtils() { @@ -16,26 +9,41 @@ private SanitizationUtils() { } static void copyThrowableInfoSafe(Throwable from, Throwable to) { - // this is only needed for transfer - if (from != to) { - // the message and stack trace are safe - ThrowableUtils.setDetailMessage(to, ThrowableUtils.getDetailMessage(from)); - to.setStackTrace(from.getStackTrace()); - } + copyThrowableInfoSafe(ThrowableInfo.getEssentialInfosSafeFrom(from), to); + } + static void copyThrowableInfoSafe(ThrowableInfo from, Throwable to) { + // make sure everything is sanitized + if (!from.isSanitized()) + from.sanitize(); // this returns either null or another Throwable instance - Throwable causeVal = invoke(from::getCause); - Throwable[] supprVal = invoke(from::getSuppressed); - - // because causeVal is never t, this will lock the cause and calls to initCause - var newCause = ThrowableSanitizer.sanitize(causeVal); - ThrowableUtils.setCause(to, newCause); - - // this breaks addSuppressed by purpose - var newSupressed = IgnorantUnmodifiableList.wrapWith( - Arrays.stream(supprVal).map(ThrowableSanitizer::sanitize).collect(Collectors.toList()), - ArtemisSecurityManager.getOnSuppressedModification()); - ThrowableUtils.setSuppressedException(to, newSupressed); + Throwable fromCause = from.getCause(); + StackTraceElement[] fromStackTrace = from.getStackTrace(); + Throwable[] fromSuppr = from.getSuppressed(); + + Throwable toCause = invoke(to::getCause); + Throwable[] toSuppr = to.getSuppressed(); // OK: final method + + // if toCause is not null, we have to assume it is set and sanitized + if (toCause == null) { + try { + // because causeVal is never from, this will lock the cause + invoke(() -> to.initCause(fromCause)); + } catch (@SuppressWarnings("unused") IllegalStateException ignored) { + /* + * initCause can fail if a cause is already present, so we add it as suppressed + * to not loose information + */ + if (fromCause != null) + to.addSuppressed(fromCause); // OK: final method + } + } + // note that this has the possibility of silently failing + invoke(() -> to.setStackTrace(fromStackTrace)); + // add suppressed Throwables only if there are none in to but some in from + if (toSuppr.length == 0 && fromSuppr.length > 0) + for (Throwable suppressed : toSuppr) + to.addSuppressed(suppressed); // OK: final method } static T sanitizeWithinScopeOf(Object scope, SanitizationAction sanitizationAction) { @@ -51,4 +59,11 @@ static T sanitizeWithinScopeOf(Class scope, SanitizationAction sanitiz throw new SanitizationException(scope, t); } } + + static String removeSuffixMatching(String s, String suffix) { + int end = s.lastIndexOf(suffix); + if (end == -1 || end + suffix.length() < s.length()) + return null; + return s.substring(0, end); + } } diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/SimpleThrowableSanitizer.java b/src/main/java/de/tum/in/test/api/internal/sanitization/SimpleThrowableSanitizer.java deleted file mode 100644 index 55356a02..00000000 --- a/src/main/java/de/tum/in/test/api/internal/sanitization/SimpleThrowableSanitizer.java +++ /dev/null @@ -1,16 +0,0 @@ -package de.tum.in.test.api.internal.sanitization; - -enum SimpleThrowableSanitizer implements SpecificThrowableSanitizer { - INSTANCE; - - @Override - public boolean canSanitize(Throwable t) { - return ThrowableSets.SAFE_TYPES.contains(t.getClass()); - } - - @Override - public Throwable sanitize(Throwable t) { - SanitizationUtils.copyThrowableInfoSafe(t, t); - return t; - } -} diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/SoftAssertionErrorSanitizer.java b/src/main/java/de/tum/in/test/api/internal/sanitization/SoftAssertionErrorSanitizer.java index 72217234..b2f636b3 100644 --- a/src/main/java/de/tum/in/test/api/internal/sanitization/SoftAssertionErrorSanitizer.java +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/SoftAssertionErrorSanitizer.java @@ -18,10 +18,10 @@ public boolean canSanitize(Throwable t) { } @Override - public Throwable sanitize(Throwable t) { - SoftAssertionError sae = new SoftAssertionError( - invoke(() -> List.copyOf(((SoftAssertionError) t).getErrors()))); - SanitizationUtils.copyThrowableInfoSafe(t, sae); - return sae; + public Throwable sanitize(Throwable t, MessageTransformer messageTransformer) { + SoftAssertionError sae = (SoftAssertionError) t; + SoftAssertionError newSae = new SoftAssertionError(invoke(() -> List.copyOf(sae.getErrors()))); + SanitizationUtils.copyThrowableInfoSafe(sae, newSae); + return newSae; } } diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/SpecificThrowableSanitizer.java b/src/main/java/de/tum/in/test/api/internal/sanitization/SpecificThrowableSanitizer.java index b4abd9f9..3f8f997c 100644 --- a/src/main/java/de/tum/in/test/api/internal/sanitization/SpecificThrowableSanitizer.java +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/SpecificThrowableSanitizer.java @@ -4,5 +4,9 @@ interface SpecificThrowableSanitizer { boolean canSanitize(Throwable t); - Throwable sanitize(Throwable t); + Throwable sanitize(Throwable t, MessageTransformer messageTransformer); + + default Throwable sanitize(Throwable t) { + return sanitize(t, MessageTransformer.IDENTITY); + } } diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableCreator.java b/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableCreator.java new file mode 100644 index 00000000..bc89356b --- /dev/null +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableCreator.java @@ -0,0 +1,7 @@ +package de.tum.in.test.api.internal.sanitization; + +@FunctionalInterface +interface ThrowableCreator { + + Throwable create(ThrowableInfo throwableInfo); +} \ No newline at end of file diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableInfo.java b/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableInfo.java new file mode 100644 index 00000000..0a30505f --- /dev/null +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableInfo.java @@ -0,0 +1,235 @@ +package de.tum.in.test.api.internal.sanitization; + +import static de.tum.in.test.api.internal.BlacklistedInvoker.invoke; +import static de.tum.in.test.api.internal.sanitization.ThrowableUtils.*; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.BiFunction; +import java.util.function.UnaryOperator; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +/** + * Contains information about a Throwable without the Throwable instance itself. + * This allows the values to be modified by using the setters. Getters of this + * class will return a copy or unmodifiable version. + *

+ * A {@link ThrowableInfo} can be sanitized, which will guarantee that all + * information contained is safe to use. Sanitization can be performed by the + * methods {@link #sanitize()} and {@link #sanitize(BiFunction)}. The + * sanitization status can be checked by using {@link #isSanitized()}. Some + * setters of this class (all that involve collections or arrays) reset the + * sanitization status. + *

+ * The default sanitization status is "not sanitized". This is also the one in + * which instances of this class are if they are created by one of the static + * factory methods. + */ +@API(status = Status.INTERNAL) +public final class ThrowableInfo { + + private final Class type; + private String message; + private Throwable cause; + private StackTraceElement[] stackTrace; + private Throwable[] suppressed; + private Map additionalProperties; + + private boolean sanitized; + + private ThrowableInfo(Class type, String message, Throwable cause, + StackTraceElement[] stackTrace, Throwable[] suppressed, Map additionalProperties) { + this.type = type; + this.message = message; + this.cause = cause; + this.stackTrace = stackTrace; + this.suppressed = suppressed; + this.additionalProperties = additionalProperties; + } + + public Class getType() { + return type; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public Throwable getCause() { + return cause; + } + + public void setCause(Throwable cause) { + this.cause = cause; + sanitized = false; + } + + public StackTraceElement[] getStackTrace() { + return stackTrace; + } + + public void setStackTrace(StackTraceElement[] stackTrace) { + this.stackTrace = stackTrace; + } + + public Throwable[] getSuppressed() { + return suppressed.clone(); + } + + public void setSuppressed(Throwable[] suppressed) { + this.suppressed = suppressed; + sanitized = false; + } + + public Map getAdditionalProperties() { + return Collections.unmodifiableMap(additionalProperties); + } + + public void setAdditionalProperties(Map additionalProperties) { + this.additionalProperties = additionalProperties; + sanitized = false; + } + + public boolean isSanitized() { + return sanitized; + } + + /** + * Does a best effort attempt to sanitize all regular {@link Throwable} + * properties. Is this {@link ThrowableInfo} has additional properties, this + * method will sanitize only the regular properties, but {@link #isSanitized()} + * will stay false. if this is a problem, + * {@link #sanitize(BiFunction)} should be used. + * + * @return this + */ + public ThrowableInfo sanitize() { + cause = ThrowableSanitizer.sanitize(cause); + // defensive copy + suppressed = suppressed.clone(); + for (int i = 0; i < suppressed.length; i++) + suppressed[i] = ThrowableSanitizer.sanitize(suppressed[i]); + sanitized = additionalProperties.isEmpty(); + return this; + } + + /** + * Sanitizes all information in this {@link ThrowableInfo}. + * + * @param additionalPropertySanitizer a {@link BiFunction} that needs to be able + * to sanitize all additional properties. The + * first parameter is the property name, the + * second is the value. A sanitized version + * of the value should then be returned. + * @return this + */ + public ThrowableInfo sanitize(BiFunction additionalPropertySanitizer) { + sanitize(); + // defensive copy + additionalProperties = new HashMap<>(additionalProperties); + // we have to assume that additionalPropertySanitizer is properly working + additionalProperties.replaceAll(additionalPropertySanitizer); + sanitized = true; + return this; + } + + public Map toPropertyMap() { + HashMap properties = new HashMap<>(additionalProperties); + properties.put(MESSAGE, message); + properties.put(CAUSE, cause); + properties.put(STACK_TRACE, stackTrace); + properties.put(SUPPRESSED, suppressed); + return properties; + } + + public T getProperty(ProperyKey key) { + return key.cast(additionalProperties.get(key.name())); + } + + public void setProperty(ProperyKey key, T newValue) { + additionalProperties.put(key.name(), key.cast(newValue)); + } + + public static ThrowableInfo of(Class type, String message, Throwable cause, + StackTraceElement[] stackTrace, Throwable[] suppressed, Map additionalProperties) { + return new ThrowableInfo(type, message, cause, stackTrace.clone(), suppressed.clone(), + new HashMap<>(additionalProperties)); + } + + public static ThrowableInfo of(Class type, Map properties) { + var message = (String) properties.get(MESSAGE); + var cause = (Throwable) properties.get(CAUSE); + var stackTrace = (StackTraceElement[]) properties.get(STACK_TRACE); + var suppressed = (Throwable[]) properties.get(SUPPRESSED); + var additionalProperties = new HashMap<>(properties); + for (var throwableProperty : THROWABLE_PROPERTIES) + additionalProperties.remove(throwableProperty); + return new ThrowableInfo(type, message, cause, stackTrace.clone(), suppressed.clone(), additionalProperties); + } + + public static ThrowableInfo getEssentialInfosSafeFrom(Throwable source) { + String message = invoke(source::getMessage); + Throwable cause = invoke(source::getCause); + StackTraceElement[] stackTrace = invoke(source::getStackTrace); + Throwable[] suppressed = source.getSuppressed(); // OK: final method + return ThrowableInfo.of(source.getClass(), message, cause, stackTrace, suppressed, Map.of()); + } + + public static class ProperyKey { + + private final Class type; + private final String name; + private final UnaryOperator sanitizer; + + public ProperyKey(Class type, String name, UnaryOperator sanitizer) { + this.type = Objects.requireNonNull(type); + this.name = Objects.requireNonNull(name); + this.sanitizer = Objects.requireNonNull(sanitizer); + } + + public ProperyKey(Class type, String name) { + this(type, name, UnaryOperator.identity()); + } + + public final String name() { + return name; + } + + public final Class type() { + return type; + } + + public T sanitize(T value) { + return sanitizer.apply(value); + } + + @SuppressWarnings("unchecked") + final T cast(Object value) { + if (value == null && type.isPrimitive()) + throw new NullPointerException("cannot cast null to primitive: " + type); + if (byte.class.equals(type)) + return (T) Byte.class.cast(value); + if (short.class.equals(type)) + return (T) Short.class.cast(value); + if (char.class.equals(type)) + return (T) Character.class.cast(value); + if (int.class.equals(type)) + return (T) Integer.class.cast(value); + if (long.class.equals(type)) + return (T) Long.class.cast(value); + if (float.class.equals(type)) + return (T) Float.class.cast(value); + if (double.class.equals(type)) + return (T) Double.class.cast(value); + return type.cast(value); + } + } +} \ No newline at end of file diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableSanitizer.java b/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableSanitizer.java index 3e949abd..22d92fec 100644 --- a/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableSanitizer.java +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableSanitizer.java @@ -18,21 +18,23 @@ private ThrowableSanitizer() { ThrowableSets.init(); } - private static final List SANITIZERS = List.of(SimpleThrowableSanitizer.INSTANCE, + private static final List SANITIZERS = List.of(SafeTypeThrowableSanitizer.INSTANCE, AssertionFailedErrorSanitizer.INSTANCE, PrivilegedExceptionSanitizer.INSTANCE, MultipleFailuresErrorSanitizer.INSTANCE, MultipleAssertionsErrorSanitizer.INSTANCE, ExceptionInInitializerErrorSanitizer.INSTANCE, SoftAssertionErrorSanitizer.INSTANCE); public static Throwable sanitize(final Throwable t) { + return sanitize(t, MessageTransformer.IDENTITY); + } + + public static Throwable sanitize(final Throwable t, MessageTransformer messageTransformer) { if (t == null) return null; return SanitizationUtils.sanitizeWithinScopeOf(t, () -> { if (UnexpectedExceptionError.class.equals(t.getClass())) return t; var firstPossibleSan = SANITIZERS.stream().filter(s -> s.canSanitize(t)).findFirst(); - if (firstPossibleSan.isPresent()) - return firstPossibleSan.get().sanitize(t); - return UnexpectedExceptionError.wrap(t); + return firstPossibleSan.orElse(ArbitraryThrowableSanitizer.INSTANCE).sanitize(t, messageTransformer); }); } } diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableUtils.java b/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableUtils.java new file mode 100644 index 00000000..034dbb0a --- /dev/null +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableUtils.java @@ -0,0 +1,170 @@ +package de.tum.in.test.api.internal.sanitization; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.platform.commons.support.ReflectionSupport; + +final class ThrowableUtils { + + static final String MESSAGE = "message"; + static final String CAUSE = "cause"; + static final String STACK_TRACE = "stackTrace"; + static final String SUPPRESSED = "suppressed"; + + static final Set IGNORE_PROPERTIES = Set.of("class", "toString", "hashCode", "fillInStackTrace", + "localizedMessage"); + + static final Set THROWABLE_PROPERTIES = Stream + .concat(IGNORE_PROPERTIES.stream(), Stream.of(MESSAGE, STACK_TRACE, CAUSE, SUPPRESSED)) + .collect(Collectors.toUnmodifiableSet()); + + static final BiFunction PROPERTY_SANITIZER = (name, value) -> { + if (value == null) + return value; + if (value instanceof Throwable) + return ThrowableSanitizer.sanitize((Throwable) value); + if (value instanceof Throwable[]) + return Arrays.stream((Throwable[]) value).map(ThrowableSanitizer::sanitize).toArray(Throwable[]::new); + return value; + }; + + private static final Comparator> ORDER_PROPERTIES_BY_RELEVANCE = Map.Entry + .comparingByKey((firstProperty, secondProperty) -> { + boolean firstIsLessImportant = THROWABLE_PROPERTIES.contains(firstProperty); + boolean secondIsLessImportant = THROWABLE_PROPERTIES.contains(secondProperty); + if (firstIsLessImportant && !secondIsLessImportant) + return 1; + if (secondIsLessImportant && !firstIsLessImportant) + return -1; + return firstProperty.compareTo(secondProperty); + }); + + private ThrowableUtils() { + + } + + static ThrowableCreator getThrowableCreatorFor(Class type) { + var preferredConstructor = findPreferredConstructor(type); + return getThrowableCreatorFor(type, preferredConstructor); + } + + static ThrowableCreator getThrowableCreatorFor(Class type, + Constructor constructor) { + var propertiesWithMethods = getRelevantPropertiesWithMethods(type, IGNORE_PROPERTIES); + return info -> { + var originalProperties = info.toPropertyMap(); + var originalPropertyValuesByType = getValuesByPropertyType( + propertiesWithMethods.stream().sorted(ORDER_PROPERTIES_BY_RELEVANCE), originalProperties); + var argumentsForCopy = provideArguments(constructor, originalPropertyValuesByType); + try { + return constructor.newInstance(argumentsForCopy); + } catch (InvocationTargetException e) { + throw new SanitizationException(type, e.getCause()); + } catch (Exception e) { + throw new SanitizationException(type, e); + } + }; + } + + static Constructor findPreferredConstructor(Class type) { + @SuppressWarnings("unchecked") + var allConstructors = (Constructor[]) type.getConstructors(); + if (allConstructors.length == 0) + return null; + var classPropertyTypeCount = getRelevantPropertiesWithMethods(type, IGNORE_PROPERTIES).stream() + .collect(Collectors.groupingBy(property -> property.getValue().getReturnType(), Collectors.counting())); + return Stream.of(allConstructors) + .filter(constructor -> isSatisfiableByProperties(constructor, classPropertyTypeCount)) + .sorted(getConstructorPreferenceOrder()).findFirst().orElse(allConstructors[0]); + } + + static boolean isSatisfiableByProperties(Constructor constructor, + Map, Long> propertyTypeCount) { + var parameterTypeCount = Stream.of(constructor.getParameterTypes()) + .collect(Collectors.groupingBy(Function.identity(), Collectors.counting())); + return parameterTypeCount.entrySet().stream() + .mapToLong(e -> propertyTypeCount.getOrDefault(e.getKey(), 0L) - e.getValue()).min().orElse(0) >= 0; + } + + static Comparator> getConstructorPreferenceOrder() { + return Comparator.>comparingInt(Constructor::getParameterCount) + .thenComparingLong(x -> Stream.of(x.getParameterTypes()).filter(String.class::equals).count()) + .reversed(); + } + + static Set> getRelevantPropertiesWithMethods(Class type, Set namesToIgnore) { + return Stream.of(type.getMethods()).map(method -> { + var propertyName = extractProperty(method); + if (propertyName == null || namesToIgnore.contains(propertyName)) + return null; + return Map.entry(propertyName, method); + }).filter(Objects::nonNull).collect(Collectors.toUnmodifiableSet()); + } + + static Map retrievePropertyValues(Object instance) { + return retrievePropertyValues(instance, getPropertiesWithMethods(instance.getClass())); + } + + static Map retrievePropertyValues(Object instance, Set> properties) { + return properties.stream() + .collect(Collectors.groupingBy(Map.Entry::getKey, + Collectors.mapping(property -> ReflectionSupport.invokeMethod(property.getValue(), instance), + Collectors.reducing(null, (a, b) -> a != null ? a : b)))); + } + + static Set> getPropertiesWithMethods(Class type) { + return Stream.of(type.getMethods()).map(method -> { + var propertyName = extractProperty(method); + return propertyName == null ? null : Map.entry(propertyName, method); + }).filter(Objects::nonNull).collect(Collectors.toUnmodifiableSet()); + } + + static Map, List> getValuesByPropertyType(Stream> propertiesWithMethods, + Map concretePropertyValues) { + return propertiesWithMethods.collect(Collectors.groupingBy(property -> property.getValue().getReturnType(), + Collectors.mapping(property -> concretePropertyValues.get(property.getKey()), Collectors.toList()))); + } + + static String extractProperty(Method method) { + if (method.getParameterCount() > 0) + return null; + if (method.isBridge() || method.isSynthetic()) + return null; + if (void.class.equals(method.getReturnType())) + return null; + String name = method.getName(); + if (name.startsWith("get") && name.length() > 3 && Character.isUpperCase(name.charAt(3))) { + StringBuilder propertyName = new StringBuilder(name); + propertyName.delete(0, 3); + propertyName.setCharAt(0, Character.toLowerCase(propertyName.charAt(0))); + name = propertyName.toString(); + } + return name; + } + + static Object[] provideArguments(Constructor constructor, Map, List> valueSource) { + var used = new HashSet<>(); + return Stream.of(constructor.getParameterTypes()).map(argType -> { + var valueList = valueSource.get(argType); + if (valueList != null) + for (var value : valueList) + if (used.add(value)) + return value; + return null; + }).toArray(); + } +} diff --git a/src/main/java/de/tum/in/test/api/security/FixSystemErrAppender.java b/src/main/java/de/tum/in/test/api/security/FixSystemErrAppender.java index 001fea16..4d800692 100644 --- a/src/main/java/de/tum/in/test/api/security/FixSystemErrAppender.java +++ b/src/main/java/de/tum/in/test/api/security/FixSystemErrAppender.java @@ -13,7 +13,6 @@ */ public class FixSystemErrAppender extends OutputStreamAppender { public FixSystemErrAppender() { - super(); setOutputStream(SecurityConstants.SYSTEM_ERR); } } \ No newline at end of file diff --git a/src/main/java/de/tum/in/test/api/util/UnexpectedExceptionError.java b/src/main/java/de/tum/in/test/api/util/UnexpectedExceptionError.java index 69dd2b4e..7ace358c 100644 --- a/src/main/java/de/tum/in/test/api/util/UnexpectedExceptionError.java +++ b/src/main/java/de/tum/in/test/api/util/UnexpectedExceptionError.java @@ -3,6 +3,8 @@ import static de.tum.in.test.api.internal.BlacklistedInvoker.invoke; import static de.tum.in.test.api.internal.sanitization.ThrowableSanitizer.sanitize; +import java.util.Objects; + import org.apiguardian.api.API; import org.apiguardian.api.API.Status; @@ -12,11 +14,12 @@ public final class UnexpectedExceptionError extends Error { private static final long serialVersionUID = 1L; private final Class originalType; - private UnexpectedExceptionError(Throwable cause) { - super(invoke(cause::toString), sanitize(invoke(cause::getCause)), true, true); - originalType = cause.getClass(); - setStackTrace(invoke(cause::getStackTrace)); - for (Throwable sup : invoke(cause::getSuppressed)) + private UnexpectedExceptionError(Class originalType, String message, Throwable cause, + StackTraceElement[] stackTrace, Throwable[] suppressed) { + super(message, sanitize(cause), true, true); + this.originalType = originalType; + setStackTrace(stackTrace); + for (Throwable sup : suppressed) addSuppressed(sanitize(sup)); } @@ -27,6 +30,17 @@ public Class getOriginalType() { public static UnexpectedExceptionError wrap(Throwable t) { if (t == null) return null; - return new UnexpectedExceptionError(t); + var message = invoke(t::toString); + var cause = invoke(t::getCause); + var originalType = t.getClass(); + var stackTrace = invoke(t::getStackTrace); + var suppressed = invoke(t::getSuppressed); + return new UnexpectedExceptionError(originalType, message, cause, stackTrace, suppressed); + } + + public static UnexpectedExceptionError create(Class originalType, String message, + Throwable cause, StackTraceElement[] stackTrace, Throwable[] suppressed) { + return new UnexpectedExceptionError(Objects.requireNonNull(originalType), message, cause, + Objects.requireNonNull(stackTrace), Objects.requireNonNull(suppressed)); } } diff --git a/src/test/java/de/tum/in/test/api/ExceptionFailureTest.java b/src/test/java/de/tum/in/test/api/ExceptionFailureTest.java index d39fc92a..a8ff9e22 100644 --- a/src/test/java/de/tum/in/test/api/ExceptionFailureTest.java +++ b/src/test/java/de/tum/in/test/api/ExceptionFailureTest.java @@ -48,7 +48,7 @@ void test_assertJMultipleFailures() { message(m -> m.contains("Multiple Failures (2 failures)") // && m.contains("-- failure 1 --A") // && m.contains("-- failure 2 --B")), - new Condition<>(t -> t.getSuppressed().length == 0, "no suppressed exceptions")))); + new Condition<>(t -> t.getSuppressed().length == 2, "failures added as suppressed")))); } @TestTest @@ -124,7 +124,10 @@ void test_nullPointer() { @TestTest void test_softAssertion() { tests.assertThatEvents().haveExactly(1, - event(test(softAssertion), finishedWithFailure(instanceOf(SoftAssertionError.class), - message(m -> m.contains("The following 2 assertions failed:\n1) A\n2) B"))))); + event(test(softAssertion), + finishedWithFailure(instanceOf(SoftAssertionError.class), + message(m -> m.contains("The following 2 assertions failed:") // + && m.contains("1) A") // + && m.contains("2) B"))))); } } diff --git a/src/test/java/de/tum/in/test/api/internal/ThrowableUtilsTest.java b/src/test/java/de/tum/in/test/api/internal/ThrowableUtilsTest.java deleted file mode 100644 index dc0e331c..00000000 --- a/src/test/java/de/tum/in/test/api/internal/ThrowableUtilsTest.java +++ /dev/null @@ -1,77 +0,0 @@ -package de.tum.in.test.api.internal; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.Collections; -import java.util.List; - -import org.junit.jupiter.api.Test; - -class ThrowableUtilsTest { - - private Throwable target = new Exception("ABC"); - - @Test - void testDetailMessage() { - var message = ThrowableUtils.getDetailMessage(target); - // not initialized - assertThat(message).isEqualTo("ABC"); - - // set cause - var newMessage = "XYZ"; - ThrowableUtils.setDetailMessage(target, newMessage); - - message = ThrowableUtils.getDetailMessage(target); - // set to new cause - assertThat(message).isSameAs(newMessage) // - .isSameAs(target.getMessage()); - } - - @Test - void testCause() { - var cause = ThrowableUtils.getCause(target); - // not initialized - assertThat(cause).isSameAs(target); - - // set cause - var newCause = new NullPointerException(); - ThrowableUtils.setCause(target, newCause); - - cause = ThrowableUtils.getCause(target); - // set to new cause - assertThat(cause).isSameAs(newCause) // - .isSameAs(target.getCause()); - } - - @Test - void testStackTrace() { - var stackTrace = ThrowableUtils.getStackTrace(target); - // not initialized - assertThat(stackTrace).isEmpty(); - - // set suppressed - var newStackTrace = new StackTraceElement[] { new StackTraceElement("A", "B", "as.df", 42) }; - ThrowableUtils.setStackTrace(target, newStackTrace); - - stackTrace = ThrowableUtils.getStackTrace(target); - // set to new suppressed - assertThat(stackTrace).isSameAs(newStackTrace) // - .containsExactly(target.getStackTrace()); - } - - @Test - void testSuppressedExceptions() { - var suppressed = ThrowableUtils.getSuppressedExceptions(target); - // not initialized - assertThat(suppressed).isSameAs(Collections.emptyList()); - - // set suppressed - var newSuppressed = List.of(new NullPointerException()); - ThrowableUtils.setSuppressedException(target, newSuppressed); - - suppressed = ThrowableUtils.getSuppressedExceptions(target); - // set to new suppressed - assertThat(suppressed).isSameAs(newSuppressed) // - .containsExactly(target.getSuppressed()); - } -} diff --git a/src/test/java/de/tum/in/test/api/internal/sanitization/ThrowableUtilsTest.java b/src/test/java/de/tum/in/test/api/internal/sanitization/ThrowableUtilsTest.java new file mode 100644 index 00000000..c0f75c44 --- /dev/null +++ b/src/test/java/de/tum/in/test/api/internal/sanitization/ThrowableUtilsTest.java @@ -0,0 +1,174 @@ +package de.tum.in.test.api.internal.sanitization; + +import static de.tum.in.test.api.internal.sanitization.ThrowableUtils.*; +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.platform.commons.support.ReflectionSupport; + +class ThrowableUtilsTest { + + private static final Map, List> TEST_VALUES = Map.ofEntries( // + typeEntry(char.class, 'a', 'b', 'c'), // + typeEntry(int.class, 42, 43, 44), // + typeEntry(boolean.class, true, false), // + typeEntry(long.class, 123L, 124L, 125L), // + typeEntry(float.class, 1234.5f, 1235.0f, 1236.0f), // + typeEntry(double.class, 1234.5, 1235.0, 1236.0), // + typeEntry(Object.class, new Object(), new Object(), new Object()), // + typeEntry(Class.class, Class.class, Object.class, String.class), // + typeEntry(String.class, "penguin1", "penguin2", "penguin3", "penguin4"), // + typeEntry(CharSequence.class, "penguinA", "penguinB"), // + typeEntry(Throwable.class, new Exception(), new Exception(), new Exception()), // + typeEntry(IOException.class, new IOException()), // + typeEntry(TimeUnit.class, TimeUnit.DAYS), // + typeEntry(Object[].class, new Object[] { "a" }, new Object[] { "b" }), // + typeEntry(Method.class, ReflectionSupport.findMethod(String.class, "length").orElseThrow()) // + ); + + private static final Set FALSE_POSITIVES = Set.of("junit.framework.AssertionFailedError", + "java.util.IllformedLocaleException"); + + private static final Set> SAFE_PROPERTY_TYPES = Set.of(Throwable.class, Throwable[].class); + + @Test + void testConstructorInstantiation() { + var duplicationFailures = ThrowableSets.SAFE_TYPES.stream().filter(type -> { + String typeName = type.getName(); + if (FALSE_POSITIVES.contains(typeName)) + return false; + if (SafeTypeThrowableSanitizer.NON_DUPLICATABLE_SAFE_TYPES.contains(typeName)) + return false; + if (SafeTypeThrowableSanitizer.SPECIFIC_CREATORS.containsKey(typeName)) + return false; + + var preferredConstructor = findPreferredConstructor(type); + return !validate(type, preferredConstructor); + }).sorted(Comparator.comparing(Object::toString)).collect(Collectors.toList()); + assertThat(duplicationFailures) + .as("the detault Throwable duplication works for all SAFE_TYPE classes that are not specially handeled") + .isEmpty(); + } + + @Test + void testSpecificCreatorInstantiation() { + var duplicationFailures = ThrowableSets.SAFE_TYPES.stream().filter(type -> { + String typeName = type.getName(); + if (FALSE_POSITIVES.contains(typeName)) + return false; + var specificCreator = SafeTypeThrowableSanitizer.SPECIFIC_CREATORS.get(typeName); + if (specificCreator == null) + return false; + + return !validate(type, specificCreator); + }).sorted(Comparator.comparing(Object::toString)).collect(Collectors.toList()); + assertThat(duplicationFailures) + .as("the Throwable duplication works for all SAFE_TYPE classes that have a designated creator") + .isEmpty(); + } + + /** + * This tests that {@link ThrowableUtils#PROPERTY_SANITIZER} can sanitize + * everything. Adjust the linked sanitizer if this fails, as well as the safe + * property type set {@link #SAFE_PROPERTY_TYPES}. + */ + @Test + void checkProperties() { + Set> potentiallyUnsafeProperties = ThrowableSets.SAFE_TYPES.stream() + .map(ThrowableUtils::getPropertiesWithMethods).flatMap(Set::stream).map(Map.Entry::getValue) + .map(Method::getReturnType).distinct().filter(type -> { + Class containedType; + if (type.isArray()) + containedType = Stream.>iterate(type, Class::getComponentType) + .takeWhile(Objects::nonNull).reduce(null, (a, b) -> b); + else + containedType = type; + if (containedType.isPrimitive()) + return false; + if (Modifier.isFinal(containedType.getModifiers())) + return false; + if (SAFE_PROPERTY_TYPES.stream().anyMatch(safeType -> safeType.isAssignableFrom(containedType))) + return false; + return true; + }).collect(Collectors.toSet()); + assertThat(potentiallyUnsafeProperties).as("property types are all safe or sanitizable").isEmpty(); + } + + static boolean validate(Class type, Constructor constructor) { + if (constructor == null) + return false; + return validate(type, getThrowableCreatorFor(type, constructor)); + } + + static boolean validate(Class type, ThrowableCreator creator) { + if (creator == null) + return false; + @SuppressWarnings("unchecked") + var allConstructors = (Constructor[]) type.getConstructors(); + var propertiesWithMethods = getRelevantPropertiesWithMethods(type, IGNORE_PROPERTIES); + for (var constructorToCheck : allConstructors) + try { + var arguments = provideArguments(constructorToCheck, TEST_VALUES); + var originalInstance = constructorToCheck.newInstance(arguments); + var originalProperties = retrievePropertyValues(originalInstance, propertiesWithMethods); + + var copyInstance = creator.create(ThrowableInfo.of(type, originalProperties)); + copyThrowableAttributesIfNecessary(originalInstance, copyInstance); + var propertiesOfCopy = retrievePropertyValues(copyInstance, propertiesWithMethods); + + var propertiesEqual = deepEquals(propertiesOfCopy, originalProperties); + var toStringEqual = copyInstance.toString().equals(originalInstance.toString()); + var everythingOk = propertiesEqual && toStringEqual; + if (!everythingOk) + return false; + } catch (@SuppressWarnings("unused") Exception e) { + return false; + } + return true; + } + + private static void copyThrowableAttributesIfNecessary(Throwable from, Throwable to) { + if (to.getCause() != from.getCause()) + to.initCause(from.getCause()); + to.setStackTrace(from.getStackTrace()); + } + + @SafeVarargs + static Entry, List> typeEntry(Class clazz, T... instances) { + return Map.entry(clazz, List.of(instances)); + } + + static boolean deepEquals(Map m1, Map m2) { + if (!m1.keySet().equals(m2.keySet())) + return false; + for (var entry : m1.entrySet()) { + var key = entry.getKey(); + var v1 = entry.getValue(); + var v2 = m2.get(key); + if (v1 == v2) + continue; + if (v1 == null || v2 == null) + return false; + if (v1.getClass().isArray() && v2.getClass().isArray() && Arrays.equals((Object[]) v1, (Object[]) v2)) + continue; + if (!v1.equals(v2)) + return false; + } + return true; + } +} diff --git a/src/test/java/de/tum/in/testuser/ExceptionFailureUser.java b/src/test/java/de/tum/in/testuser/ExceptionFailureUser.java index 9d07157e..941b8f47 100644 --- a/src/test/java/de/tum/in/testuser/ExceptionFailureUser.java +++ b/src/test/java/de/tum/in/testuser/ExceptionFailureUser.java @@ -51,7 +51,7 @@ public FaultyToStringException() { } @Override - public String toString() { + public String getMessage() { throw new IllegalStateException("Faulty"); } } From 2a498c0e779c942adf240ee5f645f62aa355aaba Mon Sep 17 00:00:00 2001 From: Christian Femers Date: Fri, 19 Mar 2021 16:28:26 +0100 Subject: [PATCH 4/8] Test with full release in CI --- .github/workflows/maven.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 7eec1398..dd032184 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [11, 12, 13, 14, 15, 16-ea] + java: [11, 12, 13, 14, 15, 16] name: Java ${{ matrix.java }} Run steps: - uses: actions/checkout@v2 From c1357fc8596423f79c0fc05bf62d46ac3fccfa27 Mon Sep 17 00:00:00 2001 From: Christian Femers Date: Fri, 19 Mar 2021 16:37:35 +0100 Subject: [PATCH 5/8] Ignore java.awt.HeadlessException because CI system has problems with it --- .../api/internal/sanitization/ThrowableUtilsTest.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/test/java/de/tum/in/test/api/internal/sanitization/ThrowableUtilsTest.java b/src/test/java/de/tum/in/test/api/internal/sanitization/ThrowableUtilsTest.java index c0f75c44..227da6e0 100644 --- a/src/test/java/de/tum/in/test/api/internal/sanitization/ThrowableUtilsTest.java +++ b/src/test/java/de/tum/in/test/api/internal/sanitization/ThrowableUtilsTest.java @@ -41,8 +41,11 @@ class ThrowableUtilsTest { typeEntry(Method.class, ReflectionSupport.findMethod(String.class, "length").orElseThrow()) // ); - private static final Set FALSE_POSITIVES = Set.of("junit.framework.AssertionFailedError", - "java.util.IllformedLocaleException"); + private static final Set FALSE_POSITIVES = Set.of( // + "junit.framework.AssertionFailedError", // deviation for null message is not an issue + "java.util.IllformedLocaleException", // we don't need the index attribute itself + "java.awt.HeadlessException" // CI systems don't like this, but the constructor works + ); private static final Set> SAFE_PROPERTY_TYPES = Set.of(Throwable.class, Throwable[].class); @@ -61,7 +64,7 @@ void testConstructorInstantiation() { return !validate(type, preferredConstructor); }).sorted(Comparator.comparing(Object::toString)).collect(Collectors.toList()); assertThat(duplicationFailures) - .as("the detault Throwable duplication works for all SAFE_TYPE classes that are not specially handeled") + .as("the default Throwable duplication works for all SAFE_TYPE classes that are not specially handeled") .isEmpty(); } From a572eaa4422c3482851bd3c23fa66e69906f22cf Mon Sep 17 00:00:00 2001 From: Christian Femers Date: Fri, 19 Mar 2021 16:43:54 +0100 Subject: [PATCH 6/8] Reorder imports --- .../internal/sanitization/SafeTypeThrowableSanitizer.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/SafeTypeThrowableSanitizer.java b/src/main/java/de/tum/in/test/api/internal/sanitization/SafeTypeThrowableSanitizer.java index 1176b738..1fa04b45 100644 --- a/src/main/java/de/tum/in/test/api/internal/sanitization/SafeTypeThrowableSanitizer.java +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/SafeTypeThrowableSanitizer.java @@ -11,6 +11,9 @@ import java.util.Set; import java.util.function.Supplier; +import junit.framework.ComparisonCompactor; +import junit.framework.ComparisonFailure; + import org.junit.experimental.theories.internal.ParameterizedAssertionError; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,9 +21,6 @@ import de.tum.in.test.api.internal.sanitization.ThrowableInfo.ProperyKey; import de.tum.in.test.api.util.LruCache; -import junit.framework.ComparisonCompactor; -import junit.framework.ComparisonFailure; - enum SafeTypeThrowableSanitizer implements SpecificThrowableSanitizer { INSTANCE; From 96221d202b65b64da8ce2faf2e12c6afc8bac610 Mon Sep 17 00:00:00 2001 From: Christian Femers Date: Fri, 19 Mar 2021 16:45:24 +0100 Subject: [PATCH 7/8] Update settings --- .project | 6 -- .settings/org.eclipse.jdt.core.prefs | 17 +++- .settings/org.eclipse.jdt.ui.prefs | 121 ++++++++++++++++++++++++++- 3 files changed, 134 insertions(+), 10 deletions(-) diff --git a/.project b/.project index 36a346f9..43903a72 100644 --- a/.project +++ b/.project @@ -15,15 +15,9 @@ - - net.sourceforge.pmd.eclipse.plugin.pmdBuilder - - - org.eclipse.jdt.core.javanature org.eclipse.m2e.core.maven2Nature - net.sourceforge.pmd.eclipse.plugin.pmdNature diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs index 03a32cea..f3e62197 100644 --- a/.settings/org.eclipse.jdt.core.prefs +++ b/.settings/org.eclipse.jdt.core.prefs @@ -1,10 +1,13 @@ eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled org.eclipse.jdt.core.compiler.codegen.targetPlatform=11 org.eclipse.jdt.core.compiler.compliance=11 +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning -org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=ignore -org.eclipse.jdt.core.compiler.release=disabled +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled org.eclipse.jdt.core.compiler.source=11 org.eclipse.jdt.core.formatter.align_assignment_statements_on_columns=false org.eclipse.jdt.core.formatter.align_fields_grouping_blank_lines=2147483647 @@ -12,12 +15,20 @@ org.eclipse.jdt.core.formatter.align_type_members_on_columns=false org.eclipse.jdt.core.formatter.align_variable_declarations_on_columns=false org.eclipse.jdt.core.formatter.align_with_spaces=false org.eclipse.jdt.core.formatter.alignment_for_additive_operator=16 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_enum_constant=49 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_field=49 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_local_variable=49 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_method=49 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_package=49 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_parameter=0 +org.eclipse.jdt.core.formatter.alignment_for_annotations_on_type=49 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=0 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16 +org.eclipse.jdt.core.formatter.alignment_for_assertion_message=0 org.eclipse.jdt.core.formatter.alignment_for_assignment=0 org.eclipse.jdt.core.formatter.alignment_for_bitwise_operator=16 org.eclipse.jdt.core.formatter.alignment_for_compact_if=16 @@ -47,6 +58,7 @@ org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_record_declarati org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16 org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16 +org.eclipse.jdt.core.formatter.alignment_for_type_annotations=0 org.eclipse.jdt.core.formatter.alignment_for_type_arguments=0 org.eclipse.jdt.core.formatter.alignment_for_type_parameters=0 org.eclipse.jdt.core.formatter.alignment_for_union_type_in_multicatch=16 @@ -371,6 +383,7 @@ org.eclipse.jdt.core.formatter.text_block_indentation=0 org.eclipse.jdt.core.formatter.use_on_off_tags=true org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false org.eclipse.jdt.core.formatter.wrap_before_additive_operator=true +org.eclipse.jdt.core.formatter.wrap_before_assertion_message_operator=true org.eclipse.jdt.core.formatter.wrap_before_assignment_operator=false org.eclipse.jdt.core.formatter.wrap_before_bitwise_operator=true org.eclipse.jdt.core.formatter.wrap_before_conditional_operator=true diff --git a/.settings/org.eclipse.jdt.ui.prefs b/.settings/org.eclipse.jdt.ui.prefs index 98218ff7..0c3fcb7b 100644 --- a/.settings/org.eclipse.jdt.ui.prefs +++ b/.settings/org.eclipse.jdt.ui.prefs @@ -1,9 +1,126 @@ +cleanup.add_all=true +cleanup.add_default_serial_version_id=false +cleanup.add_generated_serial_version_id=true +cleanup.add_missing_annotations=true +cleanup.add_missing_deprecated_annotations=true +cleanup.add_missing_methods=false +cleanup.add_missing_nls_tags=false +cleanup.add_missing_override_annotations=true +cleanup.add_missing_override_annotations_interface_methods=true +cleanup.add_serial_version_id=true +cleanup.always_use_blocks=false +cleanup.always_use_parentheses_in_expressions=true +cleanup.always_use_this_for_non_static_field_access=false +cleanup.always_use_this_for_non_static_method_access=false +cleanup.arrays_fill=true +cleanup.bitwise_conditional_expression=true +cleanup.boolean_literal=true +cleanup.break_loop=true +cleanup.collection_cloning=true +cleanup.comparing_on_criteria=true +cleanup.comparison_statement=true +cleanup.controlflow_merge=true +cleanup.convert_functional_interfaces=true +cleanup.convert_to_enhanced_for_loop=true +cleanup.convert_to_enhanced_for_loop_if_loop_var_used=true +cleanup.convert_to_switch_expressions=false +cleanup.correct_indentation=true +cleanup.double_negation=true +cleanup.else_if=true +cleanup.embedded_if=true +cleanup.evaluate_nullable=true +cleanup.extract_increment=false +cleanup.format_source_code=true +cleanup.format_source_code_changes_only=false +cleanup.hash=true +cleanup.if_condition=true +cleanup.insert_inferred_type_arguments=false +cleanup.instanceof=false +cleanup.invert_equals=true +cleanup.join=true +cleanup.lazy_logical_operator=true +cleanup.make_local_variable_final=false +cleanup.make_parameters_final=false +cleanup.make_private_fields_final=true +cleanup.make_type_abstract_if_missing_method=false +cleanup.make_variable_declarations_final=true +cleanup.map_cloning=true +cleanup.merge_conditional_blocks=true +cleanup.multi_catch=true +cleanup.never_use_blocks=true +cleanup.never_use_parentheses_in_expressions=false +cleanup.no_string_creation=true +cleanup.no_super=true +cleanup.number_suffix=true +cleanup.objects_equals=true +cleanup.organize_imports=true +cleanup.overridden_assignment=true +cleanup.plain_replacement=true +cleanup.precompile_regex=true +cleanup.primitive_comparison=true +cleanup.primitive_parsing=true +cleanup.primitive_serialization=true +cleanup.pull_up_assignment=true +cleanup.push_down_negation=true +cleanup.qualify_static_field_accesses_with_declaring_class=false +cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true +cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true +cleanup.qualify_static_member_accesses_with_declaring_class=true +cleanup.qualify_static_method_accesses_with_declaring_class=false +cleanup.reduce_indentation=true +cleanup.redundant_falling_through_block_end=true +cleanup.remove_private_constructors=true +cleanup.remove_redundant_modifiers=true +cleanup.remove_redundant_semicolons=true +cleanup.remove_redundant_type_arguments=true +cleanup.remove_trailing_whitespaces=true +cleanup.remove_trailing_whitespaces_all=true +cleanup.remove_trailing_whitespaces_ignore_empty=false +cleanup.remove_unnecessary_array_creation=true +cleanup.remove_unnecessary_casts=true +cleanup.remove_unnecessary_nls_tags=true +cleanup.remove_unused_imports=true +cleanup.remove_unused_local_variables=true +cleanup.remove_unused_private_fields=true +cleanup.remove_unused_private_members=false +cleanup.remove_unused_private_methods=true +cleanup.remove_unused_private_types=true +cleanup.simplify_lambda_expression_and_method_ref=true +cleanup.single_used_field=true +cleanup.sort_members=false +cleanup.sort_members_all=false +cleanup.standard_comparison=false +cleanup.static_inner_class=true +cleanup.strictly_equal_or_different=true +cleanup.stringbuilder=true +cleanup.substring=true +cleanup.switch=true +cleanup.ternary_operator=true +cleanup.try_with_resource=true +cleanup.unlooped_while=true +cleanup.unreachable_block=true +cleanup.use_anonymous_class_creation=false +cleanup.use_autoboxing=true +cleanup.use_blocks=true +cleanup.use_blocks_only_for_return_and_throw=false +cleanup.use_directly_map_method=true +cleanup.use_lambda=true +cleanup.use_parentheses_in_expressions=false +cleanup.use_this_for_non_static_field_access=true +cleanup.use_this_for_non_static_field_access_only_if_necessary=true +cleanup.use_this_for_non_static_method_access=true +cleanup.use_this_for_non_static_method_access_only_if_necessary=true +cleanup.use_unboxing=true +cleanup.use_var=false +cleanup.useless_continue=true +cleanup.useless_return=true +cleanup_profile=_Basic cleanup_settings_version=2 eclipse.preferences.version=1 formatter_profile=_Eclipse adjusted -formatter_settings_version=19 +formatter_settings_version=21 org.eclipse.jdt.ui.ignorelowercasenames=true -org.eclipse.jdt.ui.importorder=java;javax;org;com;net;ch;info;de.tum; +org.eclipse.jdt.ui.importorder=java;javax;junit;org;com;net;ch;info;de.tum; org.eclipse.jdt.ui.ondemandthreshold=99 org.eclipse.jdt.ui.staticondemandthreshold=2 org.eclipse.jdt.ui.text.custom_code_templates= From 434ef7f9721cbe8de9f6b3e22a96c073df64ba49 Mon Sep 17 00:00:00 2001 From: Christian Femers Date: Fri, 19 Mar 2021 16:52:48 +0100 Subject: [PATCH 8/8] Small style improvements --- .../in/test/api/internal/sanitization/MessageTransformer.java | 2 +- .../sanitization/MultipleAssertionsErrorSanitizer.java | 4 ++-- .../in/test/api/internal/sanitization/ThrowableCreator.java | 2 +- .../tum/in/test/api/internal/sanitization/ThrowableInfo.java | 2 +- .../de/tum/in/test/api/security/FixSystemErrAppender.java | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/MessageTransformer.java b/src/main/java/de/tum/in/test/api/internal/sanitization/MessageTransformer.java index cbaec3a6..59ebbc2a 100644 --- a/src/main/java/de/tum/in/test/api/internal/sanitization/MessageTransformer.java +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/MessageTransformer.java @@ -10,4 +10,4 @@ public interface MessageTransformer { MessageTransformer IDENTITY = ThrowableInfo::getMessage; String apply(ThrowableInfo throwableInfo); -} \ No newline at end of file +} diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/MultipleAssertionsErrorSanitizer.java b/src/main/java/de/tum/in/test/api/internal/sanitization/MultipleAssertionsErrorSanitizer.java index 4e0a8b60..61c753f9 100644 --- a/src/main/java/de/tum/in/test/api/internal/sanitization/MultipleAssertionsErrorSanitizer.java +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/MultipleAssertionsErrorSanitizer.java @@ -36,8 +36,8 @@ public Throwable sanitize(Throwable t, MessageTransformer messageTransformer) { description = start.substring(1, start.length() - 2); } /* - * note that this will only affect the description, not the whole message (this - * is not possible) + * Note that this will only affect the description, not the whole message (this + * is not possible). */ info.setMessage(description); description = messageTransformer.apply(info); diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableCreator.java b/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableCreator.java index bc89356b..7daa8428 100644 --- a/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableCreator.java +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableCreator.java @@ -4,4 +4,4 @@ interface ThrowableCreator { Throwable create(ThrowableInfo throwableInfo); -} \ No newline at end of file +} diff --git a/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableInfo.java b/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableInfo.java index 0a30505f..0921ec26 100644 --- a/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableInfo.java +++ b/src/main/java/de/tum/in/test/api/internal/sanitization/ThrowableInfo.java @@ -232,4 +232,4 @@ final T cast(Object value) { return type.cast(value); } } -} \ No newline at end of file +} diff --git a/src/main/java/de/tum/in/test/api/security/FixSystemErrAppender.java b/src/main/java/de/tum/in/test/api/security/FixSystemErrAppender.java index 4d800692..95ca7cd8 100644 --- a/src/main/java/de/tum/in/test/api/security/FixSystemErrAppender.java +++ b/src/main/java/de/tum/in/test/api/security/FixSystemErrAppender.java @@ -15,4 +15,4 @@ public class FixSystemErrAppender extends OutputStreamAppender { public FixSystemErrAppender() { setOutputStream(SecurityConstants.SYSTEM_ERR); } -} \ No newline at end of file +}