From 5fc1d79f4511bd878f8de99d749576cb6693e9ff Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Wed, 10 Mar 2021 15:35:23 +1100 Subject: [PATCH] Introduce continuous testing This commit adds the ability to test Quarkus applications running in dev mode in a continous manner, only running tests that are affected by the changed code. To this is it changes the test ClassLoading model to work the same as the dev mode model, and adds the ability to launch a completly isolated test application inside the same JVM as an existing Quarkus app. --- bom/application/pom.xml | 7 +- build-parent/pom.xml | 23 +- core/deployment/pom.xml | 27 + .../GeneratedClassGizmoAdaptor.java | 25 + .../quarkus/deployment/QuarkusAugmentor.java | 10 +- .../io/quarkus/deployment/TestConfig.java | 108 ++- .../builditem/LaunchModeBuildItem.java | 17 +- .../deployment/configuration/TestConfig.java | 21 - .../deployment/dev/ClassScanResult.java | 56 +- .../deployment/dev/DevModeContext.java | 210 ++++-- .../quarkus/deployment/dev/DevModeMain.java | 21 +- .../deployment/dev/IDEDevModeMain.java | 43 +- .../deployment/dev/IsolatedDevModeMain.java | 76 +- .../dev/IsolatedRemoteDevModeMain.java | 9 +- ...aderCompiler.java => QuarkusCompiler.java} | 68 +- .../dev/QuarkusDevModeLauncher.java | 28 +- .../dev/RuntimeUpdatesProcessor.java | 482 +++++++++---- .../deployment/dev/console/AeshConsole.java | 267 +++++++ .../deployment/dev/console/ConsoleHelper.java | 43 ++ .../dev/console/RedirectPrintStream.java | 188 +++++ .../dev/testing/CurrentTestApplication.java | 17 + .../dev/testing/HtmlAnsiOutputStream.java | 178 +++++ .../dev/testing/JunitTestRunner.java | 656 ++++++++++++++++++ .../dev/testing/TestClassResult.java | 60 ++ .../dev/testing/TestClassUsages.java | 134 ++++ .../dev/testing/TestConsoleHandler.java | 209 ++++++ .../dev/testing/TestController.java | 12 + .../deployment/dev/testing/TestListener.java | 21 + .../deployment/dev/testing/TestResult.java | 56 ++ .../dev/testing/TestRunListener.java | 30 + .../dev/testing/TestRunResults.java | 156 +++++ .../deployment/dev/testing/TestRunner.java | 349 ++++++++++ .../deployment/dev/testing/TestState.java | 134 ++++ .../deployment/dev/testing/TestSupport.java | 258 +++++++ .../dev/testing/TestTracingProcessor.java | 143 ++++ .../deployment/jbang/JBangAugmentorImpl.java | 43 +- .../logging/LoggingResourceProcessor.java | 75 +- .../deployment/mutability/DevModeTask.java | 12 +- .../deployment/steps/MainClassBuildStep.java | 9 + .../quarkus/deployment/util/FSWatchUtil.java | 13 +- .../runner/bootstrap/AugmentActionImpl.java | 4 +- .../runner/bootstrap/StartupActionImpl.java | 41 +- .../io/quarkus/dev/console/BasicConsole.java | 88 +++ .../io/quarkus/dev/console/InputHandler.java | 14 + .../quarkus/dev/console/QuarkusConsole.java | 152 ++++ .../ContinuousTestingWebsocketListener.java | 61 ++ .../quarkus/dev/testing/TracingHandler.java | 62 ++ .../java/io/quarkus/runtime/Application.java | 35 +- .../runtime/ApplicationLifecycleManager.java | 20 +- .../io/quarkus/runtime/ExecutorRecorder.java | 1 - .../runtime/logging/LogCleanupFilter.java | 1 + .../runtime/logging/LoggingSetupRecorder.java | 46 +- .../java/io/quarkus/gradle/QuarkusPlugin.java | 4 +- .../gradle/builder/QuarkusModelBuilder.java | 18 +- .../extension/QuarkusPluginExtension.java | 4 +- .../io/quarkus/gradle/tasks/QuarkusDev.java | 85 ++- .../gradle/tasks/QuarkusGradleUtils.java | 5 +- .../main/java/io/quarkus/maven/DevMojo.java | 96 ++- .../java/io/quarkus/maven/RemoteDevMojo.java | 2 +- .../DevServicesDatasourceProcessor.java | 21 +- .../ElasticsearchClientConfigTest.java | 2 +- .../quarkus/grpc/server/GrpcHealthTest.java | 1 - .../MutinyGrpcServiceWithPlainTextTest.java | 15 +- .../server/MutinyGrpcServiceWithSSLTest.java | 15 +- .../RegularGrpcServiceWithPlainTextTest.java | 15 +- ...arGrpcServiceWithSSLFromClasspathTest.java | 17 +- .../server/RegularGrpcServiceWithSSLTest.java | 16 +- .../blocking/BlockingAndNonBlockingTest.java | 16 +- .../server/blocking/BlockingMethodsTest.java | 14 +- .../BlockingMethodsWithMutinyImplTest.java | 14 +- .../server/blocking/BlockingServiceTest.java | 12 +- .../export/PrometheusEnabledTest.java | 1 + .../export/SecondPrometheusTest.java | 1 + .../mpmetrics/MpMetricRegistrationTest.java | 1 + .../binder/mpmetrics/CounterAdapter.java | 2 +- .../binder/mpmetrics/HistogramAdapter.java | 2 +- .../binder/mpmetrics/MeterAdapter.java | 2 +- .../mpmetrics/MetricRegistryAdapter.java | 2 +- .../binder/mpmetrics/TimerAdapter.java | 2 +- .../mutiny/deployment/MutinyProcessor.java | 13 +- .../mutiny/runtime/MutinyInfrastructure.java | 11 +- .../TestTransactionInterceptor.java | 2 +- .../runtime/AbstractTokensProducer.java | 2 +- .../deployment/MessageBundleProcessor.java | 3 +- .../qute/deployment/QuteProcessor.java | 9 +- .../serializers/SerializerFactoryBase.java | 2 +- .../jaxb/deployment/ConsumesXMLTestCase.java | 2 +- .../runtime/JaxrsClientReactiveRecorder.java | 9 - extensions/vertx-http/deployment/pom.xml | 13 + .../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 | 139 ++++ .../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 | 75 ++ .../testrunner/tags/IncludeTagsTestCase.java | 72 ++ .../vertx/http/testrunner/tags/TaggedET.java | 63 ++ extensions/vertx-http/runtime/pom.xml | 4 + .../ContinuousTestWebSocketHandler.java | 80 +++ .../runtime/devmode/DevConsoleRecorder.java | 4 + .../web/deployment/VertxWebProcessor.java | 10 +- .../quarkus/bootstrap/BootstrapConstants.java | 1 + .../bootstrap/BootstrapAppModelFactory.java | 7 +- .../bootstrap/app/ConfiguredClassLoading.java | 5 +- .../bootstrap/app/CuratedApplication.java | 4 +- .../bootstrap/app/QuarkusBootstrap.java | 69 +- .../classloading/QuarkusClassLoader.java | 4 + .../bootstrap/devmode/DependenciesFilter.java | 3 + .../bootstrap/utils/BuildToolHelper.java | 4 +- .../bootstrap/util/QuarkusModelHelper.java | 11 +- .../maven/workspace/LocalProject.java | 12 + .../io/quarkus/bootstrap/runner/Timing.java | 20 +- .../main/java/io/quarkus/qute/Results.java | 7 +- .../gradle/InjectBeanFromTestConfigTest.java | 5 +- ...MultiModuleWithEmptyModuleDevModeTest.java | 6 +- .../src/main/resources/application.properties | 4 + .../src/main/resources/application.properties | 3 + test-framework/arquillian/pom.xml | 18 + .../test/common/GroovyCacheCleaner.java | 34 + .../quarkus/test/common/PathTestHelper.java | 4 +- .../main/java/io/quarkus/test/ClearCache.java | 23 + .../io/quarkus/test/QuarkusDevModeTest.java | 94 ++- .../java/io/quarkus/test/QuarkusUnitTest.java | 36 + .../test/junit/IntegrationTestUtil.java | 2 +- .../test/junit/QuarkusTestExtension.java | 70 +- .../QuarkusSecurityTestExtension.java | 2 +- 145 files changed, 6576 insertions(+), 731 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/ConsoleHelper.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/console/BasicConsole.java create mode 100644 core/devmode-spi/src/main/java/io/quarkus/dev/console/InputHandler.java create mode 100644 core/devmode-spi/src/main/java/io/quarkus/dev/console/QuarkusConsole.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 integration-tests/gradle/src/test/resources/test-fixtures-module/src/main/resources/application.properties create mode 100644 test-framework/common/src/main/java/io/quarkus/test/common/GroovyCacheCleaner.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 63d6e2d36e4691..44ee6ace8e4e65 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -213,6 +213,7 @@ 1.10.2 0.8.6 1.15.2 + 2.1 @@ -4760,7 +4761,6 @@ - software.amazon.awssdk apache-client @@ -5159,6 +5159,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 2417018b849dc1..efff8f8937f913 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -483,13 +483,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: @@ -500,6 +498,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/GeneratedClassGizmoAdaptor.java b/core/deployment/src/main/java/io/quarkus/deployment/GeneratedClassGizmoAdaptor.java index 1ada9ee481056d..241708a6142227 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/GeneratedClassGizmoAdaptor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/GeneratedClassGizmoAdaptor.java @@ -2,11 +2,15 @@ import java.io.StringWriter; import java.io.Writer; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; import java.util.function.Predicate; import io.quarkus.bootstrap.BootstrapDebug; +import io.quarkus.bootstrap.classloading.ClassPathElement; +import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.gizmo.ClassOutput; @@ -33,6 +37,18 @@ public GeneratedClassGizmoAdaptor(BuildProducer generat this.sources = BootstrapDebug.DEBUG_SOURCES_DIR != null ? new ConcurrentHashMap<>() : null; } + public GeneratedClassGizmoAdaptor(BuildProducer generatedClasses, + Function generatedToBaseNameFunction) { + this.generatedClasses = generatedClasses; + this.applicationClassPredicate = new Predicate() { + @Override + public boolean test(String s) { + return isApplicationClass(generatedToBaseNameFunction.apply(s)); + } + }; + this.sources = BootstrapDebug.DEBUG_SOURCES_DIR != null ? new ConcurrentHashMap<>() : null; + } + @Override public void write(String className, byte[] bytes) { String source = null; @@ -56,4 +72,13 @@ public Writer getSourceWriter(String className) { return ClassOutput.super.getSourceWriter(className); } + public static boolean isApplicationClass(String className) { + 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(className.replace(".", "/") + ".class", true); + return !res.isEmpty(); + } + } 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 1db0ac8edafe54..bf392f0a74685a 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 { @@ -141,7 +143,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)) @@ -196,6 +198,7 @@ public static final class Builder { Consumer configCustomizer; ClassLoader deploymentClassLoader; DevModeType devModeType; + boolean auxiliaryApplication; public Builder addBuildChainCustomizer(Consumer customizer) { this.buildChainCustomizers.add(customizer); @@ -216,6 +219,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..97e581e8ea62da 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,101 @@ /** * 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; + + /** + * Changes tests to use the 'flat' ClassPath used in Quarkus 1.x versions. + * + * This means all Quarkus and test classes are loaded in the same ClassLoader, + * however it means you cannot use continuous testing. + * + * Note that if you find this necessary for your application then you + * may also have problems running in development mode, which cannot use + * a flat class path. + */ + @ConfigItem(defaultValue = "false") + public boolean flatClassPath; /** * Duration to wait for the native image to built during testing */ @@ -35,6 +124,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 +152,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..4fe6f17149aa24 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.addedClasses); + 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.addedClasses); + 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..e61c3419e76cba 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); } @@ -120,13 +120,14 @@ public void start() throws Exception { bootstrapBuilder.addLocalArtifact(i); } - for (DevModeContext.ModuleInfo i : context.getAllModules()) { - if (i.getClassesPath() != null) { - Path classesPath = Paths.get(i.getClassesPath()); + for (DevModeContext.ModuleInfo i : context.getAdditionalModules()) { + 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..3e114f8236fa7d 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,9 +43,12 @@ import io.quarkus.deployment.CodeGenerator; import io.quarkus.deployment.builditem.ApplicationClassPredicateBuildItem; import io.quarkus.deployment.codegen.CodeGenData; +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; +import io.quarkus.dev.console.InputHandler; +import io.quarkus.dev.console.QuarkusConsole; import io.quarkus.dev.spi.DevModeType; import io.quarkus.dev.spi.HotReplacementSetup; import io.quarkus.runner.bootstrap.AugmentActionImpl; @@ -68,8 +71,11 @@ public class IsolatedDevModeMain implements BiConsumer codeGens) { + ClassLoader old = Thread.currentThread().getContextClassLoader(); try { @@ -89,17 +95,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); @@ -168,7 +188,7 @@ private void startCodeGenWatcher(QuarkusClassLoader classLoader, List changedResources, ClassScanResult result) { @@ -217,21 +237,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())) { @@ -276,6 +294,7 @@ public void stop() { public void close() { //don't attempt to restart in the exit code handler restarting = true; + fsWatchUtil.shutdown(); try { stop(); } finally { @@ -289,7 +308,19 @@ public void close() { i.close(); } } finally { - curatedApplication.close(); + try { + curatedApplication.close(); + } finally { + if (shutdownThread != null) { + try { + Runtime.getRuntime().removeShutdownHook(shutdownThread); + } catch (IllegalStateException ignore) { + + } + shutdownThread = null; + } + shutdownLatch.countDown(); + } } } } @@ -359,7 +390,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 +404,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 +416,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 +430,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..208bc783f91767 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.getClassesPath() == 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..f519d0c8e4bfb1 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; } @@ -218,6 +230,13 @@ public B mainModule(ModuleInfo mainModule) { return (B) this; } + public boolean isTestsPresent() { + if (main == null) { + return false; + } + return main.getTest().isPresent(); + } + @SuppressWarnings("unchecked") public B dependency(ModuleInfo module) { dependencies.add(module); @@ -264,7 +283,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 +410,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..8b5570c5a73d63 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,7 +52,9 @@ import io.quarkus.bootstrap.runner.Timing; import io.quarkus.changeagent.ClassChangeAgent; -import io.quarkus.deployment.util.FSWatchUtil; +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.FileUtil; import io.quarkus.dev.spi.DevModeType; import io.quarkus.dev.spi.HotReplacementContext; @@ -60,11 +66,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 +80,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 +100,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 +109,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 +125,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 +286,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 +449,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 +494,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 +521,11 @@ && sourceFileWasRecentModified(p, ignoreFirstScanChanges)) } - checkForClassFilesChangesInModule(module, moduleChangedSourceFilePaths, ignoreFirstScanChanges, classScanResult); + checkForClassFilesChangesInModule(module, moduleChangedSourceFilePaths, ignoreFirstScanChanges, classScanResult, + cuf, timestampSet); + } - this.firstScanDone = true; return classScanResult; } @@ -389,13 +534,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 +554,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 +591,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 +621,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 +657,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 +697,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 +708,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 +728,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 +784,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 +803,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,8 +846,10 @@ public static void setLastStartIndex(IndexView lastStartIndex) { @Override public void close() throws IOException { + if (timer != null) { + timer.cancel(); + } compiler.close(); - FSWatchUtil.shutdown(); } private Map expandGlobPattern(Path root, Path configFile) { @@ -711,4 +876,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..f42a2bb89c26eb --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/AeshConsole.java @@ -0,0 +1,267 @@ +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; + +import io.quarkus.dev.console.InputHandler; +import io.quarkus.dev.console.QuarkusConsole; + +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/ConsoleHelper.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/ConsoleHelper.java new file mode 100644 index 00000000000000..6f57065e1ef884 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/ConsoleHelper.java @@ -0,0 +1,43 @@ +package io.quarkus.deployment.dev.console; + +import java.io.IOException; +import java.util.function.Consumer; + +import org.aesh.readline.tty.terminal.TerminalConnection; +import org.aesh.terminal.Connection; + +import io.quarkus.deployment.TestConfig; +import io.quarkus.dev.console.BasicConsole; +import io.quarkus.dev.console.QuarkusConsole; + +public class ConsoleHelper { + + public static synchronized void installConsole(TestConfig config) { + if (QuarkusConsole.installed) { + return; + } + QuarkusConsole.installed = true; + if (config.basicConsole) { + QuarkusConsole.INSTANCE = new BasicConsole(config.disableColor, true, System.out); + } else { + try { + new TerminalConnection(new Consumer() { + @Override + public void accept(Connection connection) { + if (connection.supportsAnsi()) { + QuarkusConsole.INSTANCE = new AeshConsole(connection); + } else { + connection.close(); + QuarkusConsole.INSTANCE = new BasicConsole(config.disableColor, true, System.out); + } + } + }); + } catch (IOException e) { + QuarkusConsole.INSTANCE = new BasicConsole(config.disableColor, true, System.out); + } + } + RedirectPrintStream ps = new RedirectPrintStream(); + System.setOut(ps); + System.setErr(ps); + } +} 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..2da9202a19bf27 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/RedirectPrintStream.java @@ -0,0 +1,188 @@ +package io.quarkus.deployment.dev.console; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.util.Formatter; +import java.util.Locale; + +import io.quarkus.dev.console.QuarkusConsole; + +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..fe2070ac4c2ab9 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java @@ -0,0 +1,656 @@ +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.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 + for (TestRunListener i : listeners) { + i.noTests(); + } + 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) { + try { + listener.runAborted(); + } catch (Throwable t) { + log.error("Failed to invoke test listener", t); + } + } + 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..a97e4224ea0bc3 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConsoleHandler.java @@ -0,0 +1,209 @@ +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.dev.console.InputHandler; +import io.quarkus.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 noTests() { + firstRun = false; + lastStatus = "No tests to run"; + promptHandler.setStatus(lastStatus); + promptHandler.setPrompt(RUNNING_PROMPT); + } + + @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..6a15acb0fa1f72 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunListener.java @@ -0,0 +1,30 @@ +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) { + + } + + default void noTests() { + + } +} 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..3dad4273acb5af --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunner.java @@ -0,0 +1,349 @@ +package io.quarkus.deployment.dev.testing; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +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; + + String appPropertiesIncludeTags; + String appPropertiesExcludeTags; + String appPropertiesIncludePattern; + String appPropertiesExcludePattern; + + 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 { + try { + runInternal(classScanResult, reRunFailures); + } finally { + waitTillResumed(); + boolean run = false; + ClassScanResult current = null; + synchronized (TestRunner.this) { + if (!disabled) { + if (testsQueued) { + testsQueued = false; + run = true; + } + current = queuedChanges; + queuedChanges = null; + } + testsRunning = run; + } + if (run) { + runTests(current); + } + } + } catch (Throwable t) { + log.error("Internal error running tests", t); + } + } + }, "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(); + + synchronized (this) { + if (runner != null) { + throw new IllegalStateException("Tests already in progress"); + } + if (disabled) { + return; + } + handleApplicationPropertiesChange(); + 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; + } + if (disabled) { + return; + } + if (firstRun) { + firstRun = false; + } + + } + + /** + * HUGE HACK + *

+ * config is driven from the outer dev mode startup, if the user modified test + * related config in application.properties it will cause a re-test, but the + * values will not be applied until a dev mode restart happens. + *

+ * We also can't apply this as part of the test startup, as by then it is too + * late, and the filters have already been resloved. + *

+ * We manually check for application.properties changes and apply them. + */ + private void handleApplicationPropertiesChange() { + for (Path rootPath : testApplication.getQuarkusBootstrap().getApplicationRoot()) { + Path appProps = rootPath.resolve("application.properties"); + if (Files.exists(appProps)) { + Properties p = new Properties(); + try (InputStream in = Files.newInputStream(appProps)) { + p.load(in); + } catch (IOException e) { + throw new RuntimeException(e); + } + String includeTags = p.getProperty("quarkus.test.include-tags"); + String excludeTags = p.getProperty("quarkus.test.exclude-tags"); + String includePattern = p.getProperty("quarkus.test.include-pattern"); + String excludePattern = p.getProperty("quarkus.test.exclude-pattern"); + if (!firstRun) { + if (!Objects.equals(includeTags, appPropertiesIncludeTags)) { + if (includeTags == null) { + testSupport.includeTags = Collections.emptyList(); + } else { + testSupport.includeTags = Arrays.stream(includeTags.split(",")).map(String::trim) + .collect(Collectors.toList()); + } + } + if (!Objects.equals(excludeTags, appPropertiesExcludeTags)) { + if (excludeTags == null) { + testSupport.excludeTags = Collections.emptyList(); + } else { + testSupport.excludeTags = Arrays.stream(excludeTags.split(",")).map(String::trim) + .collect(Collectors.toList()); + } + } + if (!Objects.equals(includePattern, appPropertiesIncludePattern)) { + if (includePattern == null) { + testSupport.include = null; + } else { + testSupport.include = Pattern.compile(includePattern); + } + } + if (!Objects.equals(excludePattern, appPropertiesExcludePattern)) { + if (excludePattern == null) { + testSupport.exclude = null; + } else { + testSupport.exclude = Pattern.compile(excludePattern); + } + } + } + appPropertiesIncludeTags = includeTags; + appPropertiesExcludeTags = excludeTags; + appPropertiesIncludePattern = includePattern; + appPropertiesExcludePattern = excludePattern; + break; + } + } + + } + + 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..d5bf5d2c583279 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java @@ -0,0 +1,143 @@ +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.ConsoleHelper; +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 + || config.flatClassPath) { + return null; + } + if (consoleInstalled) { + return null; + } + consoleInstalled = true; + if (config.console) { + ConsoleHelper.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 + || config.flatClassPath) { + return null; + } + if (launchModeBuildItem.getDevModeType().orElse(null) != DevModeType.LOCAL) { + return null; + } + TestSupport testSupport = TestSupport.instance().get(); + 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); + if (!liveReloadBuildItem.isLiveReload()) { + if (config.continuousTesting == TestConfig.Mode.ENABLED) { + testSupport.start(); + } else if (config.continuousTesting == TestConfig.Mode.PAUSED) { + testSupport.init(); + testSupport.stop(); + } + } + 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..0f708f0849b7c1 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 @@ -55,6 +55,7 @@ import io.quarkus.deployment.pkg.builditem.AppCDSRequestedBuildItem; import io.quarkus.deployment.recording.BytecodeRecorderImpl; import io.quarkus.dev.appstate.ApplicationStateNotification; +import io.quarkus.dev.console.QuarkusConsole; import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.CatchBlockCreator; import io.quarkus.gizmo.ClassCreator; @@ -135,6 +136,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); @@ -284,6 +290,9 @@ void build(List staticInitTasks, featuresHandle, activeProfile, tryBlock.load(LaunchMode.DEVELOPMENT.equals(launchMode.getLaunchMode()))); + + tryBlock.invokeStaticMethod( + ofMethod(QuarkusConsole.class, "start", void.class)); cb = tryBlock.addCatch(Throwable.class); // an exception was thrown before logging was actually setup, we simply dump everything to the console diff --git a/core/deployment/src/main/java/io/quarkus/deployment/util/FSWatchUtil.java b/core/deployment/src/main/java/io/quarkus/deployment/util/FSWatchUtil.java index 7cb29fe253780a..dffc226c1f597d 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/util/FSWatchUtil.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/util/FSWatchUtil.java @@ -20,14 +20,16 @@ public class FSWatchUtil { private static final Logger log = Logger.getLogger(FSWatchUtil.class); - private static final List executors = new ArrayList<>(); + private final List executors = new ArrayList<>(); + + private volatile boolean closed = false; /** * in a loop, checks for modifications in the files * * @param watchers list of {@link Watcher}s */ - public static void observe(Collection watchers, + public void observe(Collection watchers, long intervalMs) { ExecutorService executorService = Executors.newSingleThreadExecutor(); executorService.execute( @@ -35,13 +37,13 @@ public static void observe(Collection watchers, executors.add(executorService); } - private static void doObserve(Collection watchers, long intervalMs) { + private void doObserve(Collection watchers, long intervalMs) { Map lastModified = new HashMap<>(); long lastCheck = 0; // we're assuming no changes between the compilation and first execution, so don't trigger the watcher on first run boolean firstRun = true; //noinspection InfiniteLoopStatement - while (true) { + while (!closed) { for (Watcher watcher : watchers) { try { Path rootPath = watcher.rootPath; @@ -79,9 +81,10 @@ private static void doObserve(Collection watchers, long intervalMs) { } } - public static void shutdown() { + public void shutdown() { executors.forEach(ExecutorService::shutdown); executors.clear(); + closed = true; } public static class Watcher { 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..b7ab742c08823b 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,7 +53,7 @@ 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) { + if (curatedApplication.getQuarkusBootstrap().isFlatClassPath()) { resources.putAll(extractGeneratedResources(false)); baseClassLoader.reset(resources, transformedClasses); runtimeClassLoader = baseClassLoader; @@ -68,40 +66,6 @@ public StartupActionImpl(CuratedApplication curatedApplication, BuildResult buil 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 +190,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/console/BasicConsole.java b/core/devmode-spi/src/main/java/io/quarkus/dev/console/BasicConsole.java new file mode 100644 index 00000000000000..9f6167e0b533d5 --- /dev/null +++ b/core/devmode-spi/src/main/java/io/quarkus/dev/console/BasicConsole.java @@ -0,0 +1,88 @@ +package io.quarkus.dev.console; + +import java.io.IOException; +import java.io.PrintStream; +import java.nio.charset.Charset; +import java.util.logging.Level; +import java.util.logging.Logger; + +public class BasicConsole extends QuarkusConsole { + + private static final Logger log = Logger.getLogger(BasicConsole.class.getName()); + private static final Logger statusLogger = Logger.getLogger("quarkus"); + + final PrintStream printStream; + final boolean noColor; + + public 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.log(Level.SEVERE, "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; + } + statusLogger.info(prompt); + } + + @Override + protected void setStatusMessage(String status) { + if (status == null) { + return; + } + statusLogger.info(status); + } + }; + } + + @Override + public void write(String s) { + if (outputFilter != null) { + if (!outputFilter.test(s)) { + return; + } + } + if (noColor || !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/devmode-spi/src/main/java/io/quarkus/dev/console/InputHandler.java b/core/devmode-spi/src/main/java/io/quarkus/dev/console/InputHandler.java new file mode 100644 index 00000000000000..191ae5cb72b46a --- /dev/null +++ b/core/devmode-spi/src/main/java/io/quarkus/dev/console/InputHandler.java @@ -0,0 +1,14 @@ +package io.quarkus.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/devmode-spi/src/main/java/io/quarkus/dev/console/QuarkusConsole.java b/core/devmode-spi/src/main/java/io/quarkus/dev/console/QuarkusConsole.java new file mode 100644 index 00000000000000..8d79acfe4d8663 --- /dev/null +++ b/core/devmode-spi/src/main/java/io/quarkus/dev/console/QuarkusConsole.java @@ -0,0 +1,152 @@ +package io.quarkus.dev.console; + +import java.util.ArrayDeque; +import java.util.Locale; +import java.util.function.Predicate; + +public abstract class QuarkusConsole { + + public static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows"); + + /** + * ConEmu ANSI X3.64 support enabled, + * used by cmder + */ + public static final boolean IS_CON_EMU_ANSI = IS_WINDOWS && "ON".equals(System.getenv("ConEmuANSI")); + + /** + * These tests are same as used in jansi + * Source: https://github.com/fusesource/jansi/commit/bb3d538315c44f799d34fd3426f6c91c8e8dfc55 + */ + public static final boolean IS_CYGWIN = IS_WINDOWS + && System.getenv("PWD") != null + && System.getenv("PWD").startsWith("/") + && !"cygwin".equals(System.getenv("TERM")); + + public static final boolean IS_MINGW_XTERM = IS_WINDOWS + && System.getenv("MSYSTEM") != null + && System.getenv("MSYSTEM").startsWith("MINGW") + && "xterm".equals(System.getenv("TERM")); + protected final ArrayDeque inputHandlers = new ArrayDeque<>(); + + public static volatile QuarkusConsole INSTANCE = new BasicConsole(false, false, System.out); + + public static volatile boolean installed; + + protected volatile Predicate outputFilter; + + private volatile boolean started = false; + + public static boolean hasColorSupport() { + + if (IS_WINDOWS) { + // On Windows without a known good emulator + // TODO: optimally we would check if Win32 getConsoleMode has + // ENABLE_VIRTUAL_TERMINAL_PROCESSING enabled or enable it via + // setConsoleMode. + // For now we turn it off to not generate noisy output for most + // users. + // Must be on some Unix variant or ANSI-enabled windows terminal... + return IS_CON_EMU_ANSI || IS_CYGWIN || IS_MINGW_XTERM; + } else { + // on sane operating systems having a console is a good indicator + // you are attached to a TTY with colors. + return System.console() != null; + } + } + + public synchronized void pushInputHandler(InputHandler inputHandler) { + InputHolder holder = inputHandlers.peek(); + if (holder != null) { + holder.setEnabled(false); + } + holder = createHolder(inputHandler); + inputHandler.promptHandler(holder); + if (started) { + holder.setEnabled(true); + } + inputHandlers.push(holder); + } + + public synchronized void popInputHandler() { + InputHolder holder = inputHandlers.pop(); + holder.setEnabled(false); + holder = inputHandlers.peek(); + if (holder != null) { + holder.setEnabled(true); + } + } + + public static void start() { + INSTANCE.startInternal(); + } + + private synchronized void startInternal() { + if (started) { + return; + } + started = true; + InputHolder 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); + + 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 { + public 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/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..ef17ec7fa78044 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/ApplicationLifecycleManager.java @@ -61,9 +61,10 @@ private ApplicationLifecycleManager() { //guard for all state private static final Lock stateLock = Locks.reentrantLock(); private static final Condition stateCond = stateLock.newCondition(); + private static ShutdownHookThread shutdownHookThread; 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; @@ -81,9 +82,8 @@ public static void run(Application application, Class exitCodeH if (ImageInfo.inImageRuntimeCode() && System.getenv(DISABLE_SIGNAL_HANDLERS) == null) { registerSignalHandlers(exitCodeHandler); } - final ShutdownHookThread shutdownHookThread = new ShutdownHookThread(); + shutdownHookThread = new ShutdownHookThread(); Runtime.getRuntime().addShutdownHook(shutdownHookThread); } diff --git a/core/runtime/src/main/java/io/quarkus/runtime/ExecutorRecorder.java b/core/runtime/src/main/java/io/quarkus/runtime/ExecutorRecorder.java index cc93df1a2c9cc0..cdcdcb1cab9c20 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/ExecutorRecorder.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/ExecutorRecorder.java @@ -52,7 +52,6 @@ public void run() { } }); executor = devModeExecutor; - Runtime.getRuntime().addShutdownHook(new Thread(shutdownTask, "Executor shutdown thread")); } else { shutdownContext.addLastShutdownTask(shutdownTask); executor = underlying; diff --git a/core/runtime/src/main/java/io/quarkus/runtime/logging/LogCleanupFilter.java b/core/runtime/src/main/java/io/quarkus/runtime/logging/LogCleanupFilter.java index 17f69775007447..927956fd7ccced 100644 --- a/core/runtime/src/main/java/io/quarkus/runtime/logging/LogCleanupFilter.java +++ b/core/runtime/src/main/java/io/quarkus/runtime/logging/LogCleanupFilter.java @@ -25,6 +25,7 @@ public boolean isLoggable(LogRecord record) { if (record.getLevel().intValue() > 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..0caec179cb378e 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 @@ -9,7 +9,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Objects; @@ -37,6 +36,7 @@ import org.jboss.logmanager.handlers.SyslogHandler; import io.quarkus.bootstrap.logging.InitialConfigurator; +import io.quarkus.dev.console.QuarkusConsole; import io.quarkus.runtime.RuntimeValue; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.runtime.configuration.ConfigInstantiator; @@ -49,28 +49,6 @@ public class LoggingSetupRecorder { private static final org.jboss.logging.Logger log = org.jboss.logging.Logger.getLogger(LoggingSetupRecorder.class); - private static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("windows"); - - /** - * ConEmu ANSI X3.64 support enabled, - * used by cmder - */ - private static final boolean IS_CON_EMU_ANSI = IS_WINDOWS && "ON".equals(System.getenv("ConEmuANSI")); - - /** - * These tests are same as used in jansi - * Source: https://github.com/fusesource/jansi/commit/bb3d538315c44f799d34fd3426f6c91c8e8dfc55 - */ - private static final boolean IS_CYGWIN = IS_WINDOWS - && System.getenv("PWD") != null - && System.getenv("PWD").startsWith("/") - && !"cygwin".equals(System.getenv("TERM")); - - private static final boolean IS_MINGW_XTERM = IS_WINDOWS - && System.getenv("MSYSTEM") != null - && System.getenv("MSYSTEM").startsWith("MINGW") - && "xterm".equals(System.getenv("TERM")); - public LoggingSetupRecorder() { } @@ -327,24 +305,6 @@ public void initializeLoggingForImageBuild() { } } - private static boolean hasColorSupport() { - - if (IS_WINDOWS) { - // On Windows without a known good emulator - // TODO: optimally we would check if Win32 getConsoleMode has - // ENABLE_VIRTUAL_TERMINAL_PROCESSING enabled or enable it via - // setConsoleMode. - // For now we turn it off to not generate noisy output for most - // users. - // Must be on some Unix variant or ANSI-enabled windows terminal... - return IS_CON_EMU_ANSI || IS_CYGWIN || IS_MINGW_XTERM; - } else { - // on sane operating systems having a console is a good indicator - // you are attached to a TTY with colors. - return System.console() != null; - } - } - private static Handler configureConsoleHandler(final ConsoleConfig config, final ErrorManager defaultErrorManager, final List filterElements, final List>> possibleFormatters, @@ -366,7 +326,7 @@ private static Handler configureConsoleHandler(final ConsoleConfig config, final if (possibleBannerSupplier != null && possibleBannerSupplier.getValue().isPresent()) { bannerSupplier = possibleBannerSupplier.getValue().get(); } - if (config.color.orElse(hasColorSupport())) { + if (config.color.orElse(QuarkusConsole.hasColorSupport())) { ColorPatternFormatter colorPatternFormatter = new ColorPatternFormatter(config.darken, config.format); if (bannerSupplier != null) { @@ -383,7 +343,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/QuarkusPlugin.java b/devtools/gradle/src/main/java/io/quarkus/gradle/QuarkusPlugin.java index 8612ec82cbdf22..b8dee8f17413ec 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/QuarkusPlugin.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/QuarkusPlugin.java @@ -161,7 +161,9 @@ public void execute(Task test) { Task classesTask = tasks.getByName(JavaPlugin.CLASSES_TASK_NAME); Task resourcesTask = tasks.getByName(JavaPlugin.PROCESS_RESOURCES_TASK_NAME); - quarkusDev.dependsOn(classesTask, resourcesTask, quarkusGenerateCode); + Task testClassesTask = tasks.getByName(JavaPlugin.TEST_CLASSES_TASK_NAME); + Task testResourcesTask = tasks.getByName(JavaPlugin.PROCESS_TEST_RESOURCES_TASK_NAME); + quarkusDev.dependsOn(classesTask, resourcesTask, testClassesTask, testResourcesTask, quarkusGenerateCode); quarkusRemoteDev.dependsOn(classesTask, resourcesTask); quarkusBuild.dependsOn(classesTask, resourcesTask, tasks.getByName(JavaPlugin.JAR_TASK_NAME)); diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/builder/QuarkusModelBuilder.java b/devtools/gradle/src/main/java/io/quarkus/gradle/builder/QuarkusModelBuilder.java index 5bc7725896f36c..203f410d53518e 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/builder/QuarkusModelBuilder.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/builder/QuarkusModelBuilder.java @@ -349,15 +349,21 @@ private void collectDependencies(ResolvedConfiguration configuration, continue; } final DependencyImpl dep = initDependency(a); - if (LaunchMode.DEVELOPMENT.equals(mode) && + if ((LaunchMode.DEVELOPMENT.equals(mode) || LaunchMode.TEST.equals(mode)) && a.getId().getComponentIdentifier() instanceof ProjectComponentIdentifier) { IncludedBuild includedBuild = includedBuild(project, a.getName()); - if (includedBuild != null) { - addSubstitutedProject(dep, includedBuild.getProjectDir()); + if ("test-fixtures".equals(a.getClassifier()) || "test".equals(a.getClassifier())) { + //TODO: test-fixtures are broken under the new ClassLoading model + dep.addPath(a.getFile()); } else { - Project projectDep = project.getRootProject() - .findProject(((ProjectComponentIdentifier) a.getId().getComponentIdentifier()).getProjectPath()); - addDevModePaths(dep, a, projectDep); + if (includedBuild != null) { + addSubstitutedProject(dep, includedBuild.getProjectDir()); + } else { + Project projectDep = project.getRootProject() + .findProject( + ((ProjectComponentIdentifier) a.getId().getComponentIdentifier()).getProjectPath()); + addDevModePaths(dep, a, projectDep); + } } } else { dep.addPath(a.getFile()); diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/extension/QuarkusPluginExtension.java b/devtools/gradle/src/main/java/io/quarkus/gradle/extension/QuarkusPluginExtension.java index e350e49532903f..14a9b066923989 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/extension/QuarkusPluginExtension.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/extension/QuarkusPluginExtension.java @@ -62,8 +62,8 @@ public void beforeTest(Test task) { final AppModel appModel = getAppModelResolver(LaunchMode.TEST) .resolveModel(getAppArtifact()); - final Path serializedModel = QuarkusGradleUtils.serializeAppModel(appModel, task); - props.put(BootstrapConstants.SERIALIZED_APP_MODEL, serializedModel.toString()); + final Path serializedModel = QuarkusGradleUtils.serializeAppModel(appModel, task, true); + props.put(BootstrapConstants.SERIALIZED_TEST_APP_MODEL, serializedModel.toString()); StringJoiner outputSourcesDir = new StringJoiner(","); for (File outputSourceDir : combinedOutputSourceDirs()) { 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..f49b481eb38ad7 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 @@ -216,7 +216,8 @@ private QuarkusDevModeLauncher newLauncher() throws Exception { .outputDir(getBuildDir()) .debug(System.getProperty("debug")) .debugHost(System.getProperty("debugHost", "localhost")) - .suspend(System.getProperty("suspend")); + .suspend(System.getProperty("suspend")) + .jvmArgs("-Dquarkus.test.basic-console=true"); //TODO: figure out how to fix the console if (getJvmArgs() != null) { builder.jvmArgs(getJvmArgs()); @@ -293,10 +294,26 @@ private QuarkusDevModeLauncher newLauncher() throws Exception { modifyDevModeContext(builder); - final Path serializedModel = QuarkusGradleUtils.serializeAppModel(appModel, this); + final Path serializedModel = QuarkusGradleUtils.serializeAppModel(appModel, this, false); serializedModel.toFile().deleteOnExit(); builder.jvmArgs("-D" + BootstrapConstants.SERIALIZED_APP_MODEL + "=" + serializedModel.toAbsolutePath()); + if (builder.isTestsPresent()) { + final AppModel testAppModel; + final AppModelResolver testModelResolver = extension().getAppModelResolver(LaunchMode.TEST); + try { + final AppArtifact appArtifact = extension().getAppArtifact(); + appArtifact.setPaths(QuarkusGradleUtils.getOutputPaths(project)); + testAppModel = testModelResolver.resolveModel(appArtifact); + } catch (AppModelResolverException e) { + throw new GradleException( + "Failed to resolve application model " + extension().getAppArtifact() + " dependencies", + e); + } + final Path serializedTestModel = QuarkusGradleUtils.serializeAppModel(testAppModel, this, true); + serializedTestModel.toFile().deleteOnExit(); + builder.jvmArgs("-D" + BootstrapConstants.SERIALIZED_TEST_APP_MODEL + "=" + serializedTestModel.toAbsolutePath()); + } extension().outputDirectory().mkdirs(); if (!args.isEmpty()) { @@ -383,16 +400,60 @@ 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.Builder moduleBuilder = 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()); + + SourceSet testSourceSet = sourceSets.findByName(SourceSet.TEST_SOURCE_SET_NAME); + if (testSourceSet != null) { + + Set testSourcePaths = new HashSet<>(); + Set testSourceParentPaths = new HashSet<>(); + + for (File sourceDir : testSourceSet.getAllJava().getSrcDirs()) { + if (sourceDir.exists()) { + testSourcePaths.add(sourceDir.getAbsolutePath()); + testSourceParentPaths.add(sourceDir.toPath().getParent().toAbsolutePath().toString()); + } + } + //TODO: multiple resource directories + final File testResourcesSrcDir = testSourceSet.getResources().getSourceDirectories().getSingleFile(); + // resourcesSrcDir may exist but if it's empty the resources output dir won't be created + final File testResourcesOutputDir = testSourceSet.getOutput().getResourcesDir(); + + if (!testSourcePaths.isEmpty() || (testResourcesOutputDir != null && testResourcesOutputDir.exists())) { + String testClassesDir = QuarkusGradleUtils.getClassesDir(testSourceSet, project.getBuildDir()); + if (testClassesDir != null) { + File testClassesDirFile = new File(testClassesDir); + if (testClassesDirFile.exists()) { + final String testResourcesOutputPath; + if (testResourcesOutputDir.exists()) { + testResourcesOutputPath = testResourcesOutputDir.getAbsolutePath(); + if (!Files.exists(Paths.get(testClassesDir))) { + // currently classesDir can't be null and is expected to exist + testClassesDir = testResourcesOutputPath; + } + } else { + // currently resources dir should exist + testResourcesOutputPath = testClassesDir; + } + moduleBuilder.setTestSourcePaths(testSourcePaths) + .setTestClassesPath(testClassesDir) + .setTestResourcePath(testResourcesSrcDir.getAbsolutePath()) + .setTestResourcesOutputPath(testResourcesOutputPath); + } + } + } + } + DevModeContext.ModuleInfo wsModuleInfo = moduleBuilder + .build(); if (root) { builder.mainModule(wsModuleInfo); diff --git a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusGradleUtils.java b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusGradleUtils.java index 9b5d936a13aef7..99dcdc0c2a136a 100644 --- a/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusGradleUtils.java +++ b/devtools/gradle/src/main/java/io/quarkus/gradle/tasks/QuarkusGradleUtils.java @@ -24,8 +24,9 @@ public class QuarkusGradleUtils { private static final String ERROR_COLLECTING_PROJECT_CLASSES = "Failed to collect project's classes in a temporary dir"; - public static Path serializeAppModel(final AppModel appModel, Task context) throws IOException { - final Path serializedModel = context.getTemporaryDir().toPath().resolve("quarkus-app-model.dat"); + public static Path serializeAppModel(final AppModel appModel, Task context, boolean test) throws IOException { + final Path serializedModel = context.getTemporaryDir().toPath() + .resolve("quarkus-app" + (test ? "-test" : "") + "-model.dat"); try (ObjectOutputStream out = new ObjectOutputStream(Files.newOutputStream(serializedModel))) { out.writeObject(appModel); } 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..41e7bc1e29d418 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,14 @@ private void handleAutoCompile() throws MojoExecutionException { if (prepareNeeded) { triggerPrepare(); } - triggerCompile(); + triggerCompile(false); + } + if (testCompileNeeded) { + try { + triggerCompile(true); + } catch (Throwable t) { + getLog().error("Test compile failed, you will need to fix your tests before you can use continuous testing", t); + } } } @@ -397,25 +428,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 +535,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 +550,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 +564,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 +576,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 +596,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/elasticsearch-rest-client/deployment/src/test/java/io/quarkus/elasticsearch/restclient/lowlevel/runtime/ElasticsearchClientConfigTest.java b/extensions/elasticsearch-rest-client/deployment/src/test/java/io/quarkus/elasticsearch/restclient/lowlevel/runtime/ElasticsearchClientConfigTest.java index 2f56c48eff7c35..23b80db7a51114 100644 --- a/extensions/elasticsearch-rest-client/deployment/src/test/java/io/quarkus/elasticsearch/restclient/lowlevel/runtime/ElasticsearchClientConfigTest.java +++ b/extensions/elasticsearch-rest-client/deployment/src/test/java/io/quarkus/elasticsearch/restclient/lowlevel/runtime/ElasticsearchClientConfigTest.java @@ -20,7 +20,7 @@ public class ElasticsearchClientConfigTest { @RegisterExtension static final QuarkusUnitTest TEST = new QuarkusUnitTest() .setArchiveProducer( - () -> ShrinkWrap.create(JavaArchive.class).addClass(TestConfigurator.class) + () -> ShrinkWrap.create(JavaArchive.class).addClasses(TestConfigurator.class, RestClientBuilderHelper.class) .addAsResource(new StringAsset("quarkus.elasticsearch.hosts=elasticsearch:9200"), "application.properties")); diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/GrpcHealthTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/GrpcHealthTest.java index f876a70c1d0f07..9ae2e7a65f28ab 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/GrpcHealthTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/GrpcHealthTest.java @@ -36,7 +36,6 @@ public class GrpcHealthTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( () -> ShrinkWrap.create(JavaArchive.class) - .addPackage(HealthGrpc.class.getPackage()) .addPackage(GreeterGrpc.class.getPackage()) .addClass(HelloService.class)) .withConfigurationResource("health-config.properties"); diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/MutinyGrpcServiceWithPlainTextTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/MutinyGrpcServiceWithPlainTextTest.java index 9f1b620a2fec09..2d18e7aae3b6be 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/MutinyGrpcServiceWithPlainTextTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/MutinyGrpcServiceWithPlainTextTest.java @@ -27,12 +27,13 @@ public class MutinyGrpcServiceWithPlainTextTest extends GrpcServiceTestBase { @RegisterExtension - static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( - () -> ShrinkWrap.create(JavaArchive.class) - .addClasses(MutinyHelloService.class, MutinyTestService.class, AssertHelper.class, - GreeterGrpc.class, HelloRequest.class, HelloReply.class, MutinyGreeterGrpc.class, - HelloRequestOrBuilder.class, HelloReplyOrBuilder.class, - EmptyProtos.class, Messages.class, MutinyTestServiceGrpc.class, - TestServiceGrpc.class)); + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setFlatClassPath(true).setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addClasses(MutinyHelloService.class, MutinyTestService.class, AssertHelper.class, + GreeterGrpc.class, HelloRequest.class, HelloReply.class, MutinyGreeterGrpc.class, + HelloRequestOrBuilder.class, HelloReplyOrBuilder.class, + EmptyProtos.class, Messages.class, MutinyTestServiceGrpc.class, + TestServiceGrpc.class)); } diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/MutinyGrpcServiceWithSSLTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/MutinyGrpcServiceWithSSLTest.java index e58596f060f7e1..7b496f5accfac0 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/MutinyGrpcServiceWithSSLTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/MutinyGrpcServiceWithSSLTest.java @@ -37,13 +37,14 @@ public class MutinyGrpcServiceWithSSLTest extends GrpcServiceTestBase { @RegisterExtension - static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( - () -> ShrinkWrap.create(JavaArchive.class) - .addClasses(MutinyHelloService.class, MutinyTestService.class, AssertHelper.class, - GreeterGrpc.class, HelloRequest.class, HelloReply.class, MutinyGreeterGrpc.class, - HelloRequestOrBuilder.class, HelloReplyOrBuilder.class, - EmptyProtos.class, Messages.class, MutinyTestServiceGrpc.class, - TestServiceGrpc.class)) + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setFlatClassPath(true).setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addClasses(MutinyHelloService.class, MutinyTestService.class, AssertHelper.class, + GreeterGrpc.class, HelloRequest.class, HelloReply.class, MutinyGreeterGrpc.class, + HelloRequestOrBuilder.class, HelloReplyOrBuilder.class, + EmptyProtos.class, Messages.class, MutinyTestServiceGrpc.class, + TestServiceGrpc.class)) .withConfigurationResource("grpc-server-tls-configuration.properties"); @Override diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/RegularGrpcServiceWithPlainTextTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/RegularGrpcServiceWithPlainTextTest.java index 61417373476906..b2361df538bbc4 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/RegularGrpcServiceWithPlainTextTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/RegularGrpcServiceWithPlainTextTest.java @@ -27,12 +27,13 @@ public class RegularGrpcServiceWithPlainTextTest extends GrpcServiceTestBase { @RegisterExtension - static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( - () -> ShrinkWrap.create(JavaArchive.class) - .addClasses(HelloService.class, TestService.class, AssertHelper.class, - GreeterGrpc.class, HelloRequest.class, HelloReply.class, MutinyGreeterGrpc.class, - HelloRequestOrBuilder.class, HelloReplyOrBuilder.class, - EmptyProtos.class, Messages.class, MutinyTestServiceGrpc.class, - TestServiceGrpc.class)); + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setFlatClassPath(true).setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addClasses(HelloService.class, TestService.class, AssertHelper.class, + GreeterGrpc.class, HelloRequest.class, HelloReply.class, MutinyGreeterGrpc.class, + HelloRequestOrBuilder.class, HelloReplyOrBuilder.class, + EmptyProtos.class, Messages.class, MutinyTestServiceGrpc.class, + TestServiceGrpc.class)); } diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/RegularGrpcServiceWithSSLFromClasspathTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/RegularGrpcServiceWithSSLFromClasspathTest.java index 20e1c1bba1e3c5..33c0f848933630 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/RegularGrpcServiceWithSSLFromClasspathTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/RegularGrpcServiceWithSSLFromClasspathTest.java @@ -38,14 +38,15 @@ public class RegularGrpcServiceWithSSLFromClasspathTest extends GrpcServiceTestBase { @RegisterExtension - static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( - () -> ShrinkWrap.create(JavaArchive.class) - .addClasses(HelloService.class, TestService.class, AssertHelper.class, - GreeterGrpc.class, HelloRequest.class, HelloReply.class, MutinyGreeterGrpc.class, - HelloRequestOrBuilder.class, HelloReplyOrBuilder.class, - EmptyProtos.class, Messages.class, MutinyTestServiceGrpc.class, - TestServiceGrpc.class) - .addAsResource(new File("src/test/resources/tls/server-keystore.jks"), "server-keystore.jks")) + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setFlatClassPath(true).setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addClasses(HelloService.class, TestService.class, AssertHelper.class, + GreeterGrpc.class, HelloRequest.class, HelloReply.class, MutinyGreeterGrpc.class, + HelloRequestOrBuilder.class, HelloReplyOrBuilder.class, + EmptyProtos.class, Messages.class, MutinyTestServiceGrpc.class, + TestServiceGrpc.class) + .addAsResource(new File("src/test/resources/tls/server-keystore.jks"), "server-keystore.jks")) .withConfigurationResource("grpc-server-tls-classpath-configuration.properties"); @Override diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/RegularGrpcServiceWithSSLTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/RegularGrpcServiceWithSSLTest.java index ccc7cb2c5b655e..835b0dd868d703 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/RegularGrpcServiceWithSSLTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/RegularGrpcServiceWithSSLTest.java @@ -37,13 +37,15 @@ public class RegularGrpcServiceWithSSLTest extends GrpcServiceTestBase { @RegisterExtension - static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( - () -> ShrinkWrap.create(JavaArchive.class) - .addClasses(HelloService.class, TestService.class, AssertHelper.class, - GreeterGrpc.class, HelloRequest.class, HelloReply.class, MutinyGreeterGrpc.class, - HelloRequestOrBuilder.class, HelloReplyOrBuilder.class, - EmptyProtos.class, Messages.class, MutinyTestServiceGrpc.class, - TestServiceGrpc.class)) + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setFlatClassPath(true) + .setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addClasses(HelloService.class, TestService.class, AssertHelper.class, + GreeterGrpc.class, HelloRequest.class, HelloReply.class, MutinyGreeterGrpc.class, + HelloRequestOrBuilder.class, HelloReplyOrBuilder.class, + EmptyProtos.class, Messages.class, MutinyTestServiceGrpc.class, + TestServiceGrpc.class)) .withConfigurationResource("grpc-server-tls-configuration.properties"); @Override diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/BlockingAndNonBlockingTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/BlockingAndNonBlockingTest.java index 463f343c4d3455..9e5893dfe13ed5 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/BlockingAndNonBlockingTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/BlockingAndNonBlockingTest.java @@ -36,13 +36,15 @@ public class BlockingAndNonBlockingTest { @RegisterExtension - static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( - () -> ShrinkWrap.create(JavaArchive.class) - .addPackage(HealthGrpc.class.getPackage()) - .addPackage(GreeterGrpc.class.getPackage()) - .addPackage(TestServiceGrpc.class.getPackage()) - .addPackage(EmptyProtos.class.getPackage()) - .addClasses(BlockingMutinyHelloService.class, TestService.class, AssertHelper.class)) + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setFlatClassPath(true) + .setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addPackage(HealthGrpc.class.getPackage()) + .addPackage(GreeterGrpc.class.getPackage()) + .addPackage(TestServiceGrpc.class.getPackage()) + .addPackage(EmptyProtos.class.getPackage()) + .addClasses(BlockingMutinyHelloService.class, TestService.class, AssertHelper.class)) .withConfigurationResource("blocking-config.properties"); @Inject diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/BlockingMethodsTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/BlockingMethodsTest.java index 79cee114b3abc5..795189e9dc22e3 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/BlockingMethodsTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/BlockingMethodsTest.java @@ -33,12 +33,14 @@ public class BlockingMethodsTest { @RegisterExtension - static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( - () -> ShrinkWrap.create(JavaArchive.class) - .addPackage(EmptyProtos.class.getPackage()) - .addPackage(Messages.class.getPackage()) - .addPackage(BlockingTestServiceGrpc.class.getPackage()) - .addClasses(BlockingTestService.class, AssertHelper.class)) + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setFlatClassPath(true) + .setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addPackage(EmptyProtos.class.getPackage()) + .addPackage(Messages.class.getPackage()) + .addPackage(BlockingTestServiceGrpc.class.getPackage()) + .addClasses(BlockingTestService.class, AssertHelper.class)) .withConfigurationResource("blocking-test-config.properties"); protected static final Duration TIMEOUT = Duration.ofSeconds(5); diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/BlockingMethodsWithMutinyImplTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/BlockingMethodsWithMutinyImplTest.java index 3a70c80fc1eb21..efdc366782bdb1 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/BlockingMethodsWithMutinyImplTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/BlockingMethodsWithMutinyImplTest.java @@ -33,12 +33,14 @@ public class BlockingMethodsWithMutinyImplTest { @RegisterExtension - static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( - () -> ShrinkWrap.create(JavaArchive.class) - .addPackage(EmptyProtos.class.getPackage()) - .addPackage(Messages.class.getPackage()) - .addPackage(BlockingTestServiceGrpc.class.getPackage()) - .addClasses(BlockingMutinyTestService.class, AssertHelper.class)) + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setFlatClassPath(true) + .setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addPackage(EmptyProtos.class.getPackage()) + .addPackage(Messages.class.getPackage()) + .addPackage(BlockingTestServiceGrpc.class.getPackage()) + .addClasses(BlockingMutinyTestService.class, AssertHelper.class)) .withConfigurationResource("blocking-test-config.properties"); protected static final Duration TIMEOUT = Duration.ofSeconds(5); diff --git a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/BlockingServiceTest.java b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/BlockingServiceTest.java index f8ed886bf276ba..55c9924d5c6a71 100644 --- a/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/BlockingServiceTest.java +++ b/extensions/grpc/deployment/src/test/java/io/quarkus/grpc/server/blocking/BlockingServiceTest.java @@ -34,11 +34,13 @@ public class BlockingServiceTest { @RegisterExtension - static final QuarkusUnitTest config = new QuarkusUnitTest().setArchiveProducer( - () -> ShrinkWrap.create(JavaArchive.class) - .addPackage(HealthGrpc.class.getPackage()) - .addPackage(GreeterGrpc.class.getPackage()) - .addClasses(BlockingMutinyHelloService.class)) + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setFlatClassPath(true) + .setArchiveProducer( + () -> ShrinkWrap.create(JavaArchive.class) + .addPackage(HealthGrpc.class.getPackage()) + .addPackage(GreeterGrpc.class.getPackage()) + .addClasses(BlockingMutinyHelloService.class)) .withConfigurationResource("reflection-config.properties"); protected ManagedChannel channel; diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/export/PrometheusEnabledTest.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/export/PrometheusEnabledTest.java index e4801a95c5b92a..4244d1bf64df99 100644 --- a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/export/PrometheusEnabledTest.java +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/export/PrometheusEnabledTest.java @@ -18,6 +18,7 @@ public class PrometheusEnabledTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() + .setFlatClassPath(true) .withConfigurationResource("test-logging.properties") .overrideConfigKey("quarkus.micrometer.binder-enabled-default", "false") .overrideConfigKey("quarkus.micrometer.export.prometheus.enabled", "true") diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/export/SecondPrometheusTest.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/export/SecondPrometheusTest.java index 3baa7f0d7c543e..224afb1d14a1b3 100644 --- a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/export/SecondPrometheusTest.java +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/deployment/export/SecondPrometheusTest.java @@ -17,6 +17,7 @@ public class SecondPrometheusTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() + .setFlatClassPath(true) .withConfigurationResource("test-logging.properties") .overrideConfigKey("quarkus.micrometer.binder-enabled-default", "false") .overrideConfigKey("quarkus.micrometer.export.prometheus.enabled", "true") diff --git a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/runtime/binder/mpmetrics/MpMetricRegistrationTest.java b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/runtime/binder/mpmetrics/MpMetricRegistrationTest.java index 9cfb7a5b21b758..c79664f1ae8e7d 100644 --- a/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/runtime/binder/mpmetrics/MpMetricRegistrationTest.java +++ b/extensions/micrometer/deployment/src/test/java/io/quarkus/micrometer/runtime/binder/mpmetrics/MpMetricRegistrationTest.java @@ -13,6 +13,7 @@ public class MpMetricRegistrationTest { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() + .setFlatClassPath(true) .withConfigurationResource("test-logging.properties") .overrideConfigKey("quarkus.micrometer.binder.mp-metrics.enabled", "true") .overrideConfigKey("quarkus.micrometer.binder-enabled-default", "false") diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/CounterAdapter.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/CounterAdapter.java index 66eef0610e6df7..826d896a75c7ee 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/CounterAdapter.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/CounterAdapter.java @@ -6,7 +6,7 @@ import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MeterRegistry; -class CounterAdapter implements org.eclipse.microprofile.metrics.Counter, MeterHolder { +public class CounterAdapter implements org.eclipse.microprofile.metrics.Counter, MeterHolder { Counter counter; diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/HistogramAdapter.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/HistogramAdapter.java index 891c7bf7b158e7..c90b6031b2ef41 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/HistogramAdapter.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/HistogramAdapter.java @@ -8,7 +8,7 @@ import io.micrometer.core.instrument.Meter; import io.micrometer.core.instrument.MeterRegistry; -class HistogramAdapter implements Histogram, MeterHolder { +public class HistogramAdapter implements Histogram, MeterHolder { DistributionSummary summary; HistogramAdapter register(MpMetadata metadata, MetricDescriptor metricInfo, MeterRegistry registry) { diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/MeterAdapter.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/MeterAdapter.java index 14072ac3be29ec..16eb82664b3938 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/MeterAdapter.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/MeterAdapter.java @@ -6,7 +6,7 @@ import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; -class MeterAdapter implements Meter, MeterHolder { +public class MeterAdapter implements Meter, MeterHolder { Counter counter; public MeterAdapter register(MpMetadata metadata, MetricDescriptor descriptor, MeterRegistry registry) { diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/MetricRegistryAdapter.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/MetricRegistryAdapter.java index bc88e7e9a1dd87..be354011e3b896 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/MetricRegistryAdapter.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/MetricRegistryAdapter.java @@ -29,7 +29,7 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tags; -class MetricRegistryAdapter implements MetricRegistry { +public class MetricRegistryAdapter implements MetricRegistry { final Type type; final MeterRegistry registry; diff --git a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/TimerAdapter.java b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/TimerAdapter.java index 10ba5860c8da94..dd6740180f35d7 100644 --- a/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/TimerAdapter.java +++ b/extensions/micrometer/runtime/src/main/java/io/quarkus/micrometer/runtime/binder/mpmetrics/TimerAdapter.java @@ -12,7 +12,7 @@ import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; -class TimerAdapter +public class TimerAdapter implements org.eclipse.microprofile.metrics.Timer, org.eclipse.microprofile.metrics.SimpleTimer, MeterHolder { final MeterRegistry registry; Timer timer; diff --git a/extensions/mutiny/deployment/src/main/java/io/quarkus/mutiny/deployment/MutinyProcessor.java b/extensions/mutiny/deployment/src/main/java/io/quarkus/mutiny/deployment/MutinyProcessor.java index f24a1834b360b3..84206cabbff948 100644 --- a/extensions/mutiny/deployment/src/main/java/io/quarkus/mutiny/deployment/MutinyProcessor.java +++ b/extensions/mutiny/deployment/src/main/java/io/quarkus/mutiny/deployment/MutinyProcessor.java @@ -2,26 +2,21 @@ import java.util.concurrent.ExecutorService; -import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.ExecutionTime; import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.ExecutorBuildItem; -import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.mutiny.runtime.MutinyInfrastructure; public class MutinyProcessor { - @BuildStep - public FeatureBuildItem registerFeature() { - return new FeatureBuildItem(Feature.MUTINY); - } - @BuildStep @Record(ExecutionTime.RUNTIME_INIT) - public void runtimeInit(ExecutorBuildItem executorBuildItem, MutinyInfrastructure recorder) { + public void runtimeInit(ExecutorBuildItem executorBuildItem, MutinyInfrastructure recorder, + ShutdownContextBuildItem shutdownContext) { ExecutorService executor = executorBuildItem.getExecutorProxy(); - recorder.configureMutinyInfrastructure(executor); + recorder.configureMutinyInfrastructure(executor, shutdownContext); } @BuildStep diff --git a/extensions/mutiny/runtime/src/main/java/io/quarkus/mutiny/runtime/MutinyInfrastructure.java b/extensions/mutiny/runtime/src/main/java/io/quarkus/mutiny/runtime/MutinyInfrastructure.java index bc3f8aa8602e43..3d606f5fd71bb2 100644 --- a/extensions/mutiny/runtime/src/main/java/io/quarkus/mutiny/runtime/MutinyInfrastructure.java +++ b/extensions/mutiny/runtime/src/main/java/io/quarkus/mutiny/runtime/MutinyInfrastructure.java @@ -8,6 +8,7 @@ import org.jboss.logging.Logger; +import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; import io.smallrye.mutiny.infrastructure.Infrastructure; @@ -16,7 +17,9 @@ public class MutinyInfrastructure { public static final String VERTX_EVENT_LOOP_THREAD_PREFIX = "vert.x-eventloop-thread-"; - public void configureMutinyInfrastructure(ExecutorService exec) { + public void configureMutinyInfrastructure(ExecutorService exec, ShutdownContext shutdownContext) { + //mutiny leaks a ScheduledExecutorService if you don't do this + Infrastructure.getDefaultWorkerPool().shutdown(); Infrastructure.setDefaultExecutor(new Executor() { @Override public void execute(Runnable command) { @@ -30,6 +33,12 @@ public void execute(Runnable command) { } } }); + shutdownContext.addLastShutdownTask(new Runnable() { + @Override + public void run() { + Infrastructure.getDefaultWorkerPool().shutdown(); + } + }); } public void configureDroppedExceptionHandler() { diff --git a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TestTransactionInterceptor.java b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TestTransactionInterceptor.java index 3e1b4a64868031..dceb2e2e28352b 100644 --- a/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TestTransactionInterceptor.java +++ b/extensions/narayana-jta/runtime/src/main/java/io/quarkus/narayana/jta/runtime/interceptor/TestTransactionInterceptor.java @@ -25,7 +25,7 @@ public class TestTransactionInterceptor { } @Inject - UserTransaction userTransaction; + public UserTransaction userTransaction; @AroundInvoke public Object intercept(InvocationContext context) throws Exception { diff --git a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/AbstractTokensProducer.java b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/AbstractTokensProducer.java index 2601e38995944f..9d90eb669aad3e 100644 --- a/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/AbstractTokensProducer.java +++ b/extensions/oidc-client/runtime/src/main/java/io/quarkus/oidc/client/runtime/AbstractTokensProducer.java @@ -19,7 +19,7 @@ public abstract class AbstractTokensProducer { @Inject @ConfigProperty(name = "quarkus.oidc-client.early-tokens-acquisition") - boolean earlyTokenAcquisition; + public boolean earlyTokenAcquisition; final TokensHelper tokensHelper = new TokensHelper(); diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java index cd87e2706aa328..6bdd1634d8ad80 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/MessageBundleProcessor.java @@ -928,7 +928,8 @@ public boolean test(String name) { } // E.g. to match the bundle class generated for a localized file; org.acme.Foo_en -> org.acme.Foo className = additionalClassNameSanitizer.apply(className); - return applicationArchives.containingArchive(className) != null; + return applicationArchives.containingArchive(className) != null + || GeneratedClassGizmoAdaptor.isApplicationClass(name); } } } diff --git a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java index f08f9e8b261372..5cec5448a2c2bd 100644 --- a/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java +++ b/extensions/qute/deployment/src/main/java/io/quarkus/qute/deployment/QuteProcessor.java @@ -64,7 +64,6 @@ import io.quarkus.arc.processor.DotNames; import io.quarkus.arc.processor.InjectionPointInfo; import io.quarkus.arc.processor.QualifierRegistrar; -import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.ApplicationArchive; import io.quarkus.deployment.Feature; import io.quarkus.deployment.GeneratedClassGizmoAdaptor; @@ -930,9 +929,9 @@ void generateValueResolvers(QuteConfig config, BuildProducer panacheEntityClasses) { IndexView index = beanArchiveIndex.getIndex(); - ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClasses, new Predicate() { + ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClasses, new Function() { @Override - public boolean test(String name) { + public String apply(String name) { int idx = name.lastIndexOf(ExtensionMethodGenerator.NAMESPACE_SUFFIX); if (idx == -1) { idx = name.lastIndexOf(ExtensionMethodGenerator.SUFFIX); @@ -944,9 +943,7 @@ public boolean test(String name) { if (className.contains(ValueResolverGenerator.NESTED_SEPARATOR)) { className = className.replace(ValueResolverGenerator.NESTED_SEPARATOR, "$"); } - //if the class is (directly) in the TCCL (and not its parent) then it is an application class - QuarkusClassLoader cl = (QuarkusClassLoader) Thread.currentThread().getContextClassLoader(); - return !cl.getElementsWithResource(className + ".class", true).isEmpty(); + return className; } }); diff --git a/extensions/reactive-messaging-http/runtime/src/main/java/io/quarkus/reactivemessaging/http/runtime/serializers/SerializerFactoryBase.java b/extensions/reactive-messaging-http/runtime/src/main/java/io/quarkus/reactivemessaging/http/runtime/serializers/SerializerFactoryBase.java index ca38e41c7c1d79..d97f63d44d744c 100644 --- a/extensions/reactive-messaging-http/runtime/src/main/java/io/quarkus/reactivemessaging/http/runtime/serializers/SerializerFactoryBase.java +++ b/extensions/reactive-messaging-http/runtime/src/main/java/io/quarkus/reactivemessaging/http/runtime/serializers/SerializerFactoryBase.java @@ -73,7 +73,7 @@ public Serializer getSerializer(String name, T payload) { } @SuppressWarnings("unused") // used by a generated subclass - void addSerializer(String className, Serializer serializer) { + public void addSerializer(String className, Serializer serializer) { serializersByClassName.put(className, serializer); } diff --git a/extensions/resteasy-classic/resteasy-jaxb/deployment/src/test/java/io/quarkus/resteasy/jaxb/deployment/ConsumesXMLTestCase.java b/extensions/resteasy-classic/resteasy-jaxb/deployment/src/test/java/io/quarkus/resteasy/jaxb/deployment/ConsumesXMLTestCase.java index 9b61f5fc39f521..93a0dc7e177c65 100644 --- a/extensions/resteasy-classic/resteasy-jaxb/deployment/src/test/java/io/quarkus/resteasy/jaxb/deployment/ConsumesXMLTestCase.java +++ b/extensions/resteasy-classic/resteasy-jaxb/deployment/src/test/java/io/quarkus/resteasy/jaxb/deployment/ConsumesXMLTestCase.java @@ -25,8 +25,8 @@ public class ConsumesXMLTestCase { @Test public void testConsumesXML() { RestAssured.given() - .body(new Bar("open", "bar")) .contentType(ContentType.XML) + .body(new Bar("open", "bar")) .when().post("/foo") .then() .log().ifValidationFails() 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..801fabbd1de0fe 100644 --- a/extensions/vertx-http/deployment/pom.xml +++ b/extensions/vertx-http/deployment/pom.xml @@ -21,6 +21,10 @@ io.quarkus quarkus-vertx-http + + io.quarkus + quarkus-mutiny-deployment + io.quarkus quarkus-kubernetes-spi @@ -34,6 +38,10 @@ org.yaml snakeyaml + + com.fasterxml.jackson.core + jackson-databind + org.webjars @@ -66,6 +74,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..f892ca077eaab1 --- /dev/null +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/devmode/tests/TestsProcessor.java @@ -0,0 +1,139 @@ +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 (testsDisabled(launchModeBuildItem, ts)) { + return null; + } + return new DevConsoleTemplateInfoBuildItem("tests", ts.get()); + } + + @BuildStep(onlyIf = IsDevelopment.class) + DevConsoleRouteBuildItem handleTestStatus(LaunchModeBuildItem launchModeBuildItem) { + Optional ts = TestSupport.instance(); + if (testsDisabled(launchModeBuildItem, ts)) { + 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 (testsDisabled(launchModeBuildItem, ts)) { + 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(); + } + } + }); + } + + private boolean testsDisabled(LaunchModeBuildItem launchModeBuildItem, Optional ts) { + return !ts.isPresent() || launchModeBuildItem.getDevModeType().orElse(null) != DevModeType.LOCAL; + } + + @BuildStep(onlyIf = IsDevelopment.class) + DevConsoleRouteBuildItem runAllTests(LaunchModeBuildItem launchModeBuildItem) { + Optional ts = TestSupport.instance(); + if (testsDisabled(launchModeBuildItem, ts)) { + 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 (testsDisabled(launchModeBuildItem, ts)) { + 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 @@ +