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 b619578c833a3..16683bee3364f 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/TestConfig.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/TestConfig.java
@@ -11,12 +11,89 @@
/**
* This is used currently only to suppress warnings about unknown properties
* when the user supplies something like: -Dquarkus.test.profile=someProfile or -Dquarkus.test.native-image-profile=someProfile
- *
+ *
* TODO refactor code to actually use these values
*/
@ConfigRoot
public class TestConfig {
+ /**
+ * If continuous testing is enabled.
+ *
+ * The default value is 'paused', which will allow you to start testing
+ * from the console or the Dev UI, but will not run tests on startup.
+ *
+ * If this is set to 'enabled' then testing will start as soon as the
+ * application has started.
+ *
+ * If this is 'disabled' then continuous testing is not enabled, and can't
+ * be enabled without restarting the application.
+ *
+ */
+ @ConfigItem(defaultValue = "paused")
+ public Mode continuousTesting;
+
+ /**
+ * If output from the running tests should be displayed in the console.
+ */
+ @ConfigItem(defaultValue = "false")
+ public boolean displayTestOutput;
+
+ /**
+ * Tags that should be included for continuous testing.
+ */
+ @ConfigItem
+ public Optional> includeTags;
+
+ /**
+ * Tags that should be excluded by default with continuous testing.
+ *
+ * This is ignored if include-tags has been set.
+ *
+ * Defaults to 'slow'
+ */
+ @ConfigItem(defaultValue = "slow")
+ public Optional> excludeTags;
+
+ /**
+ * Tests that should be included for continuous testing. This is a regular expression.
+ */
+ @ConfigItem
+ public Optional includePattern;
+
+ /**
+ * Tests that should be excluded with continuous testing. This is a regular expression.
+ *
+ * This is ignored if include-pattern has been set.
+ *
+ */
+ @ConfigItem
+ public Optional excludePattern;
+ /**
+ * Disable the testing status/prompt message at the bottom of the console
+ * and log these messages to STDOUT instead.
+ *
+ * Use this option if your terminal does not support ANSI escape sequences.
+ */
+ @ConfigItem(defaultValue = "false")
+ public boolean basicConsole;
+
+ /**
+ * Disable color in the testing status and prompt messages.
+ *
+ * Use this option if your terminal does not support color.
+ */
+ @ConfigItem(defaultValue = "false")
+ public boolean disableColor;
+
+ /**
+ * If test results and status should be displayed in the console.
+ *
+ * If this is false results can still be viewed in the dev console.
+ */
+ @ConfigItem(defaultValue = "true")
+ public boolean console;
+
/**
* Duration to wait for the native image to built during testing
*/
@@ -35,6 +112,16 @@ public class TestConfig {
@ConfigItem
Profile profile;
+ /**
+ * Configures the hang detection in @QuarkusTest. If no activity happens (i.e. no test callbacks are called) over
+ * this period then QuarkusTest will dump all threads stack traces, to help diagnose a potential hang.
+ *
+ * Note that the initial timeout (before Quarkus has started) will only apply if provided by a system property, as
+ * it is not possible to read all config sources until Quarkus has booted.
+ */
+ @ConfigItem(defaultValue = "10m")
+ Duration hangDetectionTimeout;
+
@ConfigGroup
public static class Profile {
@@ -53,4 +140,11 @@ public static class Profile {
@ConfigItem(defaultValue = "")
Optional> tags;
}
+
+ public enum Mode {
+ PAUSED,
+ ENABLED,
+ DISABLED
+
+ }
}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/LaunchModeBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/LaunchModeBuildItem.java
index 5fbcd30b00e54..b4e39029e8020 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 de0fee867fb46..0000000000000
--- 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 efd46c589c567..ff05d11c19981 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/dev/ClassScanResult.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/ClassScanResult.java
@@ -11,9 +11,34 @@ public class ClassScanResult {
final Set changedClassNames = new HashSet<>();
final Set deletedClassNames = new HashSet<>();
final Set addedClassNames = new HashSet<>();
+ boolean compilationHappened;
public boolean isChanged() {
- return !changedClasses.isEmpty() || !deletedClasses.isEmpty() || !addedClasses.isEmpty();
+ return !changedClasses.isEmpty() || !deletedClasses.isEmpty() || !addedClasses.isEmpty() || compilationHappened;
+ }
+
+ public static ClassScanResult merge(ClassScanResult m1, ClassScanResult m2) {
+ if (m1 == null) {
+ return m2;
+ }
+ if (m2 == null) {
+ return m1;
+ }
+ ClassScanResult ret = new ClassScanResult();
+ ret.changedClasses.addAll(m1.changedClasses);
+ ret.deletedClasses.addAll(m1.deletedClasses);
+ ret.addedClasses.addAll(m1.deletedClasses);
+ ret.changedClassNames.addAll(m1.changedClassNames);
+ ret.deletedClassNames.addAll(m1.deletedClassNames);
+ ret.addedClassNames.addAll(m1.addedClassNames);
+ ret.changedClasses.addAll(m2.changedClasses);
+ ret.deletedClasses.addAll(m2.deletedClasses);
+ ret.addedClasses.addAll(m2.deletedClasses);
+ ret.changedClassNames.addAll(m2.changedClassNames);
+ ret.deletedClassNames.addAll(m2.deletedClassNames);
+ ret.addedClassNames.addAll(m2.addedClassNames);
+ ret.compilationHappened = m1.compilationHappened | m2.compilationHappened;
+ return ret;
}
public void addDeletedClass(Path moduleClassesPath, Path classFilePath) {
@@ -31,9 +56,38 @@ public void addAddedClass(Path moduleClassesPath, Path classFilePath) {
addedClassNames.add(toName(moduleClassesPath, classFilePath));
}
+ public Set getChangedClassNames() {
+ return changedClassNames;
+ }
+
+ public Set getChangedClasses() {
+ return changedClasses;
+ }
+
+ public Set getDeletedClasses() {
+ return deletedClasses;
+ }
+
+ public Set getAddedClasses() {
+ return addedClasses;
+ }
+
+ public Set getDeletedClassNames() {
+ return deletedClassNames;
+ }
+
+ public Set getAddedClassNames() {
+ return addedClassNames;
+ }
+
+ public boolean isCompilationHappened() {
+ return compilationHappened;
+ }
+
private String toName(Path moduleClassesPath, Path classFilePath) {
String cf = moduleClassesPath.relativize(classFilePath).toString()
.replace(moduleClassesPath.getFileSystem().getSeparator(), ".");
return cf.substring(0, cf.length() - ".class".length());
}
+
}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeContext.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeContext.java
index 629c75a07be44..4d7815910fac7 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 e3cc91007e1d6..4b2898436f749 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/DevModeMain.java
@@ -88,14 +88,14 @@ public void start() throws Exception {
}
}
final PathsCollection.Builder appRoots = PathsCollection.builder();
- Path p = Paths.get(context.getApplicationRoot().getClassesPath());
+ Path p = Paths.get(context.getApplicationRoot().getMain().getClassesPath());
if (Files.exists(p)) {
appRoots.add(p);
}
- if (context.getApplicationRoot().getResourcesOutputPath() != null
- && !context.getApplicationRoot().getResourcesOutputPath()
- .equals(context.getApplicationRoot().getClassesPath())) {
- p = Paths.get(context.getApplicationRoot().getResourcesOutputPath());
+ if (context.getApplicationRoot().getMain().getResourcesOutputPath() != null
+ && !context.getApplicationRoot().getMain().getResourcesOutputPath()
+ .equals(context.getApplicationRoot().getMain().getClassesPath())) {
+ p = Paths.get(context.getApplicationRoot().getMain().getResourcesOutputPath());
if (Files.exists(p)) {
appRoots.add(p);
}
@@ -121,12 +121,13 @@ public void start() throws Exception {
}
for (DevModeContext.ModuleInfo i : context.getAllModules()) {
- if (i.getClassesPath() != null) {
- Path classesPath = Paths.get(i.getClassesPath());
+ if (i.getMain().getClassesPath() != null) {
+ Path classesPath = Paths.get(i.getMain().getClassesPath());
bootstrapBuilder.addAdditionalApplicationArchive(new AdditionalDependency(classesPath, true, false));
}
- if (i.getResourcesOutputPath() != null && !i.getResourcesOutputPath().equals(i.getClassesPath())) {
- Path resourceOutputPath = Paths.get(i.getResourcesOutputPath());
+ if (i.getMain().getResourcesOutputPath() != null
+ && !i.getMain().getResourcesOutputPath().equals(i.getMain().getClassesPath())) {
+ Path resourceOutputPath = Paths.get(i.getMain().getResourcesOutputPath());
bootstrapBuilder.addAdditionalApplicationArchive(new AdditionalDependency(resourceOutputPath, true, false));
}
}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/IDEDevModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/IDEDevModeMain.java
index 6281bee827090..26682bd9fe7a5 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 2356f4d01e604..4a04e38af6353 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java
@@ -43,6 +43,9 @@
import io.quarkus.deployment.CodeGenerator;
import io.quarkus.deployment.builditem.ApplicationClassPredicateBuildItem;
import io.quarkus.deployment.codegen.CodeGenData;
+import io.quarkus.deployment.dev.console.InputHandler;
+import io.quarkus.deployment.dev.console.QuarkusConsole;
+import io.quarkus.deployment.dev.testing.TestSupport;
import io.quarkus.deployment.steps.ClassTransformingBuildStep;
import io.quarkus.deployment.util.FSWatchUtil;
import io.quarkus.dev.console.DevConsoleManager;
@@ -68,8 +71,10 @@ public class IsolatedDevModeMain implements BiConsumer codeGens) {
+
ClassLoader old = Thread.currentThread().getContextClassLoader();
try {
@@ -89,17 +94,31 @@ public void accept(Integer integer) {
|| context.isAbortOnFailedStart()) {
return;
}
- System.out.println("Quarkus application exited with code " + integer);
- System.out.println("Press Enter to restart or Ctrl + C to quit");
- try {
- while (System.in.read() != '\n') {
- //noop
+ final CountDownLatch latch = new CountDownLatch(1);
+ QuarkusConsole.INSTANCE.pushInputHandler(new InputHandler() {
+ @Override
+ public void handleInput(int[] keys) {
+ for (int i : keys) {
+ if (i == 'q') {
+ System.exit(0);
+ } else {
+ QuarkusConsole.INSTANCE.popInputHandler();
+ latch.countDown();
+ }
+ }
}
- while (System.in.available() > 0) {
- System.in.read();
+
+ @Override
+ public void promptHandler(ConsoleStatus promptHandler) {
+ promptHandler.setPrompt("\u001B[91mQuarkus application exited with code " + integer
+ + "\nPress [q] or Ctrl + C to quit, any other key to restart");
}
+ });
+ try {
+ latch.await();
System.out.println("Restarting...");
- RuntimeUpdatesProcessor.INSTANCE.checkForChangedClasses();
+ RuntimeUpdatesProcessor.INSTANCE.checkForChangedClasses(false);
+ RuntimeUpdatesProcessor.INSTANCE.checkForChangedTestClasses(false);
restartApp(RuntimeUpdatesProcessor.INSTANCE.checkForFileChange(), null);
} catch (Exception e) {
log.error("Failed to restart", e);
@@ -217,21 +236,19 @@ private RuntimeUpdatesProcessor setupRuntimeCompilation(DevModeContext context,
compilationProviders.add(provider);
context.getAllModules().forEach(moduleInfo -> moduleInfo.addSourcePaths(provider.handledSourcePaths()));
}
- ClassLoaderCompiler compiler;
- try {
- compiler = new ClassLoaderCompiler(Thread.currentThread().getContextClassLoader(), curatedApplication,
- compilationProviders, context);
- } catch (Exception e) {
- log.error("Failed to create compiler, runtime compilation will be unavailable", e);
- return null;
+ QuarkusCompiler compiler = new QuarkusCompiler(curatedApplication, compilationProviders, context);
+ TestSupport testSupport = null;
+ if (devModeType == DevModeType.LOCAL && context.getApplicationRoot().getTest().isPresent()) {
+ testSupport = new TestSupport(curatedApplication, compilationProviders, context);
}
+
RuntimeUpdatesProcessor processor = new RuntimeUpdatesProcessor(appRoot, context, compiler,
devModeType, this::restartCallback, null, new BiFunction() {
@Override
public byte[] apply(String s, byte[] bytes) {
return ClassTransformingBuildStep.transform(s, bytes);
}
- });
+ }, testSupport);
for (HotReplacementSetup service : ServiceLoader.load(HotReplacementSetup.class,
curatedApplication.getBaseRuntimeClassLoader())) {
@@ -289,7 +306,14 @@ public void close() {
i.close();
}
} finally {
- curatedApplication.close();
+ try {
+ curatedApplication.close();
+ } finally {
+ if (shutdownThread != null) {
+ Runtime.getRuntime().removeShutdownHook(shutdownThread);
+ }
+ shutdownLatch.countDown();
+ }
}
}
}
@@ -359,7 +383,7 @@ public boolean test(String s) {
QuarkusClassLoader deploymentClassLoader = curatedApplication.createDeploymentClassLoader();
for (DevModeContext.ModuleInfo module : context.getAllModules()) {
- if (module.getSourceParents() != null) { // it's null for remote dev
+ if (module.getSourceParents().isEmpty() && module.getPreBuildOutputDir() != null) { // it's null for remote dev
codeGens.addAll(
CodeGenerator.init(
deploymentClassLoader,
@@ -373,7 +397,7 @@ public boolean test(String s) {
(DevModeType) params.get(DevModeType.class.getName()));
if (RuntimeUpdatesProcessor.INSTANCE != null) {
RuntimeUpdatesProcessor.INSTANCE.checkForFileChange();
- RuntimeUpdatesProcessor.INSTANCE.checkForChangedClasses();
+ RuntimeUpdatesProcessor.INSTANCE.checkForChangedClasses(true);
}
firstStart(deploymentClassLoader, codeGens);
@@ -385,7 +409,7 @@ public boolean test(String s) {
: deploymentProblem);
}
}
- Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
+ shutdownThread = new Thread(new Runnable() {
@Override
public void run() {
shutdownLatch.countDown();
@@ -399,7 +423,8 @@ public void run() {
}
}
}
- }, "Quarkus Shutdown Thread"));
+ }, "Quarkus Shutdown Thread");
+ Runtime.getRuntime().addShutdownHook(shutdownThread);
} catch (Exception e) {
throw new RuntimeException(e);
}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedRemoteDevModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedRemoteDevModeMain.java
index e80b0411662f2..31b3be79dc8aa 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 6d0467009f4ea..67421c3e1d8df 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/dev/ClassLoaderCompiler.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusCompiler.java
@@ -25,6 +25,7 @@
import org.jboss.logging.Logger;
import io.quarkus.bootstrap.app.CuratedApplication;
+import io.quarkus.bootstrap.app.QuarkusBootstrap;
import io.quarkus.bootstrap.model.AppDependency;
/**
@@ -32,9 +33,9 @@
*
* @author Stuart Douglas
*/
-public class ClassLoaderCompiler implements Closeable {
+public class QuarkusCompiler implements Closeable {
- private static final Logger log = Logger.getLogger(ClassLoaderCompiler.class);
+ private static final Logger log = Logger.getLogger(QuarkusCompiler.class);
private static final Pattern WHITESPACE_PATTERN = Pattern.compile(" ");
private final List compilationProviders;
@@ -44,8 +45,7 @@ public class ClassLoaderCompiler implements Closeable {
private final Map compilationContexts = new HashMap<>();
private final Set allHandledExtensions;
- public ClassLoaderCompiler(ClassLoader classLoader,
- CuratedApplication application,
+ public QuarkusCompiler(CuratedApplication application,
List compilationProviders,
DevModeContext context)
throws IOException {
@@ -65,8 +65,13 @@ public ClassLoaderCompiler(ClassLoader classLoader,
}
Set classPathElements = new HashSet<>();
for (DevModeContext.ModuleInfo i : context.getAllModules()) {
- if (i.getClassesPath() != null) {
- classPathElements.add(new File(i.getClassesPath()));
+ if (i.getMain().getClassesPath() != null) {
+ classPathElements.add(new File(i.getMain().getClassesPath()));
+ }
+ if (application.getQuarkusBootstrap().getMode() == QuarkusBootstrap.Mode.TEST) {
+ if (i.getTest().isPresent()) {
+ classPathElements.add(new File(i.getTest().get().getClassesPath()));
+ }
}
}
final String devModeRunnerJarCanonicalPath = context.getDevModeRunnerJarFile() == null
@@ -135,27 +140,10 @@ public ClassLoaderCompiler(ClassLoader classLoader,
}
}
for (DevModeContext.ModuleInfo i : context.getAllModules()) {
- if (!i.getSourcePaths().isEmpty()) {
- if (i.getClassesPath() == null) {
- log.warn("No classes directory found for module '" + i.getName()
- + "'. It is advised that this module be compiled before launching dev mode");
- continue;
- }
- i.getSourcePaths().forEach(sourcePath -> {
- this.compilationContexts.put(sourcePath,
- new CompilationProvider.Context(
- i.getName(),
- classPathElements,
- i.getProjectDirectory() == null ? null : new File(i.getProjectDirectory()),
- new File(sourcePath),
- new File(i.getClassesPath()),
- context.getSourceEncoding(),
- context.getCompilerOptions(),
- context.getSourceJavaVersion(),
- context.getTargetJvmVersion(),
- context.getCompilerPluginArtifacts(),
- context.getCompilerPluginsOptions()));
- });
+ setupSourceCompilationContext(context, classPathElements, i, i.getMain(),
+ "classes");
+ if (application.getQuarkusBootstrap().getMode() == QuarkusBootstrap.Mode.TEST && i.getTest().isPresent()) {
+ setupSourceCompilationContext(context, classPathElements, i, i.getTest().get(), "test classes");
}
}
this.allHandledExtensions = new HashSet<>();
@@ -164,6 +152,32 @@ public ClassLoaderCompiler(ClassLoader classLoader,
}
}
+ public void setupSourceCompilationContext(DevModeContext context, Set classPathElements, DevModeContext.ModuleInfo i,
+ DevModeContext.CompilationUnit compilationUnit, String name) {
+ if (!compilationUnit.getSourcePaths().isEmpty()) {
+ if (compilationUnit.getSourcePaths() == null) {
+ log.warn("No " + name + " directory found for module '" + i.getName()
+ + "'. It is advised that this module be compiled before launching dev mode");
+ return;
+ }
+ compilationUnit.getSourcePaths().forEach(sourcePath -> {
+ this.compilationContexts.put(sourcePath,
+ new CompilationProvider.Context(
+ i.getName(),
+ classPathElements,
+ i.getProjectDirectory() == null ? null : new File(i.getProjectDirectory()),
+ new File(sourcePath),
+ new File(compilationUnit.getClassesPath()),
+ context.getSourceEncoding(),
+ context.getCompilerOptions(),
+ context.getSourceJavaVersion(),
+ context.getTargetJvmVersion(),
+ context.getCompilerPluginArtifacts(),
+ context.getCompilerPluginsOptions()));
+ });
+ }
+ }
+
public Set allHandledExtensions() {
return allHandledExtensions;
}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java
index 92b052932c202..62ab65553a46e 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/QuarkusDevModeLauncher.java
@@ -19,6 +19,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Set;
+import java.util.function.Consumer;
import java.util.jar.Attributes;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
@@ -198,7 +199,18 @@ public B baseName(String baseName) {
@SuppressWarnings("unchecked")
public B remoteDev(boolean remoteDev) {
- QuarkusDevModeLauncher.this.remoteDev = remoteDev;
+ QuarkusDevModeLauncher.this.entryPointCustomizer = new Consumer() {
+ @Override
+ public void accept(DevModeContext devModeContext) {
+ devModeContext.setMode(QuarkusBootstrap.Mode.REMOTE_DEV_CLIENT);
+ devModeContext.setAlternateEntryPoint(IsolatedRemoteDevModeMain.class.getName());
+ }
+ };
+ return (B) this;
+ }
+
+ public B entryPointCustomizer(Consumer consumer) {
+ QuarkusDevModeLauncher.this.entryPointCustomizer = consumer;
return (B) this;
}
@@ -264,7 +276,7 @@ public R build() throws Exception {
private Set buildFiles = new HashSet<>(0);
private boolean deleteDevJar = true;
private String baseName;
- private boolean remoteDev;
+ private Consumer entryPointCustomizer;
private String applicationArgs;
private Set localArtifacts = new HashSet<>();
private ModuleInfo main;
@@ -391,9 +403,8 @@ protected void prepare() throws Exception {
// this is the jar file we will use to launch the dev mode main class
devModeContext.setDevModeRunnerJarFile(tempFile);
- if (remoteDev) {
- devModeContext.setMode(QuarkusBootstrap.Mode.REMOTE_DEV_CLIENT);
- devModeContext.setAlternateEntryPoint(IsolatedRemoteDevModeMain.class.getName());
+ if (entryPointCustomizer != null) {
+ entryPointCustomizer.accept(devModeContext);
}
try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(tempFile))) {
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java
index 6c3b92acf68fe..bca6b74a4f966 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/RuntimeUpdatesProcessor.java
@@ -28,13 +28,17 @@
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.Timer;
+import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;
+import java.util.concurrent.locks.ReentrantLock;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
+import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -48,6 +52,9 @@
import io.quarkus.bootstrap.runner.Timing;
import io.quarkus.changeagent.ClassChangeAgent;
+import io.quarkus.deployment.dev.testing.TestListener;
+import io.quarkus.deployment.dev.testing.TestRunner;
+import io.quarkus.deployment.dev.testing.TestSupport;
import io.quarkus.deployment.util.FSWatchUtil;
import io.quarkus.deployment.util.FileUtil;
import io.quarkus.dev.spi.DevModeType;
@@ -60,11 +67,11 @@ public class RuntimeUpdatesProcessor implements HotReplacementContext, Closeable
private static final String CLASS_EXTENSION = ".class";
- static volatile RuntimeUpdatesProcessor INSTANCE;
+ public static volatile RuntimeUpdatesProcessor INSTANCE;
private final Path applicationRoot;
private final DevModeContext context;
- private final ClassLoaderCompiler compiler;
+ private final QuarkusCompiler compiler;
private final DevModeType devModeType;
volatile Throwable compileProblem;
@@ -74,21 +81,13 @@ public class RuntimeUpdatesProcessor implements HotReplacementContext, Closeable
private volatile Predicate disableInstrumentationForClassPredicate = new AlwaysFalsePredicate<>();
private volatile Predicate disableInstrumentationForIndexPredicate = new AlwaysFalsePredicate<>();
+ private static volatile boolean instrumentationLogPrinted = false;
/**
- * A first scan is considered done when we have visited all modules at least once.
- * This is useful in two ways.
- * - To make sure that source time stamps have been recorded at least once
- * - To avoid re-compiling on first run by ignoring all first time changes detected by
- * {@link RuntimeUpdatesProcessor#checkIfFileModified(Path, Map, boolean)} during the first scan.
+ * dev mode replacement and test running track their changes separately
*/
- private volatile boolean firstScanDone = false;
-
- private static volatile boolean instrumentationLogPrinted = false;
-
- private final Map sourceFileTimestamps = new ConcurrentHashMap<>();
- private final Map watchedFileTimestamps = new ConcurrentHashMap<>();
- private final Map classFileChangeTimeStamps = new ConcurrentHashMap<>();
- private final Map classFilePathToSourceFilePath = new ConcurrentHashMap<>();
+ private final TimestampSet main = new TimestampSet();
+ private final TimestampSet test = new TimestampSet();
+ final Map sourceFileTimestamps = new ConcurrentHashMap<>();
/**
* Resources that appear in both src and target, these will be removed if the src resource subsequently disappears.
@@ -102,6 +101,8 @@ public class RuntimeUpdatesProcessor implements HotReplacementContext, Closeable
private final BiConsumer, ClassScanResult> restartCallback;
private final BiConsumer copyResourceNotification;
private final BiFunction classTransformers;
+ private Timer timer;
+ private final ReentrantLock scanLock = new ReentrantLock();
/**
* The index for the last successful start. Used to determine if the class has changed its structure
@@ -109,10 +110,15 @@ public class RuntimeUpdatesProcessor implements HotReplacementContext, Closeable
*/
private static volatile IndexView lastStartIndex;
- public RuntimeUpdatesProcessor(Path applicationRoot, DevModeContext context, ClassLoaderCompiler compiler,
+ private final TestSupport testSupport;
+ private volatile boolean firstTestScanComplete;
+ private volatile Boolean instrumentationEnabled;
+
+ public RuntimeUpdatesProcessor(Path applicationRoot, DevModeContext context, QuarkusCompiler compiler,
DevModeType devModeType, BiConsumer, ClassScanResult> restartCallback,
BiConsumer copyResourceNotification,
- BiFunction classTransformers) {
+ BiFunction classTransformers,
+ TestSupport testSupport) {
this.applicationRoot = applicationRoot;
this.context = context;
this.compiler = compiler;
@@ -120,30 +126,121 @@ public RuntimeUpdatesProcessor(Path applicationRoot, DevModeContext context, Cla
this.restartCallback = restartCallback;
this.copyResourceNotification = copyResourceNotification;
this.classTransformers = classTransformers;
+ this.testSupport = testSupport;
+ if (testSupport != null) {
+ testSupport.addListener(new TestListener() {
+ @Override
+ public void testsEnabled() {
+ if (!firstTestScanComplete) {
+ checkForChangedTestClasses(true);
+ firstTestScanComplete = true;
+ }
+ startTestScanningTimer();
+ }
+
+ @Override
+ public void testsDisabled() {
+ synchronized (RuntimeUpdatesProcessor.this) {
+ if (timer != null) {
+ timer.cancel();
+ timer = null;
+ }
+ }
+ }
+ });
+ }
+ }
+
+ public TestSupport getTestSupport() {
+ return testSupport;
}
@Override
public Path getClassesDir() {
//TODO: fix all these
for (DevModeContext.ModuleInfo i : context.getAllModules()) {
- return Paths.get(i.getClassesPath());
+ return Paths.get(i.getMain().getClassesPath());
}
return null;
}
@Override
public List getSourcesDir() {
- return context.getAllModules().stream().flatMap(m -> m.getSourcePaths().stream()).map(Paths::get).collect(toList());
+ return context.getAllModules().stream().flatMap(m -> m.getMain().getSourcePaths().stream()).map(Paths::get)
+ .collect(toList());
+ }
+
+ private Timer startTestScanningTimer() {
+ synchronized (this) {
+ if (timer == null) {
+ timer = new Timer("Test Compile Timer", true);
+ timer.schedule(new TimerTask() {
+ @Override
+ public void run() {
+ periodicTestCompile();
+ }
+ }, 1, 1000);
+ }
+ }
+ return timer;
+ }
+
+ private void periodicTestCompile() {
+ //noop if already scanning
+ if (scanLock.tryLock()) {
+ try {
+ ClassScanResult changedTestClassResult = compileTestClasses();
+ ClassScanResult changedApp = checkForChangedClasses(compiler, DevModeContext.ModuleInfo::getMain, false, test);
+ Set filesChanged = checkForFileChange(DevModeContext.ModuleInfo::getMain, test);
+ boolean configFileRestartNeeded = filesChanged.stream().map(watchedFilePaths::get)
+ .anyMatch(Boolean.TRUE::equals);
+ ClassScanResult merged = ClassScanResult.merge(changedTestClassResult, changedApp);
+ if (configFileRestartNeeded) {
+ if (compileProblem != null) {
+ testSupport.getTestRunner().testCompileFailed(compileProblem);
+ } else {
+ testSupport.getTestRunner().runTests(null);
+ }
+ } else if (merged.isChanged()) {
+ if (compileProblem != null) {
+ testSupport.getTestRunner().testCompileFailed(compileProblem);
+ } else {
+ testSupport.getTestRunner().runTests(merged);
+ }
+ }
+ } finally {
+ scanLock.unlock();
+ }
+ }
+ }
+
+ private ClassScanResult compileTestClasses() {
+ QuarkusCompiler testCompiler = testSupport.getCompiler();
+ TestRunner testRunner = testSupport.getTestRunner();
+ ClassScanResult changedTestClassResult = new ClassScanResult();
+ try {
+ changedTestClassResult = checkForChangedClasses(testCompiler,
+ m -> m.getTest().orElse(DevModeContext.EMPTY_COMPILATION_UNIT), false, test);
+ if (compileProblem != null) {
+ testRunner.testCompileFailed(compileProblem);
+ compileProblem = null; //we don't want to block the app over a test problem
+ } else {
+ testRunner.testCompileSucceeded();
+ }
+ } catch (Throwable e) {
+ testRunner.testCompileFailed(e);
+ }
+ return changedTestClassResult;
}
@Override
public List getResourcesDir() {
List ret = new ArrayList<>();
for (DevModeContext.ModuleInfo i : context.getAllModules()) {
- if (i.getResourcePath() != null) {
- ret.add(Paths.get(i.getResourcePath()));
- } else if (i.getResourcesOutputPath() != null) {
- ret.add(Paths.get(i.getResourcesOutputPath()));
+ if (i.getMain().getResourcePath() != null) {
+ ret.add(Paths.get(i.getMain().getResourcePath()));
+ } else if (i.getMain().getResourcesOutputPath() != null) {
+ ret.add(Paths.get(i.getMain().getResourcesOutputPath()));
}
}
Collections.reverse(ret); //make sure the actual project is before dependencies
@@ -190,103 +287,119 @@ public DevModeType getDevModeType() {
@Override
public boolean doScan(boolean userInitiated) throws IOException {
-
- final long startNanoseconds = System.nanoTime();
- for (Runnable step : preScanSteps) {
- try {
- step.run();
- } catch (Throwable t) {
- log.error("Pre Scan step failed", t);
+ scanLock.lock();
+ try {
+ if (testSupport != null) {
+ testSupport.pause();
}
- }
-
- ClassScanResult changedClassResults = checkForChangedClasses();
- Set filesChanged = checkForFileChange();
- boolean configFileRestartNeeded = filesChanged.stream().map(watchedFilePaths::get).anyMatch(Boolean.TRUE::equals);
- boolean instrumentationChange = false;
- if (ClassChangeAgent.getInstrumentation() != null && lastStartIndex != null && !configFileRestartNeeded
- && devModeType != DevModeType.REMOTE_LOCAL_SIDE) {
- //attempt to do an instrumentation based reload
- //if only code has changed and not the class structure, then we can do a reload
- //using the JDK instrumentation API (assuming we were started with the javaagent)
- if (changedClassResults.deletedClasses.isEmpty()
- && changedClassResults.addedClasses.isEmpty()
- && !changedClassResults.changedClasses.isEmpty()) {
+ final long startNanoseconds = System.nanoTime();
+ for (Runnable step : preScanSteps) {
try {
- Indexer indexer = new Indexer();
- //attempt to use the instrumentation API
- ClassDefinition[] defs = new ClassDefinition[changedClassResults.changedClasses.size()];
- int index = 0;
- for (Path i : changedClassResults.changedClasses) {
- byte[] bytes = Files.readAllBytes(i);
- String name = indexer.index(new ByteArrayInputStream(bytes)).name().toString();
- defs[index++] = new ClassDefinition(Thread.currentThread().getContextClassLoader().loadClass(name),
- classTransformers.apply(name, bytes));
- }
- Index current = indexer.complete();
- boolean ok = instrumentationEnabled()
- && !disableInstrumentationForIndexPredicate.test(current);
- if (ok) {
- for (ClassInfo clazz : current.getKnownClasses()) {
- ClassInfo old = lastStartIndex.getClassByName(clazz.name());
- if (!ClassComparisonUtil.isSameStructure(clazz, old)
- || disableInstrumentationForClassPredicate.test(clazz)) {
- ok = false;
- break;
+ step.run();
+ } catch (Throwable t) {
+ log.error("Pre Scan step failed", t);
+ }
+ }
+
+ ClassScanResult changedClassResults = checkForChangedClasses(compiler, DevModeContext.ModuleInfo::getMain, false,
+ main);
+ Set filesChanged = checkForFileChange(DevModeContext.ModuleInfo::getMain, main);
+
+ boolean configFileRestartNeeded = filesChanged.stream().map(watchedFilePaths::get).anyMatch(Boolean.TRUE::equals);
+ boolean instrumentationChange = false;
+ if (ClassChangeAgent.getInstrumentation() != null && lastStartIndex != null && !configFileRestartNeeded
+ && devModeType != DevModeType.REMOTE_LOCAL_SIDE) {
+ //attempt to do an instrumentation based reload
+ //if only code has changed and not the class structure, then we can do a reload
+ //using the JDK instrumentation API (assuming we were started with the javaagent)
+ if (changedClassResults.deletedClasses.isEmpty()
+ && changedClassResults.addedClasses.isEmpty()
+ && !changedClassResults.changedClasses.isEmpty()) {
+ try {
+ Indexer indexer = new Indexer();
+ //attempt to use the instrumentation API
+ ClassDefinition[] defs = new ClassDefinition[changedClassResults.changedClasses.size()];
+ int index = 0;
+ for (Path i : changedClassResults.changedClasses) {
+ byte[] bytes = Files.readAllBytes(i);
+ String name = indexer.index(new ByteArrayInputStream(bytes)).name().toString();
+ defs[index++] = new ClassDefinition(Thread.currentThread().getContextClassLoader().loadClass(name),
+ classTransformers.apply(name, bytes));
+ }
+ Index current = indexer.complete();
+ boolean ok = instrumentationEnabled()
+ && !disableInstrumentationForIndexPredicate.test(current);
+ if (ok) {
+ for (ClassInfo clazz : current.getKnownClasses()) {
+ ClassInfo old = lastStartIndex.getClassByName(clazz.name());
+ if (!ClassComparisonUtil.isSameStructure(clazz, old)
+ || disableInstrumentationForClassPredicate.test(clazz)) {
+ ok = false;
+ break;
+ }
}
}
- }
- if (ok) {
- log.info("Application restart not required, replacing classes via instrumentation");
- ClassChangeAgent.getInstrumentation().redefineClasses(defs);
- instrumentationChange = true;
+ if (ok) {
+ log.info("Application restart not required, replacing classes via instrumentation");
+ ClassChangeAgent.getInstrumentation().redefineClasses(defs);
+ instrumentationChange = true;
+ }
+ } catch (Exception e) {
+ log.error("Failed to replace classes via instrumentation", e);
+ instrumentationChange = false;
}
- } catch (Exception e) {
- log.error("Failed to replace classes via instrumentation", e);
- instrumentationChange = false;
}
}
- }
- //if there is a deployment problem we always restart on scan
- //this is because we can't setup the config file watches
- //in an ideal world we would just check every resource file for changes, however as everything is already
- //all broken we just assume the reason that they have refreshed is because they have fixed something
- //trying to watch all resource files is complex and this is likely a good enough solution for what is already an edge case
- boolean restartNeeded = !instrumentationChange && (changedClassResults.isChanged()
- || (IsolatedDevModeMain.deploymentProblem != null && userInitiated) || configFileRestartNeeded);
- if (restartNeeded) {
- restartCallback.accept(filesChanged, changedClassResults);
- long timeNanoSeconds = System.nanoTime() - startNanoseconds;
- log.infof("Live reload total time: %ss ", Timing.convertToBigDecimalSeconds(timeNanoSeconds));
- if (TimeUnit.SECONDS.convert(timeNanoSeconds, TimeUnit.NANOSECONDS) >= 4 && !instrumentationEnabled()) {
- if (!instrumentationLogPrinted) {
- instrumentationLogPrinted = true;
- log.info(
- "Live reload took more than 4 seconds, you may want to enable instrumentation based reload (quarkus.live-reload.instrumentation=true). This allows small changes to take effect without restarting Quarkus.");
+ //if there is a deployment problem we always restart on scan
+ //this is because we can't setup the config file watches
+ //in an ideal world we would just check every resource file for changes, however as everything is already
+ //all broken we just assume the reason that they have refreshed is because they have fixed something
+ //trying to watch all resource files is complex and this is likely a good enough solution for what is already an edge case
+ boolean restartNeeded = !instrumentationChange && (changedClassResults.isChanged()
+ || (IsolatedDevModeMain.deploymentProblem != null && userInitiated) || configFileRestartNeeded);
+ if (restartNeeded) {
+ restartCallback.accept(filesChanged, changedClassResults);
+ long timeNanoSeconds = System.nanoTime() - startNanoseconds;
+ log.infof("Live reload total time: %ss ", Timing.convertToBigDecimalSeconds(timeNanoSeconds));
+ if (TimeUnit.SECONDS.convert(timeNanoSeconds, TimeUnit.NANOSECONDS) >= 4 && !instrumentationEnabled()) {
+ if (!instrumentationLogPrinted) {
+ instrumentationLogPrinted = true;
+ log.info(
+ "Live reload took more than 4 seconds, you may want to enable instrumentation based reload (quarkus.live-reload.instrumentation=true). This allows small changes to take effect without restarting Quarkus.");
+ }
}
- }
- return true;
- } else if (!filesChanged.isEmpty()) {
- for (Consumer> consumer : noRestartChangesConsumers) {
- try {
- consumer.accept(filesChanged);
- } catch (Throwable t) {
- log.error("Changed files consumer failed", t);
+ return true;
+ } else if (!filesChanged.isEmpty()) {
+ for (Consumer> consumer : noRestartChangesConsumers) {
+ try {
+ consumer.accept(filesChanged);
+ } catch (Throwable t) {
+ log.error("Changed files consumer failed", t);
+ }
}
+ log.infof("Files changed but restart not needed - notified extensions in: %ss ",
+ Timing.convertToBigDecimalSeconds(System.nanoTime() - startNanoseconds));
+ } else if (instrumentationChange) {
+ log.infof("Live reload performed via instrumentation, no restart needed, total time: %ss ",
+ Timing.convertToBigDecimalSeconds(System.nanoTime() - startNanoseconds));
+ }
+ return false;
+
+ } finally {
+ scanLock.unlock();
+ if (testSupport != null) {
+ testSupport.resume();
}
- log.infof("Files changed but restart not needed - notified extensions in: %ss ",
- Timing.convertToBigDecimalSeconds(System.nanoTime() - startNanoseconds));
- } else if (instrumentationChange) {
- log.infof("Live reload performed via instrumentation, no restart needed, total time: %ss ",
- Timing.convertToBigDecimalSeconds(System.nanoTime() - startNanoseconds));
}
- return false;
}
private Boolean instrumentationEnabled() {
+ if (instrumentationEnabled != null) {
+ return instrumentationEnabled;
+ }
ClassLoader old = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(getClass().getClassLoader());
@@ -337,14 +450,42 @@ public Set syncState(Map fileHashes) {
}
}
- ClassScanResult checkForChangedClasses() throws IOException {
+ ClassScanResult checkForChangedClasses(boolean firstScan) {
+ ClassScanResult classScanResult = checkForChangedClasses(compiler, DevModeContext.ModuleInfo::getMain, firstScan, main);
+ test.merge(main);
+ return classScanResult;
+ }
+
+ ClassScanResult checkForChangedTestClasses(boolean firstScan) {
+ if (!testSupport.isStarted()) {
+ return new ClassScanResult();
+ }
+ ClassScanResult ret = checkForChangedClasses(testSupport.getCompiler(),
+ s -> s.getTest().orElse(DevModeContext.EMPTY_COMPILATION_UNIT), firstScan,
+ test);
+ if (firstScan) {
+ startTestScanningTimer();
+ }
+ return ret;
+ }
+
+ /**
+ * A first scan is considered done when we have visited all modules at least once.
+ * This is useful in two ways.
+ * - To make sure that source time stamps have been recorded at least once
+ * - To avoid re-compiling on first run by ignoring all first time changes detected by
+ * {@link RuntimeUpdatesProcessor#checkIfFileModified(Path, Map, boolean)} during the first scan.
+ */
+ ClassScanResult checkForChangedClasses(QuarkusCompiler compiler,
+ Function cuf, boolean firstScan,
+ TimestampSet timestampSet) {
ClassScanResult classScanResult = new ClassScanResult();
- boolean ignoreFirstScanChanges = !firstScanDone;
+ boolean ignoreFirstScanChanges = firstScan;
for (DevModeContext.ModuleInfo module : context.getAllModules()) {
final List moduleChangedSourceFilePaths = new ArrayList<>();
- for (String sourcePath : module.getSourcePaths()) {
+ for (String sourcePath : cuf.apply(module).getSourcePaths()) {
final Set changedSourceFiles;
Path start = Paths.get(sourcePath);
if (!Files.exists(start)) {
@@ -354,13 +495,17 @@ ClassScanResult checkForChangedClasses() throws IOException {
changedSourceFiles = sourcesStream
.parallel()
.filter(p -> matchingHandledExtension(p).isPresent()
- && sourceFileWasRecentModified(p, ignoreFirstScanChanges))
+ && sourceFileWasRecentModified(p, ignoreFirstScanChanges, timestampSet))
.map(Path::toFile)
//Needing a concurrent Set, not many standard options:
.collect(Collectors.toCollection(ConcurrentSkipListSet::new));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
}
if (!changedSourceFiles.isEmpty()) {
- log.info("Changed source files detected, recompiling " + changedSourceFiles);
+ classScanResult.compilationHappened = true;
+ log.info("Changed source files detected, recompiling "
+ + changedSourceFiles.stream().map(File::getName).collect(Collectors.joining(", ")));
try {
final Set changedPaths = changedSourceFiles.stream()
.map(File::toPath)
@@ -377,10 +522,11 @@ && sourceFileWasRecentModified(p, ignoreFirstScanChanges))
}
- checkForClassFilesChangesInModule(module, moduleChangedSourceFilePaths, ignoreFirstScanChanges, classScanResult);
+ checkForClassFilesChangesInModule(module, moduleChangedSourceFilePaths, ignoreFirstScanChanges, classScanResult,
+ cuf, timestampSet);
+
}
- this.firstScanDone = true;
return classScanResult;
}
@@ -389,13 +535,14 @@ public Throwable getCompileProblem() {
}
private void checkForClassFilesChangesInModule(DevModeContext.ModuleInfo module, List moduleChangedSourceFiles,
- boolean isInitialRun, ClassScanResult classScanResult) {
- if (module.getClassesPath() == null) {
+ boolean isInitialRun, ClassScanResult classScanResult,
+ Function cuf, TimestampSet timestampSet) {
+ if (cuf.apply(module).getClassesPath() == null) {
return;
}
try {
- for (String folder : module.getClassesPath().split(File.pathSeparator)) {
+ for (String folder : cuf.apply(module).getClassesPath().split(File.pathSeparator)) {
final Path moduleClassesPath = Paths.get(folder);
if (!Files.exists(moduleClassesPath)) {
continue;
@@ -408,32 +555,32 @@ private void checkForClassFilesChangesInModule(DevModeContext.ModuleInfo module,
for (Path classFilePath : classFilePaths) {
final Path sourceFilePath = retrieveSourceFilePathForClassFile(classFilePath, moduleChangedSourceFiles,
- module);
+ module, cuf, timestampSet);
if (sourceFilePath != null) {
if (!sourceFilePath.toFile().exists()) {
// Source file has been deleted. Delete class and restart
- cleanUpClassFile(classFilePath);
+ cleanUpClassFile(classFilePath, timestampSet);
sourceFileTimestamps.remove(sourceFilePath);
classScanResult.addDeletedClass(moduleClassesPath, classFilePath);
} else {
- classFilePathToSourceFilePath.put(classFilePath, sourceFilePath);
- if (classFileWasAdded(classFilePath, isInitialRun)) {
+ timestampSet.classFilePathToSourceFilePath.put(classFilePath, sourceFilePath);
+ if (classFileWasAdded(classFilePath, isInitialRun, timestampSet)) {
// At least one class was recently modified. Restart.
classScanResult.addAddedClass(moduleClassesPath, classFilePath);
- } else if (classFileWasRecentModified(classFilePath, isInitialRun)) {
+ } else if (classFileWasRecentModified(classFilePath, isInitialRun, timestampSet)) {
// At least one class was recently modified. Restart.
classScanResult.addChangedClass(moduleClassesPath, classFilePath);
} else if (moduleChangedSourceFiles.contains(sourceFilePath)) {
// Source file has been modified, but not the class file
// must be a removed inner class
- cleanUpClassFile(classFilePath);
+ cleanUpClassFile(classFilePath, timestampSet);
classScanResult.addDeletedClass(moduleClassesPath, classFilePath);
}
}
- } else if (classFileWasAdded(classFilePath, isInitialRun)) {
+ } else if (classFileWasAdded(classFilePath, isInitialRun, timestampSet)) {
classScanResult.addAddedClass(moduleClassesPath, classFilePath);
- } else if (classFileWasRecentModified(classFilePath, isInitialRun)) {
+ } else if (classFileWasRecentModified(classFilePath, isInitialRun, timestampSet)) {
classScanResult.addChangedClass(moduleClassesPath, classFilePath);
}
}
@@ -445,18 +592,20 @@ private void checkForClassFilesChangesInModule(DevModeContext.ModuleInfo module,
}
private Path retrieveSourceFilePathForClassFile(Path classFilePath, List moduleChangedSourceFiles,
- DevModeContext.ModuleInfo module) {
- Path sourceFilePath = classFilePathToSourceFilePath.get(classFilePath);
+ DevModeContext.ModuleInfo module, Function cuf,
+ TimestampSet timestampSet) {
+ Path sourceFilePath = timestampSet.classFilePathToSourceFilePath.get(classFilePath);
if (sourceFilePath == null || moduleChangedSourceFiles.contains(sourceFilePath)) {
- sourceFilePath = compiler.findSourcePath(classFilePath, module.getSourcePaths(), module.getClassesPath());
+ sourceFilePath = compiler.findSourcePath(classFilePath, cuf.apply(module).getSourcePaths(),
+ cuf.apply(module).getClassesPath());
}
return sourceFilePath;
}
- private void cleanUpClassFile(Path classFilePath) throws IOException {
+ private void cleanUpClassFile(Path classFilePath, TimestampSet timestampSet) throws IOException {
Files.deleteIfExists(classFilePath);
- classFileChangeTimeStamps.remove(classFilePath);
- classFilePathToSourceFilePath.remove(classFilePath);
+ timestampSet.classFileChangeTimeStamps.remove(classFilePath);
+ timestampSet.classFilePathToSourceFilePath.remove(classFilePath);
}
private Optional matchingHandledExtension(Path p) {
@@ -473,15 +622,20 @@ private String getFileExtension(File file) {
}
Set checkForFileChange() {
+ return checkForFileChange(DevModeContext.ModuleInfo::getMain, main);
+ }
+
+ Set checkForFileChange(Function cuf,
+ TimestampSet timestampSet) {
Set ret = new HashSet<>();
for (DevModeContext.ModuleInfo module : context.getAllModules()) {
final Set moduleResources = correspondingResources.computeIfAbsent(module.getName(),
m -> Collections.newSetFromMap(new ConcurrentHashMap<>()));
boolean doCopy = true;
- String rootPath = module.getResourcePath();
- String outputPath = module.getResourcesOutputPath();
+ String rootPath = cuf.apply(module).getResourcePath();
+ String outputPath = cuf.apply(module).getResourcesOutputPath();
if (rootPath == null) {
- rootPath = module.getClassesPath();
+ rootPath = cuf.apply(module).getClassesPath();
outputPath = rootPath;
doCopy = false;
}
@@ -504,7 +658,7 @@ Set checkForFileChange() {
Path relative = root.relativize(path);
Path target = outputDir.resolve(relative);
seen.remove(target);
- if (!watchedFileTimestamps.containsKey(path)) {
+ if (!timestampSet.watchedFileTimestamps.containsKey(path)) {
moduleResources.add(target);
if (!Files.exists(target) || Files.getLastModifiedTime(target).toMillis() < Files
.getLastModifiedTime(path).toMillis()) {
@@ -544,7 +698,7 @@ Set checkForFileChange() {
if (file.toFile().exists()) {
try {
long value = Files.getLastModifiedTime(file).toMillis();
- Long existing = watchedFileTimestamps.get(file);
+ Long existing = timestampSet.watchedFileTimestamps.get(file);
if (value > existing) {
ret.add(path);
log.infof("File change detected: %s", file);
@@ -555,13 +709,13 @@ Set checkForFileChange() {
out.write(data);
}
}
- watchedFileTimestamps.put(file, value);
+ timestampSet.watchedFileTimestamps.put(file, value);
}
} catch (IOException e) {
throw new UncheckedIOException(e);
}
} else {
- watchedFileTimestamps.put(file, 0L);
+ timestampSet.watchedFileTimestamps.put(file, 0L);
Path target = outputDir.resolve(path);
try {
FileUtil.deleteDirectory(target);
@@ -575,19 +729,21 @@ Set checkForFileChange() {
return ret;
}
- private boolean sourceFileWasRecentModified(final Path sourcePath, boolean ignoreFirstScanChanges) {
+ private boolean sourceFileWasRecentModified(final Path sourcePath, boolean ignoreFirstScanChanges,
+ TimestampSet timestampSet) {
return checkIfFileModified(sourcePath, sourceFileTimestamps, ignoreFirstScanChanges);
}
- private boolean classFileWasRecentModified(final Path classFilePath, boolean ignoreFirstScanChanges) {
- return checkIfFileModified(classFilePath, classFileChangeTimeStamps, ignoreFirstScanChanges);
+ private boolean classFileWasRecentModified(final Path classFilePath, boolean ignoreFirstScanChanges,
+ TimestampSet timestampSet) {
+ return checkIfFileModified(classFilePath, timestampSet.classFileChangeTimeStamps, ignoreFirstScanChanges);
}
- private boolean classFileWasAdded(final Path classFilePath, boolean ignoreFirstScanChanges) {
- final Long lastRecordedChange = classFileChangeTimeStamps.get(classFilePath);
+ private boolean classFileWasAdded(final Path classFilePath, boolean ignoreFirstScanChanges, TimestampSet timestampSet) {
+ final Long lastRecordedChange = timestampSet.classFileChangeTimeStamps.get(classFilePath);
if (lastRecordedChange == null) {
try {
- classFileChangeTimeStamps.put(classFilePath, Files.getLastModifiedTime(classFilePath).toMillis());
+ timestampSet.classFileChangeTimeStamps.put(classFilePath, Files.getLastModifiedTime(classFilePath).toMillis());
} catch (IOException e) {
throw new UncheckedIOException(e);
}
@@ -629,14 +785,15 @@ public RuntimeUpdatesProcessor setDisableInstrumentationForIndexPredicate(
}
public RuntimeUpdatesProcessor setWatchedFilePaths(Map watchedFilePaths) {
+ boolean includeTest = test.watchedFileTimestamps.isEmpty();
this.watchedFilePaths = watchedFilePaths;
- watchedFileTimestamps.clear();
+ main.watchedFileTimestamps.clear();
Map extraWatchedFilePaths = new HashMap<>();
for (DevModeContext.ModuleInfo module : context.getAllModules()) {
- String rootPath = module.getResourcePath();
+ String rootPath = module.getMain().getResourcePath();
if (rootPath == null) {
- rootPath = module.getClassesPath();
+ rootPath = module.getMain().getClassesPath();
}
if (rootPath == null) {
continue;
@@ -647,17 +804,24 @@ public RuntimeUpdatesProcessor setWatchedFilePaths(Map watchedF
if (config.toFile().exists()) {
try {
FileTime lastModifiedTime = Files.getLastModifiedTime(config);
- watchedFileTimestamps.put(config, lastModifiedTime.toMillis());
+ main.watchedFileTimestamps.put(config, lastModifiedTime.toMillis());
+ if (includeTest) {
+ test.watchedFileTimestamps.put(config, lastModifiedTime.toMillis());
+ }
} catch (IOException e) {
throw new UncheckedIOException(e);
}
} else {
- watchedFileTimestamps.put(config, 0L);
+ main.watchedFileTimestamps.put(config, 0L);
Map extraWatchedFileTimestamps = expandGlobPattern(root, config);
- watchedFileTimestamps.putAll(extraWatchedFileTimestamps);
+ main.watchedFileTimestamps.putAll(extraWatchedFileTimestamps);
for (Path extraPath : extraWatchedFileTimestamps.keySet()) {
extraWatchedFilePaths.put(root.relativize(extraPath).toString(), this.watchedFilePaths.get(path));
}
+ if (includeTest) {
+ test.watchedFileTimestamps.put(config, 0L);
+ main.watchedFileTimestamps.putAll(extraWatchedFileTimestamps);
+ }
}
}
}
@@ -683,6 +847,9 @@ public static void setLastStartIndex(IndexView lastStartIndex) {
@Override
public void close() throws IOException {
+ if (timer != null) {
+ timer.cancel();
+ }
compiler.close();
FSWatchUtil.shutdown();
}
@@ -711,4 +878,25 @@ public FileVisitResult visitFileFailed(Path file, IOException exc) {
return files;
}
+ public void toggleInstrumentation() {
+ instrumentationEnabled = !instrumentationEnabled();
+ if (instrumentationEnabled) {
+ log.info("Instrumentation based restart enabled");
+ } else {
+ log.info("Instrumentation based restart disabled");
+ }
+ }
+
+ static class TimestampSet {
+ final Map watchedFileTimestamps = new ConcurrentHashMap<>();
+ final Map classFileChangeTimeStamps = new ConcurrentHashMap<>();
+ final Map classFilePathToSourceFilePath = new ConcurrentHashMap<>();
+
+ public void merge(TimestampSet other) {
+ watchedFileTimestamps.putAll(other.watchedFileTimestamps);
+ classFileChangeTimeStamps.putAll(other.classFileChangeTimeStamps);
+ classFilePathToSourceFilePath.putAll(other.classFilePathToSourceFilePath);
+ }
+ }
+
}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/console/AeshConsole.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/AeshConsole.java
new file mode 100644
index 0000000000000..bfb8b26fd2487
--- /dev/null
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/AeshConsole.java
@@ -0,0 +1,264 @@
+package io.quarkus.deployment.dev.console;
+
+import org.aesh.terminal.Attributes;
+import org.aesh.terminal.Connection;
+import org.aesh.terminal.tty.Size;
+import org.aesh.terminal.utils.ANSI;
+
+public class AeshConsole extends QuarkusConsole {
+
+ private final Connection connection;
+ private Size size;
+ private Attributes attributes;
+
+ private String statusMessage;
+ private String promptMessage;
+ private int totalStatusLines = 0;
+ private int lastWriteCursorX;
+
+ public AeshConsole(Connection connection) {
+ INSTANCE = this;
+ this.connection = connection;
+ connection.openNonBlocking();
+ setup(connection);
+ Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
+ @Override
+ public void run() {
+ connection.close();
+ }
+ }, "Console Shutdown Hoot"));
+ }
+
+ private synchronized AeshConsole setStatusMessage(String statusMessage) {
+ StringBuilder buffer = new StringBuilder();
+ clearStatusMessages(buffer);
+ int newLines = countLines(statusMessage) + countLines(promptMessage);
+ if (statusMessage == null) {
+ if (promptMessage != null) {
+ newLines += 2;
+ }
+ } else if (promptMessage == null) {
+ newLines += 2;
+ } else {
+ newLines += 3;
+ }
+ if (newLines > totalStatusLines) {
+ for (int i = 0; i < newLines - totalStatusLines; ++i) {
+ buffer.append("\n");
+ }
+ }
+ this.statusMessage = statusMessage;
+ this.totalStatusLines = newLines;
+ printStatusAndPrompt(buffer);
+ connection.write(buffer.toString());
+ return this;
+ }
+
+ public AeshInputHolder createHolder(InputHandler inputHandler) {
+ return new AeshInputHolder(inputHandler);
+ }
+
+ private synchronized AeshConsole setPromptMessage(String promptMessage) {
+ StringBuilder buffer = new StringBuilder();
+ clearStatusMessages(buffer);
+ int newLines = countLines(statusMessage) + countLines(promptMessage);
+ if (statusMessage == null) {
+ if (promptMessage != null) {
+ newLines += 2;
+ }
+ } else if (promptMessage == null) {
+ newLines += 2;
+ } else {
+ newLines += 3;
+ }
+ if (newLines > totalStatusLines) {
+ for (int i = 0; i < newLines - totalStatusLines; ++i) {
+ buffer.append("\n");
+ }
+ }
+ this.promptMessage = promptMessage;
+ this.totalStatusLines = newLines;
+ printStatusAndPrompt(buffer);
+ connection.write(buffer.toString());
+ return this;
+ }
+
+ private synchronized void end(Connection conn) {
+ conn.write(ANSI.MAIN_BUFFER);
+ conn.write(ANSI.CURSOR_SHOW);
+ conn.setAttributes(attributes);
+ conn.write("\033[c");
+ }
+
+ private void setup(Connection conn) {
+ size = conn.size();
+ // Ctrl-C ends the game
+ conn.setSignalHandler(event -> {
+ switch (event) {
+ case INT:
+ //todo: why does async exit not work here
+ //Quarkus.asyncExit();
+ //end(conn);
+ new Thread(new Runnable() {
+ @Override
+ public void run() {
+ System.exit(0);
+ }
+ }).start();
+ break;
+ }
+ });
+ // Keyboard handling
+ conn.setStdinHandler(keys -> {
+ InputHolder handler = inputHandlers.peek();
+ if (handler != null) {
+ handler.handler.handleInput(keys);
+ }
+ });
+
+ conn.setCloseHandler(close -> end(conn));
+ conn.setSizeHandler(size -> setup(conn));
+
+ //switch to alternate buffer
+ //conn.write(ANSI.ALTERNATE_BUFFER);
+ //conn.write(ANSI.CURSOR_HIDE);
+
+ attributes = conn.enterRawMode();
+
+ StringBuilder sb = new StringBuilder();
+ printStatusAndPrompt(sb);
+ conn.write(sb.toString());
+ }
+
+ /**
+ * prints the status messages
+ *
+ * this will overwrite the bottom part of the screen
+ * callers are responsible for writing enough newlines to
+ * preserve any console history they want.
+ *
+ * @param buffer
+ */
+ private void printStatusAndPrompt(StringBuilder buffer) {
+ if (totalStatusLines == 0) {
+ return;
+ }
+
+ clearStatusMessages(buffer);
+ gotoLine(buffer, size.getHeight() - totalStatusLines);
+ buffer.append("\n--\n");
+ if (statusMessage != null) {
+ buffer.append(statusMessage);
+ if (promptMessage != null) {
+ buffer.append("\n");
+ }
+ }
+ if (promptMessage != null) {
+ buffer.append(promptMessage);
+ }
+ }
+
+ private void clearStatusMessages(StringBuilder buffer) {
+ gotoLine(buffer, size.getHeight() - totalStatusLines);
+ buffer.append("\033[J");
+ }
+
+ private StringBuilder gotoLine(StringBuilder builder, int line) {
+ return builder.append("\033[").append(line).append(";").append(0).append("H");
+ }
+
+ int countLines(String s) {
+ return countLines(s, 0);
+ }
+
+ int countLines(String s, int cursorPos) {
+ if (s == null) {
+ return 0;
+ }
+ s = stripAnsiCodes(s);
+ int lines = 0;
+ int curLength = cursorPos;
+ for (int i = 0; i < s.length(); ++i) {
+ if (s.charAt(i) == '\n') {
+ lines++;
+ curLength = 0;
+ } else if (curLength++ == size.getWidth()) {
+ lines++;
+ curLength = 0;
+ }
+ }
+ return lines;
+ }
+
+ public synchronized void write(String s) {
+ if (outputFilter != null) {
+ if (!outputFilter.test(s)) {
+ return;
+ }
+ }
+ StringBuilder buffer = new StringBuilder();
+ clearStatusMessages(buffer);
+ int cursorPos = lastWriteCursorX;
+ gotoLine(buffer, size.getHeight());
+ String stripped = stripAnsiCodes(s);
+ int lines = countLines(s, cursorPos);
+ int trailing = 0;
+ int index = stripped.lastIndexOf("\n");
+ if (index == -1) {
+ trailing = stripped.length();
+ } else {
+ trailing = stripped.length() - index - 1;
+ }
+
+ int newCursorPos;
+ if (lines == 0) {
+ newCursorPos = trailing + cursorPos;
+ } else {
+ newCursorPos = trailing;
+ }
+
+ if (cursorPos > 1 && lines == 0) {
+ buffer.append(s);
+ lastWriteCursorX = newCursorPos;
+ //partial line, just write it
+ connection.write(buffer.toString());
+ return;
+ }
+ if (lines == 0) {
+ lines++;
+ }
+ //move the existing content up by the number of lines
+ int appendLines = cursorPos > 1 ? lines - 1 : lines;
+ for (int i = 0; i < appendLines; ++i) {
+ buffer.append("\n");
+ }
+ buffer.append("\033[").append(size.getHeight() - totalStatusLines - lines).append(";").append(0).append("H");
+ buffer.append(s);
+ lastWriteCursorX = newCursorPos;
+ printStatusAndPrompt(buffer);
+ connection.write(buffer.toString());
+
+ }
+
+ public void write(byte[] buf, int off, int len) {
+ write(new String(buf, off, len, connection.outputEncoding()));
+ }
+
+ class AeshInputHolder extends InputHolder {
+
+ protected AeshInputHolder(InputHandler handler) {
+ super(handler);
+ }
+
+ @Override
+ protected void setPromptMessage(String prompt) {
+ AeshConsole.this.setPromptMessage(prompt);
+ }
+
+ @Override
+ protected void setStatusMessage(String status) {
+ AeshConsole.this.setStatusMessage(status);
+
+ }
+ }
+}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/console/BasicConsole.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/BasicConsole.java
new file mode 100644
index 0000000000000..c0d902a0326a1
--- /dev/null
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/BasicConsole.java
@@ -0,0 +1,89 @@
+package io.quarkus.deployment.dev.console;
+
+import java.io.IOException;
+import java.io.PrintStream;
+import java.nio.charset.Charset;
+
+import org.jboss.logging.Logger;
+
+import io.quarkus.runtime.logging.LoggingSetupRecorder;
+
+class BasicConsole extends QuarkusConsole {
+
+ private final Logger log = Logger.getLogger(BasicConsole.class);
+
+ final PrintStream printStream;
+ final boolean noColor;
+
+ BasicConsole(boolean noColor, boolean inputSupport, PrintStream printStream) {
+ this.noColor = noColor;
+ this.printStream = printStream;
+ if (inputSupport) {
+ Thread t = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ while (true) {
+ try {
+ int val = System.in.read();
+ if (val == -1) {
+ return;
+ }
+ InputHolder handler = inputHandlers.peek();
+ if (handler != null) {
+ handler.handler.handleInput(new int[] { val });
+ }
+ } catch (IOException e) {
+ log.error("Failed to read user input", e);
+ return;
+ }
+ }
+
+ }
+ }, "Quarkus Terminal Reader");
+ t.setDaemon(true);
+ t.start();
+ }
+ }
+
+ @Override
+ public InputHolder createHolder(InputHandler inputHandler) {
+ return new InputHolder(inputHandler) {
+ @Override
+ protected void setPromptMessage(String prompt) {
+ if (prompt == null) {
+ return;
+ }
+ write("\n" + prompt + "\n");
+ }
+
+ @Override
+ protected void setStatusMessage(String status) {
+ if (status == null) {
+ return;
+ }
+ write("\n" + status + "\n");
+ }
+ };
+ }
+
+ @Override
+ public void write(String s) {
+ if (outputFilter != null) {
+ if (!outputFilter.test(s)) {
+ return;
+ }
+ }
+ if (noColor || !LoggingSetupRecorder.hasColorSupport()) {
+ printStream.print(stripAnsiCodes(s));
+ } else {
+ printStream.print(s);
+ }
+
+ }
+
+ @Override
+ public void write(byte[] buf, int off, int len) {
+ write(new String(buf, off, len, Charset.defaultCharset()));
+ }
+
+}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/console/InputHandler.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/InputHandler.java
new file mode 100644
index 0000000000000..e980111537719
--- /dev/null
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/InputHandler.java
@@ -0,0 +1,14 @@
+package io.quarkus.deployment.dev.console;
+
+public interface InputHandler {
+
+ void handleInput(int[] keys);
+
+ void promptHandler(ConsoleStatus promptHandler);
+
+ interface ConsoleStatus {
+ void setPrompt(String prompt);
+
+ void setStatus(String status);
+ }
+}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/console/QuarkusConsole.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/QuarkusConsole.java
new file mode 100644
index 0000000000000..ef6488aeb3d71
--- /dev/null
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/QuarkusConsole.java
@@ -0,0 +1,130 @@
+package io.quarkus.deployment.dev.console;
+
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+import org.aesh.readline.tty.terminal.TerminalConnection;
+import org.aesh.terminal.Connection;
+
+import io.quarkus.deployment.TestConfig;
+
+public abstract class QuarkusConsole {
+
+ protected final ArrayDeque inputHandlers = new ArrayDeque<>();
+
+ public static volatile QuarkusConsole INSTANCE = new BasicConsole(false, false, System.out);
+
+ private static volatile boolean installed;
+
+ protected volatile Predicate outputFilter;
+
+ public synchronized void pushInputHandler(InputHandler inputHandler) {
+ InputHolder holder = inputHandlers.peek();
+ if (holder != null) {
+ holder.setEnabled(false);
+ }
+ holder = createHolder(inputHandler);
+ inputHandler.promptHandler(holder);
+ holder.setEnabled(true);
+ inputHandlers.push(holder);
+ }
+
+ public void popInputHandler() {
+ InputHolder holder = inputHandlers.pop();
+ holder.setEnabled(false);
+ holder = inputHandlers.peek();
+ if (holder != null) {
+ holder.setEnabled(true);
+ }
+ }
+
+ public abstract InputHolder createHolder(InputHandler inputHandler);
+
+ public abstract void write(String s);
+
+ public abstract void write(byte[] buf, int off, int len);
+
+ public static synchronized void installConsole(TestConfig config) {
+ if (installed) {
+ return;
+ }
+ installed = true;
+ if (config.basicConsole) {
+ INSTANCE = new BasicConsole(config.disableColor, true, System.out);
+ } else {
+ try {
+ new TerminalConnection(new Consumer() {
+ @Override
+ public void accept(Connection connection) {
+ if (connection.supportsAnsi()) {
+ INSTANCE = new AeshConsole(connection);
+ RedirectPrintStream ps = new RedirectPrintStream();
+ System.setOut(ps);
+ System.setErr(ps);
+ } else {
+ connection.close();
+ INSTANCE = new BasicConsole(config.disableColor, true, System.out);
+ }
+
+ }
+ });
+ } catch (IOException e) {
+ INSTANCE = new BasicConsole(config.disableColor, true, System.out);
+ }
+ }
+ }
+
+ protected String stripAnsiCodes(String s) {
+ if (s == null) {
+ return null;
+ }
+ s = s.replaceAll("\\u001B\\[(.*?)[a-zA-Z]", "");
+ return s;
+ }
+
+ public void setOutputFilter(Predicate logHandler) {
+ this.outputFilter = logHandler;
+ }
+
+ protected static abstract class InputHolder implements InputHandler.ConsoleStatus {
+ final InputHandler handler;
+ volatile boolean enabled;
+ String prompt;
+ String status;
+
+ protected InputHolder(InputHandler handler) {
+ this.handler = handler;
+ }
+
+ public InputHolder setEnabled(boolean enabled) {
+ this.enabled = enabled;
+ if (enabled) {
+ setStatus(status);
+ setPrompt(prompt);
+ }
+ return this;
+ }
+
+ @Override
+ public void setPrompt(String prompt) {
+ this.prompt = prompt;
+ if (enabled) {
+ setPromptMessage(prompt);
+ }
+ }
+
+ protected abstract void setPromptMessage(String prompt);
+
+ @Override
+ public void setStatus(String status) {
+ this.status = status;
+ if (enabled) {
+ setStatusMessage(status);
+ }
+ }
+
+ protected abstract void setStatusMessage(String status);
+ }
+}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/console/RedirectPrintStream.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/RedirectPrintStream.java
new file mode 100644
index 0000000000000..6da10e117ab67
--- /dev/null
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/console/RedirectPrintStream.java
@@ -0,0 +1,186 @@
+package io.quarkus.deployment.dev.console;
+
+import java.io.ByteArrayOutputStream;
+import java.io.PrintStream;
+import java.util.Formatter;
+import java.util.Locale;
+
+public class RedirectPrintStream extends PrintStream {
+
+ private Formatter formatter;
+
+ RedirectPrintStream() {
+ super(new ByteArrayOutputStream(0)); // never used
+ }
+
+ @Override
+ public void write(byte[] buf, int off, int len) {
+ QuarkusConsole.INSTANCE.write(buf, off, len);
+ }
+
+ void write(String s) {
+ QuarkusConsole.INSTANCE.write(s);
+ }
+
+ @Override
+ public void write(int b) {
+ write(new byte[] { (byte) b });
+ }
+
+ //@Overide
+ public void write(byte[] buf) {
+ write(buf, 0, buf.length);
+ }
+
+ //@Override
+ public void writeBytes(byte[] buf) {
+ write(buf, 0, buf.length);
+ }
+
+ @Override
+ public void print(boolean b) {
+ write(String.valueOf(b));
+ }
+
+ @Override
+ public void print(char c) {
+ write(String.valueOf(c));
+ }
+
+ @Override
+ public void print(int i) {
+ write(String.valueOf(i));
+ }
+
+ @Override
+ public void print(long l) {
+ write(String.valueOf(l));
+ }
+
+ @Override
+ public void print(float f) {
+ write(String.valueOf(f));
+ }
+
+ @Override
+ public void print(double d) {
+ write(String.valueOf(d));
+ }
+
+ @Override
+ public void print(char[] s) {
+ write(String.valueOf(s));
+ }
+
+ @Override
+ public void print(String s) {
+ write(String.valueOf(s));
+ }
+
+ @Override
+ public void print(Object obj) {
+ write(String.valueOf(obj));
+ }
+
+ @Override
+ public void println() {
+ write("\n");
+ }
+
+ @Override
+ public void println(boolean x) {
+ write(String.valueOf(x) + "\n");
+ }
+
+ @Override
+ public void println(char x) {
+ write(String.valueOf(x) + "\n");
+ }
+
+ @Override
+ public void println(int x) {
+ write(String.valueOf(x) + "\n");
+ }
+
+ @Override
+ public void println(long x) {
+ write(String.valueOf(x) + "\n");
+ }
+
+ @Override
+ public void println(float x) {
+ write(String.valueOf(x) + "\n");
+ }
+
+ @Override
+ public void println(double x) {
+ write(String.valueOf(x) + "\n");
+ }
+
+ @Override
+ public void println(char[] x) {
+ write(String.valueOf(x) + "\n");
+ }
+
+ @Override
+ public void println(String x) {
+ write(String.valueOf(x) + "\n");
+ }
+
+ @Override
+ public void println(Object x) {
+ write(String.valueOf(x) + "\n");
+ }
+
+ @Override
+ public PrintStream printf(String format, Object... args) {
+ return format(format, args);
+ }
+
+ @Override
+ public PrintStream printf(Locale l, String format, Object... args) {
+ return format(l, format, args);
+ }
+
+ @Override
+ public PrintStream format(String format, Object... args) {
+ synchronized (this) {
+ if ((formatter == null)
+ || (formatter.locale() != Locale.getDefault(Locale.Category.FORMAT)))
+ formatter = new Formatter((Appendable) this);
+ formatter.format(Locale.getDefault(Locale.Category.FORMAT),
+ format, args);
+ }
+ return this;
+ }
+
+ @Override
+ public PrintStream format(Locale l, String format, Object... args) {
+ synchronized (this) {
+ if ((formatter == null)
+ || (formatter.locale() != l))
+ formatter = new Formatter(this, l);
+ formatter.format(l, format, args);
+ }
+ return this;
+ }
+
+ @Override
+ public PrintStream append(CharSequence csq) {
+ print(String.valueOf(csq));
+ return this;
+ }
+
+ @Override
+ public PrintStream append(CharSequence csq, int start, int end) {
+ if (csq == null)
+ csq = "null";
+ return append(csq.subSequence(start, end));
+ }
+
+ @Override
+ public PrintStream append(char c) {
+ print(c);
+ return this;
+ }
+}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/CurrentTestApplication.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/CurrentTestApplication.java
new file mode 100644
index 0000000000000..67c405779b8f3
--- /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 0000000000000..b7347d0ce56d4
--- /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("" + attr + ">");
+ }
+ 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 0000000000000..b32fa1406ef09
--- /dev/null
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java
@@ -0,0 +1,649 @@
+package io.quarkus.deployment.dev.testing;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingDeque;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.function.Predicate;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.Index;
+import org.jboss.jandex.Indexer;
+import org.jboss.logging.Logger;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Tags;
+import org.junit.platform.engine.FilterResult;
+import org.junit.platform.engine.TestDescriptor;
+import org.junit.platform.engine.TestExecutionResult;
+import org.junit.platform.engine.TestSource;
+import org.junit.platform.engine.UniqueId;
+import org.junit.platform.engine.discovery.DiscoverySelectors;
+import org.junit.platform.engine.reporting.ReportEntry;
+import org.junit.platform.engine.support.descriptor.ClassSource;
+import org.junit.platform.engine.support.descriptor.MethodSource;
+import org.junit.platform.launcher.Launcher;
+import org.junit.platform.launcher.LauncherDiscoveryRequest;
+import org.junit.platform.launcher.PostDiscoveryFilter;
+import org.junit.platform.launcher.TestExecutionListener;
+import org.junit.platform.launcher.TestIdentifier;
+import org.junit.platform.launcher.TestPlan;
+import org.junit.platform.launcher.core.LauncherConfig;
+import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
+import org.junit.platform.launcher.core.LauncherFactory;
+import org.opentest4j.TestAbortedException;
+
+import io.quarkus.bootstrap.app.CuratedApplication;
+import io.quarkus.deployment.dev.ClassScanResult;
+import io.quarkus.deployment.dev.DevModeContext;
+import io.quarkus.deployment.dev.console.QuarkusConsole;
+import io.quarkus.dev.testing.TracingHandler;
+
+/**
+ * This class is responsible for running a single run of JUnit tests.
+ */
+public class JunitTestRunner {
+
+ private static final Logger log = Logger.getLogger(JunitTestRunner.class);
+ private final long runId;
+ private final DevModeContext devModeContext;
+ private final CuratedApplication testApplication;
+ private final ClassScanResult classScanResult;
+ private final TestClassUsages testClassUsages;
+ private final TestState testState;
+ private final List listeners;
+ List additionalFilters;
+ private final Set includeTags;
+ private final Set excludeTags;
+ private final Pattern include;
+ private final Pattern exclude;
+ private final boolean displayInConsole;
+
+ private volatile boolean testsRunning = false;
+ private volatile boolean aborted;
+ private volatile boolean paused;
+
+ public JunitTestRunner(Builder builder) {
+ this.runId = builder.runId;
+ this.devModeContext = builder.devModeContext;
+ this.testApplication = builder.testApplication;
+ this.classScanResult = builder.classScanResult;
+ this.testClassUsages = builder.testClassUsages;
+ this.listeners = builder.listeners;
+ this.additionalFilters = builder.additionalFilters;
+ this.testState = builder.testState;
+ this.includeTags = new HashSet<>(builder.includeTags);
+ this.excludeTags = new HashSet<>(builder.excludeTags);
+ this.include = builder.include;
+ this.exclude = builder.exclude;
+ this.displayInConsole = builder.displayInConsole;
+ }
+
+ public void runTests() {
+ long start = System.currentTimeMillis();
+ ClassLoader old = Thread.currentThread().getContextClassLoader();
+ try {
+
+ ClassLoader tcl = testApplication.createDeploymentClassLoader();
+ Thread.currentThread().setContextClassLoader(tcl);
+ ((Consumer) tcl.loadClass(CurrentTestApplication.class.getName()).newInstance()).accept(testApplication);
+
+ List> quarkusTestClasses = discoverTestClasses(devModeContext);
+
+ Launcher launcher = LauncherFactory.create(LauncherConfig.builder().build());
+ LauncherDiscoveryRequestBuilder launchBuilder = new LauncherDiscoveryRequestBuilder()
+ .selectors(quarkusTestClasses.stream().map(DiscoverySelectors::selectClass).collect(Collectors.toList()));
+ if (classScanResult != null) {
+ launchBuilder.filters(testClassUsages.getTestsToRun(classScanResult.getChangedClassNames(), testState));
+ }
+ if (!includeTags.isEmpty()) {
+ launchBuilder.filters(new TagFilter(false, includeTags));
+ } else if (!excludeTags.isEmpty()) {
+ launchBuilder.filters(new TagFilter(true, excludeTags));
+ }
+ if (include != null) {
+ launchBuilder.filters(new RegexFilter(false, include));
+ } else if (exclude != null) {
+ launchBuilder.filters(new RegexFilter(true, exclude));
+ }
+ if (!additionalFilters.isEmpty()) {
+ launchBuilder.filters(additionalFilters.toArray(new PostDiscoveryFilter[0]));
+ }
+ LauncherDiscoveryRequest request = launchBuilder
+ .build();
+ TestPlan testPlan = launcher.discover(request);
+ if (!testPlan.containsTests()) {
+ //nothing to see here
+ return;
+ }
+ long toRun = testPlan.countTestIdentifiers(TestIdentifier::isTest);
+ for (TestRunListener listener : listeners) {
+ listener.runStarted(toRun);
+ }
+ log.debug("Starting test run with " + quarkusTestClasses.size() + " test cases");
+ TestLogCapturingHandler logHandler = new TestLogCapturingHandler();
+ QuarkusConsole.INSTANCE.setOutputFilter(logHandler);
+
+ final Deque> touchedClasses = new LinkedBlockingDeque<>();
+ final AtomicReference> startupClasses = new AtomicReference<>();
+ TracingHandler.setTracingHandler(new TracingHandler.TraceListener() {
+ @Override
+ public void touched(String className) {
+ Set set = touchedClasses.peek();
+ if (set != null) {
+ set.add(className);
+ }
+ }
+
+ @Override
+ public void quarkusStarting() {
+ startupClasses.set(touchedClasses.peek());
+ }
+ });
+
+ Map> resultsByClass = new HashMap<>();
+
+ launcher.execute(testPlan, new TestExecutionListener() {
+
+ @Override
+ public void executionStarted(TestIdentifier testIdentifier) {
+ String className = "";
+ if (testIdentifier.getSource().isPresent()) {
+ if (testIdentifier.getSource().get() instanceof MethodSource) {
+ className = ((MethodSource) testIdentifier.getSource().get()).getClassName();
+ } else if (testIdentifier.getSource().get() instanceof ClassSource) {
+ className = ((ClassSource) testIdentifier.getSource().get()).getClassName();
+ }
+ }
+ for (TestRunListener listener : listeners) {
+ listener.testStarted(testIdentifier, className);
+ }
+ waitTillResumed();
+ touchedClasses.push(Collections.synchronizedSet(new HashSet<>()));
+ }
+
+ @Override
+ public void executionSkipped(TestIdentifier testIdentifier, String reason) {
+ waitTillResumed();
+ }
+
+ @Override
+ public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
+
+ if (aborted) {
+ return;
+ }
+ Class> testClass = null;
+ String displayName = testIdentifier.getDisplayName();
+ TestSource testSource = testIdentifier.getSource().orElse(null);
+ Set touched = touchedClasses.pop();
+ UniqueId id = UniqueId.parse(testIdentifier.getUniqueId());
+ if (testSource instanceof ClassSource) {
+ testClass = ((ClassSource) testSource).getJavaClass();
+ if (testExecutionResult.getStatus() != TestExecutionResult.Status.ABORTED) {
+ for (Set i : touchedClasses) {
+ //also add the parent touched classes
+ touched.addAll(i);
+ }
+ if (startupClasses.get() != null) {
+ touched.addAll(startupClasses.get());
+ }
+ testClassUsages.updateTestData(testClass.getName(), touched);
+ }
+ } else if (testSource instanceof MethodSource) {
+ testClass = ((MethodSource) testSource).getJavaClass();
+ displayName = testClass.getSimpleName() + "#" + displayName;
+
+ if (testExecutionResult.getStatus() != TestExecutionResult.Status.ABORTED) {
+ for (Set i : touchedClasses) {
+ //also add the parent touched classes
+ touched.addAll(i);
+ }
+ if (startupClasses.get() != null) {
+ touched.addAll(startupClasses.get());
+ }
+ testClassUsages.updateTestData(testClass.getName(), id,
+ touched);
+ }
+ }
+ if (testClass != null) {
+ Map results = resultsByClass.computeIfAbsent(testClass.getName(),
+ s -> new HashMap<>());
+ TestResult result = new TestResult(displayName, testClass.getName(), id, testExecutionResult,
+ logHandler.captureOutput(), testIdentifier.isTest(), runId);
+ results.put(id, result);
+ if (result.isTest()) {
+ for (TestRunListener listener : listeners) {
+ listener.testComplete(result);
+ }
+ }
+ }
+ if (testExecutionResult.getStatus() == TestExecutionResult.Status.FAILED) {
+ Throwable throwable = testExecutionResult.getThrowable().get();
+ if (testClass != null) {
+ //first we cut all the platform stuff out of the stack trace
+ StackTraceElement[] st = throwable.getStackTrace();
+ for (int i = st.length - 1; i >= 0; --i) {
+ StackTraceElement elem = st[i];
+ if (elem.getClassName().equals(testClass.getName())) {
+ StackTraceElement[] newst = new StackTraceElement[i + 1];
+ System.arraycopy(st, 0, newst, 0, i + 1);
+ st = newst;
+ break;
+ }
+ }
+
+ //now cut out all the restassured internals
+ //TODO: this should be pluggable
+ for (int i = st.length - 1; i >= 0; --i) {
+ StackTraceElement elem = st[i];
+ if (elem.getClassName().startsWith("io.restassured")) {
+ StackTraceElement[] newst = new StackTraceElement[st.length - i];
+ System.arraycopy(st, i, newst, 0, st.length - i);
+ st = newst;
+ break;
+ }
+ }
+ throwable.setStackTrace(st);
+ }
+ }
+ }
+
+ @Override
+ public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry entry) {
+
+ }
+ });
+ if (aborted) {
+ return;
+ }
+ testState.updateResults(resultsByClass);
+ if (classScanResult != null) {
+ testState.classesRemoved(classScanResult.getDeletedClassNames());
+ }
+
+ QuarkusConsole.INSTANCE.setOutputFilter(null);
+ List historicFailures = testState.getHistoricFailures(resultsByClass);
+
+ for (TestRunListener listener : listeners) {
+ listener.runComplete(new TestRunResults(runId, classScanResult, classScanResult == null, start,
+ System.currentTimeMillis(), toResultsMap(historicFailures, resultsByClass)));
+ }
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ } finally {
+ QuarkusConsole.INSTANCE.setOutputFilter(null);
+ Thread.currentThread().setContextClassLoader(old);
+ }
+ }
+
+ public synchronized void abort() {
+ for (TestRunListener listener : listeners) {
+ listener.runAborted();
+ }
+ aborted = true;
+ notifyAll();
+ }
+
+ public synchronized void pause() {
+ //todo
+ paused = true;
+ }
+
+ public synchronized void resume() {
+ paused = false;
+ notifyAll();
+ }
+
+ private Map toResultsMap(List historicFailures,
+ Map> resultsByClass) {
+ Map resultMap = new HashMap<>();
+ Map> historicMap = new HashMap<>();
+ for (TestResult i : historicFailures) {
+ historicMap.computeIfAbsent(i.getTestClass(), s -> new ArrayList<>()).add(i);
+ }
+ Set classes = new HashSet<>(resultsByClass.keySet());
+ classes.addAll(historicMap.keySet());
+ for (String clazz : classes) {
+ List passing = new ArrayList<>();
+ List failing = new ArrayList<>();
+ List skipped = new ArrayList<>();
+ for (TestResult i : Optional.ofNullable(resultsByClass.get(clazz)).orElse(Collections.emptyMap()).values()) {
+ if (i.getTestExecutionResult().getStatus() == TestExecutionResult.Status.FAILED) {
+ failing.add(i);
+ } else if (i.getTestExecutionResult().getStatus() == TestExecutionResult.Status.ABORTED) {
+ skipped.add(i);
+ } else {
+ passing.add(i);
+ }
+ }
+ for (TestResult i : Optional.ofNullable(historicMap.get(clazz)).orElse(Collections.emptyList())) {
+ if (i.getTestExecutionResult().getStatus() == TestExecutionResult.Status.FAILED) {
+ failing.add(i);
+ } else if (i.getTestExecutionResult().getStatus() == TestExecutionResult.Status.ABORTED) {
+ skipped.add(i);
+ } else {
+ passing.add(i);
+ }
+ }
+ resultMap.put(clazz, new TestClassResult(clazz, passing, failing, skipped));
+ }
+ return resultMap;
+ }
+
+ public void waitTillResumed() {
+ synchronized (JunitTestRunner.this) {
+ while (paused && !aborted) {
+ try {
+ JunitTestRunner.this.wait();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ if (aborted) {
+ throw new TestAbortedException("Tests are disabled");
+ }
+ }
+ }
+
+ private static List> discoverTestClasses(DevModeContext devModeContext) {
+ //maven has a lot of rules around this and is configurable
+ //for now this is out of scope, we are just going to consider all @QuarkusTest classes
+ //we can revisit this later
+
+ //simple class loading
+ List classRoots = new ArrayList<>();
+ try {
+ for (DevModeContext.ModuleInfo i : devModeContext.getAllModules()) {
+ classRoots.add(Paths.get(i.getMain().getClassesPath()).toFile().toURL());
+ }
+ //we know test is not empty, otherwise we would not be runnning
+ classRoots.add(Paths.get(devModeContext.getApplicationRoot().getTest().get().getClassesPath()).toFile().toURL());
+ } catch (MalformedURLException e) {
+ throw new RuntimeException(e);
+ }
+ URLClassLoader ucl = new URLClassLoader(classRoots.toArray(new URL[0]), Thread.currentThread().getContextClassLoader());
+
+ //we also only run tests from the current module, which we can also revisit later
+ Indexer indexer = new Indexer();
+ try (Stream files = Files.walk(Paths.get(devModeContext.getApplicationRoot().getTest().get().getClassesPath()))) {
+ files.filter(s -> s.getFileName().toString().endsWith(".class")).forEach(s -> {
+ try (InputStream in = Files.newInputStream(s)) {
+ indexer.index(in);
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ });
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+
+ //todo: sort by profile, account for modules
+ Index index = indexer.complete();
+ List> ret = new ArrayList<>();
+ for (AnnotationInstance i : index.getAnnotations(DotName.createSimple("io.quarkus.test.junit.QuarkusTest"))) {
+ try {
+ ret.add(ucl.loadClass(i.target().asClass().name().toString()));
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ ret.sort(Comparator.comparing(new Function, String>() {
+ @Override
+ public String apply(Class> aClass) {
+ ClassInfo def = index.getClassByName(DotName.createSimple(aClass.getName()));
+ AnnotationInstance testProfile = def.classAnnotation(DotName.createSimple("io.quarkus.test.junit.TestProfile"));
+ if (testProfile == null) {
+ return "$$" + aClass.getName();
+ }
+ return testProfile.value().asClass().name().toString() + "$$" + aClass.getName();
+ }
+ }));
+ return ret;
+ }
+
+ public TestState getResults() {
+ return testState;
+ }
+
+ public boolean isRunning() {
+ return testsRunning;
+ }
+
+ private class TestLogCapturingHandler implements Predicate {
+
+ private final List logOutput;
+
+ public TestLogCapturingHandler() {
+ this.logOutput = new ArrayList<>();
+ }
+
+ public List captureOutput() {
+ List ret = new ArrayList<>(logOutput);
+ logOutput.clear();
+ return ret;
+ }
+
+ @Override
+ public boolean test(String logRecord) {
+ Thread thread = Thread.currentThread();
+ ClassLoader cl = thread.getContextClassLoader();
+ while (cl.getParent() != null) {
+ if (cl == testApplication.getAugmentClassLoader()
+ || cl == testApplication.getBaseRuntimeClassLoader()) {
+ //TODO: for convenience we save the log records as HTML rather than ansci here
+ synchronized (logOutput) {
+ ByteArrayOutputStream out = new ByteArrayOutputStream();
+ HtmlAnsiOutputStream outputStream = new HtmlAnsiOutputStream(out) {
+ };
+ try {
+ outputStream.write(logRecord.getBytes(StandardCharsets.UTF_8));
+ logOutput.add(new String(out.toByteArray(), StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ log.error("Failed to capture log record", e);
+ logOutput.add(logRecord);
+ }
+ }
+ return displayInConsole;
+ }
+ cl = cl.getParent();
+ }
+ return true;
+ }
+ }
+
+ static class Builder {
+ private TestState testState;
+ private long runId = -1;
+ private DevModeContext devModeContext;
+ private CuratedApplication testApplication;
+ private ClassScanResult classScanResult;
+ private TestClassUsages testClassUsages;
+ private final List listeners = new ArrayList<>();
+ private final List additionalFilters = new ArrayList<>();
+ private List includeTags = Collections.emptyList();
+ private List excludeTags = Collections.emptyList();
+ private Pattern include;
+ private Pattern exclude;
+ private boolean displayInConsole;
+
+ public Builder setRunId(long runId) {
+ this.runId = runId;
+ return this;
+ }
+
+ public Builder setDevModeContext(DevModeContext devModeContext) {
+ this.devModeContext = devModeContext;
+ return this;
+ }
+
+ public Builder setTestApplication(CuratedApplication testApplication) {
+ this.testApplication = testApplication;
+ return this;
+ }
+
+ public Builder setClassScanResult(ClassScanResult classScanResult) {
+ this.classScanResult = classScanResult;
+ return this;
+ }
+
+ public Builder setIncludeTags(List includeTags) {
+ this.includeTags = includeTags;
+ return this;
+ }
+
+ public Builder setExcludeTags(List excludeTags) {
+ this.excludeTags = excludeTags;
+ return this;
+ }
+
+ public Builder setTestClassUsages(TestClassUsages testClassUsages) {
+ this.testClassUsages = testClassUsages;
+ return this;
+ }
+
+ public Builder addListener(TestRunListener listener) {
+ this.listeners.add(listener);
+ return this;
+ }
+
+ public Builder addAdditionalFilter(PostDiscoveryFilter filter) {
+ this.additionalFilters.add(filter);
+ return this;
+ }
+
+ public Builder setTestState(TestState testState) {
+ this.testState = testState;
+ return this;
+ }
+
+ public Builder setInclude(Pattern include) {
+ this.include = include;
+ return this;
+ }
+
+ public Builder setExclude(Pattern exclude) {
+ this.exclude = exclude;
+ return this;
+ }
+
+ public Builder setDisplayInConsole(boolean displayInConsole) {
+ this.displayInConsole = displayInConsole;
+ return this;
+ }
+
+ public JunitTestRunner build() {
+ Objects.requireNonNull(devModeContext, "devModeContext");
+ Objects.requireNonNull(testClassUsages, "testClassUsages");
+ Objects.requireNonNull(testApplication, "testApplication");
+ Objects.requireNonNull(testState, "testState");
+ return new JunitTestRunner(this);
+ }
+
+ }
+
+ private static class TagFilter implements PostDiscoveryFilter {
+
+ final boolean exclude;
+ final Set tags;
+
+ private TagFilter(boolean exclude, Set tags) {
+ this.exclude = exclude;
+ this.tags = tags;
+ }
+
+ @Override
+ public FilterResult apply(TestDescriptor testDescriptor) {
+ if (testDescriptor.getSource().isPresent()) {
+ if (testDescriptor.getSource().get() instanceof MethodSource) {
+ MethodSource methodSource = (MethodSource) testDescriptor.getSource().get();
+ Method m = methodSource.getJavaMethod();
+ FilterResult res = filterTags(m);
+ if (res != null) {
+ return res;
+ }
+ res = filterTags(methodSource.getJavaClass());
+ if (res != null) {
+ return res;
+ }
+ return FilterResult.includedIf(exclude);
+ }
+ }
+ return FilterResult.included("not a method");
+ }
+
+ public FilterResult filterTags(AnnotatedElement clz) {
+ Tag tag = clz.getAnnotation(Tag.class);
+ Tags tagsAnn = clz.getAnnotation(Tags.class);
+ List all = null;
+ if (tag != null) {
+ all = Collections.singletonList(tag);
+ } else if (tagsAnn != null) {
+ all = Arrays.asList(tagsAnn.value());
+ } else {
+ return null;
+ }
+ for (Tag i : all) {
+ if (tags.contains(i.value())) {
+ return FilterResult.includedIf(!exclude);
+ }
+ }
+ return FilterResult.includedIf(exclude);
+ }
+ }
+
+ private static class RegexFilter implements PostDiscoveryFilter {
+
+ final boolean exclude;
+ final Pattern pattern;
+
+ private RegexFilter(boolean exclude, Pattern pattern) {
+ this.exclude = exclude;
+ this.pattern = pattern;
+ }
+
+ @Override
+ public FilterResult apply(TestDescriptor testDescriptor) {
+ if (testDescriptor.getSource().isPresent()) {
+ if (testDescriptor.getSource().get() instanceof MethodSource) {
+ MethodSource methodSource = (MethodSource) testDescriptor.getSource().get();
+ String name = methodSource.getJavaClass().getName();
+ if (pattern.matcher(name).matches()) {
+ return FilterResult.includedIf(!exclude);
+ }
+ return FilterResult.includedIf(exclude);
+ }
+ }
+ return FilterResult.included("not a method");
+ }
+ }
+
+}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestClassResult.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestClassResult.java
new file mode 100644
index 0000000000000..255e9af8028f8
--- /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 0000000000000..e5e80c9c19389
--- /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 0000000000000..905c668d9bb29
--- /dev/null
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestConsoleHandler.java
@@ -0,0 +1,201 @@
+package io.quarkus.deployment.dev.testing;
+
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+
+import org.jboss.logging.Logger;
+import org.junit.platform.engine.TestExecutionResult;
+import org.junit.platform.launcher.TestIdentifier;
+
+import io.quarkus.deployment.dev.RuntimeUpdatesProcessor;
+import io.quarkus.deployment.dev.console.InputHandler;
+import io.quarkus.deployment.dev.console.QuarkusConsole;
+
+public class TestConsoleHandler implements TestListener {
+
+ private static final Logger log = Logger.getLogger("io.quarkus.test");
+
+ public static final String DISABLED_PROMPT = "\u001b[33mTests Disabled, press [e] to enable\u001b[0m";
+ public static final String FIRST_RUN_PROMPT = "\u001b[33mRunning Tests for the first time\u001b[0m";
+ public static final String RUNNING_PROMPT = "Press [r] to re-run, [v] to view full results, [d] to disable, [h] for more options>";
+ public static final String ABORTED_PROMPT = "Test run aborted.";
+
+ boolean firstRun = true;
+ boolean disabled = true;
+ volatile InputHandler.ConsoleStatus promptHandler;
+ volatile TestController testController;
+ private String lastStatus;
+
+ public void install() {
+ QuarkusConsole.INSTANCE.pushInputHandler(inputHandler);
+ }
+
+ private final InputHandler inputHandler = new InputHandler() {
+
+ @Override
+ public void handleInput(int[] keys) {
+ if (disabled) {
+ for (int i : keys) {
+ if (i == 'e') {
+ TestSupport.instance().get().start();
+ }
+ }
+ } else if (!firstRun) {
+ //TODO: some of this is a bit yuck, this needs some work
+ for (int k : keys) {
+ if (k == 'r') {
+ testController.runAllTests();
+ }
+ if (k == 'f') {
+ testController.runFailedTests();
+ } else if (k == 'v') {
+ printFullResults();
+ } else if (k == 'i') {
+ RuntimeUpdatesProcessor.INSTANCE.toggleInstrumentation();
+ } else if (k == 'o') {
+ TestSupport.instance().get().setDisplayTestOutput(!TestSupport.instance().get().displayTestOutput);
+ if (TestSupport.instance().get().displayTestOutput) {
+ log.info("Test output enabled");
+ } else {
+ log.info("Test output disabled");
+ }
+ } else if (k == 'd') {
+ TestSupport.instance().get().stop();
+ } else if (k == 'h') {
+ printUsage();
+ }
+ }
+ }
+ }
+
+ @Override
+ public void promptHandler(InputHandler.ConsoleStatus promptHandler) {
+ TestConsoleHandler.this.promptHandler = promptHandler;
+ }
+ };
+
+ @Override
+ public void listenerRegistered(TestController testController) {
+ this.testController = testController;
+ promptHandler.setStatus(DISABLED_PROMPT);
+ }
+
+ public void printUsage() {
+ System.out.println("r - Re-run all tests");
+ System.out.println("f - Re-run failed tests");
+ System.out.println("v - Print failures from the last test run");
+ System.out.println("o - Toggle test output");
+ System.out.println("i - Toggle instrumentation based reload");
+ System.out.println("d - Disable tests");
+ System.out.println("h - Display this help");
+
+ }
+
+ private void printFullResults() {
+ if (testController.currentState().getFailingClasses().isEmpty()) {
+ log.info("All tests passed, no output to display");
+ }
+ for (TestClassResult i : testController.currentState().getFailingClasses()) {
+ for (TestResult failed : i.getFailing()) {
+ log.error(
+ "Test " + failed.getDisplayName() + " failed "
+ + failed.getTestExecutionResult().getStatus()
+ + "\n",
+ failed.getTestExecutionResult().getThrowable().get());
+ }
+ }
+
+ }
+
+ @Override
+ public void testsEnabled() {
+ disabled = false;
+ if (firstRun) {
+ promptHandler.setStatus(null);
+ promptHandler.setPrompt(FIRST_RUN_PROMPT);
+ } else {
+ promptHandler.setPrompt(RUNNING_PROMPT);
+ promptHandler.setStatus(lastStatus);
+ }
+ }
+
+ @Override
+ public void testsDisabled() {
+ disabled = true;
+ promptHandler.setPrompt(DISABLED_PROMPT);
+ promptHandler.setStatus(null);
+ }
+
+ @Override
+ public void testRunStarted(Consumer listenerConsumer) {
+
+ AtomicLong totalNoTests = new AtomicLong();
+ AtomicLong skipped = new AtomicLong();
+ AtomicLong methodCount = new AtomicLong();
+ AtomicLong failureCount = new AtomicLong();
+ listenerConsumer.accept(new TestRunListener() {
+ @Override
+ public void runStarted(long toRun) {
+ totalNoTests.set(toRun);
+ promptHandler.setStatus("Running 0/" + toRun + ".");
+ }
+
+ @Override
+ public void testComplete(TestResult result) {
+ if (result.getTestExecutionResult().getStatus() == TestExecutionResult.Status.FAILED) {
+ failureCount.incrementAndGet();
+ } else if (result.getTestExecutionResult().getStatus() == TestExecutionResult.Status.ABORTED) {
+ skipped.incrementAndGet();
+ }
+ methodCount.incrementAndGet();
+ }
+
+ @Override
+ public void runComplete(TestRunResults results) {
+ firstRun = false;
+ if (results.getCurrentFailing().isEmpty()) {
+ lastStatus = "\u001B[32mTests all passed, " + methodCount.get() + " tests were run, " + skipped.get()
+ + " were skipped. Tests took " + (results.getTotalTime())
+ + "ms." + "\u001b[0m";
+ } else {
+ StringBuilder sb = new StringBuilder(
+ "\u001B[91mTest run failed, " + methodCount.get() + " tests were run, "
+ + results.getCurrentFailing().size()
+ + " failed, "
+ + skipped.get()
+ + " were skipped. Tests took " + results.getTotalTime() + "ms");
+ for (Map.Entry classEntry : results.getCurrentFailing().entrySet()) {
+ for (TestResult test : classEntry.getValue().getFailing()) {
+ log.error(
+ "Test " + test.getDisplayName() + " failed \n",
+ test.getTestExecutionResult().getThrowable().get());
+ }
+ }
+ lastStatus = sb.toString() + "\u001b[0m";
+ }
+ //this will re-print when using the basic console
+ promptHandler.setPrompt(RUNNING_PROMPT);
+ promptHandler.setStatus(lastStatus);
+ }
+
+ @Override
+ public void runAborted() {
+ promptHandler.setStatus(ABORTED_PROMPT);
+ promptHandler.setPrompt(RUNNING_PROMPT);
+ firstRun = false;
+ }
+
+ @Override
+ public void testStarted(TestIdentifier testIdentifier, String className) {
+ promptHandler.setStatus("Running " + methodCount.get() + "/" + totalNoTests
+ + (failureCount.get() == 0 ? "."
+ : ". " + failureCount + " failures so far.")
+ + " Running: "
+ + className + "#" + testIdentifier.getDisplayName());
+ }
+ });
+
+ }
+
+}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestController.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestController.java
new file mode 100644
index 0000000000000..c13612d9ea80b
--- /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 0000000000000..a4bc1c3d68d31
--- /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 0000000000000..a803d9ac61db7
--- /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 0000000000000..0d8f959de484d
--- /dev/null
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunListener.java
@@ -0,0 +1,26 @@
+package io.quarkus.deployment.dev.testing;
+
+import org.junit.platform.launcher.TestIdentifier;
+
+public interface TestRunListener {
+
+ default void runStarted(long toRun) {
+
+ }
+
+ default void testComplete(TestResult result) {
+
+ }
+
+ default void runComplete(TestRunResults results) {
+
+ }
+
+ default void runAborted() {
+
+ }
+
+ default void testStarted(TestIdentifier testIdentifier, String className) {
+
+ }
+}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunResults.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunResults.java
new file mode 100644
index 0000000000000..e6e11fb65e823
--- /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 0000000000000..98af9b1f17416
--- /dev/null
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestRunner.java
@@ -0,0 +1,264 @@
+package io.quarkus.deployment.dev.testing;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.jboss.logging.Logger;
+import org.junit.platform.engine.FilterResult;
+import org.junit.platform.engine.TestDescriptor;
+import org.junit.platform.engine.UniqueId;
+import org.junit.platform.launcher.PostDiscoveryFilter;
+import org.opentest4j.TestAbortedException;
+
+import io.quarkus.bootstrap.app.CuratedApplication;
+import io.quarkus.deployment.dev.ClassScanResult;
+import io.quarkus.deployment.dev.DevModeContext;
+
+public class TestRunner {
+
+ private static final Logger log = Logger.getLogger(TestRunner.class);
+ private static final AtomicLong COUNTER = new AtomicLong();
+
+ private final TestSupport testSupport;
+ private final DevModeContext devModeContext;
+ private final CuratedApplication testApplication;
+
+ private boolean testsRunning = false;
+ private boolean testsQueued = false;
+ private ClassScanResult queuedChanges = null;
+ private boolean queuedFailureRun;
+
+ private Throwable compileProblem;
+
+ private final TestClassUsages testClassUsages = new TestClassUsages();
+ private boolean paused;
+ /**
+ * disabled is different to paused, when the runner is disabled we abort all runs rather than pausing them.
+ */
+ private volatile boolean disabled = true;
+ private volatile boolean firstRun = true;
+ private JunitTestRunner runner;
+
+ public TestRunner(TestSupport testSupport, DevModeContext devModeContext, CuratedApplication testApplication) {
+ this.testSupport = testSupport;
+ this.devModeContext = devModeContext;
+ this.testApplication = testApplication;
+ }
+
+ public void runTests() {
+ runTests(null);
+ }
+
+ public synchronized long getRunningTestRunId() {
+ if (testsRunning) {
+ return COUNTER.get();
+ }
+ return -1;
+ }
+
+ public void runFailedTests() {
+ runTests(null, true);
+ }
+
+ public void runTests(ClassScanResult classScanResult) {
+ runTests(classScanResult, false);
+ }
+
+ private void runTests(ClassScanResult classScanResult, boolean reRunFailures) {
+ if (compileProblem != null) {
+ return;
+ }
+ if (testApplication == null) {
+ return;
+ }
+ if (disabled) {
+ return;
+ }
+ if (reRunFailures && testSupport.testRunResults == null) {
+ return;
+ }
+ if (reRunFailures && testSupport.testRunResults.getCurrentFailing().isEmpty()) {
+ log.error("Not re-running failed tests, as all tests passed");
+ return;
+ }
+ synchronized (TestRunner.this) {
+ if (testsRunning) {
+ if (reRunFailures) {
+ log.error("Not re-running failed tests, as tests are already in progress.");
+ return;
+ }
+ if (testsQueued) {
+ if (queuedChanges != null) { //if this is null a full run is scheduled
+ this.queuedChanges = ClassScanResult.merge(this.queuedChanges, classScanResult);
+ }
+ } else {
+ testsQueued = true;
+ this.queuedChanges = classScanResult;
+ }
+ return;
+ } else {
+ testsRunning = true;
+ }
+ }
+ Thread t = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ runInternal(classScanResult, reRunFailures);
+ } finally {
+ waitTillResumed();
+ boolean run = false;
+ ClassScanResult current = null;
+ synchronized (TestRunner.this) {
+ if (!disabled) {
+ testsRunning = false;
+ if (testsQueued) {
+ testsQueued = false;
+ run = true;
+ }
+ current = queuedChanges;
+ queuedChanges = null;
+ }
+ }
+ if (run) {
+ runTests(current);
+ }
+ }
+ }
+ }, "Test runner thread");
+ t.setDaemon(true);
+ t.start();
+ }
+
+ public synchronized void pause() {
+ paused = true;
+ if (runner != null) {
+ runner.pause();
+ }
+ }
+
+ public synchronized void resume() {
+ paused = false;
+ notifyAll();
+ if (runner != null) {
+ runner.resume();
+ }
+ }
+
+ public synchronized void disable() {
+ disabled = true;
+ notifyAll();
+ if (runner != null) {
+ runner.abort();
+ }
+ }
+
+ public synchronized void enable() {
+ if (!disabled) {
+ return;
+ }
+ disabled = false;
+ if (firstRun) {
+ runTests();
+ }
+ }
+
+ private void runInternal(ClassScanResult classScanResult, boolean reRunFailures) {
+ final long runId = COUNTER.incrementAndGet();
+
+ AtomicReference resultsRef = new AtomicReference<>();
+ synchronized (this) {
+ if (runner != null) {
+ throw new IllegalStateException("Tests already in progress");
+ }
+ if (disabled) {
+ return;
+ }
+ JunitTestRunner.Builder builder = new JunitTestRunner.Builder()
+ .setClassScanResult(classScanResult)
+ .setDevModeContext(devModeContext)
+ .setRunId(runId)
+ .setTestState(testSupport.testState)
+ .setTestClassUsages(testClassUsages)
+ .setTestApplication(testApplication)
+ .setDisplayInConsole(testSupport.displayTestOutput)
+ .setIncludeTags(testSupport.includeTags)
+ .setExcludeTags(testSupport.excludeTags)
+ .setInclude(testSupport.include)
+ .setExclude(testSupport.exclude);
+ if (reRunFailures) {
+ Set ids = new HashSet<>();
+ for (Map.Entry e : testSupport.testRunResults.getCurrentFailing().entrySet()) {
+ for (TestResult test : e.getValue().getFailing()) {
+ ids.add(test.uniqueId);
+ }
+ }
+ builder.addAdditionalFilter(new PostDiscoveryFilter() {
+ @Override
+ public FilterResult apply(TestDescriptor testDescriptor) {
+ return FilterResult.includedIf(ids.contains(testDescriptor.getUniqueId()));
+ }
+ });
+ }
+ for (TestListener i : testSupport.testListeners) {
+ i.testRunStarted(builder::addListener);
+ }
+ builder.addListener(new TestRunListener() {
+ @Override
+ public void runComplete(TestRunResults results) {
+ testSupport.testRunResults = results;
+
+ }
+ });
+ runner = builder
+ .build();
+ if (paused) {
+ runner.pause();
+ }
+ }
+ runner.runTests();
+ synchronized (this) {
+ runner = null;
+ }
+ TestRunResults results = resultsRef.get();
+ if (disabled || results == null) {
+ return;
+ }
+ if (firstRun) {
+ firstRun = false;
+ }
+
+ }
+
+ public void waitTillResumed() {
+ synchronized (TestRunner.this) {
+ while (paused && !disabled) {
+ try {
+ TestRunner.this.wait();
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+ if (disabled) {
+ throw new TestAbortedException("Tests are disabled");
+ }
+ }
+ }
+
+ public synchronized void testCompileFailed(Throwable e) {
+ compileProblem = e;
+ log.error("Test compile failed", e);
+ }
+
+ public synchronized void testCompileSucceeded() {
+ compileProblem = null;
+ }
+
+ public boolean isRunning() {
+ return testsRunning;
+ }
+
+}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestState.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestState.java
new file mode 100644
index 0000000000000..413dde9f262dd
--- /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 0000000000000..98d6d65270207
--- /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 0000000000000..fe3864817f229
--- /dev/null
+++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java
@@ -0,0 +1,141 @@
+package io.quarkus.deployment.dev.testing;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.function.BiFunction;
+
+import org.jboss.jandex.ClassInfo;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+
+import io.quarkus.bootstrap.classloading.ClassPathElement;
+import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
+import io.quarkus.deployment.IsDevelopment;
+import io.quarkus.deployment.IsNormal;
+import io.quarkus.deployment.IsTest;
+import io.quarkus.deployment.TestConfig;
+import io.quarkus.deployment.annotations.BuildProducer;
+import io.quarkus.deployment.annotations.BuildStep;
+import io.quarkus.deployment.annotations.Produce;
+import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem;
+import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
+import io.quarkus.deployment.builditem.LaunchModeBuildItem;
+import io.quarkus.deployment.builditem.LiveReloadBuildItem;
+import io.quarkus.deployment.builditem.LogHandlerBuildItem;
+import io.quarkus.deployment.builditem.ServiceStartBuildItem;
+import io.quarkus.deployment.dev.console.QuarkusConsole;
+import io.quarkus.deployment.logging.LogCleanupFilterBuildItem;
+import io.quarkus.dev.spi.DevModeType;
+import io.quarkus.dev.testing.TracingHandler;
+
+/**
+ * processor that instruments test and application classes to trace the code path that is in use during a test run.
+ *
+ * This allows for fine grained running of tests when a file changes.
+ */
+public class TestTracingProcessor {
+
+ private static TestConfig.Mode lastEnabledValue;
+ private static boolean consoleInstalled = false;
+
+ @BuildStep(onlyIfNot = IsNormal.class)
+ LogCleanupFilterBuildItem handle() {
+ return new LogCleanupFilterBuildItem("org.junit.platform.launcher.core.EngineDiscoveryOrchestrator", "0 containers");
+ }
+
+ @BuildStep(onlyIf = IsDevelopment.class)
+ ServiceStartBuildItem setupConsole(TestConfig config) {
+ if (!TestSupport.instance().isPresent() || config.continuousTesting == TestConfig.Mode.DISABLED) {
+ return null;
+ }
+ if (consoleInstalled) {
+ return null;
+ }
+ consoleInstalled = true;
+ if (config.console) {
+ QuarkusConsole.installConsole(config);
+ TestConsoleHandler consoleHandler = new TestConsoleHandler();
+ consoleHandler.install();
+ TestSupport.instance().get().addListener(consoleHandler);
+ }
+ return null;
+ }
+
+ @BuildStep(onlyIfNot = IsNormal.class)
+ @Produce(LogHandlerBuildItem.class)
+ ServiceStartBuildItem startTesting(TestConfig config, LiveReloadBuildItem liveReloadBuildItem,
+ LaunchModeBuildItem launchModeBuildItem) {
+ if (!TestSupport.instance().isPresent() || config.continuousTesting == TestConfig.Mode.DISABLED) {
+ return null;
+ }
+ if (launchModeBuildItem.getDevModeType().orElse(null) != DevModeType.LOCAL) {
+ return null;
+ }
+ TestSupport testSupport = TestSupport.instance().get();
+ if (!liveReloadBuildItem.isLiveReload()) {
+ if (config.continuousTesting == TestConfig.Mode.ENABLED) {
+ testSupport.start();
+ } else if (config.continuousTesting == TestConfig.Mode.PAUSED) {
+ testSupport.init();
+ testSupport.stop();
+ }
+ }
+ testSupport.setTags(config.includeTags.orElse(Collections.emptyList()),
+ config.excludeTags.orElse(Collections.emptyList()));
+ testSupport.setPatterns(config.includePattern.orElse(null),
+ config.excludePattern.orElse(null));
+ testSupport.setConfiguredDisplayTestOutput(config.displayTestOutput);
+ return null;
+ }
+
+ @BuildStep(onlyIf = IsTest.class)
+ public void instrumentTestClasses(CombinedIndexBuildItem combinedIndexBuildItem,
+ LaunchModeBuildItem launchModeBuildItem,
+ BuildProducer transformerProducer) {
+ if (!launchModeBuildItem.isAuxiliaryApplication()) {
+ return;
+ }
+ for (ClassInfo clazz : combinedIndexBuildItem.getIndex().getKnownClasses()) {
+ String theClassName = clazz.name().toString();
+ if (isAppClass(theClassName)) {
+ transformerProducer.produce(new BytecodeTransformerBuildItem(false, theClassName,
+ new BiFunction() {
+ @Override
+ public ClassVisitor apply(String s, ClassVisitor classVisitor) {
+ return new ClassVisitor(Opcodes.ASM9, classVisitor) {
+ @Override
+ public MethodVisitor visitMethod(int access, String name, String descriptor,
+ String signature, String[] exceptions) {
+ MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions);
+ if (name.equals("") || name.equals("")) {
+ return mv;
+ }
+ return new MethodVisitor(Opcodes.ASM9, mv) {
+ @Override
+ public void visitCode() {
+ super.visitCode();
+ visitLdcInsn(theClassName);
+ visitMethodInsn(Opcodes.INVOKESTATIC,
+ TracingHandler.class.getName().replace(".", "/"), "trace",
+ "(Ljava/lang/String;)V", false);
+ }
+ };
+ }
+ };
+ }
+ }, true));
+ }
+ }
+
+ }
+
+ public boolean isAppClass(String theClassName) {
+ QuarkusClassLoader cl = (QuarkusClassLoader) Thread.currentThread()
+ .getContextClassLoader();
+ //if the class file is present in this (and not the parent) CL then it is an application class
+ List res = cl
+ .getElementsWithResource(theClassName.replace(".", "/") + ".class", true);
+ return !res.isEmpty();
+ }
+}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/jbang/JBangAugmentorImpl.java b/core/deployment/src/main/java/io/quarkus/deployment/jbang/JBangAugmentorImpl.java
index 5f3632e14a8d6..07f9a21f9cedc 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 9e7a1c55553dc..d2896fe30f50d 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