From e032d0ea6532a47a4de8a290fb01416e5739294c Mon Sep 17 00:00:00 2001 From: Christian Femers Date: Thu, 24 Mar 2022 17:55:00 +0100 Subject: [PATCH] Rework Ares IOTester registration, make custom implementation possible This fixes #105 and should now be a completely satisfactory solution. We could now also add a system property to set the default IOManager on startup, if requested. --- .../java/de/tum/in/test/api/MirrorOutput.java | 15 ++- .../de/tum/in/test/api/WithIOManager.java | 71 ++++++++++++ .../tum/in/test/api/context/AresContext.java | 26 +++++ .../{internal => context}/TestContext.java | 9 +- .../TestContextUtils.java | 4 +- .../api/{internal => context}/TestType.java | 6 +- .../test/api/internal/ConfigurationUtils.java | 2 + .../test/api/internal/IOExtensionUtils.java | 87 +++++++++++++++ .../in/test/api/internal/IOTesterManager.java | 40 ------- .../in/test/api/internal/ReportingUtils.java | 1 + .../in/test/api/internal/TestGuardUtils.java | 2 + .../in/test/api/internal/TimeoutUtils.java | 2 + .../de/tum/in/test/api/io/AresIOContext.java | 48 +++++++++ .../java/de/tum/in/test/api/io/IOManager.java | 78 ++++++++++++++ .../tum/in/test/api/io/IOTesterManager.java | 31 ++++++ .../java/de/tum/in/test/api/jqwik/Hidden.java | 2 +- .../tum/in/test/api/jqwik/JqwikAresTest.java | 2 +- .../tum/in/test/api/jqwik/JqwikContext.java | 6 +- .../in/test/api/jqwik/JqwikIOExtension.java | 38 ++++--- .../tum/in/test/api/jqwik/JqwikTestGuard.java | 2 +- .../java/de/tum/in/test/api/jqwik/Public.java | 2 +- .../de/tum/in/test/api/jupiter/Hidden.java | 2 +- .../tum/in/test/api/jupiter/HiddenTest.java | 2 +- .../in/test/api/jupiter/JupiterAresTest.java | 2 +- .../in/test/api/jupiter/JupiterContext.java | 6 +- .../test/api/jupiter/JupiterIOExtension.java | 15 +-- .../de/tum/in/test/api/jupiter/Public.java | 2 +- .../tum/in/test/api/jupiter/PublicTest.java | 2 +- .../AresSecurityConfigurationBuilder.java | 2 +- .../AresSecurityConfigurationTest.java | 4 +- .../in/test/integration/InputOutputTest.java | 22 ++++ .../integration/testuser/InputOutputUser.java | 101 ++++++++++++++++-- 32 files changed, 533 insertions(+), 101 deletions(-) create mode 100644 src/main/java/de/tum/in/test/api/WithIOManager.java create mode 100644 src/main/java/de/tum/in/test/api/context/AresContext.java rename src/main/java/de/tum/in/test/api/{internal => context}/TestContext.java (70%) rename src/main/java/de/tum/in/test/api/{internal => context}/TestContextUtils.java (95%) rename src/main/java/de/tum/in/test/api/{internal => context}/TestType.java (79%) create mode 100644 src/main/java/de/tum/in/test/api/internal/IOExtensionUtils.java delete mode 100644 src/main/java/de/tum/in/test/api/internal/IOTesterManager.java create mode 100644 src/main/java/de/tum/in/test/api/io/AresIOContext.java create mode 100644 src/main/java/de/tum/in/test/api/io/IOManager.java create mode 100644 src/main/java/de/tum/in/test/api/io/IOTesterManager.java diff --git a/src/main/java/de/tum/in/test/api/MirrorOutput.java b/src/main/java/de/tum/in/test/api/MirrorOutput.java index 9267f2b2..14d55deb 100644 --- a/src/main/java/de/tum/in/test/api/MirrorOutput.java +++ b/src/main/java/de/tum/in/test/api/MirrorOutput.java @@ -11,13 +11,15 @@ import org.apiguardian.api.API; import org.apiguardian.api.API.Status; +import de.tum.in.test.api.io.IOManager; import de.tum.in.test.api.io.IOTester; /** * This annotation can be applied to a class or method and tells the - * {@link IOTester}, whether to pipe the output to the original standard output - * as well (mirroring everything received). It does not affect the test result - * (unless the standard output throws an exception). + * {@link IOTester} (or an alternative implementation provided by + * {@link IOManager}), whether to pipe the output to the original standard + * output as well (mirroring everything received). It does not affect the test + * result (unless the standard output throws an exception). *

* A {@link MirrorOutput} annotation on a method always overrides the one on the * class level. @@ -27,7 +29,8 @@ * * @author Christian Femers * @since 0.1.0 - * @version 1.2.0 + * @version 1.2.1 + * @see IOManager */ @API(status = Status.STABLE) @Inherited @@ -38,6 +41,10 @@ long DEFAULT_MAX_STD_OUT = 100_000_000L; + /** + * If the output should be mirrored and printed to the original standard output + * in addition to being recorded. + */ MirrorOutputPolicy value() default MirrorOutputPolicy.ENABLED; /** diff --git a/src/main/java/de/tum/in/test/api/WithIOManager.java b/src/main/java/de/tum/in/test/api/WithIOManager.java new file mode 100644 index 00000000..53d86640 --- /dev/null +++ b/src/main/java/de/tum/in/test/api/WithIOManager.java @@ -0,0 +1,71 @@ +package de.tum.in.test.api; + +import static java.lang.annotation.ElementType.*; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +import de.tum.in.test.api.io.AresIOContext; +import de.tum.in.test.api.io.IOManager; +import de.tum.in.test.api.io.IOTester; + +/** + * Allows to overwrite the default IO test implementation of Ares with is using + * {@link IOTester}. + *

+ * A custom {@link IOManager} class must have a constructor that takes no + * arguments and be accessible to Ares. All classes used for that purpose should + * be trusted/whitelisted. + * + * @author Christian Femers + * @since 1.9.1 + * @version 1.0.0 + * @see IOManager + */ +@API(status = Status.EXPERIMENTAL) +@Inherited +@Documented +@Retention(RUNTIME) +@Target({ TYPE, METHOD, ANNOTATION_TYPE }) +public @interface WithIOManager { + + /** + * The {@link IOManager} implementation to use for testing in the annotated + * element. + */ + Class> value(); + + /** + * Effectively no {@link IOManager}. {@link System#out}, {@link System#err} and + * {@link System#in} are unchanged. Not recommended. Consider a custom but + * functional {@link IOManager} implementation first. + */ + public final class None implements IOManager { + + @Override + public void beforeTestExecution(AresIOContext context) { + // do nothing + } + + @Override + public void afterTestExecution(AresIOContext context) { + // do nothing + } + + @Override + public Void getControllerInstance(AresIOContext context) { + return null; + } + + @Override + public Class getControllerClass() { + return null; + } + } +} diff --git a/src/main/java/de/tum/in/test/api/context/AresContext.java b/src/main/java/de/tum/in/test/api/context/AresContext.java new file mode 100644 index 00000000..825d8b43 --- /dev/null +++ b/src/main/java/de/tum/in/test/api/context/AresContext.java @@ -0,0 +1,26 @@ +package de.tum.in.test.api.context; + +import java.util.Objects; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +@API(status = Status.INTERNAL) +public abstract class AresContext { + + private final TestContext testContext; + + protected AresContext(TestContext testContext) { + this.testContext = Objects.requireNonNull(testContext); + } + + /** + * Returns the current test context. + * + * @return the {@link TestContext}, never null. + * @author Christian Femers + */ + public final TestContext testContext() { + return testContext; + } +} diff --git a/src/main/java/de/tum/in/test/api/internal/TestContext.java b/src/main/java/de/tum/in/test/api/context/TestContext.java similarity index 70% rename from src/main/java/de/tum/in/test/api/internal/TestContext.java rename to src/main/java/de/tum/in/test/api/context/TestContext.java index 1b7c3e61..e395126f 100644 --- a/src/main/java/de/tum/in/test/api/internal/TestContext.java +++ b/src/main/java/de/tum/in/test/api/context/TestContext.java @@ -1,4 +1,4 @@ -package de.tum.in.test.api.internal; +package de.tum.in.test.api.context; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Method; @@ -10,10 +10,15 @@ /** * Adapter for different JUnit 5 test runner contexts. This interface provides * the common properties of a test context. + *

+ * All properties are optional as different test types and phases in the test + * life cycle of different test engines have different execution environments. + * While you can expect most properties to be present most of the time, use them + * defensively. * * @author Christian Femers */ -@API(status = Status.INTERNAL) +@API(status = Status.MAINTAINED) public abstract class TestContext { public abstract Optional testMethod(); diff --git a/src/main/java/de/tum/in/test/api/internal/TestContextUtils.java b/src/main/java/de/tum/in/test/api/context/TestContextUtils.java similarity index 95% rename from src/main/java/de/tum/in/test/api/internal/TestContextUtils.java rename to src/main/java/de/tum/in/test/api/context/TestContextUtils.java index 3ad77060..92fc2c01 100644 --- a/src/main/java/de/tum/in/test/api/internal/TestContextUtils.java +++ b/src/main/java/de/tum/in/test/api/context/TestContextUtils.java @@ -1,4 +1,4 @@ -package de.tum.in.test.api.internal; +package de.tum.in.test.api.context; import static org.junit.platform.commons.support.AnnotationSupport.*; @@ -19,7 +19,7 @@ * * @author Christian Femers */ -@API(status = Status.INTERNAL) +@API(status = Status.MAINTAINED) public final class TestContextUtils { private TestContextUtils() { diff --git a/src/main/java/de/tum/in/test/api/internal/TestType.java b/src/main/java/de/tum/in/test/api/context/TestType.java similarity index 79% rename from src/main/java/de/tum/in/test/api/internal/TestType.java rename to src/main/java/de/tum/in/test/api/context/TestType.java index 1dd94f26..12553ea3 100644 --- a/src/main/java/de/tum/in/test/api/internal/TestType.java +++ b/src/main/java/de/tum/in/test/api/context/TestType.java @@ -1,4 +1,4 @@ -package de.tum.in.test.api.internal; +package de.tum.in.test.api.context; import org.apiguardian.api.API; import org.apiguardian.api.API.Status; @@ -17,9 +17,9 @@ * @see PublicTest * @author Christian Femers * @since 0.2.0 - * @version 1.0.1 + * @version 1.1.0 */ -@API(status = Status.INTERNAL) +@API(status = Status.MAINTAINED) public enum TestType { PUBLIC, HIDDEN diff --git a/src/main/java/de/tum/in/test/api/internal/ConfigurationUtils.java b/src/main/java/de/tum/in/test/api/internal/ConfigurationUtils.java index 3864d24d..94aacf78 100644 --- a/src/main/java/de/tum/in/test/api/internal/ConfigurationUtils.java +++ b/src/main/java/de/tum/in/test/api/internal/ConfigurationUtils.java @@ -26,6 +26,8 @@ import de.tum.in.test.api.WhitelistClass; import de.tum.in.test.api.WhitelistPackage; import de.tum.in.test.api.WhitelistPath; +import de.tum.in.test.api.context.TestContext; +import de.tum.in.test.api.context.TestContextUtils; import de.tum.in.test.api.security.AresSecurityConfiguration; import de.tum.in.test.api.security.AresSecurityConfigurationBuilder; import de.tum.in.test.api.util.PackageRule; diff --git a/src/main/java/de/tum/in/test/api/internal/IOExtensionUtils.java b/src/main/java/de/tum/in/test/api/internal/IOExtensionUtils.java new file mode 100644 index 00000000..2be9efee --- /dev/null +++ b/src/main/java/de/tum/in/test/api/internal/IOExtensionUtils.java @@ -0,0 +1,87 @@ +package de.tum.in.test.api.internal; + +import static java.lang.invoke.MethodType.methodType; + +import java.lang.annotation.AnnotationFormatError; +import java.lang.invoke.LambdaMetafactory; +import java.lang.invoke.MethodHandles; +import java.util.HashMap; +import java.util.Objects; +import java.util.function.Supplier; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +import de.tum.in.test.api.WithIOManager; +import de.tum.in.test.api.context.TestContext; +import de.tum.in.test.api.context.TestContextUtils; +import de.tum.in.test.api.io.AresIOContext; +import de.tum.in.test.api.io.IOManager; +import de.tum.in.test.api.io.IOTesterManager; +import de.tum.in.test.api.security.ArtemisSecurityManager; + +@API(status = Status.INTERNAL) +public final class IOExtensionUtils { + + private static final Class DEFAULT_IO_MANAGER = IOTesterManager.class; + + static { + /* + * Initialize SecurityManager when we are still in the main thread + */ + ArtemisSecurityManager.isInstalled(); + } + + private static final HashMap>, Supplier>> ioManagerCache = new HashMap<>(); + + private final AresIOContext context; + private final IOManager ioManager; + private final Class controllerClass; + + public IOExtensionUtils(TestContext testContext) { + context = AresIOContext.from(testContext); + ioManager = createIOManagerFor(testContext); + controllerClass = ioManager.getControllerClass(); + } + + public void beforeTestExecution() { + ioManager.beforeTestExecution(context); + } + + public void afterTestExecution() { + ioManager.afterTestExecution(context); + } + + public boolean providesController() { + return controllerClass != null; + } + + public Object getControllerInstance() { + return providesController() ? ioManager.getControllerInstance(context) : null; + } + + public boolean canProvideControllerFor(Class targetType) { + return providesController() && Objects.class != targetType && targetType.isAssignableFrom(controllerClass); + } + + private static IOManager createIOManagerFor(TestContext testContext) { + var ioManagerClass = TestContextUtils.findAnnotationIn(testContext, WithIOManager.class) + .>>map(WithIOManager::value).orElse(DEFAULT_IO_MANAGER); + return ioManagerCache.computeIfAbsent(ioManagerClass, IOExtensionUtils::generateIOManagerSupplier).get(); + } + + private static Supplier> generateIOManagerSupplier(Class> ioManagerClass) { + try { + var lookup = MethodHandles.lookup(); + var contructor = lookup.findConstructor(ioManagerClass, methodType(void.class)); + var factory = LambdaMetafactory + .metafactory(lookup, "get", methodType(Supplier.class), contructor.type().generic(), contructor, //$NON-NLS-1$ + contructor.type()) + .getTarget(); + return (Supplier>) factory.invokeExact(); + } catch (Throwable e) { + throw new AnnotationFormatError("Could not create IOManager Supplier for type " //$NON-NLS-1$ + + ioManagerClass.getCanonicalName() + ". Make sure a public no-args constructor is available.", e); //$NON-NLS-1$ + } + } +} diff --git a/src/main/java/de/tum/in/test/api/internal/IOTesterManager.java b/src/main/java/de/tum/in/test/api/internal/IOTesterManager.java deleted file mode 100644 index 71b194d7..00000000 --- a/src/main/java/de/tum/in/test/api/internal/IOTesterManager.java +++ /dev/null @@ -1,40 +0,0 @@ -package de.tum.in.test.api.internal; - -import org.apiguardian.api.API; -import org.apiguardian.api.API.Status; - -import de.tum.in.test.api.io.IOTester; -import de.tum.in.test.api.security.ArtemisSecurityManager; - -@API(status = Status.INTERNAL) -public final class IOTesterManager { - - static { - /* - * Initialize SecurityManager when we are still in the main thread - */ - ArtemisSecurityManager.isInstalled(); - } - - private final TestContext context; - private IOTester ioTester; - - public IOTesterManager(TestContext context) { - this.context = context; - } - - public void beforeTestExecution() { - boolean mirrorOutput = ConfigurationUtils.shouldMirrorOutput(context); - long maxStdOut = ConfigurationUtils.getMaxStandardOutput(context); - ioTester = IOTester.installNew(mirrorOutput, maxStdOut); - } - - public void afterTestExecution() { - IOTester.uninstallCurrent(); - ioTester = null; - } - - public IOTester getIOTester() { - return ioTester; - } -} 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 3e432d9a..ed890832 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 @@ -11,6 +11,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import de.tum.in.test.api.context.TestContext; 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; diff --git a/src/main/java/de/tum/in/test/api/internal/TestGuardUtils.java b/src/main/java/de/tum/in/test/api/internal/TestGuardUtils.java index 05b2b0e7..98bcd232 100644 --- a/src/main/java/de/tum/in/test/api/internal/TestGuardUtils.java +++ b/src/main/java/de/tum/in/test/api/internal/TestGuardUtils.java @@ -24,6 +24,8 @@ import de.tum.in.test.api.ActivateHiddenBefore; import de.tum.in.test.api.Deadline; import de.tum.in.test.api.ExtendedDeadline; +import de.tum.in.test.api.context.TestContext; +import de.tum.in.test.api.context.TestType; /** * This class handles public/hidden tests and deadline evaluation. diff --git a/src/main/java/de/tum/in/test/api/internal/TimeoutUtils.java b/src/main/java/de/tum/in/test/api/internal/TimeoutUtils.java index cfd25365..6f57e968 100644 --- a/src/main/java/de/tum/in/test/api/internal/TimeoutUtils.java +++ b/src/main/java/de/tum/in/test/api/internal/TimeoutUtils.java @@ -23,6 +23,8 @@ import de.tum.in.test.api.PrivilegedExceptionsOnly; import de.tum.in.test.api.StrictTimeout; +import de.tum.in.test.api.context.TestContext; +import de.tum.in.test.api.context.TestContextUtils; import de.tum.in.test.api.security.ArtemisSecurityManager; @API(status = Status.INTERNAL) diff --git a/src/main/java/de/tum/in/test/api/io/AresIOContext.java b/src/main/java/de/tum/in/test/api/io/AresIOContext.java new file mode 100644 index 00000000..d54c51e4 --- /dev/null +++ b/src/main/java/de/tum/in/test/api/io/AresIOContext.java @@ -0,0 +1,48 @@ +package de.tum.in.test.api.io; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +import de.tum.in.test.api.MirrorOutput; +import de.tum.in.test.api.context.AresContext; +import de.tum.in.test.api.context.TestContext; +import de.tum.in.test.api.internal.ConfigurationUtils; + +@API(status = Status.EXPERIMENTAL) +public final class AresIOContext extends AresContext { + + private final boolean mirrorOutput; + private final long maxStdOut; + + private AresIOContext(TestContext testContext, boolean mirrorOutput, long maxStdOut) { + super(testContext); + this.mirrorOutput = mirrorOutput; + this.maxStdOut = maxStdOut; + } + + /** + * Returns true if the user requested to mirror recorded output to the console. + * + * @return the mirror output value. + * @see MirrorOutput#value() + */ + public boolean mirrorOutput() { + return mirrorOutput; + } + + /** + * Returns the maximal number of chars that the test should allow to be printed. + * + * @return the maximal number of chars. + * @see MirrorOutput#maxCharCount() + */ + public long maxStdOut() { + return maxStdOut; + } + + public static AresIOContext from(TestContext testContext) { + boolean mirrorOutput = ConfigurationUtils.shouldMirrorOutput(testContext); + long maxStdOut = ConfigurationUtils.getMaxStandardOutput(testContext); + return new AresIOContext(testContext, mirrorOutput, maxStdOut); + } +} diff --git a/src/main/java/de/tum/in/test/api/io/IOManager.java b/src/main/java/de/tum/in/test/api/io/IOManager.java new file mode 100644 index 00000000..25e5aa27 --- /dev/null +++ b/src/main/java/de/tum/in/test/api/io/IOManager.java @@ -0,0 +1,78 @@ +package de.tum.in.test.api.io; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; + +import net.jqwik.api.lifecycle.AroundPropertyHook; + +import de.tum.in.test.api.WithIOManager; + +/** + * Manages how IO testing is performed in the Ares test extension for IO. + *

+ * Ares does not make any guarantee whether instances are reused across + * different tests or if a new manager instance is created for each test. + *

+ * Implementations are highly encouraged to respect/reuse the user settings in + * the given {@link AresIOContext}, if feasible. + * + * @param the type of the controller object, that is an object that can be + * used by testers to control IO testing inside the test method. See + * e.g. {@link IOTester} + * @author Christian Femers + * @implSpec Implementations that are used in {@link WithIOManager} must provide + * a public default constructor with no arguments. + */ +@API(status = Status.EXPERIMENTAL) +public interface IOManager { + + /** + * Invoked before each test is executed. + * + * @param context the current Ares IO context + * @see BeforeEachCallback + * @see AroundPropertyHook + */ + void beforeTestExecution(AresIOContext context); + + /** + * Invoked each the test is executed. + * + * @param context the current Ares IO context + * @see AfterEachCallback + * @see AroundPropertyHook + */ + void afterTestExecution(AresIOContext context); + + /** + * Provides an instance of an object to control IO testing that is available as + * parameter in the test method. + * + * @param context the current Ares IO context + * @return a tester instance. This should only be null if + * {@link #getControllerClass()} returns null as well or no test is + * currently running. May be a subclass of + * {@link #getControllerClass()}. + * @implSpec This may return different instances each time, but as they are e.g. + * passed as parameter to the test method, they must remain valid for + * the whole life cycle of that test (in between + * {@link #beforeTestExecution(AresIOContext) beforeTest} and + * {@link #afterTestExecution(AresIOContext) afterTest}). + */ + T getControllerInstance(AresIOContext context); + + /** + * The class of the type provided by + * {@link #getControllerInstance(AresIOContext)} such that Ares can register a + * parameter provider. + * + * @return the tester class, or null if no such controller object exists. This + * must not be {@link Object}. + * @implSpec this should be stateless, if different implementations are needed + * in certain situations, introduce an appropriate generalization in + * form of a common super class/interface or use composition + */ + Class getControllerClass(); +} diff --git a/src/main/java/de/tum/in/test/api/io/IOTesterManager.java b/src/main/java/de/tum/in/test/api/io/IOTesterManager.java new file mode 100644 index 00000000..15a892a3 --- /dev/null +++ b/src/main/java/de/tum/in/test/api/io/IOTesterManager.java @@ -0,0 +1,31 @@ +package de.tum.in.test.api.io; + +import org.apiguardian.api.API; +import org.apiguardian.api.API.Status; + +@API(status = Status.INTERNAL) +public final class IOTesterManager implements IOManager { + + private IOTester ioTester; + + @Override + public void beforeTestExecution(AresIOContext context) { + ioTester = IOTester.installNew(context.mirrorOutput(), context.maxStdOut()); + } + + @Override + public void afterTestExecution(AresIOContext context) { + IOTester.uninstallCurrent(); + ioTester = null; + } + + @Override + public IOTester getControllerInstance(AresIOContext context) { + return ioTester; + } + + @Override + public Class getControllerClass() { + return IOTester.class; + } +} diff --git a/src/main/java/de/tum/in/test/api/jqwik/Hidden.java b/src/main/java/de/tum/in/test/api/jqwik/Hidden.java index f1f56975..2d00b3b5 100644 --- a/src/main/java/de/tum/in/test/api/jqwik/Hidden.java +++ b/src/main/java/de/tum/in/test/api/jqwik/Hidden.java @@ -12,7 +12,7 @@ import org.apiguardian.api.API.Status; import de.tum.in.test.api.Deadline; -import de.tum.in.test.api.internal.TestType; +import de.tum.in.test.api.context.TestType; import de.tum.in.test.api.io.IOTester; /** diff --git a/src/main/java/de/tum/in/test/api/jqwik/JqwikAresTest.java b/src/main/java/de/tum/in/test/api/jqwik/JqwikAresTest.java index 63e12a90..65b6add0 100644 --- a/src/main/java/de/tum/in/test/api/jqwik/JqwikAresTest.java +++ b/src/main/java/de/tum/in/test/api/jqwik/JqwikAresTest.java @@ -14,7 +14,7 @@ import net.jqwik.api.lifecycle.AddLifecycleHook; import net.jqwik.api.lifecycle.PropagationMode; -import de.tum.in.test.api.internal.TestType; +import de.tum.in.test.api.context.TestType; /** * This is only for internal use, to reduce redundancy. diff --git a/src/main/java/de/tum/in/test/api/jqwik/JqwikContext.java b/src/main/java/de/tum/in/test/api/jqwik/JqwikContext.java index 9f259c29..859d749c 100644 --- a/src/main/java/de/tum/in/test/api/jqwik/JqwikContext.java +++ b/src/main/java/de/tum/in/test/api/jqwik/JqwikContext.java @@ -9,9 +9,9 @@ import net.jqwik.api.lifecycle.PropertyLifecycleContext; -import de.tum.in.test.api.internal.TestContext; -import de.tum.in.test.api.internal.TestContextUtils; -import de.tum.in.test.api.internal.TestType; +import de.tum.in.test.api.context.TestContext; +import de.tum.in.test.api.context.TestContextUtils; +import de.tum.in.test.api.context.TestType; @API(status = Status.INTERNAL) public class JqwikContext extends TestContext { diff --git a/src/main/java/de/tum/in/test/api/jqwik/JqwikIOExtension.java b/src/main/java/de/tum/in/test/api/jqwik/JqwikIOExtension.java index 0de79431..aa707159 100644 --- a/src/main/java/de/tum/in/test/api/jqwik/JqwikIOExtension.java +++ b/src/main/java/de/tum/in/test/api/jqwik/JqwikIOExtension.java @@ -1,6 +1,7 @@ package de.tum.in.test.api.jqwik; import java.util.Set; +import java.util.function.Predicate; import org.apiguardian.api.API; import org.apiguardian.api.API.Status; @@ -15,8 +16,7 @@ import net.jqwik.api.providers.TypeUsage; import net.jqwik.engine.providers.RegisteredArbitraryProviders; -import de.tum.in.test.api.internal.IOTesterManager; -import de.tum.in.test.api.io.IOTester; +import de.tum.in.test.api.internal.IOExtensionUtils; /** *

@@ -35,33 +35,43 @@ public int aroundPropertyProximity() { @Override public PropertyExecutionResult aroundProperty(PropertyLifecycleContext context, PropertyExecutor property) throws Throwable { - IOTesterManager ioTesterManager = new IOTesterManager(JqwikContext.of(context)); - ioTesterManager.beforeTestExecution(); - IOTesterProvider ioTesterProvider = new IOTesterProvider(ioTesterManager.getIOTester()); - RegisteredArbitraryProviders.register(ioTesterProvider); + IOExtensionUtils ioExtensionUtils = new IOExtensionUtils(JqwikContext.of(context)); + ioExtensionUtils.beforeTestExecution(); + // register controller if possible + ControllerProvider controllerProvider = null; + if (ioExtensionUtils.providesController()) { + controllerProvider = new ControllerProvider(ioExtensionUtils::canProvideControllerFor, + ioExtensionUtils.getControllerInstance()); + RegisteredArbitraryProviders.register(controllerProvider); + } try { return property.execute(); } finally { - RegisteredArbitraryProviders.unregister(ioTesterProvider); - ioTesterManager.afterTestExecution(); + // unregister controller if necessary + if (ioExtensionUtils.providesController()) + RegisteredArbitraryProviders.unregister(controllerProvider); + ioExtensionUtils.afterTestExecution(); } } - private static class IOTesterProvider implements ArbitraryProvider { - private final IOTester ioTester; + private static class ControllerProvider implements ArbitraryProvider { + + private final Predicate> canProvideControllerFor; + private final Object controllerInstance; - public IOTesterProvider(IOTester ioTester) { - this.ioTester = ioTester; + private ControllerProvider(Predicate> canProvideControllerFor, Object controllerInstance) { + this.canProvideControllerFor = canProvideControllerFor; + this.controllerInstance = controllerInstance; } @Override public boolean canProvideFor(TypeUsage targetType) { - return targetType.getRawType().equals(IOTester.class); + return canProvideControllerFor.test(targetType.getRawType()); } @Override public Set> provideFor(TypeUsage targetType, SubtypeProvider subtypeProvider) { - return Set.of(Arbitraries.just(ioTester)); + return Set.of(Arbitraries.just(controllerInstance)); } } } diff --git a/src/main/java/de/tum/in/test/api/jqwik/JqwikTestGuard.java b/src/main/java/de/tum/in/test/api/jqwik/JqwikTestGuard.java index e03b4cd4..20ff106b 100644 --- a/src/main/java/de/tum/in/test/api/jqwik/JqwikTestGuard.java +++ b/src/main/java/de/tum/in/test/api/jqwik/JqwikTestGuard.java @@ -10,8 +10,8 @@ import net.jqwik.api.lifecycle.PropertyLifecycleContext; import de.tum.in.test.api.Deadline; +import de.tum.in.test.api.context.TestContext; import de.tum.in.test.api.internal.ReportingUtils; -import de.tum.in.test.api.internal.TestContext; import de.tum.in.test.api.jupiter.HiddenTest; /** diff --git a/src/main/java/de/tum/in/test/api/jqwik/Public.java b/src/main/java/de/tum/in/test/api/jqwik/Public.java index f1ef6e9a..cd0cfcf8 100644 --- a/src/main/java/de/tum/in/test/api/jqwik/Public.java +++ b/src/main/java/de/tum/in/test/api/jqwik/Public.java @@ -15,7 +15,7 @@ import net.jqwik.api.ForAll; import net.jqwik.api.Property; -import de.tum.in.test.api.internal.TestType; +import de.tum.in.test.api.context.TestType; import de.tum.in.test.api.io.IOTester; /** diff --git a/src/main/java/de/tum/in/test/api/jupiter/Hidden.java b/src/main/java/de/tum/in/test/api/jupiter/Hidden.java index 2d8e5d64..8e93a1e4 100644 --- a/src/main/java/de/tum/in/test/api/jupiter/Hidden.java +++ b/src/main/java/de/tum/in/test/api/jupiter/Hidden.java @@ -16,7 +16,7 @@ import org.junit.jupiter.api.BeforeEach; import de.tum.in.test.api.Deadline; -import de.tum.in.test.api.internal.TestType; +import de.tum.in.test.api.context.TestType; import de.tum.in.test.api.io.IOTester; /** diff --git a/src/main/java/de/tum/in/test/api/jupiter/HiddenTest.java b/src/main/java/de/tum/in/test/api/jupiter/HiddenTest.java index 53da356c..69dee485 100644 --- a/src/main/java/de/tum/in/test/api/jupiter/HiddenTest.java +++ b/src/main/java/de/tum/in/test/api/jupiter/HiddenTest.java @@ -12,7 +12,7 @@ import org.junit.jupiter.api.Test; import de.tum.in.test.api.Deadline; -import de.tum.in.test.api.internal.TestType; +import de.tum.in.test.api.context.TestType; import de.tum.in.test.api.io.IOTester; /** diff --git a/src/main/java/de/tum/in/test/api/jupiter/JupiterAresTest.java b/src/main/java/de/tum/in/test/api/jupiter/JupiterAresTest.java index 222a770f..a463c171 100644 --- a/src/main/java/de/tum/in/test/api/jupiter/JupiterAresTest.java +++ b/src/main/java/de/tum/in/test/api/jupiter/JupiterAresTest.java @@ -12,7 +12,7 @@ import org.apiguardian.api.API.Status; import org.junit.jupiter.api.extension.ExtendWith; -import de.tum.in.test.api.internal.TestType; +import de.tum.in.test.api.context.TestType; /** * This is only for internal use, to reduce redundancy. diff --git a/src/main/java/de/tum/in/test/api/jupiter/JupiterContext.java b/src/main/java/de/tum/in/test/api/jupiter/JupiterContext.java index 60dfdd1f..7cc2a7db 100644 --- a/src/main/java/de/tum/in/test/api/jupiter/JupiterContext.java +++ b/src/main/java/de/tum/in/test/api/jupiter/JupiterContext.java @@ -11,9 +11,9 @@ import org.apiguardian.api.API.Status; import org.junit.jupiter.api.extension.ExtensionContext; -import de.tum.in.test.api.internal.TestContext; -import de.tum.in.test.api.internal.TestContextUtils; -import de.tum.in.test.api.internal.TestType; +import de.tum.in.test.api.context.TestContext; +import de.tum.in.test.api.context.TestContextUtils; +import de.tum.in.test.api.context.TestType; @API(status = Status.INTERNAL) public class JupiterContext extends TestContext { diff --git a/src/main/java/de/tum/in/test/api/jupiter/JupiterIOExtension.java b/src/main/java/de/tum/in/test/api/jupiter/JupiterIOExtension.java index c3aece5d..38cb42dc 100644 --- a/src/main/java/de/tum/in/test/api/jupiter/JupiterIOExtension.java +++ b/src/main/java/de/tum/in/test/api/jupiter/JupiterIOExtension.java @@ -8,32 +8,33 @@ import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolver; -import de.tum.in.test.api.internal.IOTesterManager; -import de.tum.in.test.api.io.IOTester; +import de.tum.in.test.api.internal.IOExtensionUtils; @API(status = Status.INTERNAL) public class JupiterIOExtension implements BeforeEachCallback, AfterEachCallback, ParameterResolver { - private IOTesterManager ioTesterManager; + private IOExtensionUtils ioTesterManager; @Override public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { - return parameterContext.getParameter().getType().equals(IOTester.class); + return ioTesterManager.canProvideControllerFor(parameterContext.getParameter().getType()); } @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { - return ioTesterManager.getIOTester(); + return ioTesterManager.getControllerInstance(); } @Override public void beforeEach(ExtensionContext context) throws Exception { - ioTesterManager = new IOTesterManager(JupiterContext.of(context)); + ioTesterManager = new IOExtensionUtils(JupiterContext.of(context)); ioTesterManager.beforeTestExecution(); } @Override public void afterEach(ExtensionContext context) throws Exception { - ioTesterManager.afterTestExecution(); + // If this is null, there was an exception in before, so ignore it here + if (ioTesterManager != null) + ioTesterManager.afterTestExecution(); } } diff --git a/src/main/java/de/tum/in/test/api/jupiter/Public.java b/src/main/java/de/tum/in/test/api/jupiter/Public.java index 4b4c01b8..fc8f9595 100644 --- a/src/main/java/de/tum/in/test/api/jupiter/Public.java +++ b/src/main/java/de/tum/in/test/api/jupiter/Public.java @@ -15,7 +15,7 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; -import de.tum.in.test.api.internal.TestType; +import de.tum.in.test.api.context.TestType; import de.tum.in.test.api.io.IOTester; /** diff --git a/src/main/java/de/tum/in/test/api/jupiter/PublicTest.java b/src/main/java/de/tum/in/test/api/jupiter/PublicTest.java index f41f0050..0cdc96f9 100644 --- a/src/main/java/de/tum/in/test/api/jupiter/PublicTest.java +++ b/src/main/java/de/tum/in/test/api/jupiter/PublicTest.java @@ -11,7 +11,7 @@ import org.apiguardian.api.API.Status; import org.junit.jupiter.api.Test; -import de.tum.in.test.api.internal.TestType; +import de.tum.in.test.api.context.TestType; import de.tum.in.test.api.io.IOTester; /** diff --git a/src/main/java/de/tum/in/test/api/security/AresSecurityConfigurationBuilder.java b/src/main/java/de/tum/in/test/api/security/AresSecurityConfigurationBuilder.java index 99be9a5c..8846c20b 100644 --- a/src/main/java/de/tum/in/test/api/security/AresSecurityConfigurationBuilder.java +++ b/src/main/java/de/tum/in/test/api/security/AresSecurityConfigurationBuilder.java @@ -24,7 +24,7 @@ import de.tum.in.test.api.AllowLocalPort; import de.tum.in.test.api.TrustedThreads.TrustScope; -import de.tum.in.test.api.internal.TestContext; +import de.tum.in.test.api.context.TestContext; import de.tum.in.test.api.util.PackageRule; import de.tum.in.test.api.util.PathRule; diff --git a/src/test/java/de/tum/in/test/api/security/AresSecurityConfigurationTest.java b/src/test/java/de/tum/in/test/api/security/AresSecurityConfigurationTest.java index 15d6f9c1..7b9a0112 100644 --- a/src/test/java/de/tum/in/test/api/security/AresSecurityConfigurationTest.java +++ b/src/test/java/de/tum/in/test/api/security/AresSecurityConfigurationTest.java @@ -23,9 +23,9 @@ import de.tum.in.test.api.WhitelistClass; import de.tum.in.test.api.WhitelistPackage; import de.tum.in.test.api.WhitelistPath; +import de.tum.in.test.api.context.TestContext; +import de.tum.in.test.api.context.TestType; import de.tum.in.test.api.internal.ConfigurationUtils; -import de.tum.in.test.api.internal.TestContext; -import de.tum.in.test.api.internal.TestType; import de.tum.in.test.api.util.PathRule; import de.tum.in.test.api.util.RuleType; diff --git a/src/test/java/de/tum/in/test/integration/InputOutputTest.java b/src/test/java/de/tum/in/test/integration/InputOutputTest.java index 850afef5..f32d8ae6 100644 --- a/src/test/java/de/tum/in/test/integration/InputOutputTest.java +++ b/src/test/java/de/tum/in/test/integration/InputOutputTest.java @@ -3,7 +3,10 @@ import static de.tum.in.test.testutilities.CustomConditions.*; import static org.junit.platform.testkit.engine.EventConditions.*; +import java.lang.annotation.AnnotationFormatError; + import org.junit.ComparisonFailure; +import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.platform.testkit.engine.Events; import de.tum.in.test.integration.testuser.InputOutputUser; @@ -17,7 +20,9 @@ class InputOutputTest { @UserTestResults private static Events tests; + private final String customStringBuilderManager = "customStringBuilderManager"; private final String makeUTF8Error = "makeUTF8Error"; + private final String noneManagerInvalidParameter = "noneManagerInvalidParameter"; private final String testLinesMatch = "testLinesMatch"; private final String testPenguin1 = "testPenguin1"; private final String testPenguin2 = "testPenguin2"; @@ -26,12 +31,24 @@ class InputOutputTest { private final String testSquareWrong = "testSquareWrong"; private final String testTooManyChars = "testTooManyChars"; private final String testTooManyReads = "testTooManyReads"; + private final String wrongCustomManager = "wrongCustomManager"; + + @TestTest + void test_customStringBuilderManager() { + tests.assertThatEvents().haveExactly(1, event(test(customStringBuilderManager), finishedSuccessfullyRep())); + } @TestTest void test_makeUTF8Error() { tests.assertThatEvents().haveExactly(1, testFailedWith(makeUTF8Error, IllegalArgumentException.class)); } + @TestTest + void test_noneManagerInvalidParameter() { + tests.assertThatEvents().haveExactly(1, + testFailedWith(noneManagerInvalidParameter, ParameterResolutionException.class)); + } + @TestTest void test_testLinesMatch() { tests.assertThatEvents().haveExactly(1, event(test(testLinesMatch), finishedSuccessfullyRep())); @@ -71,4 +88,9 @@ void test_testTooManyChars() { void test_testTooManyReads() { tests.assertThatEvents().haveExactly(1, testFailedWith(testTooManyReads, IllegalStateException.class)); } + + @TestTest + void test_wrongCustomManager() { + tests.assertThatEvents().haveExactly(1, testFailedWith(wrongCustomManager, AnnotationFormatError.class)); + } } diff --git a/src/test/java/de/tum/in/test/integration/testuser/InputOutputUser.java b/src/test/java/de/tum/in/test/integration/testuser/InputOutputUser.java index 0bf6bdd8..a937edbb 100644 --- a/src/test/java/de/tum/in/test/integration/testuser/InputOutputUser.java +++ b/src/test/java/de/tum/in/test/integration/testuser/InputOutputUser.java @@ -1,13 +1,19 @@ package de.tum.in.test.integration.testuser; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.MethodOrderer.MethodName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import org.opentest4j.AssertionFailedError; @@ -17,13 +23,17 @@ import de.tum.in.test.api.PathType; import de.tum.in.test.api.StrictTimeout; import de.tum.in.test.api.WhitelistPath; +import de.tum.in.test.api.WithIOManager; +import de.tum.in.test.api.io.AresIOContext; +import de.tum.in.test.api.io.IOManager; import de.tum.in.test.api.io.IOTester; import de.tum.in.test.api.io.Line; import de.tum.in.test.api.io.OutputTestOptions; -import de.tum.in.test.api.jupiter.PublicTest; +import de.tum.in.test.api.jupiter.Public; import de.tum.in.test.api.localization.UseLocale; import de.tum.in.test.integration.testuser.subject.InputOutputPenguin; +@Public @UseLocale("en") @MirrorOutput(MirrorOutputPolicy.DISABLED) @StrictTimeout(value = 300, unit = TimeUnit.MILLISECONDS) @@ -33,12 +43,74 @@ @SuppressWarnings("static-method") public class InputOutputUser { - @PublicTest + public static class CustomManager implements IOManager { + + private PrintStream previousOut; + private StringBuilder current; + + public CustomManager() { + } + + @Override + public void afterTestExecution(AresIOContext context) { + System.setOut(previousOut); + current = null; + } + + @Override + @SuppressWarnings("resource") + public void beforeTestExecution(AresIOContext context) { + current = new StringBuilder(); + var output = new ByteArrayOutputStream() { + @Override + public void flush() throws IOException { + super.flush(); + current.append(toString(StandardCharsets.UTF_8)); + reset(); + } + }; + previousOut = System.out; + System.setOut(new PrintStream(output, true, StandardCharsets.UTF_8)); + } + + @Override + public Class getControllerClass() { + return StringBuilder.class; + } + + @Override + public StringBuilder getControllerInstance(AresIOContext context) { + return current; + } + } + + public static class WrongCustomManager extends CustomManager { + @SuppressWarnings("unused") + public WrongCustomManager(int x) { + // one parameter instead of none + } + } + + @Test + @WithIOManager(CustomManager.class) + void customStringBuilderManager(StringBuilder output) { + InputOutputPenguin.writeTwoLines(); + assertThat(output).contains("Nieder mit den Eisbären!", "Pinguine sind die Besten!"); + } + + @Test void makeUTF8Error() throws IOException { System.out.write(new byte[] { 'P', 'i', 'n', 'g', 'u', (byte) 0xFF }); } - @PublicTest + @Test + @WithIOManager(WithIOManager.None.class) + void noneManagerInvalidParameter(IOTester tester) { + tester.provideInputLines(""); + InputOutputPenguin.readTwoTimes(); + } + + @Test void testLinesMatch(IOTester tester) { System.out.println("ABC (("); System.out.println("x"); @@ -125,7 +197,7 @@ void testLinesMatch(IOTester tester) { "This should not pass ==> fast-forward(12) error: not enough actual lines remaining (11)"); } - @PublicTest + @Test void testPenguin1(IOTester tester) { InputOutputPenguin.writeTwoLines(); @@ -138,7 +210,7 @@ void testPenguin1(IOTester tester) { assertEquals("Pinguine sind die Besten!", firstLine); } - @PublicTest + @Test void testPenguin2(IOTester tester) { InputOutputPenguin.writeTwoLines(); @@ -148,7 +220,7 @@ void testPenguin2(IOTester tester) { assertEquals("Pinguine sind die Besten!", secondLine); } - @PublicTest + @Test void testPolarBear(IOTester tester) { InputOutputPenguin.writeTwoLines(); @@ -163,7 +235,7 @@ void testPolarBear(IOTester tester) { .isEqualTo("Pinguine sind die Besten!\nNieder mit den Eisbären!\n"); } - @PublicTest + @Test void testSquareCorrect(IOTester tester) { tester.provideInputLines("5"); @@ -177,23 +249,30 @@ void testSquareCorrect(IOTester tester) { assertEquals("25", out.get(2).text()); } - @PublicTest + @Test void testSquareWrong(IOTester tester) { InputOutputPenguin.calculateSquare(); assertEquals("Keine Fehlerausgabe erwartet", 0, tester.err().getLines().size()); } - @PublicTest + @Test @MirrorOutput(maxCharCount = 10, value = MirrorOutputPolicy.DISABLED) void testTooManyChars() { InputOutputPenguin.writeTwoLines(); } - @PublicTest + @Test void testTooManyReads(IOTester tester) { tester.provideInputLines("12"); InputOutputPenguin.readTwoTimes(); } + + @Test + @WithIOManager(WrongCustomManager.class) + void wrongCustomManager(StringBuilder output) { + InputOutputPenguin.writeTwoLines(); + assertThat(output).contains("Nieder mit den Eisbären!", "Pinguine sind die Besten!"); + } }