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 index cf5be62f97e40..24fe9708f9be3 100644 --- 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 @@ -754,7 +754,6 @@ static class Builder { private TestType testType = TestType.ALL; private TestState testState; private long runId = -1; - private DevModeContext devModeContext; private CuratedApplication testApplication; private ClassScanResult classScanResult; private TestClassUsages testClassUsages; @@ -783,11 +782,6 @@ public Builder setTestType(TestType testType) { return this; } - public Builder setDevModeContext(DevModeContext devModeContext) { - this.devModeContext = devModeContext; - return this; - } - public Builder setTestApplication(CuratedApplication testApplication) { this.testApplication = testApplication; return this; @@ -849,7 +843,6 @@ public Builder setExcludeEngines(List excludeEngines) { } public JunitTestRunner build() { - Objects.requireNonNull(devModeContext, "devModeContext"); Objects.requireNonNull(testClassUsages, "testClassUsages"); Objects.requireNonNull(testApplication, "testApplication"); Objects.requireNonNull(testState, "testState"); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/ModuleTestRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/ModuleTestRunner.java index 20d5f23873897..be0ca21445bf0 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/ModuleTestRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/ModuleTestRunner.java @@ -17,17 +17,15 @@ public class ModuleTestRunner { final TestState testState = new TestState(); private final TestSupport testSupport; - private final DevModeContext devModeContext; private final CuratedApplication testApplication; private final DevModeContext.ModuleInfo moduleInfo; private final TestClassUsages testClassUsages = new TestClassUsages(); private JunitTestRunner runner; - public ModuleTestRunner(TestSupport testSupport, DevModeContext devModeContext, CuratedApplication testApplication, + public ModuleTestRunner(TestSupport testSupport, CuratedApplication testApplication, DevModeContext.ModuleInfo moduleInfo) { this.testSupport = testSupport; - this.devModeContext = devModeContext; this.testApplication = testApplication; this.moduleInfo = moduleInfo; } @@ -50,7 +48,6 @@ Runnable prepare(ClassScanResult classScanResult, boolean reRunFailures, long ru } JunitTestRunner.Builder builder = new JunitTestRunner.Builder() .setClassScanResult(classScanResult) - .setDevModeContext(devModeContext) .setRunId(runId) .setTestState(testState) .setTestClassUsages(testClassUsages) 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 index 3773e5467423e..b72f5753c298e 100644 --- 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 @@ -2,14 +2,13 @@ import java.io.IOException; import java.io.InputStream; +import java.io.UncheckedIOException; 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.HashMap; -import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -18,6 +17,7 @@ import java.util.Set; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicLong; +import java.util.function.Consumer; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -26,14 +26,19 @@ import io.quarkus.bootstrap.app.CuratedApplication; import io.quarkus.bootstrap.app.QuarkusBootstrap; +import io.quarkus.bootstrap.app.QuarkusBootstrap.Mode; +import io.quarkus.bootstrap.classloading.ClassPathElement; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; +import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.deployment.dev.ClassScanResult; import io.quarkus.deployment.dev.CompilationProvider; import io.quarkus.deployment.dev.DevModeContext; +import io.quarkus.deployment.dev.DevModeContext.ModuleInfo; import io.quarkus.deployment.dev.QuarkusCompiler; import io.quarkus.deployment.dev.RuntimeUpdatesProcessor; import io.quarkus.dev.spi.DevModeType; import io.quarkus.dev.testing.TestWatchedFiles; +import io.quarkus.maven.dependency.ResolvedDependency; import io.quarkus.paths.PathList; import io.quarkus.runtime.configuration.HyphenateEnumConverter; @@ -143,46 +148,36 @@ public void start() { } } + private static Pattern getCompiledPatternOrNull(Optional patternStr) { + return patternStr.isPresent() ? Pattern.compile(patternStr.get()) : null; + } + public void init() { if (moduleRunners.isEmpty()) { TestWatchedFiles.setWatchedFilesListener((s) -> RuntimeUpdatesProcessor.INSTANCE.setWatchedFilePaths(s, true)); + final Pattern includeModulePattern = getCompiledPatternOrNull(config.includeModulePattern); + final Pattern excludeModulePattern = getCompiledPatternOrNull(config.excludeModulePattern); for (var module : context.getAllModules()) { - boolean mainModule = module == context.getApplicationRoot(); + final boolean mainModule = module == context.getApplicationRoot(); if (config.onlyTestApplicationModule && !mainModule) { continue; - } else if (config.includeModulePattern.isPresent()) { - Pattern p = Pattern.compile(config.includeModulePattern.get()); - if (!p.matcher(module.getArtifactKey().getGroupId() + ":" + module.getArtifactKey().getArtifactId()) + } else if (includeModulePattern != null) { + if (!includeModulePattern + .matcher(module.getArtifactKey().getGroupId() + ":" + module.getArtifactKey().getArtifactId()) .matches()) { continue; } - } else if (config.excludeModulePattern.isPresent()) { - Pattern p = Pattern.compile(config.excludeModulePattern.get()); - if (p.matcher(module.getArtifactKey().getGroupId() + ":" + module.getArtifactKey().getArtifactId()) + } else if (excludeModulePattern != null) { + if (excludeModulePattern + .matcher(module.getArtifactKey().getGroupId() + ":" + module.getArtifactKey().getArtifactId()) .matches()) { continue; } } try { - Set paths = new LinkedHashSet<>(); - module.getTest().ifPresent(test -> { - paths.add(Paths.get(test.getClassesPath())); - if (test.getResourcesOutputPath() != null) { - paths.add(Paths.get(test.getResourcesOutputPath())); - } - }); - if (mainModule) { - curatedApplication.getQuarkusBootstrap().getApplicationRoot().forEach(paths::add); - } else { - paths.add(Paths.get(module.getMain().getClassesPath())); - } - for (var i : paths) { - if (!Files.exists(i)) { - Files.createDirectories(i); - } - } - QuarkusBootstrap.Builder builder = curatedApplication.getQuarkusBootstrap().clonedBuilder() + final Path projectDir = Path.of(module.getProjectDirectory()); + final QuarkusBootstrap.Builder bootstrapConfig = curatedApplication.getQuarkusBootstrap().clonedBuilder() .setMode(QuarkusBootstrap.Mode.TEST) .setAssertionsEnabled(true) .setDisableClasspathCache(false) @@ -192,20 +187,62 @@ public void init() { .setTest(true) .setAuxiliaryApplication(true) .setHostApplicationIsTestOnly(devModeType == DevModeType.TEST_ONLY) - .setProjectRoot(Paths.get(module.getProjectDirectory())) - .setApplicationRoot(PathList.from(paths)) + .setProjectRoot(projectDir) + .setApplicationRoot(getRootPaths(module, mainModule)) .clearLocalArtifacts(); + + final QuarkusClassLoader ctParentFirstCl; + final Mode currentMode = curatedApplication.getQuarkusBootstrap().getMode(); + // in case of quarkus:test the application model will already include test dependencies + if (Mode.CONTINUOUS_TEST != currentMode && Mode.TEST != currentMode) { + // In this case the current application model does not include test dependencies. + // 1) we resolve an application model for test mode; + // 2) we create a new CT base classloader that includes parent-first test scoped dependencies + // so that they are not loaded by augment and base runtime classloaders. + var appModelFactory = curatedApplication.getQuarkusBootstrap().newAppModelFactory(); + appModelFactory.setBootstrapAppModelResolver(null); + appModelFactory.setTest(true); + appModelFactory.setLocalArtifacts(Set.of()); + if (!mainModule) { + appModelFactory.setAppArtifact(null); + appModelFactory.setProjectRoot(projectDir); + } + final ApplicationModel testModel = appModelFactory.resolveAppModel().getApplicationModel(); + bootstrapConfig.setExistingModel(testModel); + + QuarkusClassLoader.Builder clBuilder = null; + var currentParentFirst = curatedApplication.getApplicationModel().getParentFirst(); + for (ResolvedDependency d : testModel.getDependencies()) { + if (d.isClassLoaderParentFirst() && !currentParentFirst.contains(d.getKey())) { + if (clBuilder == null) { + clBuilder = QuarkusClassLoader.builder("Continuous Testing Parent-First", + getClass().getClassLoader().getParent(), false); + } + clBuilder.addElement(ClassPathElement.fromDependency(d)); + } + } + + ctParentFirstCl = clBuilder == null ? null : clBuilder.build(); + if (ctParentFirstCl != null) { + bootstrapConfig.setBaseClassLoader(ctParentFirstCl); + } + } else { + ctParentFirstCl = null; + if (mainModule) { + // the model and the app classloader already include test scoped dependencies + bootstrapConfig.setExistingModel(curatedApplication.getApplicationModel()); + } + } + //we always want to propagate parent first //so it is consistent. Some modules may not have quarkus dependencies //so they won't load junit parent first without this for (var i : curatedApplication.getApplicationModel().getDependencies()) { if (i.isClassLoaderParentFirst()) { - builder.addParentFirstArtifact(i.getKey()); + bootstrapConfig.addParentFirstArtifact(i.getKey()); } } - var testCuratedApplication = builder // we want to re-discover the local dependencies with test scope - .build() - .bootstrap(); + var testCuratedApplication = bootstrapConfig.build().bootstrap(); if (mainModule) { //horrible hack //we really need a compiler per module but we are not setup for this yet @@ -215,7 +252,7 @@ public void init() { //has complained much compiler = new QuarkusCompiler(testCuratedApplication, compilationProviders, context); } - var testRunner = new ModuleTestRunner(this, context, testCuratedApplication, module); + var testRunner = new ModuleTestRunner(this, testCuratedApplication, module); QuarkusClassLoader cl = (QuarkusClassLoader) getClass().getClassLoader(); cl.addCloseTask(new Runnable() { @Override @@ -224,6 +261,9 @@ public void run() { close(); } finally { testCuratedApplication.close(); + if (ctParentFirstCl != null) { + ctParentFirstCl.close(); + } } } }); @@ -235,6 +275,37 @@ public void run() { } } + private PathList getRootPaths(ModuleInfo module, final boolean mainModule) { + final PathList.Builder pathBuilder = PathList.builder(); + final Consumer paths = new Consumer<>() { + @Override + public void accept(Path t) { + if (!pathBuilder.contains(t)) { + if (!Files.exists(t)) { + try { + Files.createDirectories(t); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + pathBuilder.add(t); + } + } + }; + module.getTest().ifPresent(test -> { + paths.accept(Path.of(test.getClassesPath())); + if (test.getResourcesOutputPath() != null) { + paths.accept(Path.of(test.getResourcesOutputPath())); + } + }); + if (mainModule) { + curatedApplication.getQuarkusBootstrap().getApplicationRoot().forEach(paths::accept); + } else { + paths.accept(Path.of(module.getMain().getClassesPath())); + } + return pathBuilder.build(); + } + public synchronized void close() { closed = true; stop(); @@ -522,10 +593,6 @@ public boolean isStarted() { return started; } - public CuratedApplication getCuratedApplication() { - return curatedApplication; - } - public QuarkusCompiler getCompiler() { return compiler; } diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/BootstrapAppModelFactory.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/BootstrapAppModelFactory.java index 410640b31a9e7..6c068b880292c 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/BootstrapAppModelFactory.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/BootstrapAppModelFactory.java @@ -11,12 +11,10 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; -import org.apache.maven.model.Dependency; import org.jboss.logging.Logger; import io.quarkus.bootstrap.app.CurationResult; @@ -74,9 +72,9 @@ public static BootstrapAppModelFactory newInstance() { private MavenArtifactResolver mavenArtifactResolver; private BootstrapMavenContext mvnContext; - Set reloadableModules = Collections.emptySet(); + Set reloadableModules = Set.of(); - private Collection forcedDependencies = Collections.emptyList(); + private Collection forcedDependencies = List.of(); private BootstrapAppModelFactory() { } @@ -306,9 +304,23 @@ private boolean isWorkspaceDiscoveryEnabled() { } private LocalProject loadWorkspace() throws AppModelResolverException { - return projectRoot == null || !Files.isDirectory(projectRoot) - ? null - : createBootstrapMavenContext().getCurrentProject(); + if (projectRoot == null || !Files.isDirectory(projectRoot)) { + return null; + } + LocalProject project = createBootstrapMavenContext().getCurrentProject(); + if (project == null) { + return null; + } + if (project.getDir().equals(projectRoot)) { + return project; + } + for (LocalProject p : project.getWorkspace().getProjects().values()) { + if (p.getDir().equals(projectRoot)) { + return p; + } + } + log.warnf("Expected project directory %s does not match current project directory %s", projectRoot, project.getDir()); + return project; } private CurationResult createAppModelForJar(Path appArtifactPath) { @@ -321,7 +333,7 @@ private CurationResult createAppModelForJar(Path appArtifactPath) { } modelResolver.relink(appArtifact, appArtifactPath); //we need some way to figure out dependencies here - appModel = modelResolver.resolveManagedModel(appArtifact, Collections.emptyList(), managingProject, + appModel = modelResolver.resolveManagedModel(appArtifact, List.of(), managingProject, reloadableModules); } catch (AppModelResolverException | IOException e) { throw new RuntimeException("Failed to resolve initial application dependencies", e); diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java index 3757ed11b36e3..ea8d53f0c959a 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/QuarkusBootstrap.java @@ -128,19 +128,33 @@ private QuarkusBootstrap(Builder builder) { public CuratedApplication bootstrap() throws BootstrapException { //all we want to do is resolve all our dependencies //once we have this it is up to augment to set up the class loader to actually use them + final CurationResult curationResult = existingModel != null + ? new CurationResult(existingModel) + : newAppModelFactory().resolveAppModel(); - if (existingModel != null) { - final ConfiguredClassLoading classLoadingConfig = ConfiguredClassLoading.builder() - .setApplicationRoot(applicationRoot) - .setDefaultFlatTestClassPath(defaultFlatTestClassPath) - .setMode(mode) - .addParentFirstArtifacts(parentFirstArtifacts) - .setApplicationModel(existingModel) - .build(); - return new CuratedApplication(this, new CurationResult(existingModel), classLoadingConfig); + if (curationResult.getApplicationModel().getAppArtifact() != null) { + if (curationResult.getApplicationModel().getAppArtifact().getArtifactId() != null) { + buildSystemProperties.putIfAbsent("quarkus.application.name", + curationResult.getApplicationModel().getAppArtifact().getArtifactId()); + } + if (curationResult.getApplicationModel().getAppArtifact().getVersion() != null) { + buildSystemProperties.putIfAbsent("quarkus.application.version", + curationResult.getApplicationModel().getAppArtifact().getVersion()); + } } - BootstrapAppModelFactory appModelFactory = BootstrapAppModelFactory.newInstance() + final ConfiguredClassLoading classLoadingConfig = ConfiguredClassLoading.builder() + .setApplicationRoot(applicationRoot) + .setDefaultFlatTestClassPath(defaultFlatTestClassPath) + .setMode(mode) + .addParentFirstArtifacts(parentFirstArtifacts) + .setApplicationModel(curationResult.getApplicationModel()) + .build(); + return new CuratedApplication(this, curationResult, classLoadingConfig); + } + + public BootstrapAppModelFactory newAppModelFactory() { + final BootstrapAppModelFactory appModelFactory = BootstrapAppModelFactory.newInstance() .setOffline(offline) .setMavenArtifactResolver(mavenArtifactResolver) .setBootstrapAppModelResolver(appModelResolver) @@ -149,7 +163,7 @@ public CuratedApplication bootstrap() throws BootstrapException { .setManagingProject(managingProject) .setForcedDependencies(forcedDependencies) .setLocalArtifacts(localArtifacts) - .setProjectRoot(getProjectRoot()); + .setProjectRoot(projectRoot); if (mode == Mode.TEST || test) { appModelFactory.setTest(true); if (!disableClasspathCache) { @@ -162,30 +176,7 @@ public CuratedApplication bootstrap() throws BootstrapException { appModelFactory.setEnableClasspathCache(true); } } - CurationResult curationResult = appModelFactory.resolveAppModel(); - if (curationResult.getApplicationModel().getAppArtifact() != null) { - if (curationResult.getApplicationModel().getAppArtifact().getArtifactId() != null) { - buildSystemProperties.putIfAbsent("quarkus.application.name", - curationResult.getApplicationModel().getAppArtifact().getArtifactId()); - } - if (curationResult.getApplicationModel().getAppArtifact().getVersion() != null) { - buildSystemProperties.putIfAbsent("quarkus.application.version", - curationResult.getApplicationModel().getAppArtifact().getVersion()); - } - } - - final ConfiguredClassLoading classLoadingConfig = ConfiguredClassLoading.builder() - .setApplicationRoot(applicationRoot) - .setDefaultFlatTestClassPath(defaultFlatTestClassPath) - .setMode(mode) - .addParentFirstArtifacts(parentFirstArtifacts) - .setApplicationModel(curationResult.getApplicationModel()) - .build(); - return new CuratedApplication(this, curationResult, classLoadingConfig); - } - - public AppModelResolver getAppModelResolver() { - return appModelResolver; + return appModelFactory; } public PathCollection getApplicationRoot() { diff --git a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java index 9ab4fd658eb7e..c700acb94b16a 100644 --- a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java +++ b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java @@ -15,7 +15,6 @@ import java.net.URL; 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; @@ -599,16 +598,15 @@ public boolean test(String s) { + " other beans may also be removed and injection may not work as expected"); } - final Path testLocation = PathTestHelper.getTestClassesLocation(testClass); - try { + final Path testLocation = PathTestHelper.getTestClassesLocation(testClass); + final Path projectDir = Path.of("").normalize().toAbsolutePath(); QuarkusBootstrap.Builder builder = QuarkusBootstrap.builder() .setApplicationRoot(deploymentDir.resolve(APP_ROOT)) .setMode(QuarkusBootstrap.Mode.TEST) .addExcludedPath(testLocation) - .setProjectRoot(testLocation) - .setTargetDirectory( - PathTestHelper.getProjectBuildDir(Paths.get("").normalize().toAbsolutePath(), testLocation)) + .setProjectRoot(projectDir) + .setTargetDirectory(PathTestHelper.getProjectBuildDir(projectDir, testLocation)) .setFlatClassPath(flatClassPath) .setForcedDependencies(forcedDependencies); for (JavaArchive dependency : additionalDependencies) { diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java index e64a8ace36a76..19506ebfd9ca0 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java @@ -955,7 +955,6 @@ private Object runExtensionMethod(ReflectiveInvocationContext invocation while (theclass.isArray()) { theclass = theclass.getComponentType(); } - String className = theclass.getName(); if (theclass.isPrimitive()) { cloneRequired = false; } else if (TestInfo.class.isAssignableFrom(theclass)) { @@ -963,9 +962,10 @@ private Object runExtensionMethod(ReflectiveInvocationContext invocation Method newTestMethod = info.getTestMethod().isPresent() ? determineTCCLExtensionMethod(info.getTestMethod().get(), testClassFromTCCL) : null; - replacement = new TestInfoImpl(info.getDisplayName(), info.getTags(), Optional.of(testClassFromTCCL), + replacement = new TestInfoImpl(info.getDisplayName(), info.getTags(), + Optional.of(testClassFromTCCL), Optional.ofNullable(newTestMethod)); - } else if (clonePattern.matcher(className).matches()) { + } else if (clonePattern.matcher(theclass.getName()).matches()) { cloneRequired = true; } else { try {