From 811030b9c57465873746a445215ad0e39191059a Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Wed, 10 Mar 2021 15:35:23 +1100 Subject: [PATCH] Introduce continuous testing --- bom/application/pom.xml | 7 +- build-parent/pom.xml | 23 +- core/deployment/pom.xml | 27 + .../quarkus/deployment/QuarkusAugmentor.java | 10 +- .../io/quarkus/deployment/TestConfig.java | 96 ++- .../builditem/LaunchModeBuildItem.java | 17 +- .../deployment/configuration/TestConfig.java | 21 - .../deployment/dev/ClassScanResult.java | 56 +- .../deployment/dev/DevModeContext.java | 210 ++++-- .../quarkus/deployment/dev/DevModeMain.java | 19 +- .../deployment/dev/IDEDevModeMain.java | 43 +- .../deployment/dev/IsolatedDevModeMain.java | 67 +- .../dev/IsolatedRemoteDevModeMain.java | 9 +- ...aderCompiler.java => QuarkusCompiler.java} | 68 +- .../dev/QuarkusDevModeLauncher.java | 21 +- .../dev/RuntimeUpdatesProcessor.java | 480 +++++++++---- .../deployment/dev/console/AeshConsole.java | 264 +++++++ .../deployment/dev/console/BasicConsole.java | 89 +++ .../deployment/dev/console/InputHandler.java | 14 + .../dev/console/QuarkusConsole.java | 130 ++++ .../dev/console/RedirectPrintStream.java | 186 +++++ .../dev/testing/CurrentTestApplication.java | 17 + .../dev/testing/HtmlAnsiOutputStream.java | 178 +++++ .../dev/testing/JunitTestRunner.java | 649 ++++++++++++++++++ .../dev/testing/TestClassResult.java | 60 ++ .../dev/testing/TestClassUsages.java | 134 ++++ .../dev/testing/TestConsoleHandler.java | 201 ++++++ .../dev/testing/TestController.java | 12 + .../deployment/dev/testing/TestListener.java | 21 + .../deployment/dev/testing/TestResult.java | 56 ++ .../dev/testing/TestRunListener.java | 26 + .../dev/testing/TestRunResults.java | 156 +++++ .../deployment/dev/testing/TestRunner.java | 264 +++++++ .../deployment/dev/testing/TestState.java | 134 ++++ .../deployment/dev/testing/TestSupport.java | 258 +++++++ .../dev/testing/TestTracingProcessor.java | 141 ++++ .../deployment/jbang/JBangAugmentorImpl.java | 43 +- .../logging/LoggingResourceProcessor.java | 75 +- .../deployment/mutability/DevModeTask.java | 12 +- .../deployment/steps/MainClassBuildStep.java | 5 + .../runner/bootstrap/AugmentActionImpl.java | 4 +- .../runner/bootstrap/StartupActionImpl.java | 53 +- .../ContinuousTestingWebsocketListener.java | 61 ++ .../quarkus/dev/testing/TracingHandler.java | 62 ++ .../java/io/quarkus/runtime/Application.java | 35 +- .../runtime/ApplicationLifecycleManager.java | 4 +- .../runtime/logging/LogCleanupFilter.java | 1 + .../runtime/logging/LoggingSetupRecorder.java | 4 +- .../io/quarkus/gradle/tasks/QuarkusDev.java | 21 +- .../main/java/io/quarkus/maven/DevMojo.java | 92 ++- .../java/io/quarkus/maven/RemoteDevMojo.java | 2 +- .../DevServicesDatasourceProcessor.java | 21 +- .../runtime/JaxrsClientReactiveRecorder.java | 9 - extensions/vertx-http/deployment/pom.xml | 9 + .../ContinuousTestingWebSocketListener.java | 69 ++ .../devmode/console/DevConsoleProcessor.java | 36 +- .../deployment/devmode/tests/ClassResult.java | 93 +++ .../http/deployment/devmode/tests/Result.java | 82 +++ .../deployment/devmode/tests/SuiteResult.java | 23 + .../deployment/devmode/tests/TestStatus.java | 82 +++ .../devmode/tests/TestsProcessor.java | 135 ++++ .../resources/dev-static/css/dev-console.css | 5 +- .../src/main/resources/dev-static/js/tests.js | 103 +++ .../main/resources/dev-templates/index.html | 17 + .../io.quarkus.quarkus-vertx-http/test.html | 109 +++ .../dev-templates/logmanagerNav.html | 4 +- .../main/resources/dev-templates/main.html | 20 +- .../http/testrunner/DuplicateSimpleET.java | 34 + .../vertx/http/testrunner/HelloResource.java | 32 + .../vertx/http/testrunner/SimpleET.java | 34 + .../vertx/http/testrunner/StartupFailer.java | 21 + ...tChangeTrackingWhenStartFailsTestCase.java | 84 +++ .../testrunner/TestRunnerSmokeTestCase.java | 70 ++ .../http/testrunner/TestRunnerTestUtils.java | 36 + .../vertx/http/testrunner/includes/BarET.java | 21 + .../includes/ExcludePatternTestCase.java | 73 ++ .../vertx/http/testrunner/includes/FooET.java | 21 + .../includes/IncludePatternTestCase.java | 73 ++ .../testrunner/tags/ExcludeTagsTestCase.java | 72 ++ .../testrunner/tags/IncludeTagsTestCase.java | 72 ++ .../vertx/http/testrunner/tags/TaggedET.java | 63 ++ .../ContinuousTestWebSocketHandler.java | 80 +++ .../runtime/devmode/DevConsoleRecorder.java | 4 + .../bootstrap/app/CuratedApplication.java | 8 - .../bootstrap/app/QuarkusBootstrap.java | 28 +- .../bootstrap/devmode/DependenciesFilter.java | 3 + .../maven/workspace/LocalProject.java | 12 + .../io/quarkus/bootstrap/runner/Timing.java | 20 +- .../main/java/io/quarkus/qute/Results.java | 7 +- .../quarkus/test/common/PathTestHelper.java | 4 +- .../main/java/io/quarkus/test/ClearCache.java | 23 + .../io/quarkus/test/QuarkusDevModeTest.java | 92 ++- .../java/io/quarkus/test/QuarkusUnitTest.java | 20 + .../test/junit/QuarkusTestExtension.java | 66 +- 94 files changed, 5968 insertions(+), 560 deletions(-) delete mode 100644 core/deployment/src/main/java/io/quarkus/deployment/configuration/TestConfig.java rename core/deployment/src/main/java/io/quarkus/deployment/dev/{ClassLoaderCompiler.java => QuarkusCompiler.java} (75%) create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/console/AeshConsole.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/console/BasicConsole.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/console/InputHandler.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/console/QuarkusConsole.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/console/RedirectPrintStream.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/testing/CurrentTestApplication.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/testing/HtmlAnsiOutputStream.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestClassResult.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestClassUsages.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConsoleHandler.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestController.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestListener.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestResult.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunListener.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunResults.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunner.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestState.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java create mode 100644 core/devmode-spi/src/main/java/io/quarkus/dev/testing/ContinuousTestingWebsocketListener.java create mode 100644 core/devmode-spi/src/main/java/io/quarkus/dev/testing/TracingHandler.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ContinuousTestingWebSocketListener.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/ClassResult.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/Result.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/SuiteResult.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/TestStatus.java create mode 100644 extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/TestsProcessor.java create mode 100644 extensions/vertx-http/deployment/src/main/resources/dev-static/js/tests.js create mode 100644 extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/test.html create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/testrunner/DuplicateSimpleET.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/testrunner/HelloResource.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/testrunner/SimpleET.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/testrunner/StartupFailer.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/testrunner/TestChangeTrackingWhenStartFailsTestCase.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/testrunner/TestRunnerSmokeTestCase.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/testrunner/TestRunnerTestUtils.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/testrunner/includes/BarET.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/testrunner/includes/ExcludePatternTestCase.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/testrunner/includes/FooET.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/testrunner/includes/IncludePatternTestCase.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/testrunner/tags/ExcludeTagsTestCase.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/testrunner/tags/IncludeTagsTestCase.java create mode 100644 extensions/vertx-http/deployment/src/test/java/io/quarkus/vertx/http/testrunner/tags/TaggedET.java create mode 100644 extensions/vertx-http/runtime/src/main/java/io/quarkus/vertx/http/runtime/devmode/ContinuousTestWebSocketHandler.java create mode 100644 test-framework/junit5-internal/src/main/java/io/quarkus/test/ClearCache.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index ae96bcb56b16d5..dad36d97983c7a 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -212,6 +212,7 @@ 1.10.2 0.8.6 1.15.2 + 2.1 @@ -4734,7 +4735,6 @@ - software.amazon.awssdk apache-client @@ -5133,6 +5133,11 @@ windows-x86_64 ${grpc.version} + + org.aesh + readline + ${aesh-readline.version} + diff --git a/build-parent/pom.xml b/build-parent/pom.xml index 1fa93ece470d04..ed3c492f287b10 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -493,13 +493,11 @@ io.quarkus:quarkus-test-* io.rest-assured:* org.assertj:* - org.junit.jupiter:* io.quarkus:quarkus-test-*:*:*:test io.rest-assured:*:*:*:test org.assertj:*:*:*:test - org.junit.jupiter:*:*:*:test Found test dependencies with wrong scope: @@ -510,6 +508,27 @@ enforce + + enforce-test-deps-junit-scope + + + + false + + org.junit.jupiter:* + + + org.junit.jupiter:*:*:*:test + + Found JUnit dependencies with wrong scope: + + + ${enforce-test-deps-scope.skip} + + + enforce + + diff --git a/core/deployment/pom.xml b/core/deployment/pom.xml index 8a4d6727b29475..5310325950fbba 100644 --- a/core/deployment/pom.xml +++ b/core/deployment/pom.xml @@ -14,6 +14,10 @@ Quarkus - Core - Deployment + + org.aesh + readline + org.wildfly.common wildfly-common @@ -106,10 +110,33 @@ test + + org.junit.platform + junit-platform-launcher + + + org.junit.jupiter + junit-jupiter + + + maven-enforcer-plugin + + + + enforce-test-deps-junit-scope + + true + + + enforce + + + + maven-compiler-plugin diff --git a/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java b/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java index 09882de1382adc..a2e11843077124 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/QuarkusAugmentor.java @@ -60,6 +60,7 @@ public class QuarkusAugmentor { private final String baseName; private final Consumer configCustomizer; private final boolean rebuild; + private final boolean auxiliaryApplication; QuarkusAugmentor(Builder builder) { this.classLoader = builder.classLoader; @@ -78,6 +79,7 @@ public class QuarkusAugmentor { this.deploymentClassLoader = builder.deploymentClassLoader; this.rebuild = builder.rebuild; this.devModeType = builder.devModeType; + this.auxiliaryApplication = builder.auxiliaryApplication; } public BuildResult run() throws Exception { @@ -142,7 +144,7 @@ public BuildResult run() throws Exception { .produce(new ShutdownContextBuildItem()) .produce(new RawCommandLineArgumentsBuildItem()) .produce(new LaunchModeBuildItem(launchMode, - devModeType == null ? Optional.empty() : Optional.of(devModeType))) + devModeType == null ? Optional.empty() : Optional.of(devModeType), auxiliaryApplication)) .produce(new BuildSystemTargetBuildItem(targetDir, baseName, rebuild, buildSystemProperties == null ? new Properties() : buildSystemProperties)) .produce(new DeploymentClassLoaderBuildItem(deploymentClassLoader)) @@ -197,6 +199,7 @@ public static final class Builder { Consumer configCustomizer; ClassLoader deploymentClassLoader; DevModeType devModeType; + boolean auxiliaryApplication; public Builder addBuildChainCustomizer(Consumer customizer) { this.buildChainCustomizers.add(customizer); @@ -217,6 +220,11 @@ public Builder excludeFromIndexing(Collection excludedFromIndexing) { return this; } + public Builder setAuxiliaryApplication(boolean auxiliaryApplication) { + this.auxiliaryApplication = auxiliaryApplication; + return this; + } + public LaunchMode getLaunchMode() { return launchMode; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/TestConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/TestConfig.java index b619578c833a31..16683bee3364f4 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/TestConfig.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/TestConfig.java @@ -11,12 +11,89 @@ /** * This is used currently only to suppress warnings about unknown properties * when the user supplies something like: -Dquarkus.test.profile=someProfile or -Dquarkus.test.native-image-profile=someProfile - * + *

* TODO refactor code to actually use these values */ @ConfigRoot public class TestConfig { + /** + * If continuous testing is enabled. + * + * The default value is 'paused', which will allow you to start testing + * from the console or the Dev UI, but will not run tests on startup. + * + * If this is set to 'enabled' then testing will start as soon as the + * application has started. + * + * If this is 'disabled' then continuous testing is not enabled, and can't + * be enabled without restarting the application. + * + */ + @ConfigItem(defaultValue = "paused") + public Mode continuousTesting; + + /** + * If output from the running tests should be displayed in the console. + */ + @ConfigItem(defaultValue = "false") + public boolean displayTestOutput; + + /** + * Tags that should be included for continuous testing. + */ + @ConfigItem + public Optional> includeTags; + + /** + * Tags that should be excluded by default with continuous testing. + * + * This is ignored if include-tags has been set. + * + * Defaults to 'slow' + */ + @ConfigItem(defaultValue = "slow") + public Optional> excludeTags; + + /** + * Tests that should be included for continuous testing. This is a regular expression. + */ + @ConfigItem + public Optional includePattern; + + /** + * Tests that should be excluded with continuous testing. This is a regular expression. + * + * This is ignored if include-pattern has been set. + * + */ + @ConfigItem + public Optional excludePattern; + /** + * Disable the testing status/prompt message at the bottom of the console + * and log these messages to STDOUT instead. + * + * Use this option if your terminal does not support ANSI escape sequences. + */ + @ConfigItem(defaultValue = "false") + public boolean basicConsole; + + /** + * Disable color in the testing status and prompt messages. + * + * Use this option if your terminal does not support color. + */ + @ConfigItem(defaultValue = "false") + public boolean disableColor; + + /** + * If test results and status should be displayed in the console. + * + * If this is false results can still be viewed in the dev console. + */ + @ConfigItem(defaultValue = "true") + public boolean console; + /** * Duration to wait for the native image to built during testing */ @@ -35,6 +112,16 @@ public class TestConfig { @ConfigItem Profile profile; + /** + * Configures the hang detection in @QuarkusTest. If no activity happens (i.e. no test callbacks are called) over + * this period then QuarkusTest will dump all threads stack traces, to help diagnose a potential hang. + * + * Note that the initial timeout (before Quarkus has started) will only apply if provided by a system property, as + * it is not possible to read all config sources until Quarkus has booted. + */ + @ConfigItem(defaultValue = "10m") + Duration hangDetectionTimeout; + @ConfigGroup public static class Profile { @@ -53,4 +140,11 @@ public static class Profile { @ConfigItem(defaultValue = "") Optional> tags; } + + public enum Mode { + PAUSED, + ENABLED, + DISABLED + + } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/LaunchModeBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/LaunchModeBuildItem.java index 5fbcd30b00e548..b4e39029e80203 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/LaunchModeBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/LaunchModeBuildItem.java @@ -15,9 +15,12 @@ public final class LaunchModeBuildItem extends SimpleBuildItem { private final Optional devModeType; - public LaunchModeBuildItem(LaunchMode launchMode, Optional devModeType) { + private final boolean auxiliaryApplication; + + public LaunchModeBuildItem(LaunchMode launchMode, Optional devModeType, boolean auxiliaryApplication) { this.launchMode = launchMode; this.devModeType = devModeType; + this.auxiliaryApplication = auxiliaryApplication; } public LaunchMode getLaunchMode() { @@ -26,11 +29,21 @@ public LaunchMode getLaunchMode() { /** * The development mode type. - * + *

* Note that even for NORMAL launch modes this could be generating an application for the local side of remote * dev mode, so this may be set even for launch mode normal. */ public Optional getDevModeType() { return devModeType; } + + /** + * An Auxiliary Application is a second application running in the same JVM as a primary application. + *

+ * Currently this is done to allow running tests in dev mode, while the main dev mode process continues to + * run. + */ + public boolean isAuxiliaryApplication() { + return auxiliaryApplication; + } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/configuration/TestConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/configuration/TestConfig.java deleted file mode 100644 index de0fee867fb46d..00000000000000 --- a/core/deployment/src/main/java/io/quarkus/deployment/configuration/TestConfig.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.quarkus.deployment.configuration; - -import java.time.Duration; - -import io.quarkus.runtime.annotations.ConfigItem; -import io.quarkus.runtime.annotations.ConfigRoot; - -@ConfigRoot -public class TestConfig { - - /** - * Configures the hang detection in @QuarkusTest. If no activity happens (i.e. no test callbacks are called) over - * this period then QuarkusTest will dump all threads stack traces, to help diagnose a potential hang. - * - * Note that the initial timeout (before Quarkus has started) will only apply if provided by a system property, as - * it is not possible to read all config sources until Quarkus has booted. - */ - @ConfigItem(defaultValue = "10m") - Duration hangDetectionTimeout; - -} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/ClassScanResult.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/ClassScanResult.java index efd46c589c567f..ff05d11c199817 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/ClassScanResult.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/ClassScanResult.java @@ -11,9 +11,34 @@ public class ClassScanResult { final Set changedClassNames = new HashSet<>(); final Set deletedClassNames = new HashSet<>(); final Set addedClassNames = new HashSet<>(); + boolean compilationHappened; public boolean isChanged() { - return !changedClasses.isEmpty() || !deletedClasses.isEmpty() || !addedClasses.isEmpty(); + return !changedClasses.isEmpty() || !deletedClasses.isEmpty() || !addedClasses.isEmpty() || compilationHappened; + } + + public static ClassScanResult merge(ClassScanResult m1, ClassScanResult m2) { + if (m1 == null) { + return m2; + } + if (m2 == null) { + return m1; + } + ClassScanResult ret = new ClassScanResult(); + ret.changedClasses.addAll(m1.changedClasses); + ret.deletedClasses.addAll(m1.deletedClasses); + ret.addedClasses.addAll(m1.deletedClasses); + ret.changedClassNames.addAll(m1.changedClassNames); + ret.deletedClassNames.addAll(m1.deletedClassNames); + ret.addedClassNames.addAll(m1.addedClassNames); + ret.changedClasses.addAll(m2.changedClasses); + ret.deletedClasses.addAll(m2.deletedClasses); + ret.addedClasses.addAll(m2.deletedClasses); + ret.changedClassNames.addAll(m2.changedClassNames); + ret.deletedClassNames.addAll(m2.deletedClassNames); + ret.addedClassNames.addAll(m2.addedClassNames); + ret.compilationHappened = m1.compilationHappened | m2.compilationHappened; + return ret; } public void addDeletedClass(Path moduleClassesPath, Path classFilePath) { @@ -31,9 +56,38 @@ public void addAddedClass(Path moduleClassesPath, Path classFilePath) { addedClassNames.add(toName(moduleClassesPath, classFilePath)); } + public Set getChangedClassNames() { + return changedClassNames; + } + + public Set getChangedClasses() { + return changedClasses; + } + + public Set getDeletedClasses() { + return deletedClasses; + } + + public Set getAddedClasses() { + return addedClasses; + } + + public Set getDeletedClassNames() { + return deletedClassNames; + } + + public Set getAddedClassNames() { + return addedClassNames; + } + + public boolean isCompilationHappened() { + return compilationHappened; + } + private String toName(Path moduleClassesPath, Path classFilePath) { String cf = moduleClassesPath.relativize(classFilePath).toString() .replace(moduleClassesPath.getFileSystem().getSeparator(), "."); return cf.substring(0, cf.length() - ".class".length()); } + } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeContext.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeContext.java index 629c75a07be447..4d7815910fac7b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeContext.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeContext.java @@ -12,6 +12,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import io.quarkus.bootstrap.app.QuarkusBootstrap; @@ -25,6 +26,8 @@ */ public class DevModeContext implements Serializable { + public static final CompilationUnit EMPTY_COMPILATION_UNIT = new CompilationUnit(Collections.emptySet(), null, null, null); + public static final String ENABLE_PREVIEW_FLAG = "--enable-preview"; private ModuleInfo applicationRoot; @@ -228,48 +231,28 @@ public static class ModuleInfo implements Serializable { private final AppArtifactKey appArtifactKey; private final String name; private final String projectDirectory; - private final Set sourcePaths; - private final String classesPath; - private final String resourcePath; - private final String resourcesOutputPath; + private final CompilationUnit main; + private final CompilationUnit test; + private final String preBuildOutputDir; private final Set sourceParents; private final String targetDir; - public ModuleInfo(AppArtifactKey appArtifactKey, - String name, - String projectDirectory, - Set sourcePaths, - String classesPath, - String resourcePath, - String sourceParent, - String preBuildOutputDir, - String targetDir) { - this(appArtifactKey, name, projectDirectory, sourcePaths, classesPath, resourcePath, classesPath, - Collections.singleton(sourceParent), - preBuildOutputDir, targetDir); - } - - public ModuleInfo( - AppArtifactKey appArtifactKey, String name, - String projectDirectory, - Set sourcePaths, - String classesPath, - String resourcePath, - String resourceOutputPath, - Set sourceParents, - String preBuildOutputDir, - String targetDir) { - this.appArtifactKey = appArtifactKey; - this.name = name; - this.projectDirectory = projectDirectory; - this.sourcePaths = sourcePaths == null ? new LinkedHashSet<>() : new LinkedHashSet<>(sourcePaths); - this.classesPath = classesPath; - this.resourcePath = resourcePath; - this.resourcesOutputPath = resourceOutputPath; - this.sourceParents = sourceParents; - this.preBuildOutputDir = preBuildOutputDir; - this.targetDir = targetDir; + ModuleInfo(Builder builder) { + this.appArtifactKey = builder.appArtifactKey; + this.name = builder.name; + this.projectDirectory = builder.projectDirectory; + this.main = new CompilationUnit(new LinkedHashSet<>(builder.sourcePaths), builder.classesPath, builder.resourcePath, + builder.resourcesOutputPath); + if (builder.testClassesPath != null) { + this.test = new CompilationUnit(new LinkedHashSet<>(builder.testSourcePaths), + builder.testClassesPath, builder.testResourcePath, builder.testResourcesOutputPath); + } else { + this.test = null; + } + this.sourceParents = builder.sourceParents; + this.preBuildOutputDir = builder.preBuildOutputDir; + this.targetDir = builder.targetDir; } public String getName() { @@ -280,30 +263,15 @@ public String getProjectDirectory() { return projectDirectory; } - public Set getSourcePaths() { - return Collections.unmodifiableSet(sourcePaths); - } - public Set getSourceParents() { return sourceParents; } + //TODO: why isn't this immutable? public void addSourcePaths(Collection additionalPaths) { additionalPaths.stream() .map(p -> Paths.get(p).isAbsolute() ? p : (projectDirectory + File.separator + p)) - .forEach(sourcePaths::add); - } - - public String getClassesPath() { - return classesPath; - } - - public String getResourcePath() { - return resourcePath; - } - - public String getResourcesOutputPath() { - return resourcesOutputPath; + .forEach(main.sourcePaths::add); } public String getPreBuildOutputDir() { @@ -317,6 +285,138 @@ public String getTargetDir() { public AppArtifactKey getAppArtifactKey() { return appArtifactKey; } + + public CompilationUnit getMain() { + return main; + } + + public Optional getTest() { + return Optional.ofNullable(test); + } + + public static class Builder { + + private AppArtifactKey appArtifactKey; + private String name; + private String projectDirectory; + private Set sourcePaths = Collections.emptySet(); + private String classesPath; + private String resourcePath; + private String resourcesOutputPath; + + private String preBuildOutputDir; + private Set sourceParents = Collections.emptySet(); + private String targetDir; + + private Set testSourcePaths = Collections.emptySet(); + private String testClassesPath; + private String testResourcePath; + private String testResourcesOutputPath; + + public Builder setAppArtifactKey(AppArtifactKey appArtifactKey) { + this.appArtifactKey = appArtifactKey; + return this; + } + + public Builder setName(String name) { + this.name = name; + return this; + } + + public Builder setProjectDirectory(String projectDirectory) { + this.projectDirectory = projectDirectory; + return this; + } + + public Builder setSourcePaths(Set sourcePaths) { + this.sourcePaths = sourcePaths; + return this; + } + + public Builder setClassesPath(String classesPath) { + this.classesPath = classesPath; + return this; + } + + public Builder setResourcePath(String resourcePath) { + this.resourcePath = resourcePath; + return this; + } + + public Builder setResourcesOutputPath(String resourcesOutputPath) { + this.resourcesOutputPath = resourcesOutputPath; + return this; + } + + public Builder setPreBuildOutputDir(String preBuildOutputDir) { + this.preBuildOutputDir = preBuildOutputDir; + return this; + } + + public Builder setSourceParents(Set sourceParents) { + this.sourceParents = sourceParents; + return this; + } + + public Builder setTargetDir(String targetDir) { + this.targetDir = targetDir; + return this; + } + + public Builder setTestSourcePaths(Set testSourcePaths) { + this.testSourcePaths = testSourcePaths; + return this; + } + + public Builder setTestClassesPath(String testClassesPath) { + this.testClassesPath = testClassesPath; + return this; + } + + public Builder setTestResourcePath(String testResourcePath) { + this.testResourcePath = testResourcePath; + return this; + } + + public Builder setTestResourcesOutputPath(String testResourcesOutputPath) { + this.testResourcesOutputPath = testResourcesOutputPath; + return this; + } + + public ModuleInfo build() { + return new ModuleInfo(this); + } + } + } + + public static class CompilationUnit implements Serializable { + private final Set sourcePaths; + private final String classesPath; + private final String resourcePath; + private final String resourcesOutputPath; + + public CompilationUnit(Set sourcePaths, String classesPath, String resourcePath, String resourcesOutputPath) { + this.sourcePaths = sourcePaths; + this.classesPath = classesPath; + this.resourcePath = resourcePath; + this.resourcesOutputPath = resourcesOutputPath; + } + + public Set getSourcePaths() { + return sourcePaths; + } + + public String getClassesPath() { + return classesPath; + } + + public String getResourcePath() { + return resourcePath; + } + + public String getResourcesOutputPath() { + return resourcesOutputPath; + } } public boolean isEnablePreview() { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java index e3cc91007e1d65..4b2898436f749d 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java @@ -88,14 +88,14 @@ public void start() throws Exception { } } final PathsCollection.Builder appRoots = PathsCollection.builder(); - Path p = Paths.get(context.getApplicationRoot().getClassesPath()); + Path p = Paths.get(context.getApplicationRoot().getMain().getClassesPath()); if (Files.exists(p)) { appRoots.add(p); } - if (context.getApplicationRoot().getResourcesOutputPath() != null - && !context.getApplicationRoot().getResourcesOutputPath() - .equals(context.getApplicationRoot().getClassesPath())) { - p = Paths.get(context.getApplicationRoot().getResourcesOutputPath()); + if (context.getApplicationRoot().getMain().getResourcesOutputPath() != null + && !context.getApplicationRoot().getMain().getResourcesOutputPath() + .equals(context.getApplicationRoot().getMain().getClassesPath())) { + p = Paths.get(context.getApplicationRoot().getMain().getResourcesOutputPath()); if (Files.exists(p)) { appRoots.add(p); } @@ -121,12 +121,13 @@ public void start() throws Exception { } for (DevModeContext.ModuleInfo i : context.getAllModules()) { - if (i.getClassesPath() != null) { - Path classesPath = Paths.get(i.getClassesPath()); + if (i.getMain().getClassesPath() != null) { + Path classesPath = Paths.get(i.getMain().getClassesPath()); bootstrapBuilder.addAdditionalApplicationArchive(new AdditionalDependency(classesPath, true, false)); } - if (i.getResourcesOutputPath() != null && !i.getResourcesOutputPath().equals(i.getClassesPath())) { - Path resourceOutputPath = Paths.get(i.getResourcesOutputPath()); + if (i.getMain().getResourcesOutputPath() != null + && !i.getMain().getResourcesOutputPath().equals(i.getMain().getClassesPath())) { + Path resourceOutputPath = Paths.get(i.getMain().getResourcesOutputPath()); bootstrapBuilder.addAdditionalApplicationArchive(new AdditionalDependency(resourceOutputPath, true, false)); } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/IDEDevModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/IDEDevModeMain.java index 6281bee827090e..26682bd9fe7a5c 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/IDEDevModeMain.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/IDEDevModeMain.java @@ -105,26 +105,31 @@ private DevModeContext.ModuleInfo toModule(WorkspaceModule module) throws Bootst if (module.getSourceSet().getResourceDirectory() != null) { resourceDirectory = module.getSourceSet().getResourceDirectory().getPath(); } - return new DevModeContext.ModuleInfo(key, - module.getArtifactCoords().getArtifactId(), - module.getProjectRoot().getPath(), - sourceDirectories, - QuarkusModelHelper.getClassPath(module).toAbsolutePath().toString(), - module.getSourceSourceSet().getResourceDirectory().toString(), - resourceDirectory, - sourceParents, - module.getBuildDir().toPath().resolve("generated-sources").toAbsolutePath().toString(), - module.getBuildDir().toString()); + return new DevModeContext.ModuleInfo.Builder() + .setAppArtifactKey(key) + .setName(module.getArtifactCoords().getArtifactId()) + .setProjectDirectory(module.getProjectRoot().getPath()) + .setSourcePaths(sourceDirectories) + .setClassesPath(QuarkusModelHelper.getClassPath(module).toAbsolutePath().toString()) + .setResourcePath(module.getSourceSourceSet().getResourceDirectory().toString()) + .setResourcesOutputPath(resourceDirectory) + .setSourceParents(sourceParents) + .setPreBuildOutputDir(module.getBuildDir().toPath().resolve("generated-sources").toAbsolutePath().toString()) + .setTargetDir(module.getBuildDir().toString()).build(); } private DevModeContext.ModuleInfo toModule(LocalProject project) { - return new DevModeContext.ModuleInfo(project.getKey(), project.getArtifactId(), - project.getDir().toAbsolutePath().toString(), - Collections.singleton(project.getSourcesSourcesDir().toAbsolutePath().toString()), - project.getClassesDir().toAbsolutePath().toString(), - project.getResourcesSourcesDir().toAbsolutePath().toString(), - project.getSourcesDir().toString(), - project.getCodeGenOutputDir().toString(), - project.getOutputDir().toString()); + + return new DevModeContext.ModuleInfo.Builder() + .setAppArtifactKey(project.getKey()) + .setName(project.getArtifactId()) + .setProjectDirectory(project.getDir().toAbsolutePath().toString()) + .setSourcePaths(Collections.singleton(project.getSourcesSourcesDir().toAbsolutePath().toString())) + .setClassesPath(project.getClassesDir().toAbsolutePath().toString()) + .setResourcesOutputPath(project.getClassesDir().toAbsolutePath().toString()) + .setResourcePath(project.getResourcesSourcesDir().toAbsolutePath().toString()) + .setSourceParents(Collections.singleton(project.getSourcesDir().toString())) + .setPreBuildOutputDir(project.getCodeGenOutputDir().toString()) + .setTargetDir(project.getOutputDir().toString()).build(); } -} +} \ No newline at end of file diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java index 2356f4d01e6048..4a04e38af63538 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java @@ -43,6 +43,9 @@ import io.quarkus.deployment.CodeGenerator; import io.quarkus.deployment.builditem.ApplicationClassPredicateBuildItem; import io.quarkus.deployment.codegen.CodeGenData; +import io.quarkus.deployment.dev.console.InputHandler; +import io.quarkus.deployment.dev.console.QuarkusConsole; +import io.quarkus.deployment.dev.testing.TestSupport; import io.quarkus.deployment.steps.ClassTransformingBuildStep; import io.quarkus.deployment.util.FSWatchUtil; import io.quarkus.dev.console.DevConsoleManager; @@ -68,8 +71,10 @@ public class IsolatedDevModeMain implements BiConsumer codeGens) { + ClassLoader old = Thread.currentThread().getContextClassLoader(); try { @@ -89,17 +94,31 @@ public void accept(Integer integer) { || context.isAbortOnFailedStart()) { return; } - System.out.println("Quarkus application exited with code " + integer); - System.out.println("Press Enter to restart or Ctrl + C to quit"); - try { - while (System.in.read() != '\n') { - //noop + final CountDownLatch latch = new CountDownLatch(1); + QuarkusConsole.INSTANCE.pushInputHandler(new InputHandler() { + @Override + public void handleInput(int[] keys) { + for (int i : keys) { + if (i == 'q') { + System.exit(0); + } else { + QuarkusConsole.INSTANCE.popInputHandler(); + latch.countDown(); + } + } } - while (System.in.available() > 0) { - System.in.read(); + + @Override + public void promptHandler(ConsoleStatus promptHandler) { + promptHandler.setPrompt("\u001B[91mQuarkus application exited with code " + integer + + "\nPress [q] or Ctrl + C to quit, any other key to restart"); } + }); + try { + latch.await(); System.out.println("Restarting..."); - RuntimeUpdatesProcessor.INSTANCE.checkForChangedClasses(); + RuntimeUpdatesProcessor.INSTANCE.checkForChangedClasses(false); + RuntimeUpdatesProcessor.INSTANCE.checkForChangedTestClasses(false); restartApp(RuntimeUpdatesProcessor.INSTANCE.checkForFileChange(), null); } catch (Exception e) { log.error("Failed to restart", e); @@ -217,21 +236,19 @@ private RuntimeUpdatesProcessor setupRuntimeCompilation(DevModeContext context, compilationProviders.add(provider); context.getAllModules().forEach(moduleInfo -> moduleInfo.addSourcePaths(provider.handledSourcePaths())); } - ClassLoaderCompiler compiler; - try { - compiler = new ClassLoaderCompiler(Thread.currentThread().getContextClassLoader(), curatedApplication, - compilationProviders, context); - } catch (Exception e) { - log.error("Failed to create compiler, runtime compilation will be unavailable", e); - return null; + QuarkusCompiler compiler = new QuarkusCompiler(curatedApplication, compilationProviders, context); + TestSupport testSupport = null; + if (devModeType == DevModeType.LOCAL && context.getApplicationRoot().getTest().isPresent()) { + testSupport = new TestSupport(curatedApplication, compilationProviders, context); } + RuntimeUpdatesProcessor processor = new RuntimeUpdatesProcessor(appRoot, context, compiler, devModeType, this::restartCallback, null, new BiFunction() { @Override public byte[] apply(String s, byte[] bytes) { return ClassTransformingBuildStep.transform(s, bytes); } - }); + }, testSupport); for (HotReplacementSetup service : ServiceLoader.load(HotReplacementSetup.class, curatedApplication.getBaseRuntimeClassLoader())) { @@ -289,7 +306,14 @@ public void close() { i.close(); } } finally { - curatedApplication.close(); + try { + curatedApplication.close(); + } finally { + if (shutdownThread != null) { + Runtime.getRuntime().removeShutdownHook(shutdownThread); + } + shutdownLatch.countDown(); + } } } } @@ -359,7 +383,7 @@ public boolean test(String s) { QuarkusClassLoader deploymentClassLoader = curatedApplication.createDeploymentClassLoader(); for (DevModeContext.ModuleInfo module : context.getAllModules()) { - if (module.getSourceParents() != null) { // it's null for remote dev + if (module.getSourceParents().isEmpty() && module.getPreBuildOutputDir() != null) { // it's null for remote dev codeGens.addAll( CodeGenerator.init( deploymentClassLoader, @@ -373,7 +397,7 @@ public boolean test(String s) { (DevModeType) params.get(DevModeType.class.getName())); if (RuntimeUpdatesProcessor.INSTANCE != null) { RuntimeUpdatesProcessor.INSTANCE.checkForFileChange(); - RuntimeUpdatesProcessor.INSTANCE.checkForChangedClasses(); + RuntimeUpdatesProcessor.INSTANCE.checkForChangedClasses(true); } firstStart(deploymentClassLoader, codeGens); @@ -385,7 +409,7 @@ public boolean test(String s) { : deploymentProblem); } } - Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + shutdownThread = new Thread(new Runnable() { @Override public void run() { shutdownLatch.countDown(); @@ -399,7 +423,8 @@ public void run() { } } } - }, "Quarkus Shutdown Thread")); + }, "Quarkus Shutdown Thread"); + Runtime.getRuntime().addShutdownHook(shutdownThread); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedRemoteDevModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedRemoteDevModeMain.java index e80b0411662f27..31b3be79dc8aa6 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedRemoteDevModeMain.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedRemoteDevModeMain.java @@ -115,10 +115,9 @@ private RuntimeUpdatesProcessor setupRuntimeCompilation(DevModeContext context, compilationProviders.add(provider); context.getAllModules().forEach(moduleInfo -> moduleInfo.addSourcePaths(provider.handledSourcePaths())); } - ClassLoaderCompiler compiler; + QuarkusCompiler compiler; try { - compiler = new ClassLoaderCompiler(Thread.currentThread().getContextClassLoader(), curatedApplication, - compilationProviders, context); + compiler = new QuarkusCompiler(curatedApplication, compilationProviders, context); } catch (Exception e) { log.error("Failed to create compiler, runtime compilation will be unavailable", e); return null; @@ -136,7 +135,7 @@ public void accept(DevModeContext.ModuleInfo moduleInfo, String s) { public byte[] apply(String s, byte[] bytes) { return ClassTransformingBuildStep.transform(s, bytes); } - }); + }, null); for (HotReplacementSetup service : ServiceLoader.load(HotReplacementSetup.class, curatedApplication.getBaseRuntimeClassLoader())) { @@ -200,7 +199,7 @@ public void accept(CuratedApplication o, Map o2) { if (RuntimeUpdatesProcessor.INSTANCE != null) { RuntimeUpdatesProcessor.INSTANCE.checkForFileChange(); - RuntimeUpdatesProcessor.INSTANCE.checkForChangedClasses(); + RuntimeUpdatesProcessor.INSTANCE.checkForChangedClasses(true); } JarResult result = generateApplication(); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/ClassLoaderCompiler.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusCompiler.java similarity index 75% rename from core/deployment/src/main/java/io/quarkus/deployment/dev/ClassLoaderCompiler.java rename to core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusCompiler.java index 6d0467009f4ea6..67421c3e1d8dfa 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/ClassLoaderCompiler.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusCompiler.java @@ -25,6 +25,7 @@ import org.jboss.logging.Logger; import io.quarkus.bootstrap.app.CuratedApplication; +import io.quarkus.bootstrap.app.QuarkusBootstrap; import io.quarkus.bootstrap.model.AppDependency; /** @@ -32,9 +33,9 @@ * * @author Stuart Douglas */ -public class ClassLoaderCompiler implements Closeable { +public class QuarkusCompiler implements Closeable { - private static final Logger log = Logger.getLogger(ClassLoaderCompiler.class); + private static final Logger log = Logger.getLogger(QuarkusCompiler.class); private static final Pattern WHITESPACE_PATTERN = Pattern.compile(" "); private final List compilationProviders; @@ -44,8 +45,7 @@ public class ClassLoaderCompiler implements Closeable { private final Map compilationContexts = new HashMap<>(); private final Set allHandledExtensions; - public ClassLoaderCompiler(ClassLoader classLoader, - CuratedApplication application, + public QuarkusCompiler(CuratedApplication application, List compilationProviders, DevModeContext context) throws IOException { @@ -65,8 +65,13 @@ public ClassLoaderCompiler(ClassLoader classLoader, } Set classPathElements = new HashSet<>(); for (DevModeContext.ModuleInfo i : context.getAllModules()) { - if (i.getClassesPath() != null) { - classPathElements.add(new File(i.getClassesPath())); + if (i.getMain().getClassesPath() != null) { + classPathElements.add(new File(i.getMain().getClassesPath())); + } + if (application.getQuarkusBootstrap().getMode() == QuarkusBootstrap.Mode.TEST) { + if (i.getTest().isPresent()) { + classPathElements.add(new File(i.getTest().get().getClassesPath())); + } } } final String devModeRunnerJarCanonicalPath = context.getDevModeRunnerJarFile() == null @@ -135,27 +140,10 @@ public ClassLoaderCompiler(ClassLoader classLoader, } } for (DevModeContext.ModuleInfo i : context.getAllModules()) { - if (!i.getSourcePaths().isEmpty()) { - if (i.getClassesPath() == null) { - log.warn("No classes directory found for module '" + i.getName() - + "'. It is advised that this module be compiled before launching dev mode"); - continue; - } - i.getSourcePaths().forEach(sourcePath -> { - this.compilationContexts.put(sourcePath, - new CompilationProvider.Context( - i.getName(), - classPathElements, - i.getProjectDirectory() == null ? null : new File(i.getProjectDirectory()), - new File(sourcePath), - new File(i.getClassesPath()), - context.getSourceEncoding(), - context.getCompilerOptions(), - context.getSourceJavaVersion(), - context.getTargetJvmVersion(), - context.getCompilerPluginArtifacts(), - context.getCompilerPluginsOptions())); - }); + setupSourceCompilationContext(context, classPathElements, i, i.getMain(), + "classes"); + if (application.getQuarkusBootstrap().getMode() == QuarkusBootstrap.Mode.TEST && i.getTest().isPresent()) { + setupSourceCompilationContext(context, classPathElements, i, i.getTest().get(), "test classes"); } } this.allHandledExtensions = new HashSet<>(); @@ -164,6 +152,32 @@ public ClassLoaderCompiler(ClassLoader classLoader, } } + public void setupSourceCompilationContext(DevModeContext context, Set classPathElements, DevModeContext.ModuleInfo i, + DevModeContext.CompilationUnit compilationUnit, String name) { + if (!compilationUnit.getSourcePaths().isEmpty()) { + if (compilationUnit.getSourcePaths() == null) { + log.warn("No " + name + " directory found for module '" + i.getName() + + "'. It is advised that this module be compiled before launching dev mode"); + return; + } + compilationUnit.getSourcePaths().forEach(sourcePath -> { + this.compilationContexts.put(sourcePath, + new CompilationProvider.Context( + i.getName(), + classPathElements, + i.getProjectDirectory() == null ? null : new File(i.getProjectDirectory()), + new File(sourcePath), + new File(compilationUnit.getClassesPath()), + context.getSourceEncoding(), + context.getCompilerOptions(), + context.getSourceJavaVersion(), + context.getTargetJvmVersion(), + context.getCompilerPluginArtifacts(), + context.getCompilerPluginsOptions())); + }); + } + } + public Set allHandledExtensions() { return allHandledExtensions; } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java index 92b052932c2022..62ab65553a46e6 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java @@ -19,6 +19,7 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import java.util.function.Consumer; import java.util.jar.Attributes; import java.util.jar.Manifest; import java.util.zip.ZipEntry; @@ -198,7 +199,18 @@ public B baseName(String baseName) { @SuppressWarnings("unchecked") public B remoteDev(boolean remoteDev) { - QuarkusDevModeLauncher.this.remoteDev = remoteDev; + QuarkusDevModeLauncher.this.entryPointCustomizer = new Consumer() { + @Override + public void accept(DevModeContext devModeContext) { + devModeContext.setMode(QuarkusBootstrap.Mode.REMOTE_DEV_CLIENT); + devModeContext.setAlternateEntryPoint(IsolatedRemoteDevModeMain.class.getName()); + } + }; + return (B) this; + } + + public B entryPointCustomizer(Consumer consumer) { + QuarkusDevModeLauncher.this.entryPointCustomizer = consumer; return (B) this; } @@ -264,7 +276,7 @@ public R build() throws Exception { private Set buildFiles = new HashSet<>(0); private boolean deleteDevJar = true; private String baseName; - private boolean remoteDev; + private Consumer entryPointCustomizer; private String applicationArgs; private Set localArtifacts = new HashSet<>(); private ModuleInfo main; @@ -391,9 +403,8 @@ protected void prepare() throws Exception { // this is the jar file we will use to launch the dev mode main class devModeContext.setDevModeRunnerJarFile(tempFile); - if (remoteDev) { - devModeContext.setMode(QuarkusBootstrap.Mode.REMOTE_DEV_CLIENT); - devModeContext.setAlternateEntryPoint(IsolatedRemoteDevModeMain.class.getName()); + if (entryPointCustomizer != null) { + entryPointCustomizer.accept(devModeContext); } try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(tempFile))) { diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java index 6c3b92acf68fe3..bca6b74a4f9660 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java @@ -28,13 +28,17 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.Timer; +import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; +import java.util.concurrent.locks.ReentrantLock; import java.util.function.BiConsumer; import java.util.function.BiFunction; import java.util.function.Consumer; +import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -48,6 +52,9 @@ import io.quarkus.bootstrap.runner.Timing; import io.quarkus.changeagent.ClassChangeAgent; +import io.quarkus.deployment.dev.testing.TestListener; +import io.quarkus.deployment.dev.testing.TestRunner; +import io.quarkus.deployment.dev.testing.TestSupport; import io.quarkus.deployment.util.FSWatchUtil; import io.quarkus.deployment.util.FileUtil; import io.quarkus.dev.spi.DevModeType; @@ -60,11 +67,11 @@ public class RuntimeUpdatesProcessor implements HotReplacementContext, Closeable private static final String CLASS_EXTENSION = ".class"; - static volatile RuntimeUpdatesProcessor INSTANCE; + public static volatile RuntimeUpdatesProcessor INSTANCE; private final Path applicationRoot; private final DevModeContext context; - private final ClassLoaderCompiler compiler; + private final QuarkusCompiler compiler; private final DevModeType devModeType; volatile Throwable compileProblem; @@ -74,21 +81,13 @@ public class RuntimeUpdatesProcessor implements HotReplacementContext, Closeable private volatile Predicate disableInstrumentationForClassPredicate = new AlwaysFalsePredicate<>(); private volatile Predicate disableInstrumentationForIndexPredicate = new AlwaysFalsePredicate<>(); + private static volatile boolean instrumentationLogPrinted = false; /** - * A first scan is considered done when we have visited all modules at least once. - * This is useful in two ways. - * - To make sure that source time stamps have been recorded at least once - * - To avoid re-compiling on first run by ignoring all first time changes detected by - * {@link RuntimeUpdatesProcessor#checkIfFileModified(Path, Map, boolean)} during the first scan. + * dev mode replacement and test running track their changes separately */ - private volatile boolean firstScanDone = false; - - private static volatile boolean instrumentationLogPrinted = false; - - private final Map sourceFileTimestamps = new ConcurrentHashMap<>(); - private final Map watchedFileTimestamps = new ConcurrentHashMap<>(); - private final Map classFileChangeTimeStamps = new ConcurrentHashMap<>(); - private final Map classFilePathToSourceFilePath = new ConcurrentHashMap<>(); + private final TimestampSet main = new TimestampSet(); + private final TimestampSet test = new TimestampSet(); + final Map sourceFileTimestamps = new ConcurrentHashMap<>(); /** * Resources that appear in both src and target, these will be removed if the src resource subsequently disappears. @@ -102,6 +101,8 @@ public class RuntimeUpdatesProcessor implements HotReplacementContext, Closeable private final BiConsumer, ClassScanResult> restartCallback; private final BiConsumer copyResourceNotification; private final BiFunction classTransformers; + private Timer timer; + private final ReentrantLock scanLock = new ReentrantLock(); /** * The index for the last successful start. Used to determine if the class has changed its structure @@ -109,10 +110,15 @@ public class RuntimeUpdatesProcessor implements HotReplacementContext, Closeable */ private static volatile IndexView lastStartIndex; - public RuntimeUpdatesProcessor(Path applicationRoot, DevModeContext context, ClassLoaderCompiler compiler, + private final TestSupport testSupport; + private volatile boolean firstTestScanComplete; + private volatile Boolean instrumentationEnabled; + + public RuntimeUpdatesProcessor(Path applicationRoot, DevModeContext context, QuarkusCompiler compiler, DevModeType devModeType, BiConsumer, ClassScanResult> restartCallback, BiConsumer copyResourceNotification, - BiFunction classTransformers) { + BiFunction classTransformers, + TestSupport testSupport) { this.applicationRoot = applicationRoot; this.context = context; this.compiler = compiler; @@ -120,30 +126,121 @@ public RuntimeUpdatesProcessor(Path applicationRoot, DevModeContext context, Cla this.restartCallback = restartCallback; this.copyResourceNotification = copyResourceNotification; this.classTransformers = classTransformers; + this.testSupport = testSupport; + if (testSupport != null) { + testSupport.addListener(new TestListener() { + @Override + public void testsEnabled() { + if (!firstTestScanComplete) { + checkForChangedTestClasses(true); + firstTestScanComplete = true; + } + startTestScanningTimer(); + } + + @Override + public void testsDisabled() { + synchronized (RuntimeUpdatesProcessor.this) { + if (timer != null) { + timer.cancel(); + timer = null; + } + } + } + }); + } + } + + public TestSupport getTestSupport() { + return testSupport; } @Override public Path getClassesDir() { //TODO: fix all these for (DevModeContext.ModuleInfo i : context.getAllModules()) { - return Paths.get(i.getClassesPath()); + return Paths.get(i.getMain().getClassesPath()); } return null; } @Override public List getSourcesDir() { - return context.getAllModules().stream().flatMap(m -> m.getSourcePaths().stream()).map(Paths::get).collect(toList()); + return context.getAllModules().stream().flatMap(m -> m.getMain().getSourcePaths().stream()).map(Paths::get) + .collect(toList()); + } + + private Timer startTestScanningTimer() { + synchronized (this) { + if (timer == null) { + timer = new Timer("Test Compile Timer", true); + timer.schedule(new TimerTask() { + @Override + public void run() { + periodicTestCompile(); + } + }, 1, 1000); + } + } + return timer; + } + + private void periodicTestCompile() { + //noop if already scanning + if (scanLock.tryLock()) { + try { + ClassScanResult changedTestClassResult = compileTestClasses(); + ClassScanResult changedApp = checkForChangedClasses(compiler, DevModeContext.ModuleInfo::getMain, false, test); + Set filesChanged = checkForFileChange(DevModeContext.ModuleInfo::getMain, test); + boolean configFileRestartNeeded = filesChanged.stream().map(watchedFilePaths::get) + .anyMatch(Boolean.TRUE::equals); + ClassScanResult merged = ClassScanResult.merge(changedTestClassResult, changedApp); + if (configFileRestartNeeded) { + if (compileProblem != null) { + testSupport.getTestRunner().testCompileFailed(compileProblem); + } else { + testSupport.getTestRunner().runTests(null); + } + } else if (merged.isChanged()) { + if (compileProblem != null) { + testSupport.getTestRunner().testCompileFailed(compileProblem); + } else { + testSupport.getTestRunner().runTests(merged); + } + } + } finally { + scanLock.unlock(); + } + } + } + + private ClassScanResult compileTestClasses() { + QuarkusCompiler testCompiler = testSupport.getCompiler(); + TestRunner testRunner = testSupport.getTestRunner(); + ClassScanResult changedTestClassResult = new ClassScanResult(); + try { + changedTestClassResult = checkForChangedClasses(testCompiler, + m -> m.getTest().orElse(DevModeContext.EMPTY_COMPILATION_UNIT), false, test); + if (compileProblem != null) { + testRunner.testCompileFailed(compileProblem); + compileProblem = null; //we don't want to block the app over a test problem + } else { + testRunner.testCompileSucceeded(); + } + } catch (Throwable e) { + testRunner.testCompileFailed(e); + } + return changedTestClassResult; } @Override public List getResourcesDir() { List ret = new ArrayList<>(); for (DevModeContext.ModuleInfo i : context.getAllModules()) { - if (i.getResourcePath() != null) { - ret.add(Paths.get(i.getResourcePath())); - } else if (i.getResourcesOutputPath() != null) { - ret.add(Paths.get(i.getResourcesOutputPath())); + if (i.getMain().getResourcePath() != null) { + ret.add(Paths.get(i.getMain().getResourcePath())); + } else if (i.getMain().getResourcesOutputPath() != null) { + ret.add(Paths.get(i.getMain().getResourcesOutputPath())); } } Collections.reverse(ret); //make sure the actual project is before dependencies @@ -190,103 +287,119 @@ public DevModeType getDevModeType() { @Override public boolean doScan(boolean userInitiated) throws IOException { - - final long startNanoseconds = System.nanoTime(); - for (Runnable step : preScanSteps) { - try { - step.run(); - } catch (Throwable t) { - log.error("Pre Scan step failed", t); + scanLock.lock(); + try { + if (testSupport != null) { + testSupport.pause(); } - } - - ClassScanResult changedClassResults = checkForChangedClasses(); - Set filesChanged = checkForFileChange(); - boolean configFileRestartNeeded = filesChanged.stream().map(watchedFilePaths::get).anyMatch(Boolean.TRUE::equals); - boolean instrumentationChange = false; - if (ClassChangeAgent.getInstrumentation() != null && lastStartIndex != null && !configFileRestartNeeded - && devModeType != DevModeType.REMOTE_LOCAL_SIDE) { - //attempt to do an instrumentation based reload - //if only code has changed and not the class structure, then we can do a reload - //using the JDK instrumentation API (assuming we were started with the javaagent) - if (changedClassResults.deletedClasses.isEmpty() - && changedClassResults.addedClasses.isEmpty() - && !changedClassResults.changedClasses.isEmpty()) { + final long startNanoseconds = System.nanoTime(); + for (Runnable step : preScanSteps) { try { - Indexer indexer = new Indexer(); - //attempt to use the instrumentation API - ClassDefinition[] defs = new ClassDefinition[changedClassResults.changedClasses.size()]; - int index = 0; - for (Path i : changedClassResults.changedClasses) { - byte[] bytes = Files.readAllBytes(i); - String name = indexer.index(new ByteArrayInputStream(bytes)).name().toString(); - defs[index++] = new ClassDefinition(Thread.currentThread().getContextClassLoader().loadClass(name), - classTransformers.apply(name, bytes)); - } - Index current = indexer.complete(); - boolean ok = instrumentationEnabled() - && !disableInstrumentationForIndexPredicate.test(current); - if (ok) { - for (ClassInfo clazz : current.getKnownClasses()) { - ClassInfo old = lastStartIndex.getClassByName(clazz.name()); - if (!ClassComparisonUtil.isSameStructure(clazz, old) - || disableInstrumentationForClassPredicate.test(clazz)) { - ok = false; - break; + step.run(); + } catch (Throwable t) { + log.error("Pre Scan step failed", t); + } + } + + ClassScanResult changedClassResults = checkForChangedClasses(compiler, DevModeContext.ModuleInfo::getMain, false, + main); + Set filesChanged = checkForFileChange(DevModeContext.ModuleInfo::getMain, main); + + boolean configFileRestartNeeded = filesChanged.stream().map(watchedFilePaths::get).anyMatch(Boolean.TRUE::equals); + boolean instrumentationChange = false; + if (ClassChangeAgent.getInstrumentation() != null && lastStartIndex != null && !configFileRestartNeeded + && devModeType != DevModeType.REMOTE_LOCAL_SIDE) { + //attempt to do an instrumentation based reload + //if only code has changed and not the class structure, then we can do a reload + //using the JDK instrumentation API (assuming we were started with the javaagent) + if (changedClassResults.deletedClasses.isEmpty() + && changedClassResults.addedClasses.isEmpty() + && !changedClassResults.changedClasses.isEmpty()) { + try { + Indexer indexer = new Indexer(); + //attempt to use the instrumentation API + ClassDefinition[] defs = new ClassDefinition[changedClassResults.changedClasses.size()]; + int index = 0; + for (Path i : changedClassResults.changedClasses) { + byte[] bytes = Files.readAllBytes(i); + String name = indexer.index(new ByteArrayInputStream(bytes)).name().toString(); + defs[index++] = new ClassDefinition(Thread.currentThread().getContextClassLoader().loadClass(name), + classTransformers.apply(name, bytes)); + } + Index current = indexer.complete(); + boolean ok = instrumentationEnabled() + && !disableInstrumentationForIndexPredicate.test(current); + if (ok) { + for (ClassInfo clazz : current.getKnownClasses()) { + ClassInfo old = lastStartIndex.getClassByName(clazz.name()); + if (!ClassComparisonUtil.isSameStructure(clazz, old) + || disableInstrumentationForClassPredicate.test(clazz)) { + ok = false; + break; + } } } - } - if (ok) { - log.info("Application restart not required, replacing classes via instrumentation"); - ClassChangeAgent.getInstrumentation().redefineClasses(defs); - instrumentationChange = true; + if (ok) { + log.info("Application restart not required, replacing classes via instrumentation"); + ClassChangeAgent.getInstrumentation().redefineClasses(defs); + instrumentationChange = true; + } + } catch (Exception e) { + log.error("Failed to replace classes via instrumentation", e); + instrumentationChange = false; } - } catch (Exception e) { - log.error("Failed to replace classes via instrumentation", e); - instrumentationChange = false; } } - } - //if there is a deployment problem we always restart on scan - //this is because we can't setup the config file watches - //in an ideal world we would just check every resource file for changes, however as everything is already - //all broken we just assume the reason that they have refreshed is because they have fixed something - //trying to watch all resource files is complex and this is likely a good enough solution for what is already an edge case - boolean restartNeeded = !instrumentationChange && (changedClassResults.isChanged() - || (IsolatedDevModeMain.deploymentProblem != null && userInitiated) || configFileRestartNeeded); - if (restartNeeded) { - restartCallback.accept(filesChanged, changedClassResults); - long timeNanoSeconds = System.nanoTime() - startNanoseconds; - log.infof("Live reload total time: %ss ", Timing.convertToBigDecimalSeconds(timeNanoSeconds)); - if (TimeUnit.SECONDS.convert(timeNanoSeconds, TimeUnit.NANOSECONDS) >= 4 && !instrumentationEnabled()) { - if (!instrumentationLogPrinted) { - instrumentationLogPrinted = true; - log.info( - "Live reload took more than 4 seconds, you may want to enable instrumentation based reload (quarkus.live-reload.instrumentation=true). This allows small changes to take effect without restarting Quarkus."); + //if there is a deployment problem we always restart on scan + //this is because we can't setup the config file watches + //in an ideal world we would just check every resource file for changes, however as everything is already + //all broken we just assume the reason that they have refreshed is because they have fixed something + //trying to watch all resource files is complex and this is likely a good enough solution for what is already an edge case + boolean restartNeeded = !instrumentationChange && (changedClassResults.isChanged() + || (IsolatedDevModeMain.deploymentProblem != null && userInitiated) || configFileRestartNeeded); + if (restartNeeded) { + restartCallback.accept(filesChanged, changedClassResults); + long timeNanoSeconds = System.nanoTime() - startNanoseconds; + log.infof("Live reload total time: %ss ", Timing.convertToBigDecimalSeconds(timeNanoSeconds)); + if (TimeUnit.SECONDS.convert(timeNanoSeconds, TimeUnit.NANOSECONDS) >= 4 && !instrumentationEnabled()) { + if (!instrumentationLogPrinted) { + instrumentationLogPrinted = true; + log.info( + "Live reload took more than 4 seconds, you may want to enable instrumentation based reload (quarkus.live-reload.instrumentation=true). This allows small changes to take effect without restarting Quarkus."); + } } - } - return true; - } else if (!filesChanged.isEmpty()) { - for (Consumer> consumer : noRestartChangesConsumers) { - try { - consumer.accept(filesChanged); - } catch (Throwable t) { - log.error("Changed files consumer failed", t); + return true; + } else if (!filesChanged.isEmpty()) { + for (Consumer> consumer : noRestartChangesConsumers) { + try { + consumer.accept(filesChanged); + } catch (Throwable t) { + log.error("Changed files consumer failed", t); + } } + log.infof("Files changed but restart not needed - notified extensions in: %ss ", + Timing.convertToBigDecimalSeconds(System.nanoTime() - startNanoseconds)); + } else if (instrumentationChange) { + log.infof("Live reload performed via instrumentation, no restart needed, total time: %ss ", + Timing.convertToBigDecimalSeconds(System.nanoTime() - startNanoseconds)); + } + return false; + + } finally { + scanLock.unlock(); + if (testSupport != null) { + testSupport.resume(); } - log.infof("Files changed but restart not needed - notified extensions in: %ss ", - Timing.convertToBigDecimalSeconds(System.nanoTime() - startNanoseconds)); - } else if (instrumentationChange) { - log.infof("Live reload performed via instrumentation, no restart needed, total time: %ss ", - Timing.convertToBigDecimalSeconds(System.nanoTime() - startNanoseconds)); } - return false; } private Boolean instrumentationEnabled() { + if (instrumentationEnabled != null) { + return instrumentationEnabled; + } ClassLoader old = Thread.currentThread().getContextClassLoader(); try { Thread.currentThread().setContextClassLoader(getClass().getClassLoader()); @@ -337,14 +450,42 @@ public Set syncState(Map fileHashes) { } } - ClassScanResult checkForChangedClasses() throws IOException { + ClassScanResult checkForChangedClasses(boolean firstScan) { + ClassScanResult classScanResult = checkForChangedClasses(compiler, DevModeContext.ModuleInfo::getMain, firstScan, main); + test.merge(main); + return classScanResult; + } + + ClassScanResult checkForChangedTestClasses(boolean firstScan) { + if (!testSupport.isStarted()) { + return new ClassScanResult(); + } + ClassScanResult ret = checkForChangedClasses(testSupport.getCompiler(), + s -> s.getTest().orElse(DevModeContext.EMPTY_COMPILATION_UNIT), firstScan, + test); + if (firstScan) { + startTestScanningTimer(); + } + return ret; + } + + /** + * A first scan is considered done when we have visited all modules at least once. + * This is useful in two ways. + * - To make sure that source time stamps have been recorded at least once + * - To avoid re-compiling on first run by ignoring all first time changes detected by + * {@link RuntimeUpdatesProcessor#checkIfFileModified(Path, Map, boolean)} during the first scan. + */ + ClassScanResult checkForChangedClasses(QuarkusCompiler compiler, + Function cuf, boolean firstScan, + TimestampSet timestampSet) { ClassScanResult classScanResult = new ClassScanResult(); - boolean ignoreFirstScanChanges = !firstScanDone; + boolean ignoreFirstScanChanges = firstScan; for (DevModeContext.ModuleInfo module : context.getAllModules()) { final List moduleChangedSourceFilePaths = new ArrayList<>(); - for (String sourcePath : module.getSourcePaths()) { + for (String sourcePath : cuf.apply(module).getSourcePaths()) { final Set changedSourceFiles; Path start = Paths.get(sourcePath); if (!Files.exists(start)) { @@ -354,13 +495,17 @@ ClassScanResult checkForChangedClasses() throws IOException { changedSourceFiles = sourcesStream .parallel() .filter(p -> matchingHandledExtension(p).isPresent() - && sourceFileWasRecentModified(p, ignoreFirstScanChanges)) + && sourceFileWasRecentModified(p, ignoreFirstScanChanges, timestampSet)) .map(Path::toFile) //Needing a concurrent Set, not many standard options: .collect(Collectors.toCollection(ConcurrentSkipListSet::new)); + } catch (IOException e) { + throw new RuntimeException(e); } if (!changedSourceFiles.isEmpty()) { - log.info("Changed source files detected, recompiling " + changedSourceFiles); + classScanResult.compilationHappened = true; + log.info("Changed source files detected, recompiling " + + changedSourceFiles.stream().map(File::getName).collect(Collectors.joining(", "))); try { final Set changedPaths = changedSourceFiles.stream() .map(File::toPath) @@ -377,10 +522,11 @@ && sourceFileWasRecentModified(p, ignoreFirstScanChanges)) } - checkForClassFilesChangesInModule(module, moduleChangedSourceFilePaths, ignoreFirstScanChanges, classScanResult); + checkForClassFilesChangesInModule(module, moduleChangedSourceFilePaths, ignoreFirstScanChanges, classScanResult, + cuf, timestampSet); + } - this.firstScanDone = true; return classScanResult; } @@ -389,13 +535,14 @@ public Throwable getCompileProblem() { } private void checkForClassFilesChangesInModule(DevModeContext.ModuleInfo module, List moduleChangedSourceFiles, - boolean isInitialRun, ClassScanResult classScanResult) { - if (module.getClassesPath() == null) { + boolean isInitialRun, ClassScanResult classScanResult, + Function cuf, TimestampSet timestampSet) { + if (cuf.apply(module).getClassesPath() == null) { return; } try { - for (String folder : module.getClassesPath().split(File.pathSeparator)) { + for (String folder : cuf.apply(module).getClassesPath().split(File.pathSeparator)) { final Path moduleClassesPath = Paths.get(folder); if (!Files.exists(moduleClassesPath)) { continue; @@ -408,32 +555,32 @@ private void checkForClassFilesChangesInModule(DevModeContext.ModuleInfo module, for (Path classFilePath : classFilePaths) { final Path sourceFilePath = retrieveSourceFilePathForClassFile(classFilePath, moduleChangedSourceFiles, - module); + module, cuf, timestampSet); if (sourceFilePath != null) { if (!sourceFilePath.toFile().exists()) { // Source file has been deleted. Delete class and restart - cleanUpClassFile(classFilePath); + cleanUpClassFile(classFilePath, timestampSet); sourceFileTimestamps.remove(sourceFilePath); classScanResult.addDeletedClass(moduleClassesPath, classFilePath); } else { - classFilePathToSourceFilePath.put(classFilePath, sourceFilePath); - if (classFileWasAdded(classFilePath, isInitialRun)) { + timestampSet.classFilePathToSourceFilePath.put(classFilePath, sourceFilePath); + if (classFileWasAdded(classFilePath, isInitialRun, timestampSet)) { // At least one class was recently modified. Restart. classScanResult.addAddedClass(moduleClassesPath, classFilePath); - } else if (classFileWasRecentModified(classFilePath, isInitialRun)) { + } else if (classFileWasRecentModified(classFilePath, isInitialRun, timestampSet)) { // At least one class was recently modified. Restart. classScanResult.addChangedClass(moduleClassesPath, classFilePath); } else if (moduleChangedSourceFiles.contains(sourceFilePath)) { // Source file has been modified, but not the class file // must be a removed inner class - cleanUpClassFile(classFilePath); + cleanUpClassFile(classFilePath, timestampSet); classScanResult.addDeletedClass(moduleClassesPath, classFilePath); } } - } else if (classFileWasAdded(classFilePath, isInitialRun)) { + } else if (classFileWasAdded(classFilePath, isInitialRun, timestampSet)) { classScanResult.addAddedClass(moduleClassesPath, classFilePath); - } else if (classFileWasRecentModified(classFilePath, isInitialRun)) { + } else if (classFileWasRecentModified(classFilePath, isInitialRun, timestampSet)) { classScanResult.addChangedClass(moduleClassesPath, classFilePath); } } @@ -445,18 +592,20 @@ private void checkForClassFilesChangesInModule(DevModeContext.ModuleInfo module, } private Path retrieveSourceFilePathForClassFile(Path classFilePath, List moduleChangedSourceFiles, - DevModeContext.ModuleInfo module) { - Path sourceFilePath = classFilePathToSourceFilePath.get(classFilePath); + DevModeContext.ModuleInfo module, Function cuf, + TimestampSet timestampSet) { + Path sourceFilePath = timestampSet.classFilePathToSourceFilePath.get(classFilePath); if (sourceFilePath == null || moduleChangedSourceFiles.contains(sourceFilePath)) { - sourceFilePath = compiler.findSourcePath(classFilePath, module.getSourcePaths(), module.getClassesPath()); + sourceFilePath = compiler.findSourcePath(classFilePath, cuf.apply(module).getSourcePaths(), + cuf.apply(module).getClassesPath()); } return sourceFilePath; } - private void cleanUpClassFile(Path classFilePath) throws IOException { + private void cleanUpClassFile(Path classFilePath, TimestampSet timestampSet) throws IOException { Files.deleteIfExists(classFilePath); - classFileChangeTimeStamps.remove(classFilePath); - classFilePathToSourceFilePath.remove(classFilePath); + timestampSet.classFileChangeTimeStamps.remove(classFilePath); + timestampSet.classFilePathToSourceFilePath.remove(classFilePath); } private Optional matchingHandledExtension(Path p) { @@ -473,15 +622,20 @@ private String getFileExtension(File file) { } Set checkForFileChange() { + return checkForFileChange(DevModeContext.ModuleInfo::getMain, main); + } + + Set checkForFileChange(Function cuf, + TimestampSet timestampSet) { Set ret = new HashSet<>(); for (DevModeContext.ModuleInfo module : context.getAllModules()) { final Set moduleResources = correspondingResources.computeIfAbsent(module.getName(), m -> Collections.newSetFromMap(new ConcurrentHashMap<>())); boolean doCopy = true; - String rootPath = module.getResourcePath(); - String outputPath = module.getResourcesOutputPath(); + String rootPath = cuf.apply(module).getResourcePath(); + String outputPath = cuf.apply(module).getResourcesOutputPath(); if (rootPath == null) { - rootPath = module.getClassesPath(); + rootPath = cuf.apply(module).getClassesPath(); outputPath = rootPath; doCopy = false; } @@ -504,7 +658,7 @@ Set checkForFileChange() { Path relative = root.relativize(path); Path target = outputDir.resolve(relative); seen.remove(target); - if (!watchedFileTimestamps.containsKey(path)) { + if (!timestampSet.watchedFileTimestamps.containsKey(path)) { moduleResources.add(target); if (!Files.exists(target) || Files.getLastModifiedTime(target).toMillis() < Files .getLastModifiedTime(path).toMillis()) { @@ -544,7 +698,7 @@ Set checkForFileChange() { if (file.toFile().exists()) { try { long value = Files.getLastModifiedTime(file).toMillis(); - Long existing = watchedFileTimestamps.get(file); + Long existing = timestampSet.watchedFileTimestamps.get(file); if (value > existing) { ret.add(path); log.infof("File change detected: %s", file); @@ -555,13 +709,13 @@ Set checkForFileChange() { out.write(data); } } - watchedFileTimestamps.put(file, value); + timestampSet.watchedFileTimestamps.put(file, value); } } catch (IOException e) { throw new UncheckedIOException(e); } } else { - watchedFileTimestamps.put(file, 0L); + timestampSet.watchedFileTimestamps.put(file, 0L); Path target = outputDir.resolve(path); try { FileUtil.deleteDirectory(target); @@ -575,19 +729,21 @@ Set checkForFileChange() { return ret; } - private boolean sourceFileWasRecentModified(final Path sourcePath, boolean ignoreFirstScanChanges) { + private boolean sourceFileWasRecentModified(final Path sourcePath, boolean ignoreFirstScanChanges, + TimestampSet timestampSet) { return checkIfFileModified(sourcePath, sourceFileTimestamps, ignoreFirstScanChanges); } - private boolean classFileWasRecentModified(final Path classFilePath, boolean ignoreFirstScanChanges) { - return checkIfFileModified(classFilePath, classFileChangeTimeStamps, ignoreFirstScanChanges); + private boolean classFileWasRecentModified(final Path classFilePath, boolean ignoreFirstScanChanges, + TimestampSet timestampSet) { + return checkIfFileModified(classFilePath, timestampSet.classFileChangeTimeStamps, ignoreFirstScanChanges); } - private boolean classFileWasAdded(final Path classFilePath, boolean ignoreFirstScanChanges) { - final Long lastRecordedChange = classFileChangeTimeStamps.get(classFilePath); + private boolean classFileWasAdded(final Path classFilePath, boolean ignoreFirstScanChanges, TimestampSet timestampSet) { + final Long lastRecordedChange = timestampSet.classFileChangeTimeStamps.get(classFilePath); if (lastRecordedChange == null) { try { - classFileChangeTimeStamps.put(classFilePath, Files.getLastModifiedTime(classFilePath).toMillis()); + timestampSet.classFileChangeTimeStamps.put(classFilePath, Files.getLastModifiedTime(classFilePath).toMillis()); } catch (IOException e) { throw new UncheckedIOException(e); } @@ -629,14 +785,15 @@ public RuntimeUpdatesProcessor setDisableInstrumentationForIndexPredicate( } public RuntimeUpdatesProcessor setWatchedFilePaths(Map watchedFilePaths) { + boolean includeTest = test.watchedFileTimestamps.isEmpty(); this.watchedFilePaths = watchedFilePaths; - watchedFileTimestamps.clear(); + main.watchedFileTimestamps.clear(); Map extraWatchedFilePaths = new HashMap<>(); for (DevModeContext.ModuleInfo module : context.getAllModules()) { - String rootPath = module.getResourcePath(); + String rootPath = module.getMain().getResourcePath(); if (rootPath == null) { - rootPath = module.getClassesPath(); + rootPath = module.getMain().getClassesPath(); } if (rootPath == null) { continue; @@ -647,17 +804,24 @@ public RuntimeUpdatesProcessor setWatchedFilePaths(Map watchedF if (config.toFile().exists()) { try { FileTime lastModifiedTime = Files.getLastModifiedTime(config); - watchedFileTimestamps.put(config, lastModifiedTime.toMillis()); + main.watchedFileTimestamps.put(config, lastModifiedTime.toMillis()); + if (includeTest) { + test.watchedFileTimestamps.put(config, lastModifiedTime.toMillis()); + } } catch (IOException e) { throw new UncheckedIOException(e); } } else { - watchedFileTimestamps.put(config, 0L); + main.watchedFileTimestamps.put(config, 0L); Map extraWatchedFileTimestamps = expandGlobPattern(root, config); - watchedFileTimestamps.putAll(extraWatchedFileTimestamps); + main.watchedFileTimestamps.putAll(extraWatchedFileTimestamps); for (Path extraPath : extraWatchedFileTimestamps.keySet()) { extraWatchedFilePaths.put(root.relativize(extraPath).toString(), this.watchedFilePaths.get(path)); } + if (includeTest) { + test.watchedFileTimestamps.put(config, 0L); + main.watchedFileTimestamps.putAll(extraWatchedFileTimestamps); + } } } } @@ -683,6 +847,9 @@ public static void setLastStartIndex(IndexView lastStartIndex) { @Override public void close() throws IOException { + if (timer != null) { + timer.cancel(); + } compiler.close(); FSWatchUtil.shutdown(); } @@ -711,4 +878,25 @@ public FileVisitResult visitFileFailed(Path file, IOException exc) { return files; } + public void toggleInstrumentation() { + instrumentationEnabled = !instrumentationEnabled(); + if (instrumentationEnabled) { + log.info("Instrumentation based restart enabled"); + } else { + log.info("Instrumentation based restart disabled"); + } + } + + static class TimestampSet { + final Map watchedFileTimestamps = new ConcurrentHashMap<>(); + final Map classFileChangeTimeStamps = new ConcurrentHashMap<>(); + final Map classFilePathToSourceFilePath = new ConcurrentHashMap<>(); + + public void merge(TimestampSet other) { + watchedFileTimestamps.putAll(other.watchedFileTimestamps); + classFileChangeTimeStamps.putAll(other.classFileChangeTimeStamps); + classFilePathToSourceFilePath.putAll(other.classFilePathToSourceFilePath); + } + } + } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/console/AeshConsole.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/AeshConsole.java new file mode 100644 index 00000000000000..bfb8b26fd24877 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/AeshConsole.java @@ -0,0 +1,264 @@ +package io.quarkus.deployment.dev.console; + +import org.aesh.terminal.Attributes; +import org.aesh.terminal.Connection; +import org.aesh.terminal.tty.Size; +import org.aesh.terminal.utils.ANSI; + +public class AeshConsole extends QuarkusConsole { + + private final Connection connection; + private Size size; + private Attributes attributes; + + private String statusMessage; + private String promptMessage; + private int totalStatusLines = 0; + private int lastWriteCursorX; + + public AeshConsole(Connection connection) { + INSTANCE = this; + this.connection = connection; + connection.openNonBlocking(); + setup(connection); + Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { + @Override + public void run() { + connection.close(); + } + }, "Console Shutdown Hoot")); + } + + private synchronized AeshConsole setStatusMessage(String statusMessage) { + StringBuilder buffer = new StringBuilder(); + clearStatusMessages(buffer); + int newLines = countLines(statusMessage) + countLines(promptMessage); + if (statusMessage == null) { + if (promptMessage != null) { + newLines += 2; + } + } else if (promptMessage == null) { + newLines += 2; + } else { + newLines += 3; + } + if (newLines > totalStatusLines) { + for (int i = 0; i < newLines - totalStatusLines; ++i) { + buffer.append("\n"); + } + } + this.statusMessage = statusMessage; + this.totalStatusLines = newLines; + printStatusAndPrompt(buffer); + connection.write(buffer.toString()); + return this; + } + + public AeshInputHolder createHolder(InputHandler inputHandler) { + return new AeshInputHolder(inputHandler); + } + + private synchronized AeshConsole setPromptMessage(String promptMessage) { + StringBuilder buffer = new StringBuilder(); + clearStatusMessages(buffer); + int newLines = countLines(statusMessage) + countLines(promptMessage); + if (statusMessage == null) { + if (promptMessage != null) { + newLines += 2; + } + } else if (promptMessage == null) { + newLines += 2; + } else { + newLines += 3; + } + if (newLines > totalStatusLines) { + for (int i = 0; i < newLines - totalStatusLines; ++i) { + buffer.append("\n"); + } + } + this.promptMessage = promptMessage; + this.totalStatusLines = newLines; + printStatusAndPrompt(buffer); + connection.write(buffer.toString()); + return this; + } + + private synchronized void end(Connection conn) { + conn.write(ANSI.MAIN_BUFFER); + conn.write(ANSI.CURSOR_SHOW); + conn.setAttributes(attributes); + conn.write("\033[c"); + } + + private void setup(Connection conn) { + size = conn.size(); + // Ctrl-C ends the game + conn.setSignalHandler(event -> { + switch (event) { + case INT: + //todo: why does async exit not work here + //Quarkus.asyncExit(); + //end(conn); + new Thread(new Runnable() { + @Override + public void run() { + System.exit(0); + } + }).start(); + break; + } + }); + // Keyboard handling + conn.setStdinHandler(keys -> { + InputHolder handler = inputHandlers.peek(); + if (handler != null) { + handler.handler.handleInput(keys); + } + }); + + conn.setCloseHandler(close -> end(conn)); + conn.setSizeHandler(size -> setup(conn)); + + //switch to alternate buffer + //conn.write(ANSI.ALTERNATE_BUFFER); + //conn.write(ANSI.CURSOR_HIDE); + + attributes = conn.enterRawMode(); + + StringBuilder sb = new StringBuilder(); + printStatusAndPrompt(sb); + conn.write(sb.toString()); + } + + /** + * prints the status messages + *

+ * this will overwrite the bottom part of the screen + * callers are responsible for writing enough newlines to + * preserve any console history they want. + * + * @param buffer + */ + private void printStatusAndPrompt(StringBuilder buffer) { + if (totalStatusLines == 0) { + return; + } + + clearStatusMessages(buffer); + gotoLine(buffer, size.getHeight() - totalStatusLines); + buffer.append("\n--\n"); + if (statusMessage != null) { + buffer.append(statusMessage); + if (promptMessage != null) { + buffer.append("\n"); + } + } + if (promptMessage != null) { + buffer.append(promptMessage); + } + } + + private void clearStatusMessages(StringBuilder buffer) { + gotoLine(buffer, size.getHeight() - totalStatusLines); + buffer.append("\033[J"); + } + + private StringBuilder gotoLine(StringBuilder builder, int line) { + return builder.append("\033[").append(line).append(";").append(0).append("H"); + } + + int countLines(String s) { + return countLines(s, 0); + } + + int countLines(String s, int cursorPos) { + if (s == null) { + return 0; + } + s = stripAnsiCodes(s); + int lines = 0; + int curLength = cursorPos; + for (int i = 0; i < s.length(); ++i) { + if (s.charAt(i) == '\n') { + lines++; + curLength = 0; + } else if (curLength++ == size.getWidth()) { + lines++; + curLength = 0; + } + } + return lines; + } + + public synchronized void write(String s) { + if (outputFilter != null) { + if (!outputFilter.test(s)) { + return; + } + } + StringBuilder buffer = new StringBuilder(); + clearStatusMessages(buffer); + int cursorPos = lastWriteCursorX; + gotoLine(buffer, size.getHeight()); + String stripped = stripAnsiCodes(s); + int lines = countLines(s, cursorPos); + int trailing = 0; + int index = stripped.lastIndexOf("\n"); + if (index == -1) { + trailing = stripped.length(); + } else { + trailing = stripped.length() - index - 1; + } + + int newCursorPos; + if (lines == 0) { + newCursorPos = trailing + cursorPos; + } else { + newCursorPos = trailing; + } + + if (cursorPos > 1 && lines == 0) { + buffer.append(s); + lastWriteCursorX = newCursorPos; + //partial line, just write it + connection.write(buffer.toString()); + return; + } + if (lines == 0) { + lines++; + } + //move the existing content up by the number of lines + int appendLines = cursorPos > 1 ? lines - 1 : lines; + for (int i = 0; i < appendLines; ++i) { + buffer.append("\n"); + } + buffer.append("\033[").append(size.getHeight() - totalStatusLines - lines).append(";").append(0).append("H"); + buffer.append(s); + lastWriteCursorX = newCursorPos; + printStatusAndPrompt(buffer); + connection.write(buffer.toString()); + + } + + public void write(byte[] buf, int off, int len) { + write(new String(buf, off, len, connection.outputEncoding())); + } + + class AeshInputHolder extends InputHolder { + + protected AeshInputHolder(InputHandler handler) { + super(handler); + } + + @Override + protected void setPromptMessage(String prompt) { + AeshConsole.this.setPromptMessage(prompt); + } + + @Override + protected void setStatusMessage(String status) { + AeshConsole.this.setStatusMessage(status); + + } + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/console/BasicConsole.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/BasicConsole.java new file mode 100644 index 00000000000000..c0d902a0326a1b --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/BasicConsole.java @@ -0,0 +1,89 @@ +package io.quarkus.deployment.dev.console; + +import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.Charset; + +import org.jboss.logging.Logger; + +import io.quarkus.runtime.logging.LoggingSetupRecorder; + +class BasicConsole extends QuarkusConsole { + + private final Logger log = Logger.getLogger(BasicConsole.class); + + final PrintStream printStream; + final boolean noColor; + + BasicConsole(boolean noColor, boolean inputSupport, PrintStream printStream) { + this.noColor = noColor; + this.printStream = printStream; + if (inputSupport) { + Thread t = new Thread(new Runnable() { + @Override + public void run() { + while (true) { + try { + int val = System.in.read(); + if (val == -1) { + return; + } + InputHolder handler = inputHandlers.peek(); + if (handler != null) { + handler.handler.handleInput(new int[] { val }); + } + } catch (IOException e) { + log.error("Failed to read user input", e); + return; + } + } + + } + }, "Quarkus Terminal Reader"); + t.setDaemon(true); + t.start(); + } + } + + @Override + public InputHolder createHolder(InputHandler inputHandler) { + return new InputHolder(inputHandler) { + @Override + protected void setPromptMessage(String prompt) { + if (prompt == null) { + return; + } + write("\n" + prompt + "\n"); + } + + @Override + protected void setStatusMessage(String status) { + if (status == null) { + return; + } + write("\n" + status + "\n"); + } + }; + } + + @Override + public void write(String s) { + if (outputFilter != null) { + if (!outputFilter.test(s)) { + return; + } + } + if (noColor || !LoggingSetupRecorder.hasColorSupport()) { + printStream.print(stripAnsiCodes(s)); + } else { + printStream.print(s); + } + + } + + @Override + public void write(byte[] buf, int off, int len) { + write(new String(buf, off, len, Charset.defaultCharset())); + } + +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/console/InputHandler.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/InputHandler.java new file mode 100644 index 00000000000000..e9801115377194 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/InputHandler.java @@ -0,0 +1,14 @@ +package io.quarkus.deployment.dev.console; + +public interface InputHandler { + + void handleInput(int[] keys); + + void promptHandler(ConsoleStatus promptHandler); + + interface ConsoleStatus { + void setPrompt(String prompt); + + void setStatus(String status); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/console/QuarkusConsole.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/QuarkusConsole.java new file mode 100644 index 00000000000000..ef6488aeb3d717 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/QuarkusConsole.java @@ -0,0 +1,130 @@ +package io.quarkus.deployment.dev.console; + +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import org.aesh.readline.tty.terminal.TerminalConnection; +import org.aesh.terminal.Connection; + +import io.quarkus.deployment.TestConfig; + +public abstract class QuarkusConsole { + + protected final ArrayDeque inputHandlers = new ArrayDeque<>(); + + public static volatile QuarkusConsole INSTANCE = new BasicConsole(false, false, System.out); + + private static volatile boolean installed; + + protected volatile Predicate outputFilter; + + public synchronized void pushInputHandler(InputHandler inputHandler) { + InputHolder holder = inputHandlers.peek(); + if (holder != null) { + holder.setEnabled(false); + } + holder = createHolder(inputHandler); + inputHandler.promptHandler(holder); + holder.setEnabled(true); + inputHandlers.push(holder); + } + + public void popInputHandler() { + InputHolder holder = inputHandlers.pop(); + holder.setEnabled(false); + holder = inputHandlers.peek(); + if (holder != null) { + holder.setEnabled(true); + } + } + + public abstract InputHolder createHolder(InputHandler inputHandler); + + public abstract void write(String s); + + public abstract void write(byte[] buf, int off, int len); + + public static synchronized void installConsole(TestConfig config) { + if (installed) { + return; + } + installed = true; + if (config.basicConsole) { + INSTANCE = new BasicConsole(config.disableColor, true, System.out); + } else { + try { + new TerminalConnection(new Consumer() { + @Override + public void accept(Connection connection) { + if (connection.supportsAnsi()) { + INSTANCE = new AeshConsole(connection); + RedirectPrintStream ps = new RedirectPrintStream(); + System.setOut(ps); + System.setErr(ps); + } else { + connection.close(); + INSTANCE = new BasicConsole(config.disableColor, true, System.out); + } + + } + }); + } catch (IOException e) { + INSTANCE = new BasicConsole(config.disableColor, true, System.out); + } + } + } + + protected String stripAnsiCodes(String s) { + if (s == null) { + return null; + } + s = s.replaceAll("\\u001B\\[(.*?)[a-zA-Z]", ""); + return s; + } + + public void setOutputFilter(Predicate logHandler) { + this.outputFilter = logHandler; + } + + protected static abstract class InputHolder implements InputHandler.ConsoleStatus { + final InputHandler handler; + volatile boolean enabled; + String prompt; + String status; + + protected InputHolder(InputHandler handler) { + this.handler = handler; + } + + public InputHolder setEnabled(boolean enabled) { + this.enabled = enabled; + if (enabled) { + setStatus(status); + setPrompt(prompt); + } + return this; + } + + @Override + public void setPrompt(String prompt) { + this.prompt = prompt; + if (enabled) { + setPromptMessage(prompt); + } + } + + protected abstract void setPromptMessage(String prompt); + + @Override + public void setStatus(String status) { + this.status = status; + if (enabled) { + setStatusMessage(status); + } + } + + protected abstract void setStatusMessage(String status); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/console/RedirectPrintStream.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/RedirectPrintStream.java new file mode 100644 index 00000000000000..6da10e117ab676 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/RedirectPrintStream.java @@ -0,0 +1,186 @@ +package io.quarkus.deployment.dev.console; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.Formatter; +import java.util.Locale; + +public class RedirectPrintStream extends PrintStream { + + private Formatter formatter; + + RedirectPrintStream() { + super(new ByteArrayOutputStream(0)); // never used + } + + @Override + public void write(byte[] buf, int off, int len) { + QuarkusConsole.INSTANCE.write(buf, off, len); + } + + void write(String s) { + QuarkusConsole.INSTANCE.write(s); + } + + @Override + public void write(int b) { + write(new byte[] { (byte) b }); + } + + //@Overide + public void write(byte[] buf) { + write(buf, 0, buf.length); + } + + //@Override + public void writeBytes(byte[] buf) { + write(buf, 0, buf.length); + } + + @Override + public void print(boolean b) { + write(String.valueOf(b)); + } + + @Override + public void print(char c) { + write(String.valueOf(c)); + } + + @Override + public void print(int i) { + write(String.valueOf(i)); + } + + @Override + public void print(long l) { + write(String.valueOf(l)); + } + + @Override + public void print(float f) { + write(String.valueOf(f)); + } + + @Override + public void print(double d) { + write(String.valueOf(d)); + } + + @Override + public void print(char[] s) { + write(String.valueOf(s)); + } + + @Override + public void print(String s) { + write(String.valueOf(s)); + } + + @Override + public void print(Object obj) { + write(String.valueOf(obj)); + } + + @Override + public void println() { + write("\n"); + } + + @Override + public void println(boolean x) { + write(String.valueOf(x) + "\n"); + } + + @Override + public void println(char x) { + write(String.valueOf(x) + "\n"); + } + + @Override + public void println(int x) { + write(String.valueOf(x) + "\n"); + } + + @Override + public void println(long x) { + write(String.valueOf(x) + "\n"); + } + + @Override + public void println(float x) { + write(String.valueOf(x) + "\n"); + } + + @Override + public void println(double x) { + write(String.valueOf(x) + "\n"); + } + + @Override + public void println(char[] x) { + write(String.valueOf(x) + "\n"); + } + + @Override + public void println(String x) { + write(String.valueOf(x) + "\n"); + } + + @Override + public void println(Object x) { + write(String.valueOf(x) + "\n"); + } + + @Override + public PrintStream printf(String format, Object... args) { + return format(format, args); + } + + @Override + public PrintStream printf(Locale l, String format, Object... args) { + return format(l, format, args); + } + + @Override + public PrintStream format(String format, Object... args) { + synchronized (this) { + if ((formatter == null) + || (formatter.locale() != Locale.getDefault(Locale.Category.FORMAT))) + formatter = new Formatter((Appendable) this); + formatter.format(Locale.getDefault(Locale.Category.FORMAT), + format, args); + } + return this; + } + + @Override + public PrintStream format(Locale l, String format, Object... args) { + synchronized (this) { + if ((formatter == null) + || (formatter.locale() != l)) + formatter = new Formatter(this, l); + formatter.format(l, format, args); + } + return this; + } + + @Override + public PrintStream append(CharSequence csq) { + print(String.valueOf(csq)); + return this; + } + + @Override + public PrintStream append(CharSequence csq, int start, int end) { + if (csq == null) + csq = "null"; + return append(csq.subSequence(start, end)); + } + + @Override + public PrintStream append(char c) { + print(c); + return this; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/CurrentTestApplication.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/CurrentTestApplication.java new file mode 100644 index 00000000000000..67c405779b8f33 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/CurrentTestApplication.java @@ -0,0 +1,17 @@ +package io.quarkus.deployment.dev.testing; + +import java.util.function.Consumer; + +import io.quarkus.bootstrap.app.CuratedApplication; + +/** + * This class is a bit of a hack, it provides a way to pass in the current curratedApplication into the TestExtension + */ +public class CurrentTestApplication implements Consumer { + public static volatile CuratedApplication curatedApplication; + + @Override + public void accept(CuratedApplication c) { + curatedApplication = c; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/HtmlAnsiOutputStream.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/HtmlAnsiOutputStream.java new file mode 100644 index 00000000000000..b7347d0ce56d49 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/HtmlAnsiOutputStream.java @@ -0,0 +1,178 @@ +package io.quarkus.deployment.dev.testing; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + +import org.fusesource.jansi.AnsiOutputStream; + +public class HtmlAnsiOutputStream extends AnsiOutputStream { + + private boolean concealOn = false; + + @Override + public void close() throws IOException { + closeAttributes(); + super.close(); + } + + //TODO: this does not belong here. + private static final String[] COLORS = { + "#000000", "#800000", "#008000", "#808000", "#000080", "#800080", "#008080", "#c0c0c0", + "#808080", "#ff0000", "#00ff00", "#ffff00", "#0000ff", "#ff00ff", "#00ffff", "#ffffff", + "#000000", "#00005f", "#000087", "#0000af", "#0000d7", "#0000ff", "#005f00", "#005f5f", + "#005f87", "#005faf", "#005fd7", "#005fff", "#008700", "#00875f", "#008787", "#0087af", + "#0087d7", "#0087ff", "#00af00", "#00af5f", "#00af87", "#00afaf", "#00afd7", "#00afff", + "#00d700", "#00d75f", "#00d787", "#00d7af", "#00d7d7", "#00d7ff", "#00ff00", "#00ff5f", + "#00ff87", "#00ffaf", "#00ffd7", "#00ffff", "#5f0000", "#5f005f", "#5f0087", "#5f00af", + "#5f00d7", "#5f00ff", "#5f5f00", "#5f5f5f", "#5f5f87", "#5f5faf", "#5f5fd7", "#5f5fff", + "#5f8700", "#5f875f", "#5f8787", "#5f87af", "#5f87d7", "#5f87ff", "#5faf00", "#5faf5f", + "#5faf87", "#5fafaf", "#5fafd7", "#5fafff", "#5fd700", "#5fd75f", "#5fd787", "#5fd7af", + "#5fd7d7", "#5fd7ff", "#5fff00", "#5fff5f", "#5fff87", "#5fffaf", "#5fffd7", "#5fffff", + "#870000", "#87005f", "#870087", "#8700af", "#8700d7", "#8700ff", "#875f00", "#875f5f", + "#875f87", "#875faf", "#875fd7", "#875fff", "#878700", "#87875f", "#878787", "#8787af", + "#8787d7", "#8787ff", "#87af00", "#87af5f", "#87af87", "#87afaf", "#87afd7", "#87afff", + "#87d700", "#87d75f", "#87d787", "#87d7af", "#87d7d7", "#87d7ff", "#87ff00", "#87ff5f", + "#87ff87", "#87ffaf", "#87ffd7", "#87ffff", "#af0000", "#af005f", "#af0087", "#af00af", + "#af00d7", "#af00ff", "#af5f00", "#af5f5f", "#af5f87", "#af5faf", "#af5fd7", "#af5fff", + "#af8700", "#af875f", "#af8787", "#af87af", "#af87d7", "#af87ff", "#afaf00", "#afaf5f", + "#afaf87", "#afafaf", "#afafd7", "#afafff", "#afd700", "#afd75f", "#afd787", "#afd7af", + "#afd7d7", "#afd7ff", "#afff00", "#afff5f", "#afff87", "#afffaf", "#afffd7", "#afffff", + "#d70000", "#d7005f", "#d70087", "#d700af", "#d700d7", "#d700ff", "#d75f00", "#d75f5f", + "#d75f87", "#d75faf", "#d75fd7", "#d75fff", "#d78700", "#d7875f", "#d78787", "#d787af", + "#d787d7", "#d787ff", "#d7af00", "#d7af5f", "#d7af87", "#d7afaf", "#d7afd7", "#d7afff", + "#d7d700", "#d7d75f", "#d7d787", "#d7d7af", "#d7d7d7", "#d7d7ff", "#d7ff00", "#d7ff5f", + "#d7ff87", "#d7ffaf", "#d7ffd7", "#d7ffff", "#ff0000", "#ff005f", "#ff0087", "#ff00af", + "#ff00d7", "#ff00ff", "#ff5f00", "#ff5f5f", "#ff5f87", "#ff5faf", "#ff5fd7", "#ff5fff", + "#ff8700", "#ff875f", "#ff8787", "#ff87af", "#ff87d7", "#ff87ff", "#ffaf00", "#ffaf5f", + "#ffaf87", "#ffafaf", "#ffafd7", "#ffafff", "#ffd700", "#ffd75f", "#ffd787", "#ffd7af", + "#ffd7d7", "#ffd7ff", "#ffff00", "#ffff5f", "#ffff87", "#ffffaf", "#ffffd7", "#ffffff", + "#080808", "#121212", "#1c1c1c", "#262626", "#303030", "#3a3a3a", "#444444", "#4e4e4e", + "#585858", "#606060", "#666666", "#767676", "#808080", "#8a8a8a", "#949494", "#9e9e9e", + "#a8a8a8", "#b2b2b2", "#bcbcbc", "#c6c6c6", "#d0d0d0", "#dadada", "#e4e4e4", "#eeeeee", + }; + private static final String[] ANSI_COLOR_MAP = { "black", "red", + "green", "yellow", "blue", "magenta", "cyan", "white", }; + + private static final byte[] BYTES_QUOT = """.getBytes(); + private static final byte[] BYTES_AMP = "&".getBytes(); + private static final byte[] BYTES_LT = "<".getBytes(); + private static final byte[] BYTES_GT = ">".getBytes(); + + public HtmlAnsiOutputStream(OutputStream os) { + super(os); + } + + private final List closingAttributes = new ArrayList(); + + private void write(String s) throws IOException { + super.out.write(s.getBytes()); + } + + private void writeAttribute(String s) throws IOException { + write("<" + s + ">"); + closingAttributes.add(0, s.split(" ", 2)[0]); + } + + private void closeAttributes() throws IOException { + for (String attr : closingAttributes) { + write(""); + } + closingAttributes.clear(); + } + + public void write(int data) throws IOException { + switch (data) { + case 34: // " + out.write(BYTES_QUOT); + break; + case 38: // & + out.write(BYTES_AMP); + break; + case 60: // < + out.write(BYTES_LT); + break; + case 62: // > + out.write(BYTES_GT); + break; + default: + super.write(data); + } + } + + public void writeLine(byte[] buf, int offset, int len) throws IOException { + write(buf, offset, len); + closeAttributes(); + } + + @Override + protected void processSetAttribute(int attribute) throws IOException { + switch (attribute) { + case ATTRIBUTE_CONCEAL_ON: + write("\u001B[8m"); + concealOn = true; + break; + case ATTRIBUTE_INTENSITY_BOLD: + writeAttribute("b"); + break; + case ATTRIBUTE_INTENSITY_NORMAL: + closeAttributes(); + break; + case ATTRIBUTE_UNDERLINE: + writeAttribute("u"); + break; + case ATTRIBUTE_UNDERLINE_OFF: + closeAttributes(); + break; + case ATTRIBUTE_NEGATIVE_ON: + break; + case ATTRIBUTE_NEGATIVE_OFF: + break; + default: + break; + } + } + + @Override + protected void processAttributeRest() throws IOException { + if (concealOn) { + write("\u001B[0m"); + concealOn = false; + } + closeAttributes(); + } + + @Override + protected void processSetForegroundColor(int color, boolean bright) throws IOException { + writeAttribute("span style=\"color: " + ANSI_COLOR_MAP[color] + ";\""); + } + + @Override + protected void processSetBackgroundColor(int color, boolean bright) throws IOException { + writeAttribute("span style=\"background-color: " + ANSI_COLOR_MAP[color] + ";\""); + } + + @Override + protected void processSetForegroundColorExt(int r, int g, int b) throws IOException { + writeAttribute( + "span style=\"color: #" + Integer.toHexString(r) + Integer.toHexString(g) + Integer.toHexString(b) + ";\""); + } + + @Override + protected void processSetBackgroundColorExt(int r, int g, int b) throws IOException { + writeAttribute("span style=\"background-color: #" + Integer.toHexString(r) + Integer.toHexString(g) + + Integer.toHexString(b) + ";\""); + } + + @Override + protected void processSetForegroundColorExt(int paletteIndex) throws IOException { + writeAttribute("span style=\"color: " + COLORS[paletteIndex] + ";\""); + } + + @Override + protected void processSetBackgroundColorExt(int paletteIndex) throws IOException { + writeAttribute("span style=\"background-color: " + COLORS[paletteIndex] + ";\""); + } + +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java new file mode 100644 index 00000000000000..b32fa1406ef09f --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java @@ -0,0 +1,649 @@ +package io.quarkus.deployment.dev.testing; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.net.MalformedURLException; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.regex.Pattern; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Index; +import org.jboss.jandex.Indexer; +import org.jboss.logging.Logger; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Tags; +import org.junit.platform.engine.FilterResult; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.TestSource; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.engine.reporting.ReportEntry; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.descriptor.MethodSource; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.PostDiscoveryFilter; +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestIdentifier; +import org.junit.platform.launcher.TestPlan; +import org.junit.platform.launcher.core.LauncherConfig; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.launcher.core.LauncherFactory; +import org.opentest4j.TestAbortedException; + +import io.quarkus.bootstrap.app.CuratedApplication; +import io.quarkus.deployment.dev.ClassScanResult; +import io.quarkus.deployment.dev.DevModeContext; +import io.quarkus.deployment.dev.console.QuarkusConsole; +import io.quarkus.dev.testing.TracingHandler; + +/** + * This class is responsible for running a single run of JUnit tests. + */ +public class JunitTestRunner { + + private static final Logger log = Logger.getLogger(JunitTestRunner.class); + private final long runId; + private final DevModeContext devModeContext; + private final CuratedApplication testApplication; + private final ClassScanResult classScanResult; + private final TestClassUsages testClassUsages; + private final TestState testState; + private final List listeners; + List additionalFilters; + private final Set includeTags; + private final Set excludeTags; + private final Pattern include; + private final Pattern exclude; + private final boolean displayInConsole; + + private volatile boolean testsRunning = false; + private volatile boolean aborted; + private volatile boolean paused; + + public JunitTestRunner(Builder builder) { + this.runId = builder.runId; + this.devModeContext = builder.devModeContext; + this.testApplication = builder.testApplication; + this.classScanResult = builder.classScanResult; + this.testClassUsages = builder.testClassUsages; + this.listeners = builder.listeners; + this.additionalFilters = builder.additionalFilters; + this.testState = builder.testState; + this.includeTags = new HashSet<>(builder.includeTags); + this.excludeTags = new HashSet<>(builder.excludeTags); + this.include = builder.include; + this.exclude = builder.exclude; + this.displayInConsole = builder.displayInConsole; + } + + public void runTests() { + long start = System.currentTimeMillis(); + ClassLoader old = Thread.currentThread().getContextClassLoader(); + try { + + ClassLoader tcl = testApplication.createDeploymentClassLoader(); + Thread.currentThread().setContextClassLoader(tcl); + ((Consumer) tcl.loadClass(CurrentTestApplication.class.getName()).newInstance()).accept(testApplication); + + List> quarkusTestClasses = discoverTestClasses(devModeContext); + + Launcher launcher = LauncherFactory.create(LauncherConfig.builder().build()); + LauncherDiscoveryRequestBuilder launchBuilder = new LauncherDiscoveryRequestBuilder() + .selectors(quarkusTestClasses.stream().map(DiscoverySelectors::selectClass).collect(Collectors.toList())); + if (classScanResult != null) { + launchBuilder.filters(testClassUsages.getTestsToRun(classScanResult.getChangedClassNames(), testState)); + } + if (!includeTags.isEmpty()) { + launchBuilder.filters(new TagFilter(false, includeTags)); + } else if (!excludeTags.isEmpty()) { + launchBuilder.filters(new TagFilter(true, excludeTags)); + } + if (include != null) { + launchBuilder.filters(new RegexFilter(false, include)); + } else if (exclude != null) { + launchBuilder.filters(new RegexFilter(true, exclude)); + } + if (!additionalFilters.isEmpty()) { + launchBuilder.filters(additionalFilters.toArray(new PostDiscoveryFilter[0])); + } + LauncherDiscoveryRequest request = launchBuilder + .build(); + TestPlan testPlan = launcher.discover(request); + if (!testPlan.containsTests()) { + //nothing to see here + return; + } + long toRun = testPlan.countTestIdentifiers(TestIdentifier::isTest); + for (TestRunListener listener : listeners) { + listener.runStarted(toRun); + } + log.debug("Starting test run with " + quarkusTestClasses.size() + " test cases"); + TestLogCapturingHandler logHandler = new TestLogCapturingHandler(); + QuarkusConsole.INSTANCE.setOutputFilter(logHandler); + + final Deque> touchedClasses = new LinkedBlockingDeque<>(); + final AtomicReference> startupClasses = new AtomicReference<>(); + TracingHandler.setTracingHandler(new TracingHandler.TraceListener() { + @Override + public void touched(String className) { + Set set = touchedClasses.peek(); + if (set != null) { + set.add(className); + } + } + + @Override + public void quarkusStarting() { + startupClasses.set(touchedClasses.peek()); + } + }); + + Map> resultsByClass = new HashMap<>(); + + launcher.execute(testPlan, new TestExecutionListener() { + + @Override + public void executionStarted(TestIdentifier testIdentifier) { + String className = ""; + if (testIdentifier.getSource().isPresent()) { + if (testIdentifier.getSource().get() instanceof MethodSource) { + className = ((MethodSource) testIdentifier.getSource().get()).getClassName(); + } else if (testIdentifier.getSource().get() instanceof ClassSource) { + className = ((ClassSource) testIdentifier.getSource().get()).getClassName(); + } + } + for (TestRunListener listener : listeners) { + listener.testStarted(testIdentifier, className); + } + waitTillResumed(); + touchedClasses.push(Collections.synchronizedSet(new HashSet<>())); + } + + @Override + public void executionSkipped(TestIdentifier testIdentifier, String reason) { + waitTillResumed(); + } + + @Override + public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { + + if (aborted) { + return; + } + Class testClass = null; + String displayName = testIdentifier.getDisplayName(); + TestSource testSource = testIdentifier.getSource().orElse(null); + Set touched = touchedClasses.pop(); + UniqueId id = UniqueId.parse(testIdentifier.getUniqueId()); + if (testSource instanceof ClassSource) { + testClass = ((ClassSource) testSource).getJavaClass(); + if (testExecutionResult.getStatus() != TestExecutionResult.Status.ABORTED) { + for (Set i : touchedClasses) { + //also add the parent touched classes + touched.addAll(i); + } + if (startupClasses.get() != null) { + touched.addAll(startupClasses.get()); + } + testClassUsages.updateTestData(testClass.getName(), touched); + } + } else if (testSource instanceof MethodSource) { + testClass = ((MethodSource) testSource).getJavaClass(); + displayName = testClass.getSimpleName() + "#" + displayName; + + if (testExecutionResult.getStatus() != TestExecutionResult.Status.ABORTED) { + for (Set i : touchedClasses) { + //also add the parent touched classes + touched.addAll(i); + } + if (startupClasses.get() != null) { + touched.addAll(startupClasses.get()); + } + testClassUsages.updateTestData(testClass.getName(), id, + touched); + } + } + if (testClass != null) { + Map results = resultsByClass.computeIfAbsent(testClass.getName(), + s -> new HashMap<>()); + TestResult result = new TestResult(displayName, testClass.getName(), id, testExecutionResult, + logHandler.captureOutput(), testIdentifier.isTest(), runId); + results.put(id, result); + if (result.isTest()) { + for (TestRunListener listener : listeners) { + listener.testComplete(result); + } + } + } + if (testExecutionResult.getStatus() == TestExecutionResult.Status.FAILED) { + Throwable throwable = testExecutionResult.getThrowable().get(); + if (testClass != null) { + //first we cut all the platform stuff out of the stack trace + StackTraceElement[] st = throwable.getStackTrace(); + for (int i = st.length - 1; i >= 0; --i) { + StackTraceElement elem = st[i]; + if (elem.getClassName().equals(testClass.getName())) { + StackTraceElement[] newst = new StackTraceElement[i + 1]; + System.arraycopy(st, 0, newst, 0, i + 1); + st = newst; + break; + } + } + + //now cut out all the restassured internals + //TODO: this should be pluggable + for (int i = st.length - 1; i >= 0; --i) { + StackTraceElement elem = st[i]; + if (elem.getClassName().startsWith("io.restassured")) { + StackTraceElement[] newst = new StackTraceElement[st.length - i]; + System.arraycopy(st, i, newst, 0, st.length - i); + st = newst; + break; + } + } + throwable.setStackTrace(st); + } + } + } + + @Override + public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry entry) { + + } + }); + if (aborted) { + return; + } + testState.updateResults(resultsByClass); + if (classScanResult != null) { + testState.classesRemoved(classScanResult.getDeletedClassNames()); + } + + QuarkusConsole.INSTANCE.setOutputFilter(null); + List historicFailures = testState.getHistoricFailures(resultsByClass); + + for (TestRunListener listener : listeners) { + listener.runComplete(new TestRunResults(runId, classScanResult, classScanResult == null, start, + System.currentTimeMillis(), toResultsMap(historicFailures, resultsByClass))); + } + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + QuarkusConsole.INSTANCE.setOutputFilter(null); + Thread.currentThread().setContextClassLoader(old); + } + } + + public synchronized void abort() { + for (TestRunListener listener : listeners) { + listener.runAborted(); + } + aborted = true; + notifyAll(); + } + + public synchronized void pause() { + //todo + paused = true; + } + + public synchronized void resume() { + paused = false; + notifyAll(); + } + + private Map toResultsMap(List historicFailures, + Map> resultsByClass) { + Map resultMap = new HashMap<>(); + Map> historicMap = new HashMap<>(); + for (TestResult i : historicFailures) { + historicMap.computeIfAbsent(i.getTestClass(), s -> new ArrayList<>()).add(i); + } + Set classes = new HashSet<>(resultsByClass.keySet()); + classes.addAll(historicMap.keySet()); + for (String clazz : classes) { + List passing = new ArrayList<>(); + List failing = new ArrayList<>(); + List skipped = new ArrayList<>(); + for (TestResult i : Optional.ofNullable(resultsByClass.get(clazz)).orElse(Collections.emptyMap()).values()) { + if (i.getTestExecutionResult().getStatus() == TestExecutionResult.Status.FAILED) { + failing.add(i); + } else if (i.getTestExecutionResult().getStatus() == TestExecutionResult.Status.ABORTED) { + skipped.add(i); + } else { + passing.add(i); + } + } + for (TestResult i : Optional.ofNullable(historicMap.get(clazz)).orElse(Collections.emptyList())) { + if (i.getTestExecutionResult().getStatus() == TestExecutionResult.Status.FAILED) { + failing.add(i); + } else if (i.getTestExecutionResult().getStatus() == TestExecutionResult.Status.ABORTED) { + skipped.add(i); + } else { + passing.add(i); + } + } + resultMap.put(clazz, new TestClassResult(clazz, passing, failing, skipped)); + } + return resultMap; + } + + public void waitTillResumed() { + synchronized (JunitTestRunner.this) { + while (paused && !aborted) { + try { + JunitTestRunner.this.wait(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + if (aborted) { + throw new TestAbortedException("Tests are disabled"); + } + } + } + + private static List> discoverTestClasses(DevModeContext devModeContext) { + //maven has a lot of rules around this and is configurable + //for now this is out of scope, we are just going to consider all @QuarkusTest classes + //we can revisit this later + + //simple class loading + List classRoots = new ArrayList<>(); + try { + for (DevModeContext.ModuleInfo i : devModeContext.getAllModules()) { + classRoots.add(Paths.get(i.getMain().getClassesPath()).toFile().toURL()); + } + //we know test is not empty, otherwise we would not be runnning + classRoots.add(Paths.get(devModeContext.getApplicationRoot().getTest().get().getClassesPath()).toFile().toURL()); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + URLClassLoader ucl = new URLClassLoader(classRoots.toArray(new URL[0]), Thread.currentThread().getContextClassLoader()); + + //we also only run tests from the current module, which we can also revisit later + Indexer indexer = new Indexer(); + try (Stream files = Files.walk(Paths.get(devModeContext.getApplicationRoot().getTest().get().getClassesPath()))) { + files.filter(s -> s.getFileName().toString().endsWith(".class")).forEach(s -> { + try (InputStream in = Files.newInputStream(s)) { + indexer.index(in); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } catch (IOException e) { + throw new RuntimeException(e); + } + + //todo: sort by profile, account for modules + Index index = indexer.complete(); + List> ret = new ArrayList<>(); + for (AnnotationInstance i : index.getAnnotations(DotName.createSimple("io.quarkus.test.junit.QuarkusTest"))) { + try { + ret.add(ucl.loadClass(i.target().asClass().name().toString())); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + ret.sort(Comparator.comparing(new Function, String>() { + @Override + public String apply(Class aClass) { + ClassInfo def = index.getClassByName(DotName.createSimple(aClass.getName())); + AnnotationInstance testProfile = def.classAnnotation(DotName.createSimple("io.quarkus.test.junit.TestProfile")); + if (testProfile == null) { + return "$$" + aClass.getName(); + } + return testProfile.value().asClass().name().toString() + "$$" + aClass.getName(); + } + })); + return ret; + } + + public TestState getResults() { + return testState; + } + + public boolean isRunning() { + return testsRunning; + } + + private class TestLogCapturingHandler implements Predicate { + + private final List logOutput; + + public TestLogCapturingHandler() { + this.logOutput = new ArrayList<>(); + } + + public List captureOutput() { + List ret = new ArrayList<>(logOutput); + logOutput.clear(); + return ret; + } + + @Override + public boolean test(String logRecord) { + Thread thread = Thread.currentThread(); + ClassLoader cl = thread.getContextClassLoader(); + while (cl.getParent() != null) { + if (cl == testApplication.getAugmentClassLoader() + || cl == testApplication.getBaseRuntimeClassLoader()) { + //TODO: for convenience we save the log records as HTML rather than ansci here + synchronized (logOutput) { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + HtmlAnsiOutputStream outputStream = new HtmlAnsiOutputStream(out) { + }; + try { + outputStream.write(logRecord.getBytes(StandardCharsets.UTF_8)); + logOutput.add(new String(out.toByteArray(), StandardCharsets.UTF_8)); + } catch (IOException e) { + log.error("Failed to capture log record", e); + logOutput.add(logRecord); + } + } + return displayInConsole; + } + cl = cl.getParent(); + } + return true; + } + } + + static class Builder { + private TestState testState; + private long runId = -1; + private DevModeContext devModeContext; + private CuratedApplication testApplication; + private ClassScanResult classScanResult; + private TestClassUsages testClassUsages; + private final List listeners = new ArrayList<>(); + private final List additionalFilters = new ArrayList<>(); + private List includeTags = Collections.emptyList(); + private List excludeTags = Collections.emptyList(); + private Pattern include; + private Pattern exclude; + private boolean displayInConsole; + + public Builder setRunId(long runId) { + this.runId = runId; + return this; + } + + public Builder setDevModeContext(DevModeContext devModeContext) { + this.devModeContext = devModeContext; + return this; + } + + public Builder setTestApplication(CuratedApplication testApplication) { + this.testApplication = testApplication; + return this; + } + + public Builder setClassScanResult(ClassScanResult classScanResult) { + this.classScanResult = classScanResult; + return this; + } + + public Builder setIncludeTags(List includeTags) { + this.includeTags = includeTags; + return this; + } + + public Builder setExcludeTags(List excludeTags) { + this.excludeTags = excludeTags; + return this; + } + + public Builder setTestClassUsages(TestClassUsages testClassUsages) { + this.testClassUsages = testClassUsages; + return this; + } + + public Builder addListener(TestRunListener listener) { + this.listeners.add(listener); + return this; + } + + public Builder addAdditionalFilter(PostDiscoveryFilter filter) { + this.additionalFilters.add(filter); + return this; + } + + public Builder setTestState(TestState testState) { + this.testState = testState; + return this; + } + + public Builder setInclude(Pattern include) { + this.include = include; + return this; + } + + public Builder setExclude(Pattern exclude) { + this.exclude = exclude; + return this; + } + + public Builder setDisplayInConsole(boolean displayInConsole) { + this.displayInConsole = displayInConsole; + return this; + } + + public JunitTestRunner build() { + Objects.requireNonNull(devModeContext, "devModeContext"); + Objects.requireNonNull(testClassUsages, "testClassUsages"); + Objects.requireNonNull(testApplication, "testApplication"); + Objects.requireNonNull(testState, "testState"); + return new JunitTestRunner(this); + } + + } + + private static class TagFilter implements PostDiscoveryFilter { + + final boolean exclude; + final Set tags; + + private TagFilter(boolean exclude, Set tags) { + this.exclude = exclude; + this.tags = tags; + } + + @Override + public FilterResult apply(TestDescriptor testDescriptor) { + if (testDescriptor.getSource().isPresent()) { + if (testDescriptor.getSource().get() instanceof MethodSource) { + MethodSource methodSource = (MethodSource) testDescriptor.getSource().get(); + Method m = methodSource.getJavaMethod(); + FilterResult res = filterTags(m); + if (res != null) { + return res; + } + res = filterTags(methodSource.getJavaClass()); + if (res != null) { + return res; + } + return FilterResult.includedIf(exclude); + } + } + return FilterResult.included("not a method"); + } + + public FilterResult filterTags(AnnotatedElement clz) { + Tag tag = clz.getAnnotation(Tag.class); + Tags tagsAnn = clz.getAnnotation(Tags.class); + List all = null; + if (tag != null) { + all = Collections.singletonList(tag); + } else if (tagsAnn != null) { + all = Arrays.asList(tagsAnn.value()); + } else { + return null; + } + for (Tag i : all) { + if (tags.contains(i.value())) { + return FilterResult.includedIf(!exclude); + } + } + return FilterResult.includedIf(exclude); + } + } + + private static class RegexFilter implements PostDiscoveryFilter { + + final boolean exclude; + final Pattern pattern; + + private RegexFilter(boolean exclude, Pattern pattern) { + this.exclude = exclude; + this.pattern = pattern; + } + + @Override + public FilterResult apply(TestDescriptor testDescriptor) { + if (testDescriptor.getSource().isPresent()) { + if (testDescriptor.getSource().get() instanceof MethodSource) { + MethodSource methodSource = (MethodSource) testDescriptor.getSource().get(); + String name = methodSource.getJavaClass().getName(); + if (pattern.matcher(name).matches()) { + return FilterResult.includedIf(!exclude); + } + return FilterResult.includedIf(exclude); + } + } + return FilterResult.included("not a method"); + } + } + +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestClassResult.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestClassResult.java new file mode 100644 index 00000000000000..255e9af8028f8e --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestClassResult.java @@ -0,0 +1,60 @@ +package io.quarkus.deployment.dev.testing; + +import java.util.ArrayList; +import java.util.List; + +public class TestClassResult implements Comparable { + final String className; + final List passing; + final List failing; + final List skipped; + final long latestRunId; + + public TestClassResult(String className, List passing, List failing, List skipped) { + this.className = className; + this.passing = passing; + this.failing = failing; + this.skipped = skipped; + long runId = 0; + for (TestResult i : passing) { + runId = Math.max(i.runId, runId); + } + for (TestResult i : failing) { + runId = Math.max(i.runId, runId); + } + latestRunId = runId; + } + + public String getClassName() { + return className; + } + + public List getPassing() { + return passing; + } + + public List getFailing() { + return failing; + } + + public List getSkipped() { + return skipped; + } + + public long getLatestRunId() { + return latestRunId; + } + + @Override + public int compareTo(TestClassResult o) { + return className.compareTo(o.className); + } + + public List getResults() { + List ret = new ArrayList<>(); + ret.addAll(passing); + ret.addAll(failing); + ret.addAll(skipped); + return ret; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestClassUsages.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestClassUsages.java new file mode 100644 index 00000000000000..e5e80c9c19389c --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestClassUsages.java @@ -0,0 +1,134 @@ +package io.quarkus.deployment.dev.testing; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import org.junit.platform.engine.FilterResult; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestSource; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.descriptor.ClassSource; +import org.junit.platform.engine.support.descriptor.MethodSource; +import org.junit.platform.launcher.PostDiscoveryFilter; + +public class TestClassUsages implements Serializable { + + private final Map> classNames = new HashMap<>(); + + public synchronized void updateTestData(String currentclass, UniqueId test, Set touched) { + classNames.put(new ClassAndMethod(currentclass, test), touched); + } + + public synchronized void updateTestData(String currentclass, Set touched) { + classNames.put(new ClassAndMethod(currentclass, null), touched); + } + + public synchronized void merge(TestClassUsages newData) { + classNames.putAll(newData.classNames); + } + + public synchronized PostDiscoveryFilter getTestsToRun(Set changedClasses, TestState testState) { + + Set touchedIds = new HashSet<>(); + //classes that have at least one test + Set testClassesToRun = new HashSet<>(); + for (Map.Entry> entry : classNames.entrySet()) { + if (entry.getKey().uniqueId != null) { + if (changedClasses.contains(entry.getKey().className)) { + touchedIds.add(entry.getKey().uniqueId); + testClassesToRun.add(entry.getKey().className); + } else { + for (String i : changedClasses) { + if (entry.getValue().contains(i)) { + touchedIds.add(entry.getKey().uniqueId); + testClassesToRun.add(entry.getKey().className); + break; + } + } + } + } + } + + return new PostDiscoveryFilter() { + @Override + public FilterResult apply(TestDescriptor testDescriptor) { + if (testState.isFailed(testDescriptor)) { + return FilterResult.included("Test failed previously"); + } + if (!testDescriptor.getSource().isPresent()) { + return FilterResult.included("No source information"); + } + if (touchedIds.contains(testDescriptor.getUniqueId())) { + return FilterResult.included("Class was touched"); + } + TestSource source = testDescriptor.getSource().get(); + if (source instanceof ClassSource) { + String testClassName = ((ClassSource) source).getClassName(); + ClassAndMethod cm = new ClassAndMethod(testClassName, null); + if (!classNames.containsKey(cm)) { + return FilterResult.included("No test information"); + } else if (changedClasses.contains(testClassName)) { + return FilterResult.included("Test case was modified"); + } else if (testClassesToRun.contains(testClassName)) { + return FilterResult.included("Has at least one test"); + } else { + return FilterResult.excluded("Has no tests"); + } + } else if (source instanceof MethodSource) { + MethodSource ms = (MethodSource) source; + ClassAndMethod cm = new ClassAndMethod(ms.getClassName(), testDescriptor.getUniqueId()); + if (!classNames.containsKey(cm)) { + return FilterResult.included("No test information"); + } else if (changedClasses.contains(ms.getClassName())) { + return FilterResult.included("Test case was modified"); + } else if (touchedIds.contains(testDescriptor.getUniqueId())) { + return FilterResult.included("Test touches changed classes"); + } else { + return FilterResult.excluded("Test does not need to run"); + } + } else { + return FilterResult.included("Unknown source type"); + } + + } + }; + } + + private static final class ClassAndMethod implements Serializable { + private final String className; + private final UniqueId uniqueId; + + private ClassAndMethod(String className, UniqueId uniqueId) { + this.className = className; + this.uniqueId = uniqueId; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ClassAndMethod that = (ClassAndMethod) o; + return Objects.equals(className, that.className) && + Objects.equals(uniqueId, that.uniqueId); + } + + @Override + public int hashCode() { + return Objects.hash(className, uniqueId); + } + + public String getClassName() { + return className; + } + + public UniqueId getUniqueId() { + return uniqueId; + } + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConsoleHandler.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConsoleHandler.java new file mode 100644 index 00000000000000..905c668d9bb299 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConsoleHandler.java @@ -0,0 +1,201 @@ +package io.quarkus.deployment.dev.testing; + +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; + +import org.jboss.logging.Logger; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.launcher.TestIdentifier; + +import io.quarkus.deployment.dev.RuntimeUpdatesProcessor; +import io.quarkus.deployment.dev.console.InputHandler; +import io.quarkus.deployment.dev.console.QuarkusConsole; + +public class TestConsoleHandler implements TestListener { + + private static final Logger log = Logger.getLogger("io.quarkus.test"); + + public static final String DISABLED_PROMPT = "\u001b[33mTests Disabled, press [e] to enable\u001b[0m"; + public static final String FIRST_RUN_PROMPT = "\u001b[33mRunning Tests for the first time\u001b[0m"; + public static final String RUNNING_PROMPT = "Press [r] to re-run, [v] to view full results, [d] to disable, [h] for more options>"; + public static final String ABORTED_PROMPT = "Test run aborted."; + + boolean firstRun = true; + boolean disabled = true; + volatile InputHandler.ConsoleStatus promptHandler; + volatile TestController testController; + private String lastStatus; + + public void install() { + QuarkusConsole.INSTANCE.pushInputHandler(inputHandler); + } + + private final InputHandler inputHandler = new InputHandler() { + + @Override + public void handleInput(int[] keys) { + if (disabled) { + for (int i : keys) { + if (i == 'e') { + TestSupport.instance().get().start(); + } + } + } else if (!firstRun) { + //TODO: some of this is a bit yuck, this needs some work + for (int k : keys) { + if (k == 'r') { + testController.runAllTests(); + } + if (k == 'f') { + testController.runFailedTests(); + } else if (k == 'v') { + printFullResults(); + } else if (k == 'i') { + RuntimeUpdatesProcessor.INSTANCE.toggleInstrumentation(); + } else if (k == 'o') { + TestSupport.instance().get().setDisplayTestOutput(!TestSupport.instance().get().displayTestOutput); + if (TestSupport.instance().get().displayTestOutput) { + log.info("Test output enabled"); + } else { + log.info("Test output disabled"); + } + } else if (k == 'd') { + TestSupport.instance().get().stop(); + } else if (k == 'h') { + printUsage(); + } + } + } + } + + @Override + public void promptHandler(InputHandler.ConsoleStatus promptHandler) { + TestConsoleHandler.this.promptHandler = promptHandler; + } + }; + + @Override + public void listenerRegistered(TestController testController) { + this.testController = testController; + promptHandler.setStatus(DISABLED_PROMPT); + } + + public void printUsage() { + System.out.println("r - Re-run all tests"); + System.out.println("f - Re-run failed tests"); + System.out.println("v - Print failures from the last test run"); + System.out.println("o - Toggle test output"); + System.out.println("i - Toggle instrumentation based reload"); + System.out.println("d - Disable tests"); + System.out.println("h - Display this help"); + + } + + private void printFullResults() { + if (testController.currentState().getFailingClasses().isEmpty()) { + log.info("All tests passed, no output to display"); + } + for (TestClassResult i : testController.currentState().getFailingClasses()) { + for (TestResult failed : i.getFailing()) { + log.error( + "Test " + failed.getDisplayName() + " failed " + + failed.getTestExecutionResult().getStatus() + + "\n", + failed.getTestExecutionResult().getThrowable().get()); + } + } + + } + + @Override + public void testsEnabled() { + disabled = false; + if (firstRun) { + promptHandler.setStatus(null); + promptHandler.setPrompt(FIRST_RUN_PROMPT); + } else { + promptHandler.setPrompt(RUNNING_PROMPT); + promptHandler.setStatus(lastStatus); + } + } + + @Override + public void testsDisabled() { + disabled = true; + promptHandler.setPrompt(DISABLED_PROMPT); + promptHandler.setStatus(null); + } + + @Override + public void testRunStarted(Consumer listenerConsumer) { + + AtomicLong totalNoTests = new AtomicLong(); + AtomicLong skipped = new AtomicLong(); + AtomicLong methodCount = new AtomicLong(); + AtomicLong failureCount = new AtomicLong(); + listenerConsumer.accept(new TestRunListener() { + @Override + public void runStarted(long toRun) { + totalNoTests.set(toRun); + promptHandler.setStatus("Running 0/" + toRun + "."); + } + + @Override + public void testComplete(TestResult result) { + if (result.getTestExecutionResult().getStatus() == TestExecutionResult.Status.FAILED) { + failureCount.incrementAndGet(); + } else if (result.getTestExecutionResult().getStatus() == TestExecutionResult.Status.ABORTED) { + skipped.incrementAndGet(); + } + methodCount.incrementAndGet(); + } + + @Override + public void runComplete(TestRunResults results) { + firstRun = false; + if (results.getCurrentFailing().isEmpty()) { + lastStatus = "\u001B[32mTests all passed, " + methodCount.get() + " tests were run, " + skipped.get() + + " were skipped. Tests took " + (results.getTotalTime()) + + "ms." + "\u001b[0m"; + } else { + StringBuilder sb = new StringBuilder( + "\u001B[91mTest run failed, " + methodCount.get() + " tests were run, " + + results.getCurrentFailing().size() + + " failed, " + + skipped.get() + + " were skipped. Tests took " + results.getTotalTime() + "ms"); + for (Map.Entry classEntry : results.getCurrentFailing().entrySet()) { + for (TestResult test : classEntry.getValue().getFailing()) { + log.error( + "Test " + test.getDisplayName() + " failed \n", + test.getTestExecutionResult().getThrowable().get()); + } + } + lastStatus = sb.toString() + "\u001b[0m"; + } + //this will re-print when using the basic console + promptHandler.setPrompt(RUNNING_PROMPT); + promptHandler.setStatus(lastStatus); + } + + @Override + public void runAborted() { + promptHandler.setStatus(ABORTED_PROMPT); + promptHandler.setPrompt(RUNNING_PROMPT); + firstRun = false; + } + + @Override + public void testStarted(TestIdentifier testIdentifier, String className) { + promptHandler.setStatus("Running " + methodCount.get() + "/" + totalNoTests + + (failureCount.get() == 0 ? "." + : ". " + failureCount + " failures so far.") + + " Running: " + + className + "#" + testIdentifier.getDisplayName()); + } + }); + + } + +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestController.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestController.java new file mode 100644 index 00000000000000..c13612d9ea80b5 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestController.java @@ -0,0 +1,12 @@ +package io.quarkus.deployment.dev.testing; + +public interface TestController { + + TestState currentState(); + + void runAllTests(); + + void setDisplayTestOutput(boolean displayTestOutput); + + void runFailedTests(); +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestListener.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestListener.java new file mode 100644 index 00000000000000..a4bc1c3d68d31d --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestListener.java @@ -0,0 +1,21 @@ +package io.quarkus.deployment.dev.testing; + +import java.util.function.Consumer; + +public interface TestListener { + + default void listenerRegistered(TestController testController) { + + } + + default void testsEnabled() { + } + + default void testsDisabled() { + + } + + default void testRunStarted(Consumer listenerConsumer) { + + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestResult.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestResult.java new file mode 100644 index 00000000000000..a803d9ac61db77 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestResult.java @@ -0,0 +1,56 @@ +package io.quarkus.deployment.dev.testing; + +import java.util.List; + +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.UniqueId; + +public class TestResult { + + final String displayName; + final String testClass; + final UniqueId uniqueId; + final TestExecutionResult testExecutionResult; + final List logOutput; + final boolean test; + final long runId; + + public TestResult(String displayName, String testClass, UniqueId uniqueId, TestExecutionResult testExecutionResult, + List logOutput, boolean test, long runId) { + this.displayName = displayName; + this.testClass = testClass; + this.uniqueId = uniqueId; + this.testExecutionResult = testExecutionResult; + this.logOutput = logOutput; + this.test = test; + this.runId = runId; + } + + public TestExecutionResult getTestExecutionResult() { + return testExecutionResult; + } + + public List getLogOutput() { + return logOutput; + } + + public String getDisplayName() { + return displayName; + } + + public String getTestClass() { + return testClass; + } + + public UniqueId getUniqueId() { + return uniqueId; + } + + public boolean isTest() { + return test; + } + + public long getRunId() { + return runId; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunListener.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunListener.java new file mode 100644 index 00000000000000..0d8f959de484d4 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunListener.java @@ -0,0 +1,26 @@ +package io.quarkus.deployment.dev.testing; + +import org.junit.platform.launcher.TestIdentifier; + +public interface TestRunListener { + + default void runStarted(long toRun) { + + } + + default void testComplete(TestResult result) { + + } + + default void runComplete(TestRunResults results) { + + } + + default void runAborted() { + + } + + default void testStarted(TestIdentifier testIdentifier, String className) { + + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunResults.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunResults.java new file mode 100644 index 00000000000000..e6e11fb65e8236 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunResults.java @@ -0,0 +1,156 @@ +package io.quarkus.deployment.dev.testing; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.quarkus.deployment.dev.ClassScanResult; + +public class TestRunResults { + + /** + * The run id + */ + private final long id; + + /** + * The change that triggered this run, may be null. + */ + private final ClassScanResult trigger; + + /** + * If this ran all tests, or just the modified ones + */ + private final boolean full; + + private final long started; + private final long completed; + + private final Map results; + private final Map currentFailing = new HashMap<>(); + private final Map historicFailing = new HashMap<>(); + private final Map currentPassing = new HashMap<>(); + private final Map historicPassing = new HashMap<>(); + private final List failing = new ArrayList<>(); + private final List passing = new ArrayList<>(); + private final List skipped = new ArrayList<>(); + private final long testsPassed; + private final long testsFailed; + private final long testsSkipped; + + public TestRunResults(long id, ClassScanResult trigger, boolean full, long started, long completed, + Map results) { + this.id = id; + this.trigger = trigger; + this.full = full; + this.started = started; + this.completed = completed; + this.results = new HashMap<>(results); + long passedCount = 0; + long failedCount = 0; + long skippedCount = 0; + for (Map.Entry i : results.entrySet()) { + passedCount += i.getValue().getPassing().stream().filter(TestResult::isTest).count(); + failedCount += i.getValue().getFailing().stream().filter(TestResult::isTest).count(); + skippedCount += i.getValue().getSkipped().stream().filter(TestResult::isTest).count(); + boolean current = i.getValue().getLatestRunId() == id; + if (current) { + if (!i.getValue().getFailing().isEmpty()) { + currentFailing.put(i.getKey(), i.getValue()); + failing.add(i.getValue()); + } else if (!i.getValue().getPassing().isEmpty()) { + currentPassing.put(i.getKey(), i.getValue()); + passing.add(i.getValue()); + } else { + skipped.add(i.getValue()); + } + } else { + if (!i.getValue().getFailing().isEmpty()) { + historicFailing.put(i.getKey(), i.getValue()); + failing.add(i.getValue()); + } else if (!i.getValue().getPassing().isEmpty()) { + historicPassing.put(i.getKey(), i.getValue()); + passing.add(i.getValue()); + } else { + skipped.add(i.getValue()); + } + } + } + Collections.sort(passing); + Collections.sort(failing); + Collections.sort(skipped); + this.testsFailed = failedCount; + this.testsPassed = passedCount; + this.testsSkipped = skippedCount; + } + + public long getId() { + return id; + } + + public ClassScanResult getTrigger() { + return trigger; + } + + public boolean isFull() { + return full; + } + + public Map getResults() { + return Collections.unmodifiableMap(results); + } + + public Map getCurrentFailing() { + return currentFailing; + } + + public Map getHistoricFailing() { + return historicFailing; + } + + public Map getCurrentPassing() { + return currentPassing; + } + + public Map getHistoricPassing() { + return historicPassing; + } + + public long getStartedTime() { + return started; + } + + public long getCompletedTime() { + return completed; + } + + public long getTotalTime() { + return completed - started; + } + + public List getFailing() { + return failing; + } + + public List getPassing() { + return passing; + } + + public List getSkipped() { + return skipped; + } + + public long getTestsPassed() { + return testsPassed; + } + + public long getTestsFailed() { + return testsFailed; + } + + public long getTestsSkipped() { + return testsSkipped; + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunner.java new file mode 100644 index 00000000000000..98af9b1f17416b --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunner.java @@ -0,0 +1,264 @@ +package io.quarkus.deployment.dev.testing; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import org.jboss.logging.Logger; +import org.junit.platform.engine.FilterResult; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.UniqueId; +import org.junit.platform.launcher.PostDiscoveryFilter; +import org.opentest4j.TestAbortedException; + +import io.quarkus.bootstrap.app.CuratedApplication; +import io.quarkus.deployment.dev.ClassScanResult; +import io.quarkus.deployment.dev.DevModeContext; + +public class TestRunner { + + private static final Logger log = Logger.getLogger(TestRunner.class); + private static final AtomicLong COUNTER = new AtomicLong(); + + private final TestSupport testSupport; + private final DevModeContext devModeContext; + private final CuratedApplication testApplication; + + private boolean testsRunning = false; + private boolean testsQueued = false; + private ClassScanResult queuedChanges = null; + private boolean queuedFailureRun; + + private Throwable compileProblem; + + private final TestClassUsages testClassUsages = new TestClassUsages(); + private boolean paused; + /** + * disabled is different to paused, when the runner is disabled we abort all runs rather than pausing them. + */ + private volatile boolean disabled = true; + private volatile boolean firstRun = true; + private JunitTestRunner runner; + + public TestRunner(TestSupport testSupport, DevModeContext devModeContext, CuratedApplication testApplication) { + this.testSupport = testSupport; + this.devModeContext = devModeContext; + this.testApplication = testApplication; + } + + public void runTests() { + runTests(null); + } + + public synchronized long getRunningTestRunId() { + if (testsRunning) { + return COUNTER.get(); + } + return -1; + } + + public void runFailedTests() { + runTests(null, true); + } + + public void runTests(ClassScanResult classScanResult) { + runTests(classScanResult, false); + } + + private void runTests(ClassScanResult classScanResult, boolean reRunFailures) { + if (compileProblem != null) { + return; + } + if (testApplication == null) { + return; + } + if (disabled) { + return; + } + if (reRunFailures && testSupport.testRunResults == null) { + return; + } + if (reRunFailures && testSupport.testRunResults.getCurrentFailing().isEmpty()) { + log.error("Not re-running failed tests, as all tests passed"); + return; + } + synchronized (TestRunner.this) { + if (testsRunning) { + if (reRunFailures) { + log.error("Not re-running failed tests, as tests are already in progress."); + return; + } + if (testsQueued) { + if (queuedChanges != null) { //if this is null a full run is scheduled + this.queuedChanges = ClassScanResult.merge(this.queuedChanges, classScanResult); + } + } else { + testsQueued = true; + this.queuedChanges = classScanResult; + } + return; + } else { + testsRunning = true; + } + } + Thread t = new Thread(new Runnable() { + @Override + public void run() { + try { + runInternal(classScanResult, reRunFailures); + } finally { + waitTillResumed(); + boolean run = false; + ClassScanResult current = null; + synchronized (TestRunner.this) { + if (!disabled) { + testsRunning = false; + if (testsQueued) { + testsQueued = false; + run = true; + } + current = queuedChanges; + queuedChanges = null; + } + } + if (run) { + runTests(current); + } + } + } + }, "Test runner thread"); + t.setDaemon(true); + t.start(); + } + + public synchronized void pause() { + paused = true; + if (runner != null) { + runner.pause(); + } + } + + public synchronized void resume() { + paused = false; + notifyAll(); + if (runner != null) { + runner.resume(); + } + } + + public synchronized void disable() { + disabled = true; + notifyAll(); + if (runner != null) { + runner.abort(); + } + } + + public synchronized void enable() { + if (!disabled) { + return; + } + disabled = false; + if (firstRun) { + runTests(); + } + } + + private void runInternal(ClassScanResult classScanResult, boolean reRunFailures) { + final long runId = COUNTER.incrementAndGet(); + + AtomicReference resultsRef = new AtomicReference<>(); + synchronized (this) { + if (runner != null) { + throw new IllegalStateException("Tests already in progress"); + } + if (disabled) { + return; + } + JunitTestRunner.Builder builder = new JunitTestRunner.Builder() + .setClassScanResult(classScanResult) + .setDevModeContext(devModeContext) + .setRunId(runId) + .setTestState(testSupport.testState) + .setTestClassUsages(testClassUsages) + .setTestApplication(testApplication) + .setDisplayInConsole(testSupport.displayTestOutput) + .setIncludeTags(testSupport.includeTags) + .setExcludeTags(testSupport.excludeTags) + .setInclude(testSupport.include) + .setExclude(testSupport.exclude); + if (reRunFailures) { + Set ids = new HashSet<>(); + for (Map.Entry e : testSupport.testRunResults.getCurrentFailing().entrySet()) { + for (TestResult test : e.getValue().getFailing()) { + ids.add(test.uniqueId); + } + } + builder.addAdditionalFilter(new PostDiscoveryFilter() { + @Override + public FilterResult apply(TestDescriptor testDescriptor) { + return FilterResult.includedIf(ids.contains(testDescriptor.getUniqueId())); + } + }); + } + for (TestListener i : testSupport.testListeners) { + i.testRunStarted(builder::addListener); + } + builder.addListener(new TestRunListener() { + @Override + public void runComplete(TestRunResults results) { + testSupport.testRunResults = results; + + } + }); + runner = builder + .build(); + if (paused) { + runner.pause(); + } + } + runner.runTests(); + synchronized (this) { + runner = null; + } + TestRunResults results = resultsRef.get(); + if (disabled || results == null) { + return; + } + if (firstRun) { + firstRun = false; + } + + } + + public void waitTillResumed() { + synchronized (TestRunner.this) { + while (paused && !disabled) { + try { + TestRunner.this.wait(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + if (disabled) { + throw new TestAbortedException("Tests are disabled"); + } + } + } + + public synchronized void testCompileFailed(Throwable e) { + compileProblem = e; + log.error("Test compile failed", e); + } + + public synchronized void testCompileSucceeded() { + compileProblem = null; + } + + public boolean isRunning() { + return testsRunning; + } + +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestState.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestState.java new file mode 100644 index 00000000000000..413dde9f262dd4 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestState.java @@ -0,0 +1,134 @@ +package io.quarkus.deployment.dev.testing; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.engine.UniqueId; + +public class TestState { + + final Map> resultsByClass = new HashMap<>(); + final Set failing = new HashSet<>(); + + public List getClassNames() { + return new ArrayList<>(resultsByClass.keySet()).stream().sorted().collect(Collectors.toList()); + } + + public List getPassingClasses() { + List ret = new ArrayList<>(); + for (Map.Entry> i : resultsByClass.entrySet()) { + List passing = new ArrayList<>(); + List failing = new ArrayList<>(); + List skipped = new ArrayList<>(); + for (TestResult j : i.getValue().values()) { + if (j.getTestExecutionResult().getStatus() == TestExecutionResult.Status.FAILED) { + failing.add(j); + } else if (j.getTestExecutionResult().getStatus() == TestExecutionResult.Status.ABORTED) { + skipped.add(j); + } else { + passing.add(j); + } + } + if (failing.isEmpty()) { + TestClassResult p = new TestClassResult(i.getKey(), passing, failing, skipped); + ret.add(p); + } + } + + Collections.sort(ret); + return ret; + } + + public List getFailingClasses() { + List ret = new ArrayList<>(); + for (Map.Entry> i : resultsByClass.entrySet()) { + List passing = new ArrayList<>(); + List failing = new ArrayList<>(); + List skipped = new ArrayList<>(); + for (TestResult j : i.getValue().values()) { + if (j.getTestExecutionResult().getStatus() == TestExecutionResult.Status.FAILED) { + failing.add(j); + } else if (j.getTestExecutionResult().getStatus() == TestExecutionResult.Status.ABORTED) { + skipped.add(j); + } else { + passing.add(j); + } + } + if (!failing.isEmpty()) { + TestClassResult p = new TestClassResult(i.getKey(), passing, failing, skipped); + ret.add(p); + } + } + Collections.sort(ret); + return ret; + } + + public synchronized void updateResults(Map> latest) { + for (Map.Entry> entry : latest.entrySet()) { + Map existing = this.resultsByClass.get(entry.getKey()); + if (existing == null) { + resultsByClass.put(entry.getKey(), entry.getValue()); + } else { + existing.putAll(entry.getValue()); + } + for (Map.Entry r : entry.getValue().entrySet()) { + if (r.getValue().getTestExecutionResult().getStatus() == TestExecutionResult.Status.FAILED) { + failing.add(r.getKey()); + } else { + failing.remove(r.getKey()); + } + } + } + } + + public synchronized void classesRemoved(Set classNames) { + for (String i : classNames) { + resultsByClass.remove(i); + } + } + + public Map> getCurrentResults() { + return Collections.unmodifiableMap(resultsByClass); + } + + public int getTotalFailures() { + int count = 0; + for (Map i : resultsByClass.values()) { + for (TestResult j : i.values()) { + if (j.getTestExecutionResult().getStatus() == TestExecutionResult.Status.FAILED) { + count++; + } + } + } + return count; + } + + public List getHistoricFailures(Map> currentResults) { + List ret = new ArrayList<>(); + for (Map.Entry> entry : resultsByClass.entrySet()) { + for (TestResult j : entry.getValue().values()) { + if (j.getTestExecutionResult().getStatus() == TestExecutionResult.Status.FAILED) { + if (currentResults.containsKey(entry.getKey())) { + if (currentResults.get(entry.getKey()).containsKey(j.getUniqueId())) { + continue; + } + } + ret.add(j); + } + } + } + return ret; + } + + public boolean isFailed(TestDescriptor testDescriptor) { + return failing.contains(testDescriptor.getUniqueId()); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java new file mode 100644 index 00000000000000..98d6d65270207f --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java @@ -0,0 +1,258 @@ +package io.quarkus.deployment.dev.testing; + +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +import org.jboss.logging.Logger; + +import io.quarkus.bootstrap.app.AdditionalDependency; +import io.quarkus.bootstrap.app.CuratedApplication; +import io.quarkus.bootstrap.app.QuarkusBootstrap; +import io.quarkus.deployment.dev.CompilationProvider; +import io.quarkus.deployment.dev.DevModeContext; +import io.quarkus.deployment.dev.QuarkusCompiler; +import io.quarkus.deployment.dev.RuntimeUpdatesProcessor; + +public class TestSupport implements TestController { + + private static final Logger log = Logger.getLogger(TestSupport.class); + + final CuratedApplication curatedApplication; + final List compilationProviders; + final DevModeContext context; + final List testListeners = new ArrayList<>(); + final TestState testState = new TestState(); + + volatile CuratedApplication testCuratedApplication; + volatile QuarkusCompiler compiler; + volatile TestRunner testRunner; + volatile boolean started; + volatile TestRunResults testRunResults; + volatile List includeTags = Collections.emptyList(); + volatile List excludeTags = Collections.emptyList(); + volatile Pattern include = null; + volatile Pattern exclude = null; + volatile boolean displayTestOutput; + volatile Boolean explicitDisplayTestOutput; + + public TestSupport(CuratedApplication curatedApplication, List compilationProviders, + DevModeContext context) { + this.curatedApplication = curatedApplication; + this.compilationProviders = compilationProviders; + this.context = context; + } + + public static Optional instance() { + if (RuntimeUpdatesProcessor.INSTANCE == null) { + return Optional.empty(); + } + return Optional.ofNullable(RuntimeUpdatesProcessor.INSTANCE.getTestSupport()); + } + + public boolean isRunning() { + if (testRunner == null) { + return false; + } + return testRunner.isRunning(); + } + + public List getTestListeners() { + return testListeners; + } + + /** + * returns the current status of the test runner. + *

+ * This is expressed in terms of test run ids, where -1 signifies + * no result. + */ + public RunStatus getStatus() { + if (testRunner == null) { + return new RunStatus(-1, -1); + } + long last = -1; + //get the running test id before the current status + //otherwise there is a race where they both could be -1 even though it has started + long runningTestRunId = testRunner.getRunningTestRunId(); + TestRunResults tr = testRunResults; + if (tr != null) { + last = tr.getId(); + } + return new RunStatus(last, runningTestRunId); + } + + public void start() { + if (!started) { + synchronized (this) { + if (!started) { + try { + if (context.getApplicationRoot().getTest().isPresent()) { + started = true; + init(); + for (TestListener i : testListeners) { + i.testsEnabled(); + } + testRunner.enable(); + } + } catch (Exception e) { + log.error("Failed to create compiler, runtime compilation will be unavailable", e); + } + + } + } + } + } + + public void init() { + if (!context.getApplicationRoot().getTest().isPresent()) { + return; + } + if (testCuratedApplication == null) { + try { + testCuratedApplication = curatedApplication.getQuarkusBootstrap().clonedBuilder() + .setMode(QuarkusBootstrap.Mode.TEST) + .setDisableClasspathCache(true) + .setIsolateDeployment(true) + .setTest(true) + .setAuxiliaryApplication(true) + .addAdditionalApplicationArchive(new AdditionalDependency( + Paths.get(context.getApplicationRoot().getTest().get().getClassesPath()), true, + true)) + .build() + .bootstrap(); + compiler = new QuarkusCompiler(testCuratedApplication, compilationProviders, context); + testRunner = new TestRunner(this, context, testCuratedApplication); + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + public synchronized void stop() { + if (started) { + started = false; + for (TestListener i : testListeners) { + i.testsDisabled(); + } + } + if (testRunner != null) { + + testRunner.disable(); + } + } + + public void addListener(TestListener listener) { + boolean run = false; + synchronized (this) { + testListeners.add(listener); + if (started) { + run = true; + } + } + listener.listenerRegistered(this); + if (run) { + //run outside lock + listener.testsEnabled(); + } + } + + public boolean isStarted() { + return started; + } + + public TestRunner getTestRunner() { + return testRunner; + } + + public CuratedApplication getCuratedApplication() { + return curatedApplication; + } + + public QuarkusCompiler getCompiler() { + return compiler; + } + + public TestRunResults getTestRunResults() { + return testRunResults; + } + + public synchronized void pause() { + if (started) { + testRunner.pause(); + } + } + + public synchronized void resume() { + if (started) { + testRunner.resume(); + } + } + + public TestRunResults getResults() { + return testRunResults; + } + + public void setTags(List includeTags, List excludeTags) { + this.includeTags = includeTags; + this.excludeTags = excludeTags; + } + + public void setPatterns(String include, String exclude) { + this.include = include == null ? null : Pattern.compile(include); + this.exclude = exclude == null ? null : Pattern.compile(exclude); + } + + public TestSupport setConfiguredDisplayTestOutput(boolean displayTestOutput) { + if (explicitDisplayTestOutput != null) { + this.displayTestOutput = displayTestOutput; + } + this.displayTestOutput = displayTestOutput; + return this; + } + + @Override + public TestState currentState() { + return testState; + } + + @Override + public void runAllTests() { + getTestRunner().runTests(); + } + + @Override + public void setDisplayTestOutput(boolean displayTestOutput) { + this.explicitDisplayTestOutput = displayTestOutput; + this.displayTestOutput = displayTestOutput; + } + + @Override + public void runFailedTests() { + getTestRunner().runFailedTests(); + } + + public static class RunStatus { + + final long lastRun; + final long running; + + public RunStatus(long lastRun, long running) { + this.lastRun = lastRun; + this.running = running; + } + + public long getLastRun() { + return lastRun; + } + + public long getRunning() { + return running; + } + } + +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java new file mode 100644 index 00000000000000..fe3864817f2292 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java @@ -0,0 +1,141 @@ +package io.quarkus.deployment.dev.testing; + +import java.util.Collections; +import java.util.List; +import java.util.function.BiFunction; + +import org.jboss.jandex.ClassInfo; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import io.quarkus.bootstrap.classloading.ClassPathElement; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.IsNormal; +import io.quarkus.deployment.IsTest; +import io.quarkus.deployment.TestConfig; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.Produce; +import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.builditem.LiveReloadBuildItem; +import io.quarkus.deployment.builditem.LogHandlerBuildItem; +import io.quarkus.deployment.builditem.ServiceStartBuildItem; +import io.quarkus.deployment.dev.console.QuarkusConsole; +import io.quarkus.deployment.logging.LogCleanupFilterBuildItem; +import io.quarkus.dev.spi.DevModeType; +import io.quarkus.dev.testing.TracingHandler; + +/** + * processor that instruments test and application classes to trace the code path that is in use during a test run. + *

+ * This allows for fine grained running of tests when a file changes. + */ +public class TestTracingProcessor { + + private static TestConfig.Mode lastEnabledValue; + private static boolean consoleInstalled = false; + + @BuildStep(onlyIfNot = IsNormal.class) + LogCleanupFilterBuildItem handle() { + return new LogCleanupFilterBuildItem("org.junit.platform.launcher.core.EngineDiscoveryOrchestrator", "0 containers"); + } + + @BuildStep(onlyIf = IsDevelopment.class) + ServiceStartBuildItem setupConsole(TestConfig config) { + if (!TestSupport.instance().isPresent() || config.continuousTesting == TestConfig.Mode.DISABLED) { + return null; + } + if (consoleInstalled) { + return null; + } + consoleInstalled = true; + if (config.console) { + QuarkusConsole.installConsole(config); + TestConsoleHandler consoleHandler = new TestConsoleHandler(); + consoleHandler.install(); + TestSupport.instance().get().addListener(consoleHandler); + } + return null; + } + + @BuildStep(onlyIfNot = IsNormal.class) + @Produce(LogHandlerBuildItem.class) + ServiceStartBuildItem startTesting(TestConfig config, LiveReloadBuildItem liveReloadBuildItem, + LaunchModeBuildItem launchModeBuildItem) { + if (!TestSupport.instance().isPresent() || config.continuousTesting == TestConfig.Mode.DISABLED) { + return null; + } + if (launchModeBuildItem.getDevModeType().orElse(null) != DevModeType.LOCAL) { + return null; + } + TestSupport testSupport = TestSupport.instance().get(); + if (!liveReloadBuildItem.isLiveReload()) { + if (config.continuousTesting == TestConfig.Mode.ENABLED) { + testSupport.start(); + } else if (config.continuousTesting == TestConfig.Mode.PAUSED) { + testSupport.init(); + testSupport.stop(); + } + } + testSupport.setTags(config.includeTags.orElse(Collections.emptyList()), + config.excludeTags.orElse(Collections.emptyList())); + testSupport.setPatterns(config.includePattern.orElse(null), + config.excludePattern.orElse(null)); + testSupport.setConfiguredDisplayTestOutput(config.displayTestOutput); + return null; + } + + @BuildStep(onlyIf = IsTest.class) + public void instrumentTestClasses(CombinedIndexBuildItem combinedIndexBuildItem, + LaunchModeBuildItem launchModeBuildItem, + BuildProducer transformerProducer) { + if (!launchModeBuildItem.isAuxiliaryApplication()) { + return; + } + for (ClassInfo clazz : combinedIndexBuildItem.getIndex().getKnownClasses()) { + String theClassName = clazz.name().toString(); + if (isAppClass(theClassName)) { + transformerProducer.produce(new BytecodeTransformerBuildItem(false, theClassName, + new BiFunction() { + @Override + public ClassVisitor apply(String s, ClassVisitor classVisitor) { + return new ClassVisitor(Opcodes.ASM9, classVisitor) { + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, + String signature, String[] exceptions) { + MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); + if (name.equals("") || name.equals("")) { + return mv; + } + return new MethodVisitor(Opcodes.ASM9, mv) { + @Override + public void visitCode() { + super.visitCode(); + visitLdcInsn(theClassName); + visitMethodInsn(Opcodes.INVOKESTATIC, + TracingHandler.class.getName().replace(".", "/"), "trace", + "(Ljava/lang/String;)V", false); + } + }; + } + }; + } + }, true)); + } + } + + } + + public boolean isAppClass(String theClassName) { + QuarkusClassLoader cl = (QuarkusClassLoader) Thread.currentThread() + .getContextClassLoader(); + //if the class file is present in this (and not the parent) CL then it is an application class + List res = cl + .getElementsWithResource(theClassName.replace(".", "/") + ".class", true); + return !res.isEmpty(); + } +} diff --git a/core/deployment/src/main/java/io/quarkus/deployment/jbang/JBangAugmentorImpl.java b/core/deployment/src/main/java/io/quarkus/deployment/jbang/JBangAugmentorImpl.java index 5f3632e14a8d6e..07f9a21f9cedcd 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/jbang/JBangAugmentorImpl.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/jbang/JBangAugmentorImpl.java @@ -1,26 +1,19 @@ package io.quarkus.deployment.jbang; -import java.io.File; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.BiConsumer; import java.util.function.Consumer; -import io.quarkus.bootstrap.BootstrapGradleException; import io.quarkus.bootstrap.app.AdditionalDependency; import io.quarkus.bootstrap.app.CuratedApplication; import io.quarkus.bootstrap.app.QuarkusBootstrap; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; -import io.quarkus.bootstrap.model.AppArtifactKey; -import io.quarkus.bootstrap.resolver.maven.workspace.LocalProject; -import io.quarkus.bootstrap.resolver.model.WorkspaceModule; -import io.quarkus.bootstrap.util.QuarkusModelHelper; import io.quarkus.builder.BuildChainBuilder; import io.quarkus.builder.BuildResult; import io.quarkus.builder.BuildStepBuilder; @@ -31,7 +24,6 @@ import io.quarkus.deployment.builditem.LiveReloadBuildItem; import io.quarkus.deployment.builditem.MainClassBuildItem; import io.quarkus.deployment.builditem.TransformedClassesBuildItem; -import io.quarkus.deployment.dev.DevModeContext; import io.quarkus.deployment.pkg.builditem.ArtifactResultBuildItem; import io.quarkus.deployment.pkg.builditem.NativeImageBuildItem; import io.quarkus.deployment.pkg.builditem.ProcessInheritIODisabled; @@ -57,6 +49,7 @@ public void accept(CuratedApplication curatedApplication, Map re builder.setBaseName(quarkusBootstrap.getBaseName()); } + builder.setAuxiliaryApplication(curatedApplication.getQuarkusBootstrap().isAuxiliaryApplication()); builder.setLaunchMode(LaunchMode.NORMAL); builder.setRebuild(quarkusBootstrap.isRebuild()); builder.setLiveReloadState( @@ -124,38 +117,4 @@ public void accept(BuildChainBuilder builder) { throw new RuntimeException(e); } } - - private DevModeContext.ModuleInfo toModule(WorkspaceModule module) throws BootstrapGradleException { - AppArtifactKey key = new AppArtifactKey(module.getArtifactCoords().getGroupId(), - module.getArtifactCoords().getArtifactId(), module.getArtifactCoords().getClassifier()); - - Set sourceDirectories = new HashSet<>(); - Set sourceParents = new HashSet<>(); - for (File srcDir : module.getSourceSourceSet().getSourceDirectories()) { - sourceDirectories.add(srcDir.getPath()); - sourceParents.add(srcDir.getParent()); - } - - return new DevModeContext.ModuleInfo(key, - module.getArtifactCoords().getArtifactId(), - module.getProjectRoot().getPath(), - sourceDirectories, - QuarkusModelHelper.getClassPath(module).toAbsolutePath().toString(), - module.getSourceSourceSet().getResourceDirectory().toString(), - module.getSourceSet().getResourceDirectory().getPath(), - sourceParents, - module.getBuildDir().toPath().resolve("generated-sources").toAbsolutePath().toString(), - module.getBuildDir().toString()); - } - - private DevModeContext.ModuleInfo toModule(LocalProject project) { - return new DevModeContext.ModuleInfo(project.getKey(), project.getArtifactId(), - project.getDir().toAbsolutePath().toString(), - Collections.singleton(project.getSourcesSourcesDir().toAbsolutePath().toString()), - project.getClassesDir().toAbsolutePath().toString(), - project.getResourcesSourcesDir().toAbsolutePath().toString(), - project.getSourcesDir().toString(), - project.getCodeGenOutputDir().toString(), - project.getOutputDir().toString()); - } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java index 9e7a1c55553dc7..d2896fe30f50dd 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/logging/LoggingResourceProcessor.java @@ -133,50 +133,53 @@ LoggingSetupBuildItem setupLoggingRuntimeInit(LoggingSetupRecorder recorder, Log Optional possibleBannerBuildItem, LaunchModeBuildItem launchModeBuildItem, List logCleanupFilters) { - final List>> handlers = handlerBuildItems.stream() - .map(LogHandlerBuildItem::getHandlerValue) - .collect(Collectors.toList()); - final List>> namedHandlers = namedHandlerBuildItems.stream() - .map(NamedLogHandlersBuildItem::getNamedHandlersMap).collect(Collectors.toList()); - - ConsoleFormatterBannerBuildItem bannerBuildItem = null; - RuntimeValue>> possibleSupplier = null; - if (possibleBannerBuildItem.isPresent()) { - bannerBuildItem = possibleBannerBuildItem.get(); - } - if (bannerBuildItem != null) { - possibleSupplier = bannerBuildItem.getBannerSupplier(); - } - recorder.initializeLogging(log, buildLog, handlers, namedHandlers, - consoleFormatItems.stream().map(LogConsoleFormatBuildItem::getFormatterValue).collect(Collectors.toList()), - possibleSupplier); - if (launchModeBuildItem.getLaunchMode() != LaunchMode.NORMAL) { - LogConfig logConfig = new LogConfig(); - ConfigInstantiator.handleObject(logConfig); - for (LogCleanupFilterBuildItem i : logCleanupFilters) { - CleanupFilterConfig value = new CleanupFilterConfig(); - LogCleanupFilterElement filterElement = i.getFilterElement(); - value.ifStartsWith = filterElement.getMessageStarts(); - value.targetLevel = filterElement.getTargetLevel() == null ? org.jboss.logmanager.Level.DEBUG - : filterElement.getTargetLevel(); - logConfig.filters.put(filterElement.getLoggerName(), value); + if (!launchModeBuildItem.isAuxiliaryApplication()) { + final List>> handlers = handlerBuildItems.stream() + .map(LogHandlerBuildItem::getHandlerValue) + .collect(Collectors.toList()); + final List>> namedHandlers = namedHandlerBuildItems.stream() + .map(NamedLogHandlersBuildItem::getNamedHandlersMap).collect(Collectors.toList()); + + ConsoleFormatterBannerBuildItem bannerBuildItem = null; + RuntimeValue>> possibleSupplier = null; + if (possibleBannerBuildItem.isPresent()) { + bannerBuildItem = possibleBannerBuildItem.get(); } - LoggingSetupRecorder.initializeBuildTimeLogging(logConfig, buildLog); - ((QuarkusClassLoader) Thread.currentThread().getContextClassLoader()).addCloseTask(new Runnable() { - @Override - public void run() { - InitialConfigurator.DELAYED_HANDLER.buildTimeComplete(); + if (bannerBuildItem != null) { + possibleSupplier = bannerBuildItem.getBannerSupplier(); + } + recorder.initializeLogging(log, buildLog, handlers, namedHandlers, + consoleFormatItems.stream().map(LogConsoleFormatBuildItem::getFormatterValue).collect(Collectors.toList()), + possibleSupplier); + if (launchModeBuildItem.getLaunchMode() != LaunchMode.NORMAL) { + LogConfig logConfig = new LogConfig(); + ConfigInstantiator.handleObject(logConfig); + for (LogCleanupFilterBuildItem i : logCleanupFilters) { + CleanupFilterConfig value = new CleanupFilterConfig(); + LogCleanupFilterElement filterElement = i.getFilterElement(); + value.ifStartsWith = filterElement.getMessageStarts(); + value.targetLevel = filterElement.getTargetLevel() == null ? org.jboss.logmanager.Level.DEBUG + : filterElement.getTargetLevel(); + logConfig.filters.put(filterElement.getLoggerName(), value); } - }); + LoggingSetupRecorder.initializeBuildTimeLogging(logConfig, buildLog); + ((QuarkusClassLoader) Thread.currentThread().getContextClassLoader()).addCloseTask(new Runnable() { + @Override + public void run() { + InitialConfigurator.DELAYED_HANDLER.buildTimeComplete(); + } + }); + } } - return new LoggingSetupBuildItem(); } @BuildStep @Record(ExecutionTime.STATIC_INIT) - void setupLoggingStaticInit(LoggingSetupRecorder recorder) { - recorder.initializeLoggingForImageBuild(); + void setupLoggingStaticInit(LoggingSetupRecorder recorder, LaunchModeBuildItem launchModeBuildItem) { + if (!launchModeBuildItem.isAuxiliaryApplication()) { + recorder.initializeLoggingForImageBuild(); + } } // This is specifically to help out with presentations, to allow an env var to always override this value diff --git a/core/deployment/src/main/java/io/quarkus/deployment/mutability/DevModeTask.java b/core/deployment/src/main/java/io/quarkus/deployment/mutability/DevModeTask.java index ee28f31a587bbc..28b533153b409d 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/mutability/DevModeTask.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/mutability/DevModeTask.java @@ -14,7 +14,6 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.attribute.BasicFileAttributes; -import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Properties; @@ -80,12 +79,11 @@ private static DevModeContext createDevModeContext(Path appRoot, AppModel appMod public void run(AppArtifact dep, Path moduleClasses, boolean appArtifact) { dep.setPath(moduleClasses); - - DevModeContext.ModuleInfo module = new DevModeContext.ModuleInfo(dep.getKey(), dep.getArtifactId(), null, - Collections.emptySet(), - moduleClasses.toAbsolutePath().toString(), null, moduleClasses.toAbsolutePath().toString(), - // the last three params are for code generation, in remote dev it happens on the "dev" side - null, null, null); + DevModeContext.ModuleInfo module = new DevModeContext.ModuleInfo.Builder().setAppArtifactKey(dep.getKey()) + .setName(dep.getArtifactId()) + .setClassesPath(moduleClasses.toAbsolutePath().toString()) + .setResourcesOutputPath(moduleClasses.toAbsolutePath().toString()) + .build(); if (appArtifact) { context.setApplicationRoot(module); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java index 433b625a7071ee..b1d27da01bbe0b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java @@ -135,6 +135,11 @@ void build(List staticInitTasks, FieldCreator scField = file.getFieldCreator(STARTUP_CONTEXT_FIELD); scField.setModifiers(Modifier.PUBLIC | Modifier.STATIC); + MethodCreator ctor = file.getMethodCreator("", void.class); + ctor.invokeSpecialMethod(MethodDescriptor.ofMethod(Application.class, "", void.class, boolean.class), + ctor.getThis(), ctor.load(launchMode.isAuxiliaryApplication())); + ctor.returnValue(null); + MethodCreator mv = file.getMethodCreator("", void.class); mv.setModifiers(Modifier.PUBLIC | Modifier.STATIC); diff --git a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java index 5b41d3c89cf6cc..ce8b868828dc52 100644 --- a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java +++ b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/AugmentActionImpl.java @@ -323,7 +323,8 @@ public BuildResult runCustomAction(Consumer chainBuild, Consu .build(); BuildExecutionBuilder execBuilder = chain.createExecutionBuilder("main") .produce(new LaunchModeBuildItem(launchMode, - devModeType == null ? Optional.empty() : Optional.of(devModeType))) + devModeType == null ? Optional.empty() : Optional.of(devModeType), + curatedApplication.getQuarkusBootstrap().isAuxiliaryApplication())) .produce(new ShutdownContextBuildItem()) .produce(new RawCommandLineArgumentsBuildItem()) .produce(new LiveReloadBuildItem()); @@ -364,6 +365,7 @@ private BuildResult runAugment(boolean firstRun, Set changedResources, builder.setBaseName(quarkusBootstrap.getBaseName()); } + builder.setAuxiliaryApplication(curatedApplication.getQuarkusBootstrap().isAuxiliaryApplication()); builder.setLaunchMode(launchMode); builder.setDevModeType(devModeType); builder.setRebuild(quarkusBootstrap.isRebuild()); diff --git a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/StartupActionImpl.java b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/StartupActionImpl.java index bd018bbcbe0e2a..7c2f2711cc7ca9 100644 --- a/core/deployment/src/main/java/io/quarkus/runner/bootstrap/StartupActionImpl.java +++ b/core/deployment/src/main/java/io/quarkus/runner/bootstrap/StartupActionImpl.java @@ -13,8 +13,6 @@ import java.util.HashSet; import java.util.Map; import java.util.Set; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.function.Consumer; import org.jboss.logging.Logger; @@ -55,53 +53,13 @@ public StartupActionImpl(CuratedApplication curatedApplication, BuildResult buil //test mode only has a single class loader, while dev uses a disposable runtime class loader //that is discarded between restarts Map resources = new HashMap<>(extractGeneratedResources(true)); - if (curatedApplication.getQuarkusBootstrap().getMode() == QuarkusBootstrap.Mode.TEST) { - resources.putAll(extractGeneratedResources(false)); - baseClassLoader.reset(resources, transformedClasses); - runtimeClassLoader = baseClassLoader; - } else { - baseClassLoader.reset(extractGeneratedResources(false), - transformedClasses); - runtimeClassLoader = curatedApplication.createRuntimeClassLoader(baseClassLoader, - resources, transformedClasses); - } + baseClassLoader.reset(extractGeneratedResources(false), + transformedClasses); + runtimeClassLoader = curatedApplication.createRuntimeClassLoader(baseClassLoader, + resources, transformedClasses); this.runtimeClassLoader = runtimeClassLoader; } - private void handleEagerClasses(QuarkusClassLoader runtimeClassLoader, Set eagerClasses) { - int availableProcessors = Runtime.getRuntime().availableProcessors(); - if (availableProcessors == 1) { - return; - } - //leave one processor for the main startup thread - ExecutorService loadingExecutor = Executors.newFixedThreadPool(availableProcessors - 1); - for (String i : eagerClasses) { - loadingExecutor.submit(new Runnable() { - @Override - public void run() { - try { - //no need to restore the old TCCL, this thread is going away - Thread.currentThread().setContextClassLoader(runtimeClassLoader); - runtimeClassLoader.loadClass(i); - } catch (ClassNotFoundException e) { - log.debug("Failed to eagerly load class", e); - //we just ignore this for now, the problem - //will be reported for real in the startup sequence - } - } - }); - } - Thread t = new Thread(new Runnable() { - @Override - public void run() { - //when all the jobs are done we shut down - //we do this in a new thread to allow the main thread to continue doing startup - loadingExecutor.shutdown(); - } - }); - t.start(); - } - /** * Runs the application by running the main method of the main class. As this is a blocking method a new * thread is created to run this task. @@ -226,7 +184,8 @@ public void close() throws IOException { } } finally { ForkJoinClassLoading.setForkJoinClassLoader(ClassLoader.getSystemClassLoader()); - if (curatedApplication.getQuarkusBootstrap().getMode() == QuarkusBootstrap.Mode.TEST) { + if (curatedApplication.getQuarkusBootstrap().getMode() == QuarkusBootstrap.Mode.TEST && + !curatedApplication.getQuarkusBootstrap().isAuxiliaryApplication()) { //for tests we just always shut down the curated application, as it is only used once //dev mode might be about to restart, so we leave it curatedApplication.close(); diff --git a/core/devmode-spi/src/main/java/io/quarkus/dev/testing/ContinuousTestingWebsocketListener.java b/core/devmode-spi/src/main/java/io/quarkus/dev/testing/ContinuousTestingWebsocketListener.java new file mode 100644 index 00000000000000..9aa054e0bc3984 --- /dev/null +++ b/core/devmode-spi/src/main/java/io/quarkus/dev/testing/ContinuousTestingWebsocketListener.java @@ -0,0 +1,61 @@ +package io.quarkus.dev.testing; + +import java.util.function.Consumer; + +//TODO: this is pretty horrible +public class ContinuousTestingWebsocketListener { + + private static Consumer stateListener; + private static volatile State lastState = new State(false, false, 0, 0, 0, 0); + + public static Consumer getStateListener() { + return stateListener; + } + + public static void setStateListener(Consumer stateListener) { + ContinuousTestingWebsocketListener.stateListener = stateListener; + if (lastState != null) { + stateListener.accept(lastState); + } + } + + public static void setLastState(State state) { + lastState = state; + Consumer sl = stateListener; + if (sl != null) { + sl.accept(state); + } + } + + public static void setInProgress(boolean inProgress) { + State state = lastState; + if (state != null) { + setLastState(new State(state.running, inProgress, state.run, state.passed, state.failed, state.skipped)); + } + } + + public static void setRunning(boolean running) { + State state = lastState; + if (state != null) { + setLastState(new State(running, running && state.inProgress, state.run, state.passed, state.failed, state.skipped)); + } + } + + public static class State { + public final boolean running; + public final boolean inProgress; + public final long run; + public final long passed; + public final long failed; + public final long skipped; + + public State(boolean running, boolean inProgress, long run, long passed, long failed, long skipped) { + this.running = running; + this.inProgress = inProgress; + this.run = run; + this.passed = passed; + this.failed = failed; + this.skipped = skipped; + } + } +} diff --git a/core/devmode-spi/src/main/java/io/quarkus/dev/testing/TracingHandler.java b/core/devmode-spi/src/main/java/io/quarkus/dev/testing/TracingHandler.java new file mode 100644 index 00000000000000..b3d5060abbcf4d --- /dev/null +++ b/core/devmode-spi/src/main/java/io/quarkus/dev/testing/TracingHandler.java @@ -0,0 +1,62 @@ +package io.quarkus.dev.testing; + +public class TracingHandler { + + private static volatile TraceListener tracingHandler; + + public static void trace(String className) { + TraceListener t = tracingHandler; + if (t != null) { + t.touched(className); + } + } + + public static void quarkusStarting() { + TraceListener t = tracingHandler; + if (t != null) { + t.quarkusStarting(); + } + } + + public static void quarkusStopping() { + TraceListener t = tracingHandler; + if (t != null) { + t.quarkusStopping(); + } + } + + public static void quarkusStarted() { + TraceListener t = tracingHandler; + if (t != null) { + t.quarkusStarted(); + } + } + + public static void quarkusStopped() { + TraceListener t = tracingHandler; + if (t != null) { + t.quarkusStopped(); + } + } + + public static void setTracingHandler(TraceListener tracingHandler) { + TracingHandler.tracingHandler = tracingHandler; + } + + public interface TraceListener { + + void touched(String className); + + default void quarkusStarting() { + } + + default void quarkusStopping() { + } + + default void quarkusStarted() { + } + + default void quarkusStopped() { + } + } +} diff --git a/core/runtime/src/main/java/io/quarkus/runtime/Application.java b/core/runtime/src/main/java/io/quarkus/runtime/Application.java index ac632628c96362..48320c9e8510ff 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/Application.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/Application.java @@ -43,10 +43,19 @@ public abstract class Application implements Closeable { private int state = ST_INITIAL; private static volatile Application currentApplication; + /** + * Embedded applications don't setup or modify logging, and don't provide start/ + * stop notifications to the {@link ApplicationStateNotification}. + */ + private final boolean auxilaryApplication; + /** * Construct a new instance. + * + * @param auxilaryApplication */ - protected Application() { + protected Application(boolean auxilaryApplication) { + this.auxilaryApplication = auxilaryApplication; } /** @@ -59,7 +68,9 @@ protected Application() { * letting the user hook into it. */ public final void start(String[] args) { - currentApplication = this; + if (!auxilaryApplication) { + currentApplication = this; + } final Lock stateLock = this.stateLock; stateLock.lock(); try { @@ -102,14 +113,18 @@ public final void start(String[] args) { } finally { stateLock.unlock(); } - ApplicationStateNotification.notifyStartupFailed(t); + if (!auxilaryApplication) { + ApplicationStateNotification.notifyStartupFailed(t); + } throw t; } stateLock.lock(); try { state = ST_STARTED; stateCond.signalAll(); - ApplicationStateNotification.notifyStartupComplete(); + if (!auxilaryApplication) { + ApplicationStateNotification.notifyStartupComplete(); + } } finally { stateLock.unlock(); } @@ -187,7 +202,9 @@ public final void stop(Runnable afterStopTask) { ShutdownRecorder.runShutdown(); doStop(); } finally { - currentApplication = null; + if (!auxilaryApplication) { + currentApplication = null; + } if (afterStopTask != null) { try { afterStopTask.run(); @@ -198,9 +215,13 @@ public final void stop(Runnable afterStopTask) { stateLock.lock(); try { state = ST_STOPPED; - Timing.printStopTime(getName()); + //note that at the moment if these are started or stopped concurrently + //the timing will be off + Timing.printStopTime(getName(), auxilaryApplication); stateCond.signalAll(); - ApplicationStateNotification.notifyApplicationStopped(); + if (!auxilaryApplication) { + ApplicationStateNotification.notifyApplicationStopped(); + } } finally { stateLock.unlock(); } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java index 41e1e4643481c6..2a56535fadacc7 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java @@ -63,7 +63,7 @@ private ApplicationLifecycleManager() { private static final Condition stateCond = stateLock.newCondition(); private static int exitCode = -1; - private static boolean shutdownRequested; + private static volatile boolean shutdownRequested; private static Application currentApplication; private static boolean hooksRegistered; private static boolean vmShuttingDown; @@ -139,7 +139,7 @@ public static void run(Application application, Class Level.WARNING.intValue()) { return true; } + LogCleanupFilterElement filterElement = filterElements.get(record.getLoggerName()); if (filterElement != null) { for (String messageStart : filterElement.getMessageStarts()) { diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java index 9715b77d878af6..f8eefa23c68b5d 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LoggingSetupRecorder.java @@ -327,7 +327,7 @@ public void initializeLoggingForImageBuild() { } } - private static boolean hasColorSupport() { + public static boolean hasColorSupport() { if (IS_WINDOWS) { // On Windows without a known good emulator @@ -383,7 +383,7 @@ private static Handler configureConsoleHandler(final ConsoleConfig config, final } } } - final ConsoleHandler consoleHandler = new ConsoleHandler(formatter); + final ConsoleHandler consoleHandler = new ConsoleHandler(ConsoleHandler.Target.SYSTEM_OUT, formatter); consoleHandler.setLevel(config.level); consoleHandler.setErrorManager(defaultErrorManager); consoleHandler.setFilter(new LogCleanupFilter(filterElements)); diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java index 54f1e76c3b64d0..7ffe137ff9a344 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusDev.java @@ -383,16 +383,17 @@ private void addLocalProject(Project project, GradleDevModeLauncher.Builder buil resourcesOutputPath = classesDir; } - DevModeContext.ModuleInfo wsModuleInfo = new DevModeContext.ModuleInfo(key, - project.getName(), - project.getProjectDir().getAbsolutePath(), - sourcePaths, - classesDir, - resourcesSrcDir.getAbsolutePath(), - resourcesOutputPath, - sourceParentPaths, - project.getBuildDir().toPath().resolve("generated-sources").toAbsolutePath().toString(), - project.getBuildDir().toString()); + DevModeContext.ModuleInfo wsModuleInfo = new DevModeContext.ModuleInfo.Builder().setAppArtifactKey(key) + .setName(project.getName()) + .setProjectDirectory(project.getProjectDir().getAbsolutePath()) + .setSourcePaths(sourcePaths) + .setClassesPath(classesDir) + .setResourcePath(resourcesSrcDir.getAbsolutePath()) + .setResourcesOutputPath(resourcesOutputPath) + .setSourceParents(sourceParentPaths) + .setPreBuildOutputDir(project.getBuildDir().toPath().resolve("generated-sources").toAbsolutePath().toString()) + .setTargetDir(project.getBuildDir().toString()) + .build(); if (root) { builder.mainModule(wsModuleInfo); diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java index 38bad7177a3821..0f49a6f7e14bf5 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java @@ -84,7 +84,7 @@ *

* You can use this dev mode in a remote container environment with {@code remote-dev}. */ -@Mojo(name = "dev", defaultPhase = LifecyclePhase.PREPARE_PACKAGE, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME) +@Mojo(name = "dev", defaultPhase = LifecyclePhase.PREPARE_PACKAGE, requiresDependencyResolution = ResolutionScope.TEST) public class DevMojo extends AbstractMojo { private static final String EXT_PROPERTIES_PATH = "META-INF/quarkus-extension.properties"; @@ -114,6 +114,22 @@ public class DevMojo extends AbstractMojo { "install", "deploy")); + /** + * running any one of these phases means the test-compile phase will have been run, if these have + * not been run we manually run test-compile + */ + private static final Set POST_TEST_COMPILE_PHASES = new HashSet<>(Arrays.asList( + "test-compile", + "process-test-classes", + "test", + "prepare-package", + "package", + "pre-integration-test", + "integration-test", + "post-integration-test", + "verify", + "install", + "deploy")); private static final String QUARKUS_PLUGIN_GROUPID = "io.quarkus"; private static final String QUARKUS_PLUGIN_ARTIFACTID = "quarkus-maven-plugin"; private static final String QUARKUS_GENERATE_CODE_GOAL = "generate-code"; @@ -326,6 +342,8 @@ public void execute() throws MojoFailureException, MojoExecutionException { if (System.currentTimeMillis() > nextCheck) { nextCheck = System.currentTimeMillis() + 100; if (!runner.alive()) { + //reset the terminal + System.out.println("\u001B[0m"); if (runner.exitValue() != 0) { throw new MojoExecutionException("Dev mode process did not complete successfully"); } @@ -343,7 +361,8 @@ public void execute() throws MojoFailureException, MojoExecutionException { getLog().info("Changes detected to " + changed + ", restarting dev mode"); final DevModeRunner newRunner; try { - triggerCompile(); + triggerCompile(false); + triggerCompile(true); newRunner = new DevModeRunner(); } catch (Exception e) { getLog().info("Could not load changed pom.xml file, changes not applied", e); @@ -365,6 +384,7 @@ public void execute() throws MojoFailureException, MojoExecutionException { private void handleAutoCompile() throws MojoExecutionException { //we check to see if there was a compile (or later) goal before this plugin boolean compileNeeded = true; + boolean testCompileNeeded = true; boolean prepareNeeded = true; for (String goal : session.getGoals()) { if (goal.endsWith("quarkus:prepare")) { @@ -375,6 +395,10 @@ private void handleAutoCompile() throws MojoExecutionException { compileNeeded = false; break; } + if (POST_TEST_COMPILE_PHASES.contains(goal)) { + testCompileNeeded = false; + break; + } if (goal.endsWith("quarkus:dev")) { break; } @@ -385,7 +409,10 @@ private void handleAutoCompile() throws MojoExecutionException { if (prepareNeeded) { triggerPrepare(); } - triggerCompile(); + triggerCompile(false); + } + if (testCompileNeeded) { + triggerCompile(true); } } @@ -397,25 +424,25 @@ private void triggerPrepare() throws MojoExecutionException { executeIfConfigured(QUARKUS_PLUGIN_GROUPID, QUARKUS_PLUGIN_ARTIFACTID, QUARKUS_GENERATE_CODE_GOAL); } - private void triggerCompile() throws MojoExecutionException { - handleResources(); + private void triggerCompile(boolean test) throws MojoExecutionException { + handleResources(test); // compile the Kotlin sources if needed - executeIfConfigured(ORG_JETBRAINS_KOTLIN, KOTLIN_MAVEN_PLUGIN, "compile"); + executeIfConfigured(ORG_JETBRAINS_KOTLIN, KOTLIN_MAVEN_PLUGIN, test ? "testCompile" : "compile"); // Compile the Java sources if needed - executeIfConfigured(ORG_APACHE_MAVEN_PLUGINS, MAVEN_COMPILER_PLUGIN, "compile"); + executeIfConfigured(ORG_APACHE_MAVEN_PLUGINS, MAVEN_COMPILER_PLUGIN, test ? "testCompile" : "compile"); } /** * Execute the resources:resources goal if resources have been configured on the project */ - private void handleResources() throws MojoExecutionException { + private void handleResources(boolean test) throws MojoExecutionException { List resources = project.getResources(); if (resources.isEmpty()) { return; } - executeIfConfigured(ORG_APACHE_MAVEN_PLUGINS, MAVEN_RESOURCES_PLUGIN, "resources"); + executeIfConfigured(ORG_APACHE_MAVEN_PLUGINS, MAVEN_RESOURCES_PLUGIN, test ? "testResources" : "resources"); } private void executeIfConfigured(String pluginGroupId, String pluginArtifactId, String goal) throws MojoExecutionException { @@ -504,6 +531,9 @@ private void addProject(MavenDevModeLauncher.Builder builder, LocalProject local Set sourcePaths = null; String classesPath = null; String resourcePath = null; + Set testSourcePaths = null; + String testClassesPath = null; + String testResourcePath = null; final MavenProject mavenProject = session.getProjectMap().get( String.format("%s:%s:%s", localProject.getGroupId(), localProject.getArtifactId(), localProject.getVersion())); @@ -516,6 +546,13 @@ private void addProject(MavenDevModeLauncher.Builder builder, LocalProject local } else { sourcePaths = Collections.emptySet(); } + Path testSourcePath = localProject.getTestSourcesSourcesDir().toAbsolutePath(); + if (Files.isDirectory(testSourcePath)) { + testSourcePaths = Collections.singleton( + testSourcePath.toString()); + } else { + testSourcePaths = Collections.emptySet(); + } } else { projectDirectory = mavenProject.getBasedir().getPath(); sourcePaths = mavenProject.getCompileSourceRoots().stream() @@ -523,6 +560,11 @@ private void addProject(MavenDevModeLauncher.Builder builder, LocalProject local .filter(Files::isDirectory) .map(src -> src.toAbsolutePath().toString()) .collect(Collectors.toSet()); + testSourcePaths = mavenProject.getTestCompileSourceRoots().stream() + .map(Paths::get) + .filter(Files::isDirectory) + .map(src -> src.toAbsolutePath().toString()) + .collect(Collectors.toSet()); } Path sourceParent = localProject.getSourcesDir().toAbsolutePath(); @@ -530,10 +572,18 @@ private void addProject(MavenDevModeLauncher.Builder builder, LocalProject local if (Files.isDirectory(classesDir)) { classesPath = classesDir.toAbsolutePath().toString(); } + Path testClassesDir = localProject.getTestClassesDir(); + if (Files.isDirectory(testClassesDir)) { + testClassesPath = testClassesDir.toAbsolutePath().toString(); + } Path resourcesSourcesDir = localProject.getResourcesSourcesDir(); if (Files.isDirectory(resourcesSourcesDir)) { resourcePath = resourcesSourcesDir.toAbsolutePath().toString(); } + Path testResourcesSourcesDir = localProject.getTestResourcesSourcesDir(); + if (Files.isDirectory(testResourcesSourcesDir)) { + testResourcePath = testResourcesSourcesDir.toAbsolutePath().toString(); + } if (classesPath == null && (!sourcePaths.isEmpty() || resourcePath != null)) { throw new MojoExecutionException("Hot reloadable dependency " + localProject.getAppArtifact() @@ -542,15 +592,21 @@ private void addProject(MavenDevModeLauncher.Builder builder, LocalProject local Path targetDir = Paths.get(project.getBuild().getDirectory()); - DevModeContext.ModuleInfo moduleInfo = new DevModeContext.ModuleInfo(localProject.getKey(), - localProject.getArtifactId(), - projectDirectory, - sourcePaths, - classesPath, - resourcePath, - sourceParent.toAbsolutePath().toString(), - targetDir.resolve("generated-sources").toAbsolutePath().toString(), - targetDir.toAbsolutePath().toString()); + DevModeContext.ModuleInfo moduleInfo = new DevModeContext.ModuleInfo.Builder().setAppArtifactKey(localProject.getKey()) + .setName(localProject.getArtifactId()) + .setProjectDirectory(projectDirectory) + .setSourcePaths(sourcePaths) + .setClassesPath(classesPath) + .setResourcesOutputPath(classesPath) + .setResourcePath(resourcePath) + .setSourceParents(Collections.singleton(sourceParent.toAbsolutePath().toString())) + .setPreBuildOutputDir(targetDir.resolve("generated-sources").toAbsolutePath().toString()) + .setTargetDir(targetDir.toAbsolutePath().toString()) + .setTestSourcePaths(testSourcePaths) + .setTestClassesPath(testClassesPath) + .setTestResourcePath(testResourcePath) + .build(); + if (root) { builder.mainModule(moduleInfo); } else { diff --git a/devtools/maven/src/main/java/io/quarkus/maven/RemoteDevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/RemoteDevMojo.java index a2828b3bc27556..73bbb078e98fe0 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/RemoteDevMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/RemoteDevMojo.java @@ -7,7 +7,7 @@ /** * The dev mojo, that connects to a remote host. */ -@Mojo(name = "remote-dev", defaultPhase = LifecyclePhase.PREPARE_PACKAGE, requiresDependencyResolution = ResolutionScope.COMPILE_PLUS_RUNTIME) +@Mojo(name = "remote-dev", defaultPhase = LifecyclePhase.PREPARE_PACKAGE, requiresDependencyResolution = ResolutionScope.TEST) public class RemoteDevMojo extends DevMojo { @Override protected void modifyDevModeContext(MavenDevModeLauncher.Builder builder) { diff --git a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java index 31ab21817b8a02..449c0d67bdca82 100644 --- a/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java +++ b/extensions/datasource/deployment/src/main/java/io/quarkus/datasource/deployment/devservices/DevServicesDatasourceProcessor.java @@ -28,7 +28,6 @@ import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem; import io.quarkus.deployment.builditem.ServiceStartBuildItem; import io.quarkus.deployment.pkg.builditem.CurateOutcomeBuildItem; -import io.quarkus.runtime.LaunchMode; public class DevServicesDatasourceProcessor { @@ -38,6 +37,8 @@ public class DevServicesDatasourceProcessor { static volatile Map cachedProperties; + static volatile List databaseConfig; + static volatile boolean first = true; @BuildStep(onlyIfNot = IsNormal.class) @@ -52,7 +53,7 @@ DevServicesDatasourceResultBuildItem launchDatabases(CurateOutcomeBuildItem cura //figure out if we need to shut down and restart existing databases //if not and the DB's have already started we just return if (databases != null) { - boolean restartRequired = launchMode.getLaunchMode() == LaunchMode.TEST; + boolean restartRequired = false; if (!restartRequired) { for (Map.Entry i : cachedProperties.entrySet()) { if (!Objects.equals(i.getValue(), @@ -63,6 +64,9 @@ DevServicesDatasourceResultBuildItem launchDatabases(CurateOutcomeBuildItem cura } } if (!restartRequired) { + for (RunTimeConfigurationDefaultBuildItem i : databaseConfig) { + runTimeConfigurationDefaultBuildItemBuildProducer.produce(i); + } return null; } for (Closeable i : databases) { @@ -74,6 +78,7 @@ DevServicesDatasourceResultBuildItem launchDatabases(CurateOutcomeBuildItem cura } databases = null; cachedProperties = null; + databaseConfig = null; } DevServicesDatasourceResultBuildItem.DbResult defaultResult; Map namedResults = new HashMap<>(); @@ -105,10 +110,10 @@ DevServicesDatasourceResultBuildItem launchDatabases(CurateOutcomeBuildItem cura devDBProviderMap, dataSourceBuildTimeConfig.defaultDataSource, configHandlersByDbType, propertiesMap, closeableList); + List dbConfig = new ArrayList<>(); if (defaultResult != null) { for (Map.Entry i : defaultResult.getConfigProperties().entrySet()) { - runTimeConfigurationDefaultBuildItemBuildProducer - .produce(new RunTimeConfigurationDefaultBuildItem(i.getKey(), i.getValue())); + dbConfig.add(new RunTimeConfigurationDefaultBuildItem(i.getKey(), i.getValue())); } } for (Map.Entry entry : dataSourceBuildTimeConfig.namedDataSources.entrySet()) { @@ -118,11 +123,15 @@ DevServicesDatasourceResultBuildItem launchDatabases(CurateOutcomeBuildItem cura if (result != null) { namedResults.put(entry.getKey(), result); for (Map.Entry i : result.getConfigProperties().entrySet()) { - runTimeConfigurationDefaultBuildItemBuildProducer - .produce(new RunTimeConfigurationDefaultBuildItem(i.getKey(), i.getValue())); + dbConfig.add(new RunTimeConfigurationDefaultBuildItem(i.getKey(), i.getValue())); } } } + for (RunTimeConfigurationDefaultBuildItem i : dbConfig) { + runTimeConfigurationDefaultBuildItemBuildProducer + .produce(i); + } + databaseConfig = dbConfig; if (first) { first = false; diff --git a/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/java/io/quarkus/jaxrs/client/reactive/runtime/JaxrsClientReactiveRecorder.java b/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/java/io/quarkus/jaxrs/client/reactive/runtime/JaxrsClientReactiveRecorder.java index 5ba3f61b380bb9..7316187f915ea6 100644 --- a/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/java/io/quarkus/jaxrs/client/reactive/runtime/JaxrsClientReactiveRecorder.java +++ b/extensions/resteasy-reactive/jaxrs-client-reactive/runtime/src/main/java/io/quarkus/jaxrs/client/reactive/runtime/JaxrsClientReactiveRecorder.java @@ -3,9 +3,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.concurrent.Executor; import java.util.function.Function; -import java.util.function.Supplier; import javax.ws.rs.RuntimeType; import javax.ws.rs.client.WebTarget; @@ -16,19 +14,12 @@ import org.jboss.resteasy.reactive.common.core.Serialisers; import io.quarkus.resteasy.reactive.common.runtime.ResteasyReactiveCommonRecorder; -import io.quarkus.runtime.ExecutorRecorder; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; @Recorder public class JaxrsClientReactiveRecorder extends ResteasyReactiveCommonRecorder { - public static final Supplier EXECUTOR_SUPPLIER = new Supplier() { - @Override - public Executor get() { - return ExecutorRecorder.getCurrent(); - } - }; private static volatile Serialisers serialisers; private static volatile GenericTypeMapping genericTypeMapping; diff --git a/extensions/vertx-http/deployment/pom.xml b/extensions/vertx-http/deployment/pom.xml index 662c528339c9a7..57b9bb60387b78 100644 --- a/extensions/vertx-http/deployment/pom.xml +++ b/extensions/vertx-http/deployment/pom.xml @@ -34,6 +34,10 @@ org.yaml snakeyaml + + com.fasterxml.jackson.core + jackson-databind + org.webjars @@ -66,6 +70,11 @@ quarkus-junit5-internal test + + io.quarkus + quarkus-junit5 + test + io.rest-assured rest-assured diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ContinuousTestingWebSocketListener.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ContinuousTestingWebSocketListener.java new file mode 100644 index 00000000000000..4553547279f7d3 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/ContinuousTestingWebSocketListener.java @@ -0,0 +1,69 @@ +package io.quarkus.vertx.http.deployment.devmode.console; + +import java.util.function.Consumer; + +import org.junit.platform.launcher.TestIdentifier; + +import io.quarkus.deployment.dev.testing.TestController; +import io.quarkus.deployment.dev.testing.TestListener; +import io.quarkus.deployment.dev.testing.TestResult; +import io.quarkus.deployment.dev.testing.TestRunListener; +import io.quarkus.deployment.dev.testing.TestRunResults; +import io.quarkus.dev.testing.ContinuousTestingWebsocketListener; + +public class ContinuousTestingWebSocketListener implements TestListener { + + @Override + public void listenerRegistered(TestController testController) { + + } + + @Override + public void testsEnabled() { + ContinuousTestingWebsocketListener + .setLastState(new ContinuousTestingWebsocketListener.State(true, true, 0L, 0L, 0L, 0L)); + } + + @Override + public void testsDisabled() { + ContinuousTestingWebsocketListener.setRunning(false); + } + + @Override + public void testRunStarted(Consumer listenerConsumer) { + ContinuousTestingWebsocketListener.setInProgress(true); + listenerConsumer.accept(new TestRunListener() { + @Override + public void runStarted(long toRun) { + + } + + @Override + public void testComplete(TestResult result) { + + } + + @Override + public void runComplete(TestRunResults testRunResults) { + ContinuousTestingWebsocketListener.setLastState( + new ContinuousTestingWebsocketListener.State(true, false, + testRunResults.getTestsPassed() + + testRunResults.getTestsFailed() + + testRunResults.getTestsSkipped(), + testRunResults.getTestsPassed(), + testRunResults.getTestsFailed(), testRunResults.getTestsSkipped())); + } + + @Override + public void runAborted() { + ContinuousTestingWebsocketListener.setInProgress(false); + } + + @Override + public void testStarted(TestIdentifier testIdentifier, String className) { + + } + }); + + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java index 9fb2696b57393b..d40e2511bcdfa5 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/console/DevConsoleProcessor.java @@ -53,6 +53,7 @@ import io.quarkus.deployment.builditem.LogHandlerBuildItem; import io.quarkus.deployment.builditem.ServiceStartBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; +import io.quarkus.deployment.dev.testing.TestSupport; import io.quarkus.deployment.ide.EffectiveIdeBuildItem; import io.quarkus.deployment.ide.Ide; import io.quarkus.deployment.logging.LoggingSetupBuildItem; @@ -96,6 +97,7 @@ import io.quarkus.vertx.http.runtime.devmode.RuntimeDevConsoleRoute; import io.quarkus.vertx.http.runtime.logstream.HistoryHandler; import io.quarkus.vertx.http.runtime.logstream.LogStreamRecorder; +import io.vertx.core.AsyncResult; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.http.HttpMethod; @@ -134,10 +136,6 @@ public static void initializeVirtual() { @Override public void run() { virtualBootstrap = null; - if (devConsoleVertx != null) { - devConsoleVertx.close(); - devConsoleVertx = null; - } if (channel != null) { try { channel.close().sync(); @@ -145,6 +143,10 @@ public void run() { throw new RuntimeException("failed to close virtual http"); } } + if (devConsoleVertx != null) { + devConsoleVertx.close(); + devConsoleVertx = null; + } } }); virtualBootstrap = new ServerBootstrap(); @@ -229,6 +231,23 @@ public void handle(RoutingContext event) { router.route() .order(Integer.MIN_VALUE) .handler(new FlashScopeHandler()); + router.route() + .order(Integer.MIN_VALUE) + .handler(new Handler() { + @Override + public void handle(RoutingContext event) { + event.addEndHandler(new Handler>() { + @Override + public void handle(AsyncResult e) { + //we only have one request per connection + //as we don't have a nice way to close them on shutdown + //they are not real TCP connections so this is fine + event.request().connection().close(); + } + }); + event.next(); + } + }); router.route().method(HttpMethod.GET) .order(Integer.MIN_VALUE + 1) .handler(new DevConsole(engine, httpRootPath, frameworkRootPath)); @@ -363,6 +382,15 @@ public void setupDevConsoleRoutes( .handler(logStreamRecorder.websocketHandler(historyHandlerBuildItem.value)) .build()); + if (TestSupport.instance().isPresent()) { + // Add continuous testing + routeBuildItemBuildProducer.produce(nonApplicationRootPathBuildItem.routeBuilder() + .route("dev/test") + .handler(recorder.continousTestHandler()) + .build()); + TestSupport.instance().get().addListener(new ContinuousTestingWebSocketListener()); + } + for (DevConsoleRouteBuildItem i : routes) { Entry groupAndArtifact = i.groupIdAndArtifactId(curateOutcomeBuildItem); // if the handler is a proxy, then that means it's been produced by a recorder and therefore belongs in the regular runtime Vert.x instance diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/ClassResult.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/ClassResult.java new file mode 100644 index 00000000000000..69e315784671a5 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/ClassResult.java @@ -0,0 +1,93 @@ +package io.quarkus.vertx.http.deployment.devmode.tests; + +import java.util.List; +import java.util.stream.Collectors; + +import io.quarkus.deployment.dev.testing.TestClassResult; +import io.quarkus.deployment.dev.testing.TestResult; + +public class ClassResult implements Comparable { + String className; + List passing; + List failing; + List skipped; + long latestRunId; + + public ClassResult(String className, List passing, List failing, List skipped) { + this.className = className; + this.passing = passing; + this.failing = failing; + this.skipped = skipped; + long runId = 0; + for (Result i : passing) { + runId = Math.max(i.getRunId(), runId); + } + for (Result i : failing) { + runId = Math.max(i.getRunId(), runId); + } + latestRunId = runId; + } + + public ClassResult(TestClassResult res) { + this.className = res.getClassName(); + this.failing = res.getFailing().stream().map(Result::new).collect(Collectors.toList()); + this.passing = res.getPassing().stream().filter(TestResult::isTest).map(Result::new).collect(Collectors.toList()); + this.skipped = res.getSkipped().stream().filter(TestResult::isTest).map(Result::new).collect(Collectors.toList()); + this.latestRunId = res.getLatestRunId(); + } + + public ClassResult() { + + } + + public String getClassName() { + return className; + } + + public List getPassing() { + return passing; + } + + public List getFailing() { + return failing; + } + + public List getSkipped() { + return skipped; + } + + public long getLatestRunId() { + return latestRunId; + } + + public ClassResult setClassName(String className) { + this.className = className; + return this; + } + + public ClassResult setPassing(List passing) { + this.passing = passing; + return this; + } + + public ClassResult setFailing(List failing) { + this.failing = failing; + return this; + } + + public ClassResult setSkipped(List skipped) { + this.skipped = skipped; + return this; + } + + public ClassResult setLatestRunId(long latestRunId) { + this.latestRunId = latestRunId; + return this; + } + + @Override + public int compareTo(ClassResult o) { + return className.compareTo(o.className); + } + +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/Result.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/Result.java new file mode 100644 index 00000000000000..2eb30d810b871f --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/Result.java @@ -0,0 +1,82 @@ +package io.quarkus.vertx.http.deployment.devmode.tests; + +import org.junit.platform.engine.TestExecutionResult; + +import io.quarkus.deployment.dev.testing.TestResult; + +public class Result { + + private String name; + + private TestExecutionResult.Status status; + + private String exceptionType; + + private String exceptionMessage; + + private long runId; + + public Result() { + } + + public Result(String name, TestExecutionResult.Status status, String exceptionType, String exceptionMessage, long runId) { + this.name = name; + this.status = status; + this.exceptionType = exceptionType; + this.exceptionMessage = exceptionMessage; + this.runId = runId; + } + + public Result(TestResult s) { + this(s.getDisplayName(), + s.getTestExecutionResult().getStatus(), + s.getTestExecutionResult().getThrowable().map(t -> t.getClass().getName()).orElse(null), + s.getTestExecutionResult().getThrowable().map(Throwable::getMessage).orElse(null), + s.getRunId()); + } + + public String getName() { + return name; + } + + public Result setName(String name) { + this.name = name; + return this; + } + + public TestExecutionResult.Status getStatus() { + return status; + } + + public Result setStatus(TestExecutionResult.Status status) { + this.status = status; + return this; + } + + public String getExceptionType() { + return exceptionType; + } + + public Result setExceptionType(String exceptionType) { + this.exceptionType = exceptionType; + return this; + } + + public String getExceptionMessage() { + return exceptionMessage; + } + + public Result setExceptionMessage(String exceptionMessage) { + this.exceptionMessage = exceptionMessage; + return this; + } + + public long getRunId() { + return runId; + } + + public Result setRunId(long runId) { + this.runId = runId; + return this; + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/SuiteResult.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/SuiteResult.java new file mode 100644 index 00000000000000..83625a6421300f --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/SuiteResult.java @@ -0,0 +1,23 @@ +package io.quarkus.vertx.http.deployment.devmode.tests; + +import java.util.Map; + +public class SuiteResult { + private Map results; + + public SuiteResult() { + } + + public SuiteResult(Map results) { + this.results = results; + } + + public Map getResults() { + return results; + } + + public SuiteResult setResults(Map results) { + this.results = results; + return this; + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/TestStatus.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/TestStatus.java new file mode 100644 index 00000000000000..fb34d000d32443 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/TestStatus.java @@ -0,0 +1,82 @@ +package io.quarkus.vertx.http.deployment.devmode.tests; + +public class TestStatus { + + private long lastRun; + + private long running; + + private long testsRun = -1; + + private long testsPassed = -1; + + private long testsFailed = -1; + + private long testsSkipped = -1; + + public TestStatus() { + } + + public TestStatus(long lastRun, long running, long testsRun, long testsPassed, long testsFailed, long testsSkipped) { + this.lastRun = lastRun; + this.running = running; + this.testsRun = testsRun; + this.testsPassed = testsPassed; + this.testsFailed = testsFailed; + this.testsSkipped = testsSkipped; + } + + public long getLastRun() { + return lastRun; + } + + public TestStatus setLastRun(long lastRun) { + this.lastRun = lastRun; + return this; + } + + public long getRunning() { + return running; + } + + public TestStatus setRunning(long running) { + this.running = running; + return this; + } + + public long getTestsRun() { + return testsRun; + } + + public TestStatus setTestsRun(long testsRun) { + this.testsRun = testsRun; + return this; + } + + public long getTestsPassed() { + return testsPassed; + } + + public TestStatus setTestsPassed(long testsPassed) { + this.testsPassed = testsPassed; + return this; + } + + public long getTestsFailed() { + return testsFailed; + } + + public TestStatus setTestsFailed(long testsFailed) { + this.testsFailed = testsFailed; + return this; + } + + public long getTestsSkipped() { + return testsSkipped; + } + + public TestStatus setTestsSkipped(long testsSkipped) { + this.testsSkipped = testsSkipped; + return this; + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/TestsProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/TestsProcessor.java new file mode 100644 index 00000000000000..4a527050dded9c --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/TestsProcessor.java @@ -0,0 +1,135 @@ +package io.quarkus.vertx.http.deployment.devmode.tests; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import io.netty.handler.codec.http.HttpHeaderNames; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.LaunchModeBuildItem; +import io.quarkus.deployment.dev.testing.TestClassResult; +import io.quarkus.deployment.dev.testing.TestRunResults; +import io.quarkus.deployment.dev.testing.TestSupport; +import io.quarkus.dev.spi.DevModeType; +import io.quarkus.devconsole.spi.DevConsoleRouteBuildItem; +import io.quarkus.devconsole.spi.DevConsoleTemplateInfoBuildItem; +import io.vertx.core.Handler; +import io.vertx.core.MultiMap; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.RoutingContext; + +public class TestsProcessor { + @BuildStep(onlyIf = IsDevelopment.class) + public DevConsoleTemplateInfoBuildItem results(LaunchModeBuildItem launchModeBuildItem) { + Optional ts = TestSupport.instance(); + if (!ts.isPresent() || launchModeBuildItem.getDevModeType().orElse(null) != DevModeType.LOCAL) { + return null; + } + return new DevConsoleTemplateInfoBuildItem("tests", ts.get()); + } + + @BuildStep(onlyIf = IsDevelopment.class) + DevConsoleRouteBuildItem handleTestStatus(LaunchModeBuildItem launchModeBuildItem) { + Optional ts = TestSupport.instance(); + if (!ts.isPresent() || launchModeBuildItem.getDevModeType().orElse(null) != DevModeType.LOCAL) { + return null; + } + //GET tests/status + //DISABLED, RUNNING (run id), RUN (run id, start time, nextRunQueued) + //GET tests/results + + return new DevConsoleRouteBuildItem("tests/status", "GET", new Handler() { + @Override + public void handle(RoutingContext event) { + jsonResponse(event); + TestSupport.RunStatus status = ts.get().getStatus(); + TestStatus testStatus = new TestStatus(); + testStatus.setLastRun(status.getLastRun()); + testStatus.setRunning(status.getRunning()); + if (status.getLastRun() > 0) { + TestRunResults result = ts.get().getResults(); + testStatus.setTestsFailed(result.getTestsFailed()); + testStatus.setTestsPassed(result.getTestsPassed()); + testStatus.setTestsSkipped(result.getTestsSkipped()); + testStatus.setTestsRun(result.getTestsFailed() + result.getTestsPassed()); + } + event.response().end(JsonObject.mapFrom(testStatus).encode()); + } + }); + } + + @BuildStep(onlyIf = IsDevelopment.class) + DevConsoleRouteBuildItem toggleTestRunner(LaunchModeBuildItem launchModeBuildItem) { + Optional ts = TestSupport.instance(); + if (!ts.isPresent() || launchModeBuildItem.getDevModeType().orElse(null) != DevModeType.LOCAL) { + return null; + } + //GET tests/status + //DISABLED, RUNNING (run id), RUN (run id, start time, nextRunQueued) + //GET tests/results + + return new DevConsoleRouteBuildItem("tests/toggle", "POST", new Handler() { + @Override + public void handle(RoutingContext event) { + if (ts.get().isStarted()) { + ts.get().stop(); + } else { + ts.get().start(); + } + } + }); + } + + @BuildStep(onlyIf = IsDevelopment.class) + DevConsoleRouteBuildItem runAllTests(LaunchModeBuildItem launchModeBuildItem) { + Optional ts = TestSupport.instance(); + if (!ts.isPresent() || launchModeBuildItem.getDevModeType().orElse(null) != DevModeType.LOCAL) { + return null; + } + //GET tests/status + //DISABLED, RUNNING (run id), RUN (run id, start time, nextRunQueued) + //GET tests/results + + return new DevConsoleRouteBuildItem("tests/runall", "POST", new Handler() { + @Override + public void handle(RoutingContext event) { + ts.get().runAllTests(); + } + }); + } + + @BuildStep(onlyIf = IsDevelopment.class) + DevConsoleRouteBuildItem handleTestResult(LaunchModeBuildItem launchModeBuildItem) { + Optional ts = TestSupport.instance(); + if (!ts.isPresent() || launchModeBuildItem.getDevModeType().orElse(null) != DevModeType.LOCAL) { + return null; + } + //GET tests/status + //DISABLED, RUNNING (run id), RUN (run id, start time, nextRunQueued) + //GET tests/results + + return new DevConsoleRouteBuildItem("tests/result", "GET", new Handler() { + @Override + public void handle(RoutingContext event) { + TestRunResults testRunResults = ts.get().getResults(); + if (testRunResults == null) { + event.response().setStatusCode(204).end(); + } else { + + jsonResponse(event); + Map results = new HashMap<>(); + for (Map.Entry entry : testRunResults.getResults().entrySet()) { + results.put(entry.getKey(), new ClassResult(entry.getValue())); + } + SuiteResult result = new SuiteResult(results); + event.response().end(JsonObject.mapFrom(result).encode()); + } + } + }); + } + + public MultiMap jsonResponse(RoutingContext event) { + return event.response().headers().add(HttpHeaderNames.CONTENT_TYPE, "application/json; charset=UTF-8"); + } +} diff --git a/extensions/vertx-http/deployment/src/main/resources/dev-static/css/dev-console.css b/extensions/vertx-http/deployment/src/main/resources/dev-static/css/dev-console.css index 72d8496593523a..1a58f7402286b4 100644 --- a/extensions/vertx-http/deployment/src/main/resources/dev-static/css/dev-console.css +++ b/extensions/vertx-http/deployment/src/main/resources/dev-static/css/dev-console.css @@ -21,11 +21,14 @@ margin-right: 5px; } -.navbar-text { +.navbar-right { font-size: 0.9em; position: absolute; right: 5px; bottom: 9px; + display: inline-block; + padding-top: .5rem; + padding-bottom: .5rem; } .navbar { diff --git a/extensions/vertx-http/deployment/src/main/resources/dev-static/js/tests.js b/extensions/vertx-http/deployment/src/main/resources/dev-static/js/tests.js new file mode 100644 index 00000000000000..da165bbb0afd0b --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/resources/dev-static/js/tests.js @@ -0,0 +1,103 @@ +var testsMyself = $('script[src*=tests]'); + +// Get the non application root path +var testsFrameworkRootPath = myself.attr('data-frameworkRootPath'); +if (typeof frameworkRootPath === "undefined" ) { + var pathname = window.location.pathname; + var frameworkRootPath = pathname.substr(0, pathname.indexOf('/dev/')); +} +// Get the streaming path +var testsStreamingPath = myself.attr('data-streamingPath'); +if (typeof testsStreamingPath === "undefined" ) { + var testsStreamingPath = "/dev/test"; +} + +var zoom = 0.90; +var panelHeight; +var linespace = 1.00; +var tabspace = 1; +var increment = 0.05; + +var testsWebSocket; +var tab = " "; +var space = " "; + +var isRunning = true; +var logScrolling = true; + +var filter = ""; + +$('document').ready(function () { + + testOpenSocket(); + // Make sure we stop the connection when the browser close + window.onbeforeunload = function () { + testCloseSocket(); + }; + + $("#quarkus-test-result-button-pause").click(function() { + var new_uri =window.location.protocol + "//" + window.location.host + frameworkRootPath + "/dev/io.quarkus.quarkus-vertx-http/tests/toggle"; + $.post( new_uri ); + }); + + $("#quarkus-test-result-button-run-all").click(function() { + var new_uri =window.location.protocol + "//" + window.location.host + frameworkRootPath + "/dev/io.quarkus.quarkus-vertx-http/tests/runall"; + $.post( new_uri ); + }); +}); + +function testOpenSocket() { + // Ensures only one connection is open at a time + if (testsWebSocket !== undefined && testsWebSocket.readyState !== WebSocket.CLOSED) { + return; + } + // Create a new instance of the websocket + var new_uri; + if (window.location.protocol === "https:") { + new_uri = "wss:"; + } else { + new_uri = "ws:"; + } + + new_uri += "//" + window.location.host + frameworkRootPath + testsStreamingPath; + testsWebSocket = new WebSocket(new_uri); + + testsWebSocket.onmessage = function (event) { + var json = JSON.parse(event.data); + if (json.running == false) { + $("#quarkus-test-result-button").removeClass("btn-success"); + $("#quarkus-test-result-button").removeClass("btn-danger"); + $("#quarkus-test-result-button").addClass("btn-warning"); + $("#quarkus-test-result-button-caption").text("Tests not running"); + $("#quarkus-test-result-button-pause").text("Start Tests"); + } else if(json.failed == 0){ + $("#quarkus-test-result-button").removeClass("btn-warning"); + $("#quarkus-test-result-button").removeClass("btn-danger"); + $("#quarkus-test-result-button").addClass("btn-success"); + $("#quarkus-test-result-button-caption").text("All tests passed"); + $("#quarkus-test-result-button-pause").text("Pause Tests"); + } else { + $("#quarkus-test-result-button").removeClass("btn-success"); + $("#quarkus-test-result-button").removeClass("btn-warning"); + $("#quarkus-test-result-button").addClass("btn-danger"); + $("#quarkus-test-result-button-caption").text(json.failed + " tests failed"); + $("#quarkus-test-result-button-pause").text("Pause Tests"); + } + if (json.inProgress) { + $("#quarkus-test-result-button-loading").css("display", "inline-flex"); + } else { + $("#quarkus-test-result-button-loading").css("display", "none"); + } + $("#quarkus-test-result-button").parent().css("display", "inline-flex"); + console.log(json); + }; + + testsWebSocket.onclose = function () { + + }; + +} + +function testCloseSocket() { + testsWebSocket.close(); +} diff --git a/extensions/vertx-http/deployment/src/main/resources/dev-templates/index.html b/extensions/vertx-http/deployment/src/main/resources/dev-templates/index.html index 676fe54069c72b..1de19186920edb 100644 --- a/extensions/vertx-http/deployment/src/main/resources/dev-templates/index.html +++ b/extensions/vertx-http/deployment/src/main/resources/dev-templates/index.html @@ -20,6 +20,23 @@ +

+
+
+ Tests + + + +
+
+

+ + + Tests +

+
+
+
{#each actionableExtensions} {#actionableExtension it/} {/each} diff --git a/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/test.html b/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/test.html new file mode 100644 index 00000000000000..269b232e5d3eb1 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/resources/dev-templates/io.quarkus.quarkus-vertx-http/test.html @@ -0,0 +1,109 @@ +{#include main fluid=true} + +{#title}Testing{/title} +{#body} + +
+ {#for cn in info:tests.results.failing.orEmpty} +
+
+
+ {cn.className} +
+
+ {#for r in cn.failing} +
+

+ {r.displayName} +

+

+ {#for log in r.logOutput} +

+ {log.raw} +

+ {/for} +

+
+ {/for} + {#for r in cn.passing} + {#if r.test} +

+ {r.displayName} +

+ {/if} + {/for} + {#for r in cn.skipped} + {#if r.test} +

+ {r.displayName} +

+ {/if} + {/for} +
+
+
+ {/for} +
+ {#for cn in info:tests.results.passing} +
+
+
+ {cn.className} +
+
+ {#for r in cn.passing} + {#if r.test} +

+ {r.displayName} +

+ +

+ {#for log in r.logOutput} +

+ {log.raw} +

+ {/for} +

+ {/if} + {/for} + {#for r in cn.skipped} + {#if r.test} +

+ {r.displayName} +

+ +

+ {#for log in r.logOutput} +

+ {log.raw} +

+ {/for} +

+ {/if} + {/for} +
+
+
+ {/for} + {#for cn in info:tests.results.skipped} +
+
+
+ {cn.className} +
+
+ {#for r in cn.skipped} + {#if r.test} +

+ {r.displayName} +

+ {/if} + {/for} +
+
+
+ {/for} +
+ +{/body} +{/include} diff --git a/extensions/vertx-http/deployment/src/main/resources/dev-templates/logmanagerNav.html b/extensions/vertx-http/deployment/src/main/resources/dev-templates/logmanagerNav.html index 7aabed2c9dfeee..616b0840ac789b 100644 --- a/extensions/vertx-http/deployment/src/main/resources/dev-templates/logmanagerNav.html +++ b/extensions/vertx-http/deployment/src/main/resources/dev-templates/logmanagerNav.html @@ -26,13 +26,13 @@ - - + {applicationName} {applicationVersion} (powered by Quarkus {quarkusVersion}) \ No newline at end of file diff --git a/extensions/vertx-http/deployment/src/main/resources/dev-templates/main.html b/extensions/vertx-http/deployment/src/main/resources/dev-templates/main.html index e449d367b70415..3c7acd684dfd31 100644 --- a/extensions/vertx-http/deployment/src/main/resources/dev-templates/main.html +++ b/extensions/vertx-http/deployment/src/main/resources/dev-templates/main.html @@ -42,10 +42,27 @@ {/if} - {/} + {/} + + {#if applicationName and applicationVersion}{applicationName} {applicationVersion} (powered by Quarkus {quarkusVersion}){/if} +
@@ -76,6 +93,7 @@ +