Skip to content

Commit 18e4136

Browse files
committed
Create a new base classloader including parent-first test scoped dependencies when bootstrapping for CT
1 parent 0da3540 commit 18e4136

File tree

7 files changed

+159
-101
lines changed

7 files changed

+159
-101
lines changed

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

-7
Original file line numberDiff line numberDiff line change
@@ -751,7 +751,6 @@ static class Builder {
751751
private TestType testType = TestType.ALL;
752752
private TestState testState;
753753
private long runId = -1;
754-
private DevModeContext devModeContext;
755754
private CuratedApplication testApplication;
756755
private ClassScanResult classScanResult;
757756
private TestClassUsages testClassUsages;
@@ -780,11 +779,6 @@ public Builder setTestType(TestType testType) {
780779
return this;
781780
}
782781

783-
public Builder setDevModeContext(DevModeContext devModeContext) {
784-
this.devModeContext = devModeContext;
785-
return this;
786-
}
787-
788782
public Builder setTestApplication(CuratedApplication testApplication) {
789783
this.testApplication = testApplication;
790784
return this;
@@ -846,7 +840,6 @@ public Builder setExcludeEngines(List<String> excludeEngines) {
846840
}
847841

848842
public JunitTestRunner build() {
849-
Objects.requireNonNull(devModeContext, "devModeContext");
850843
Objects.requireNonNull(testClassUsages, "testClassUsages");
851844
Objects.requireNonNull(testApplication, "testApplication");
852845
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

+105-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,62 @@ 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.setBootstrapAppModelResolver(null);
204+
appModelFactory.setTest(true);
205+
appModelFactory.setLocalArtifacts(Set.of());
206+
if (!mainModule) {
207+
appModelFactory.setAppArtifact(null);
208+
appModelFactory.setProjectRoot(projectDir);
209+
}
210+
final ApplicationModel testModel = appModelFactory.resolveAppModel().getApplicationModel();
211+
bootstrapConfig.setExistingModel(testModel);
212+
213+
QuarkusClassLoader.Builder clBuilder = null;
214+
var currentParentFirst = curatedApplication.getApplicationModel().getParentFirst();
215+
for (ResolvedDependency d : testModel.getDependencies()) {
216+
if (d.isClassLoaderParentFirst() && !currentParentFirst.contains(d.getKey())) {
217+
if (clBuilder == null) {
218+
clBuilder = QuarkusClassLoader.builder("Continuous Testing Parent-First",
219+
getClass().getClassLoader().getParent(), false);
220+
}
221+
clBuilder.addElement(ClassPathElement.fromDependency(d));
222+
}
223+
}
224+
225+
ctParentFirstCl = clBuilder == null ? null : clBuilder.build();
226+
if (ctParentFirstCl != null) {
227+
bootstrapConfig.setBaseClassLoader(ctParentFirstCl);
228+
}
229+
} else {
230+
ctParentFirstCl = null;
231+
if (mainModule) {
232+
// the model and the app classloader already include test scoped dependencies
233+
bootstrapConfig.setExistingModel(curatedApplication.getApplicationModel());
234+
}
235+
}
236+
198237
//we always want to propagate parent first
199238
//so it is consistent. Some modules may not have quarkus dependencies
200239
//so they won't load junit parent first without this
201240
for (var i : curatedApplication.getApplicationModel().getDependencies()) {
202241
if (i.isClassLoaderParentFirst()) {
203-
builder.addParentFirstArtifact(i.getKey());
242+
bootstrapConfig.addParentFirstArtifact(i.getKey());
204243
}
205244
}
206-
var testCuratedApplication = builder // we want to re-discover the local dependencies with test scope
207-
.build()
208-
.bootstrap();
245+
var testCuratedApplication = bootstrapConfig.build().bootstrap();
209246
if (mainModule) {
210247
//horrible hack
211248
//we really need a compiler per module but we are not setup for this yet
@@ -215,7 +252,7 @@ public void init() {
215252
//has complained much
216253
compiler = new QuarkusCompiler(testCuratedApplication, compilationProviders, context);
217254
}
218-
var testRunner = new ModuleTestRunner(this, context, testCuratedApplication, module);
255+
var testRunner = new ModuleTestRunner(this, testCuratedApplication, module);
219256
QuarkusClassLoader cl = (QuarkusClassLoader) getClass().getClassLoader();
220257
cl.addCloseTask(new Runnable() {
221258
@Override
@@ -224,6 +261,9 @@ public void run() {
224261
close();
225262
} finally {
226263
testCuratedApplication.close();
264+
if (ctParentFirstCl != null) {
265+
ctParentFirstCl.close();
266+
}
227267
}
228268
}
229269
});
@@ -235,6 +275,37 @@ public void run() {
235275
}
236276
}
237277

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

525-
public CuratedApplication getCuratedApplication() {
526-
return curatedApplication;
527-
}
528-
529596
public QuarkusCompiler getCompiler() {
530597
return compiler;
531598
}

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

+20-8
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
}
@@ -306,9 +304,23 @@ private boolean isWorkspaceDiscoveryEnabled() {
306304
}
307305

308306
private LocalProject loadWorkspace() throws AppModelResolverException {
309-
return projectRoot == null || !Files.isDirectory(projectRoot)
310-
? null
311-
: createBootstrapMavenContext().getCurrentProject();
307+
if (projectRoot == null || !Files.isDirectory(projectRoot)) {
308+
return null;
309+
}
310+
LocalProject project = createBootstrapMavenContext().getCurrentProject();
311+
if (project == null) {
312+
return null;
313+
}
314+
if (project.getDir().equals(projectRoot)) {
315+
return project;
316+
}
317+
for (LocalProject p : project.getWorkspace().getProjects().values()) {
318+
if (p.getDir().equals(projectRoot)) {
319+
return p;
320+
}
321+
}
322+
log.warnf("Expected project directory %s does not match current project directory %s", projectRoot, project.getDir());
323+
return project;
312324
}
313325

314326
private CurationResult createAppModelForJar(Path appArtifactPath) {
@@ -321,7 +333,7 @@ private CurationResult createAppModelForJar(Path appArtifactPath) {
321333
}
322334
modelResolver.relink(appArtifact, appArtifactPath);
323335
//we need some way to figure out dependencies here
324-
appModel = modelResolver.resolveManagedModel(appArtifact, Collections.emptyList(), managingProject,
336+
appModel = modelResolver.resolveManagedModel(appArtifact, List.of(), managingProject,
325337
reloadableModules);
326338
} catch (AppModelResolverException | IOException e) {
327339
throw new RuntimeException("Failed to resolve initial application dependencies", e);

0 commit comments

Comments
 (0)