Skip to content

Commit

Permalink
Create a new base classloader including parent-first test scoped depe…
Browse files Browse the repository at this point in the history
…ndencies when bootstrapping for CT
  • Loading branch information
aloubyansky committed Mar 20, 2023
1 parent 016389f commit 0b98893
Show file tree
Hide file tree
Showing 7 changed files with 159 additions and 101 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,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;
Expand Down Expand Up @@ -780,11 +779,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;
Expand Down Expand Up @@ -846,7 +840,6 @@ public Builder setExcludeEngines(List<String> excludeEngines) {
}

public JunitTestRunner build() {
Objects.requireNonNull(devModeContext, "devModeContext");
Objects.requireNonNull(testClassUsages, "testClassUsages");
Objects.requireNonNull(testApplication, "testApplication");
Objects.requireNonNull(testState, "testState");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand All @@ -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;

Expand Down Expand Up @@ -143,46 +148,36 @@ public void start() {
}
}

private static Pattern getCompiledPatternOrNull(Optional<String> 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<Path> 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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -224,6 +261,9 @@ public void run() {
close();
} finally {
testCuratedApplication.close();
if (ctParentFirstCl != null) {
ctParentFirstCl.close();
}
}
}
});
Expand All @@ -235,6 +275,37 @@ public void run() {
}
}

private PathList getRootPaths(ModuleInfo module, final boolean mainModule) {
final PathList.Builder pathBuilder = PathList.builder();
final Consumer<Path> 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();
Expand Down Expand Up @@ -522,10 +593,6 @@ public boolean isStarted() {
return started;
}

public CuratedApplication getCuratedApplication() {
return curatedApplication;
}

public QuarkusCompiler getCompiler() {
return compiler;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -74,9 +72,9 @@ public static BootstrapAppModelFactory newInstance() {
private MavenArtifactResolver mavenArtifactResolver;

private BootstrapMavenContext mvnContext;
Set<ArtifactKey> reloadableModules = Collections.emptySet();
Set<ArtifactKey> reloadableModules = Set.of();

private Collection<io.quarkus.maven.dependency.Dependency> forcedDependencies = Collections.emptyList();
private Collection<io.quarkus.maven.dependency.Dependency> forcedDependencies = List.of();

private BootstrapAppModelFactory() {
}
Expand Down Expand Up @@ -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) {
Expand All @@ -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);
Expand Down
Loading

0 comments on commit 0b98893

Please sign in to comment.