Skip to content

Commit ac38d01

Browse files
committed
Create a new base classloader including parent-first test scoped dependencies when bootstrapping for CT
1 parent 8ee7a2e commit ac38d01

File tree

6 files changed

+155
-101
lines changed

6 files changed

+155
-101
lines changed

core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java

-7
Original file line numberDiff line numberDiff line change
@@ -754,7 +754,6 @@ static class Builder {
754754
private TestType testType = TestType.ALL;
755755
private TestState testState;
756756
private long runId = -1;
757-
private DevModeContext devModeContext;
758757
private CuratedApplication testApplication;
759758
private ClassScanResult classScanResult;
760759
private TestClassUsages testClassUsages;
@@ -783,11 +782,6 @@ public Builder setTestType(TestType testType) {
783782
return this;
784783
}
785784

786-
public Builder setDevModeContext(DevModeContext devModeContext) {
787-
this.devModeContext = devModeContext;
788-
return this;
789-
}
790-
791785
public Builder setTestApplication(CuratedApplication testApplication) {
792786
this.testApplication = testApplication;
793787
return this;
@@ -849,7 +843,6 @@ public Builder setExcludeEngines(List<String> excludeEngines) {
849843
}
850844

851845
public JunitTestRunner build() {
852-
Objects.requireNonNull(devModeContext, "devModeContext");
853846
Objects.requireNonNull(testClassUsages, "testClassUsages");
854847
Objects.requireNonNull(testApplication, "testApplication");
855848
Objects.requireNonNull(testState, "testState");

core/deployment/src/main/java/io/quarkus/deployment/dev/testing/ModuleTestRunner.java

+1-4
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,15 @@ public class ModuleTestRunner {
1717

1818
final TestState testState = new TestState();
1919
private final TestSupport testSupport;
20-
private final DevModeContext devModeContext;
2120
private final CuratedApplication testApplication;
2221
private final DevModeContext.ModuleInfo moduleInfo;
2322

2423
private final TestClassUsages testClassUsages = new TestClassUsages();
2524
private JunitTestRunner runner;
2625

27-
public ModuleTestRunner(TestSupport testSupport, DevModeContext devModeContext, CuratedApplication testApplication,
26+
public ModuleTestRunner(TestSupport testSupport, CuratedApplication testApplication,
2827
DevModeContext.ModuleInfo moduleInfo) {
2928
this.testSupport = testSupport;
30-
this.devModeContext = devModeContext;
3129
this.testApplication = testApplication;
3230
this.moduleInfo = moduleInfo;
3331
}
@@ -50,7 +48,6 @@ Runnable prepare(ClassScanResult classScanResult, boolean reRunFailures, long ru
5048
}
5149
JunitTestRunner.Builder builder = new JunitTestRunner.Builder()
5250
.setClassScanResult(classScanResult)
53-
.setDevModeContext(devModeContext)
5451
.setRunId(runId)
5552
.setTestState(testState)
5653
.setTestClassUsages(testClassUsages)

core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java

+104-38
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22

33
import java.io.IOException;
44
import java.io.InputStream;
5+
import java.io.UncheckedIOException;
56
import java.nio.file.Files;
67
import java.nio.file.Path;
7-
import java.nio.file.Paths;
88
import java.util.ArrayList;
99
import java.util.Arrays;
1010
import java.util.Collections;
1111
import java.util.HashMap;
12-
import java.util.LinkedHashSet;
1312
import java.util.List;
1413
import java.util.Map;
1514
import java.util.Objects;
@@ -18,6 +17,7 @@
1817
import java.util.Set;
1918
import java.util.concurrent.CopyOnWriteArrayList;
2019
import java.util.concurrent.atomic.AtomicLong;
20+
import java.util.function.Consumer;
2121
import java.util.regex.Pattern;
2222
import java.util.stream.Collectors;
2323

@@ -26,14 +26,19 @@
2626

2727
import io.quarkus.bootstrap.app.CuratedApplication;
2828
import io.quarkus.bootstrap.app.QuarkusBootstrap;
29+
import io.quarkus.bootstrap.app.QuarkusBootstrap.Mode;
30+
import io.quarkus.bootstrap.classloading.ClassPathElement;
2931
import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
32+
import io.quarkus.bootstrap.model.ApplicationModel;
3033
import io.quarkus.deployment.dev.ClassScanResult;
3134
import io.quarkus.deployment.dev.CompilationProvider;
3235
import io.quarkus.deployment.dev.DevModeContext;
36+
import io.quarkus.deployment.dev.DevModeContext.ModuleInfo;
3337
import io.quarkus.deployment.dev.QuarkusCompiler;
3438
import io.quarkus.deployment.dev.RuntimeUpdatesProcessor;
3539
import io.quarkus.dev.spi.DevModeType;
3640
import io.quarkus.dev.testing.TestWatchedFiles;
41+
import io.quarkus.maven.dependency.ResolvedDependency;
3742
import io.quarkus.paths.PathList;
3843
import io.quarkus.runtime.configuration.HyphenateEnumConverter;
3944

@@ -143,46 +148,36 @@ public void start() {
143148
}
144149
}
145150

151+
private static Pattern getCompiledPatternOrNull(Optional<String> patternStr) {
152+
return patternStr.isPresent() ? Pattern.compile(patternStr.get()) : null;
153+
}
154+
146155
public void init() {
147156
if (moduleRunners.isEmpty()) {
148157
TestWatchedFiles.setWatchedFilesListener((s) -> RuntimeUpdatesProcessor.INSTANCE.setWatchedFilePaths(s, true));
158+
final Pattern includeModulePattern = getCompiledPatternOrNull(config.includeModulePattern);
159+
final Pattern excludeModulePattern = getCompiledPatternOrNull(config.excludeModulePattern);
149160
for (var module : context.getAllModules()) {
150-
boolean mainModule = module == context.getApplicationRoot();
161+
final boolean mainModule = module == context.getApplicationRoot();
151162
if (config.onlyTestApplicationModule && !mainModule) {
152163
continue;
153-
} else if (config.includeModulePattern.isPresent()) {
154-
Pattern p = Pattern.compile(config.includeModulePattern.get());
155-
if (!p.matcher(module.getArtifactKey().getGroupId() + ":" + module.getArtifactKey().getArtifactId())
164+
} else if (includeModulePattern != null) {
165+
if (!includeModulePattern
166+
.matcher(module.getArtifactKey().getGroupId() + ":" + module.getArtifactKey().getArtifactId())
156167
.matches()) {
157168
continue;
158169
}
159-
} else if (config.excludeModulePattern.isPresent()) {
160-
Pattern p = Pattern.compile(config.excludeModulePattern.get());
161-
if (p.matcher(module.getArtifactKey().getGroupId() + ":" + module.getArtifactKey().getArtifactId())
170+
} else if (excludeModulePattern != null) {
171+
if (excludeModulePattern
172+
.matcher(module.getArtifactKey().getGroupId() + ":" + module.getArtifactKey().getArtifactId())
162173
.matches()) {
163174
continue;
164175
}
165176
}
166177

167178
try {
168-
Set<Path> paths = new LinkedHashSet<>();
169-
module.getTest().ifPresent(test -> {
170-
paths.add(Paths.get(test.getClassesPath()));
171-
if (test.getResourcesOutputPath() != null) {
172-
paths.add(Paths.get(test.getResourcesOutputPath()));
173-
}
174-
});
175-
if (mainModule) {
176-
curatedApplication.getQuarkusBootstrap().getApplicationRoot().forEach(paths::add);
177-
} else {
178-
paths.add(Paths.get(module.getMain().getClassesPath()));
179-
}
180-
for (var i : paths) {
181-
if (!Files.exists(i)) {
182-
Files.createDirectories(i);
183-
}
184-
}
185-
QuarkusBootstrap.Builder builder = curatedApplication.getQuarkusBootstrap().clonedBuilder()
179+
final Path projectDir = Path.of(module.getProjectDirectory());
180+
final QuarkusBootstrap.Builder bootstrapConfig = curatedApplication.getQuarkusBootstrap().clonedBuilder()
186181
.setMode(QuarkusBootstrap.Mode.TEST)
187182
.setAssertionsEnabled(true)
188183
.setDisableClasspathCache(false)
@@ -192,20 +187,61 @@ public void init() {
192187
.setTest(true)
193188
.setAuxiliaryApplication(true)
194189
.setHostApplicationIsTestOnly(devModeType == DevModeType.TEST_ONLY)
195-
.setProjectRoot(Paths.get(module.getProjectDirectory()))
196-
.setApplicationRoot(PathList.from(paths))
190+
.setProjectRoot(projectDir)
191+
.setApplicationRoot(getRootPaths(module, mainModule))
197192
.clearLocalArtifacts();
193+
194+
final QuarkusClassLoader ctParentFirstCl;
195+
final Mode currentMode = curatedApplication.getQuarkusBootstrap().getMode();
196+
// in case of quarkus:test the application model will already include test dependencies
197+
if (Mode.CONTINUOUS_TEST != currentMode && Mode.TEST != currentMode) {
198+
// In this case the current application model does not include test dependencies.
199+
// 1) we resolve an application model for test mode;
200+
// 2) we create a new CT base classloader that includes parent-first test scoped dependencies
201+
// so that they are not loaded by augment and base runtime classloaders.
202+
var appModelFactory = curatedApplication.getQuarkusBootstrap().newAppModelFactory();
203+
appModelFactory.setTest(true);
204+
appModelFactory.setLocalArtifacts(Set.of());
205+
if (!mainModule) {
206+
appModelFactory.setAppArtifact(null);
207+
appModelFactory.setProjectRoot(projectDir);
208+
}
209+
final ApplicationModel testModel = appModelFactory.resolveAppModel().getApplicationModel();
210+
bootstrapConfig.setExistingModel(testModel);
211+
212+
QuarkusClassLoader.Builder clBuilder = null;
213+
var currentParentFirst = curatedApplication.getApplicationModel().getParentFirst();
214+
for (ResolvedDependency d : testModel.getDependencies()) {
215+
if (d.isClassLoaderParentFirst() && !currentParentFirst.contains(d.getKey())) {
216+
if (clBuilder == null) {
217+
clBuilder = QuarkusClassLoader.builder("Continuous Testing Parent-First",
218+
getClass().getClassLoader().getParent(), false);
219+
}
220+
clBuilder.addElement(ClassPathElement.fromDependency(d));
221+
}
222+
}
223+
224+
ctParentFirstCl = clBuilder == null ? null : clBuilder.build();
225+
if (ctParentFirstCl != null) {
226+
bootstrapConfig.setBaseClassLoader(ctParentFirstCl);
227+
}
228+
} else {
229+
ctParentFirstCl = null;
230+
if (mainModule) {
231+
// the model and the app classloader already include test scoped dependencies
232+
bootstrapConfig.setExistingModel(curatedApplication.getApplicationModel());
233+
}
234+
}
235+
198236
//we always want to propagate parent first
199237
//so it is consistent. Some modules may not have quarkus dependencies
200238
//so they won't load junit parent first without this
201239
for (var i : curatedApplication.getApplicationModel().getDependencies()) {
202240
if (i.isClassLoaderParentFirst()) {
203-
builder.addParentFirstArtifact(i.getKey());
241+
bootstrapConfig.addParentFirstArtifact(i.getKey());
204242
}
205243
}
206-
var testCuratedApplication = builder // we want to re-discover the local dependencies with test scope
207-
.build()
208-
.bootstrap();
244+
var testCuratedApplication = bootstrapConfig.build().bootstrap();
209245
if (mainModule) {
210246
//horrible hack
211247
//we really need a compiler per module but we are not setup for this yet
@@ -215,7 +251,7 @@ public void init() {
215251
//has complained much
216252
compiler = new QuarkusCompiler(testCuratedApplication, compilationProviders, context);
217253
}
218-
var testRunner = new ModuleTestRunner(this, context, testCuratedApplication, module);
254+
var testRunner = new ModuleTestRunner(this, testCuratedApplication, module);
219255
QuarkusClassLoader cl = (QuarkusClassLoader) getClass().getClassLoader();
220256
cl.addCloseTask(new Runnable() {
221257
@Override
@@ -224,6 +260,9 @@ public void run() {
224260
close();
225261
} finally {
226262
testCuratedApplication.close();
263+
if (ctParentFirstCl != null) {
264+
ctParentFirstCl.close();
265+
}
227266
}
228267
}
229268
});
@@ -235,6 +274,37 @@ public void run() {
235274
}
236275
}
237276

277+
private PathList getRootPaths(ModuleInfo module, final boolean mainModule) {
278+
final PathList.Builder pathBuilder = PathList.builder();
279+
final Consumer<Path> paths = new Consumer<>() {
280+
@Override
281+
public void accept(Path t) {
282+
if (!pathBuilder.contains(t)) {
283+
if (!Files.exists(t)) {
284+
try {
285+
Files.createDirectories(t);
286+
} catch (IOException e) {
287+
throw new UncheckedIOException(e);
288+
}
289+
}
290+
pathBuilder.add(t);
291+
}
292+
}
293+
};
294+
module.getTest().ifPresent(test -> {
295+
paths.accept(Path.of(test.getClassesPath()));
296+
if (test.getResourcesOutputPath() != null) {
297+
paths.accept(Path.of(test.getResourcesOutputPath()));
298+
}
299+
});
300+
if (mainModule) {
301+
curatedApplication.getQuarkusBootstrap().getApplicationRoot().forEach(paths::accept);
302+
} else {
303+
paths.accept(Path.of(module.getMain().getClassesPath()));
304+
}
305+
return pathBuilder.build();
306+
}
307+
238308
public synchronized void close() {
239309
closed = true;
240310
stop();
@@ -522,10 +592,6 @@ public boolean isStarted() {
522592
return started;
523593
}
524594

525-
public CuratedApplication getCuratedApplication() {
526-
return curatedApplication;
527-
}
528-
529595
public QuarkusCompiler getCompiler() {
530596
return compiler;
531597
}

independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/BootstrapAppModelFactory.java

+20-13
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,10 @@
1111
import java.nio.file.Paths;
1212
import java.util.ArrayList;
1313
import java.util.Collection;
14-
import java.util.Collections;
1514
import java.util.HashSet;
1615
import java.util.List;
1716
import java.util.Set;
1817

19-
import org.apache.maven.model.Dependency;
2018
import org.jboss.logging.Logger;
2119

2220
import io.quarkus.bootstrap.app.CurationResult;
@@ -74,9 +72,9 @@ public static BootstrapAppModelFactory newInstance() {
7472
private MavenArtifactResolver mavenArtifactResolver;
7573

7674
private BootstrapMavenContext mvnContext;
77-
Set<ArtifactKey> reloadableModules = Collections.emptySet();
75+
Set<ArtifactKey> reloadableModules = Set.of();
7876

79-
private Collection<io.quarkus.maven.dependency.Dependency> forcedDependencies = Collections.emptyList();
77+
private Collection<io.quarkus.maven.dependency.Dependency> forcedDependencies = List.of();
8078

8179
private BootstrapAppModelFactory() {
8280
}
@@ -121,11 +119,6 @@ public BootstrapAppModelFactory setEnableClasspathCache(boolean enable) {
121119
return this;
122120
}
123121

124-
public BootstrapAppModelFactory setBootstrapAppModelResolver(AppModelResolver bootstrapAppModelResolver) {
125-
this.bootstrapAppModelResolver = bootstrapAppModelResolver;
126-
return this;
127-
}
128-
129122
public BootstrapAppModelFactory setAppArtifact(ResolvedDependency appArtifact) {
130123
this.appArtifact = appArtifact;
131124
return this;
@@ -306,9 +299,23 @@ private boolean isWorkspaceDiscoveryEnabled() {
306299
}
307300

308301
private LocalProject loadWorkspace() throws AppModelResolverException {
309-
return projectRoot == null || !Files.isDirectory(projectRoot)
310-
? null
311-
: createBootstrapMavenContext().getCurrentProject();
302+
if (projectRoot == null || !Files.isDirectory(projectRoot)) {
303+
return null;
304+
}
305+
LocalProject project = createBootstrapMavenContext().getCurrentProject();
306+
if (project == null) {
307+
return null;
308+
}
309+
if (project.getDir().equals(projectRoot)) {
310+
return project;
311+
}
312+
for (LocalProject p : project.getWorkspace().getProjects().values()) {
313+
if (p.getDir().equals(projectRoot)) {
314+
return p;
315+
}
316+
}
317+
log.warnf("Expected project directory %s does not match current project directory %s", projectRoot, project.getDir());
318+
return project;
312319
}
313320

314321
private CurationResult createAppModelForJar(Path appArtifactPath) {
@@ -321,7 +328,7 @@ private CurationResult createAppModelForJar(Path appArtifactPath) {
321328
}
322329
modelResolver.relink(appArtifact, appArtifactPath);
323330
//we need some way to figure out dependencies here
324-
appModel = modelResolver.resolveManagedModel(appArtifact, Collections.emptyList(), managingProject,
331+
appModel = modelResolver.resolveManagedModel(appArtifact, List.of(), managingProject,
325332
reloadableModules);
326333
} catch (AppModelResolverException | IOException e) {
327334
throw new RuntimeException("Failed to resolve initial application dependencies", e);

0 commit comments

Comments
 (0)