From 074f1b2dbb66fd19c8bb59a1e8e1b67e2842f4b2 Mon Sep 17 00:00:00 2001 From: Alexey Loubyansky Date: Thu, 28 Nov 2024 23:53:52 +0100 Subject: [PATCH] More appropriate test application module initialization in QuarkusDevModeTest --- .../quarkus/deployment/dev/DevModeMain.java | 106 +++++++---- .../test/DevModeTestApplicationModel.java | 92 +++++++++ .../io/quarkus/test/QuarkusDevModeTest.java | 176 ++++++++++-------- 3 files changed, 258 insertions(+), 116 deletions(-) create mode 100644 test-framework/junit5-internal/src/main/java/io/quarkus/test/DevModeTestApplicationModel.java 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 6377b9c3b908a5..fe861b0f35aafe 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 @@ -10,6 +10,7 @@ import java.io.ObjectInputStream; import java.io.PrintStream; import java.net.URI; +import java.net.URISyntaxException; import java.net.URL; import java.nio.file.FileSystemException; import java.nio.file.Files; @@ -24,6 +25,7 @@ import io.quarkus.bootstrap.app.CuratedApplication; import io.quarkus.bootstrap.app.QuarkusBootstrap; +import io.quarkus.bootstrap.model.ApplicationModel; import io.quarkus.deployment.util.ProcessUtil; import io.quarkus.dev.appstate.ApplicationStateNotification; import io.quarkus.dev.spi.DevModeType; @@ -39,12 +41,18 @@ public class DevModeMain implements Closeable { private static final Logger log = Logger.getLogger(DevModeMain.class); private final DevModeContext context; + private final ApplicationModel appModel; private volatile CuratedApplication curatedApplication; private Closeable realCloseable; public DevModeMain(DevModeContext context) { + this(context, null); + } + + public DevModeMain(DevModeContext context, ApplicationModel appModel) { this.context = context; + this.appModel = appModel; } public static void main(String... args) throws Exception { @@ -65,47 +73,15 @@ public static void main(String... args) throws Exception { public void start() throws Exception { //propagate system props - for (Map.Entry i : context.getSystemProperties().entrySet()) { - if (!System.getProperties().containsKey(i.getKey())) { - System.setProperty(i.getKey(), i.getValue()); - } - } + propagateSystemProperties(); try { - URL thisArchive = getClass().getResource(DevModeMain.class.getSimpleName() + ".class"); - int endIndex = thisArchive.getPath().indexOf("!"); - Path path; - if (endIndex != -1) { - path = Paths.get(new URI(thisArchive.getPath().substring(0, endIndex))); - } else { - path = Paths.get(thisArchive.toURI()); - path = path.getParent(); - for (char i : DevModeMain.class.getName().toCharArray()) { - if (i == '.') { - path = path.getParent(); - } - } - } - final PathList.Builder appRootsBuilder = PathList.builder(); - final Path classesPath = Path.of(context.getApplicationRoot().getMain().getClassesPath()); - if (Files.exists(classesPath)) { - appRootsBuilder.add(classesPath); - } - if (context.getApplicationRoot().getMain().getResourcesOutputPath() != null - && !context.getApplicationRoot().getMain().getResourcesOutputPath() - .equals(context.getApplicationRoot().getMain().getClassesPath())) { - final Path resourcesOutputPath = Paths.get(context.getApplicationRoot().getMain().getResourcesOutputPath()); - if (Files.exists(resourcesOutputPath)) { - appRootsBuilder.add(resourcesOutputPath); - } - } - - final PathList appRoots = appRootsBuilder.build(); QuarkusBootstrap.Builder bootstrapBuilder = QuarkusBootstrap.builder() - .setApplicationRoot(appRoots) + .setApplicationRoot(getApplicationBuildDirs()) + .setExistingModel(appModel) .setIsolateDeployment(true) .setLocalProjectDiscovery(context.isLocalProjectDiscovery()) - .addAdditionalDeploymentArchive(path) + .addAdditionalDeploymentArchive(getThisClassOrigin()) .setBaseName(context.getBaseName()) .setMode(context.getMode()); if (context.getDevModeRunnerJarFile() != null) { @@ -139,6 +115,64 @@ public void start() throws Exception { } } + private PathList getApplicationBuildDirs() { + final String classesDir = context.getApplicationRoot().getMain().getClassesPath(); + final String resourcesOutputDir = context.getApplicationRoot().getMain().getResourcesOutputPath(); + if (resourcesOutputDir == null || resourcesOutputDir.equals(classesDir)) { + return toListOfExistingOrEmpty(Path.of(classesDir)); + } + return toListOfExistingOrEmpty(Path.of(classesDir), Path.of(resourcesOutputDir)); + } + + private static PathList toListOfExistingOrEmpty(Path p1, Path p2) { + if (!Files.exists(p1)) { + return toListOfExistingOrEmpty(p2); + } + if (!Files.exists(p2)) { + return toListOfExistingOrEmpty(p1); + } + return PathList.of(p1, p2); + } + + /** + * Returns a {@link PathList} containing the path if it exists, otherwise returns an empty {@link PathList}. + * + * @param path path + * @return {@link PathList} containing the path if it exists, otherwise returns an empty {@link PathList} + */ + private static PathList toListOfExistingOrEmpty(Path path) { + return Files.exists(path) ? PathList.of(path) : PathList.empty(); + } + + /** + * Returns the classpath element containing this class + * + * @return classpath element containing this class + * @throws URISyntaxException in case of a failure + */ + private Path getThisClassOrigin() throws URISyntaxException { + URL thisArchive = getClass().getResource(DevModeMain.class.getSimpleName() + ".class"); + int endIndex = thisArchive.getPath().indexOf("!"); + if (endIndex != -1) { + return Path.of(new URI(thisArchive.getPath().substring(0, endIndex))); + } + Path path = Path.of(thisArchive.toURI()); + path = path.getParent(); + for (char i : DevModeMain.class.getName().toCharArray()) { + if (i == '.') { + path = path.getParent(); + } + } + + return path; + } + + private void propagateSystemProperties() { + for (Map.Entry i : context.getSystemProperties().entrySet()) { + System.getProperties().putIfAbsent(i.getKey(), i.getValue()); + } + } + private Path resolveProjectRoot() { final Path projectRoot; if (context.getProjectDir() != null) { diff --git a/test-framework/junit5-internal/src/main/java/io/quarkus/test/DevModeTestApplicationModel.java b/test-framework/junit5-internal/src/main/java/io/quarkus/test/DevModeTestApplicationModel.java new file mode 100644 index 00000000000000..b6aa0fee5b9487 --- /dev/null +++ b/test-framework/junit5-internal/src/main/java/io/quarkus/test/DevModeTestApplicationModel.java @@ -0,0 +1,92 @@ +package io.quarkus.test; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; + +import io.quarkus.bootstrap.model.ApplicationModel; +import io.quarkus.bootstrap.model.ExtensionCapabilities; +import io.quarkus.bootstrap.model.ExtensionDevModeConfig; +import io.quarkus.bootstrap.model.PlatformImports; +import io.quarkus.maven.dependency.ArtifactKey; +import io.quarkus.maven.dependency.ResolvedDependency; + +/** + * {@link ApplicationModel} implementation that allows overriding the application artifact + * of another {@link ApplicationModel} instance. + */ +class DevModeTestApplicationModel implements ApplicationModel { + + private final ResolvedDependency appArtifact; + private final ApplicationModel delegate; + + DevModeTestApplicationModel(ResolvedDependency testAppArtifact, ApplicationModel delegate) { + this.appArtifact = testAppArtifact; + this.delegate = delegate; + } + + @Override + public ResolvedDependency getAppArtifact() { + return appArtifact; + } + + @Override + public Collection getDependencies() { + return delegate.getDependencies(); + } + + @Override + public Iterable getDependencies(int flags) { + return delegate.getDependencies(flags); + } + + @Override + public Iterable getDependenciesWithAnyFlag(int... flags) { + return delegate.getDependenciesWithAnyFlag(flags); + } + + @Override + public Collection getRuntimeDependencies() { + return delegate.getRuntimeDependencies(); + } + + @Override + public PlatformImports getPlatforms() { + return delegate.getPlatforms(); + } + + @Override + public Collection getExtensionCapabilities() { + return delegate.getExtensionCapabilities(); + } + + @Override + public Set getParentFirst() { + return delegate.getParentFirst(); + } + + @Override + public Set getRunnerParentFirst() { + return delegate.getRunnerParentFirst(); + } + + @Override + public Set getLowerPriorityArtifacts() { + return delegate.getLowerPriorityArtifacts(); + } + + @Override + public Set getReloadableWorkspaceDependencies() { + return delegate.getReloadableWorkspaceDependencies(); + } + + @Override + public Map> getRemovedResources() { + return delegate.getRemovedResources(); + } + + @Override + public Collection getExtensionDevModeConfig() { + return delegate.getExtensionDevModeConfig(); + } +} diff --git a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusDevModeTest.java b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusDevModeTest.java index 1cad2435abab80..598fa096b4207c 100644 --- a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusDevModeTest.java +++ b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusDevModeTest.java @@ -41,13 +41,19 @@ import org.junit.jupiter.api.extension.TestInstanceFactoryContext; import org.junit.jupiter.api.extension.TestInstantiationException; +import io.quarkus.bootstrap.BootstrapAppModelFactory; +import io.quarkus.bootstrap.workspace.ArtifactSources; +import io.quarkus.bootstrap.workspace.SourceDir; +import io.quarkus.bootstrap.workspace.WorkspaceModule; +import io.quarkus.bootstrap.workspace.WorkspaceModuleId; import io.quarkus.deployment.dev.CompilationProvider; import io.quarkus.deployment.dev.DevModeContext; import io.quarkus.deployment.dev.DevModeMain; import io.quarkus.deployment.util.FileUtil; import io.quarkus.dev.appstate.ApplicationStateNotification; import io.quarkus.dev.testing.TestScanningLock; -import io.quarkus.maven.dependency.GACT; +import io.quarkus.maven.dependency.ArtifactCoords; +import io.quarkus.maven.dependency.ResolvedDependencyBuilder; import io.quarkus.paths.PathList; import io.quarkus.runtime.LaunchMode; import io.quarkus.test.common.GroovyClassValue; @@ -175,7 +181,7 @@ public QuarkusDevModeTest setTestArchiveProducer(Supplier testArchi /** * Customize the application root. * - * @param applicationRootConsumer + * @param testArchiveConsumer * @return self */ public QuarkusDevModeTest withTestArchive(Consumer testArchiveConsumer) { @@ -256,12 +262,7 @@ public void beforeEach(ExtensionContext extensionContext) throws Exception { TestResourceManager tm = testResourceManager; store.put(TestResourceManager.class.getName(), testResourceManager); - store.put(TestResourceManager.CLOSEABLE_NAME, new ExtensionContext.Store.CloseableResource() { - @Override - public void close() throws Throwable { - tm.close(); - } - }); + store.put(TestResourceManager.CLOSEABLE_NAME, (ExtensionContext.Store.CloseableResource) tm::close); } TestResourceManager tm = (TestResourceManager) store.get(TestResourceManager.class.getName()); //dev mode tests just use system properties @@ -292,15 +293,7 @@ public void close() throws Throwable { // TODO: again a hack, assumes the sources dir is one dir above java sources path Path projectSourceParent = projectSourceRoot.getParent(); - DevModeContext context = exportArchive(deploymentDir, projectSourceRoot, projectSourceParent); - context.setBaseName(extensionContext.getDisplayName() + " (QuarkusDevModeTest)"); - context.setArgs(commandLineArgs); - context.setTest(true); - context.setAbortOnFailedStart(!allowFailedStart); - context.getBuildSystemProperties().put("quarkus.banner.enabled", "false"); - context.getBuildSystemProperties().put("quarkus.console.disable-input", "true"); //surefire communicates via stdin, we don't want the test to be reading input - context.getBuildSystemProperties().putAll(buildSystemProperties); - devModeMain = new DevModeMain(context); + devModeMain = exportArchive(extensionContext, deploymentDir, projectSourceRoot, projectSourceParent); devModeMain.start(); ApplicationStateNotification.waitForApplicationStart(); } catch (Exception e) { @@ -343,9 +336,9 @@ public void afterEach(ExtensionContext extensionContext) throws Exception { inMemoryLogHandler.clearRecords(); } - private DevModeContext exportArchive(Path deploymentDir, Path testSourceDir, Path testSourcesParentDir) { + private DevModeMain exportArchive(ExtensionContext extensionContext, Path deploymentDir, Path testSourceDir, + Path testSourcesParentDir) { try { - deploymentSourcePath = deploymentDir.resolve("src/main/java"); deploymentSourceParentPath = deploymentDir.resolve("src/main"); deploymentResourcePath = deploymentDir.resolve("src/main/resources"); @@ -357,37 +350,21 @@ private DevModeContext exportArchive(Path deploymentDir, Path testSourceDir, Pat Files.createDirectories(classes); Files.createDirectories(cache); - //first we export the archive - //then we attempt to generate a source tree + final ArtifactCoords testAppArtifact = ArtifactCoords.jar("io.quarkus", "test-app", "1.0"); + final WorkspaceModule.Mutable testModuleBuilder = WorkspaceModule.builder() + .setModuleId(WorkspaceModuleId.of(testAppArtifact.getGroupId(), + testAppArtifact.getArtifactId(), testAppArtifact.getVersion())) + .addArtifactSources(ArtifactSources.main( + SourceDir.of(deploymentSourcePath, classes), + SourceDir.of(deploymentResourcePath, classes))) + .setBuildDir(targetDir) + .setModuleDir(deploymentDir); + + // export the archive and attempt to generate a source tree JavaArchive archive = archiveProducer.get(); - archive.as(ExplodedExporter.class).exportExplodedInto(classes.toFile()); - copyFromSource(testSourceDir, deploymentSourcePath, classes); + exportAndGenerateSourceTree(archive, classes, testSourceDir, deploymentSourcePath, deploymentResourcePath); copyCodeGenSources(testSourcesParentDir, deploymentSourceParentPath, codeGenSources); - //now copy resources - //we assume everything that is not a .class file is a resource - //resources are handled differently to sources as they are often not in the same location - //in the FS, or are dynamically created - try (Stream stream = Files.walk(classes)) { - stream.forEach(s -> { - if (s.toString().endsWith(".class") || - Files.isDirectory(s)) { - return; - } - String relative = classes.relativize(s).toString(); - try { - try (InputStream in = Files.newInputStream(s)) { - byte[] data = FileUtil.readFileContents(in); - Path resolved = deploymentResourcePath.resolve(relative); - Files.createDirectories(resolved.getParent()); - Files.write(resolved, data, OPEN_OPTIONS); - } - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); - } - //debugging code ExportUtil.exportToQuarkusDeploymentPath(archive); @@ -395,7 +372,7 @@ private DevModeContext exportArchive(Path deploymentDir, Path testSourceDir, Pat context.setCacheDir(cache.toFile()); DevModeContext.ModuleInfo.Builder moduleBuilder = new DevModeContext.ModuleInfo.Builder() - .setArtifactKey(GACT.fromString("io.quarkus.test:app-under-test")) + .setArtifactKey(testAppArtifact.getKey()) .setProjectDirectory(deploymentDir.toAbsolutePath().toString()) .setSourcePaths(PathList.of(deploymentSourcePath.toAbsolutePath())) .setClassesPath(classes.toAbsolutePath().toString()) @@ -416,35 +393,14 @@ private DevModeContext exportArchive(Path deploymentDir, Path testSourceDir, Pat Files.createDirectories(deploymentTestResourcePath); Files.createDirectories(testClasses); - //first we export the archive - //then we attempt to generate a source tree + testModuleBuilder.addArtifactSources(ArtifactSources.test( + SourceDir.of(deploymentTestSourcePath, testClasses), + SourceDir.of(deploymentTestResourcePath, testClasses))); + + // export the archive and attempt to generate a source tree JavaArchive testArchive = testArchiveProducer.get(); - testArchive.as(ExplodedExporter.class).exportExplodedInto(testClasses.toFile()); - copyFromSource(testSourceDir, deploymentTestSourcePath, testClasses); - - //now copy resources - //we assume everything that is not a .class file is a resource - //resources are handled differently to sources as they are often not in the same location - //in the FS, or are dynamically created - try (Stream stream = Files.walk(testClasses)) { - stream.forEach(s -> { - if (s.toString().endsWith(".class") || - Files.isDirectory(s)) { - return; - } - String relative = testClasses.relativize(s).toString(); - try { - try (InputStream in = Files.newInputStream(s)) { - byte[] data = FileUtil.readFileContents(in); - Path resolved = deploymentTestResourcePath.resolve(relative); - Files.createDirectories(resolved.getParent()); - Files.write(resolved, data, OPEN_OPTIONS); - } - } catch (IOException e) { - throw new UncheckedIOException(e); - } - }); - } + exportAndGenerateSourceTree(testArchive, testClasses, testSourceDir, deploymentTestSourcePath, + deploymentTestResourcePath); moduleBuilder .setTestSourcePaths(PathList.of(deploymentTestSourcePath.toAbsolutePath())) .setTestClassesPath(testClasses.toAbsolutePath().toString()) @@ -452,16 +408,76 @@ private DevModeContext exportArchive(Path deploymentDir, Path testSourceDir, Pat .setTestResourcesOutputPath(testClasses.toAbsolutePath().toString()); } - context.setApplicationRoot( - moduleBuilder - .build()); + context.setApplicationRoot(moduleBuilder.build()); + + context.setBaseName(extensionContext.getDisplayName() + " (QuarkusDevModeTest)"); + context.setArgs(commandLineArgs); + context.setTest(true); + context.setAbortOnFailedStart(!allowFailedStart); + context.getBuildSystemProperties().put("quarkus.banner.enabled", "false"); + context.getBuildSystemProperties().put("quarkus.console.disable-input", "true"); //surefire communicates via stdin, we don't want the test to be reading input + context.getBuildSystemProperties().putAll(buildSystemProperties); - return context; + var appModel = new DevModeTestApplicationModel( + ResolvedDependencyBuilder.newInstance() + .setCoords(testAppArtifact) + .setResolvedPath(classes) + .setWorkspaceModule(testModuleBuilder.build()) + .build(), + BootstrapAppModelFactory.newInstance() + .setTest(true) + .setProjectRoot(Path.of("").normalize().toAbsolutePath()) + .resolveAppModel() + .getApplicationModel()); + return new DevModeMain(context, appModel); } catch (Exception e) { throw new RuntimeException("Unable to create the archive", e); } } + private void exportAndGenerateSourceTree(JavaArchive archive, Path classes, Path testSourceDir, Path deploymentSourcePath, + Path deploymentResourcePath) throws IOException { + archive.as(ExplodedExporter.class).exportExplodedInto(classes.toFile()); + copyFromSource(testSourceDir, deploymentSourcePath, classes); + + //now copy resources + //we assume everything that is not a .class file is a resource + //resources are handled differently to sources as they are often not in the same location + //in the FS, or are dynamically created + copyResources(classes, deploymentResourcePath); + } + + /** + * now copy resources + * we assume everything that is not a .class file is a resource + * resources are handled differently to sources as they are often not in the same location + * in the FS, or are dynamically created + * + * @param classes classes directory + * @param deploymentResourcePath target resources path + * @throws IOException in case pf a failure + */ + private void copyResources(Path classes, Path deploymentResourcePath) throws IOException { + try (Stream stream = Files.walk(classes)) { + stream.forEach(s -> { + if (s.toString().endsWith(".class") || + Files.isDirectory(s)) { + return; + } + final String relative = classes.relativize(s).toString(); + try { + final Path resolved = deploymentResourcePath.resolve(relative); + Files.createDirectories(resolved.getParent()); + try (InputStream in = Files.newInputStream(s)) { + Files.write(resolved, FileUtil.readFileContents(in), OPEN_OPTIONS); + } + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + } + private void copyCodeGenSources(Path testSourcesParent, Path deploymentSourceParentPath, List codeGenSources) { for (String codeGenDirName : codeGenSources) { Path codeGenSource = testSourcesParent.resolve(codeGenDirName);