From 8f4895f0a3d2e7c5229f955c10bd4e0eb0b05b81 Mon Sep 17 00:00:00 2001 From: mocenas Date: Tue, 2 Jul 2024 12:52:58 +0200 Subject: [PATCH] Backport quarkus CLI update support --- .../io/quarkus/qe/QuarkusCliClientIT.java | 57 ++- .../quarkus/qe/QuarkusCliUtilsExampleIT.java | 48 +++ .../test/resources/existingSourcesApp/pom.xml | 70 ++++ .../main/java/org/acme/GreetingResource.java | 16 + quarkus-test-cli/pom.xml | 8 + .../test/bootstrap/QuarkusCliClient.java | 74 +++- .../test/bootstrap/QuarkusCliRestService.java | 28 ++ ...hostQuarkusApplicationManagedResource.java | 16 +- ...LessQuarkusApplicationManagedResource.java | 21 + .../util/DefaultQuarkusCLIAppManager.java | 70 ++++ .../test/util/IQuarkusCLIAppManager.java | 28 ++ .../io/quarkus/test/util/QuarkusCLIUtils.java | 378 ++++++++++++++++++ .../test/util/YamlPropertiesHandler.java | 60 +++ .../test/YamlPropertiesHandlerTest.java | 40 ++ .../src/test/resources/example.yaml | 19 + 15 files changed, 921 insertions(+), 12 deletions(-) create mode 100644 examples/quarkus-cli/src/test/java/io/quarkus/qe/QuarkusCliUtilsExampleIT.java create mode 100644 examples/quarkus-cli/src/test/resources/existingSourcesApp/pom.xml create mode 100644 examples/quarkus-cli/src/test/resources/existingSourcesApp/src/main/java/org/acme/GreetingResource.java create mode 100644 quarkus-test-cli/src/main/java/io/quarkus/test/services/quarkus/CliDevModeVersionLessQuarkusApplicationManagedResource.java create mode 100644 quarkus-test-cli/src/main/java/io/quarkus/test/util/DefaultQuarkusCLIAppManager.java create mode 100644 quarkus-test-cli/src/main/java/io/quarkus/test/util/IQuarkusCLIAppManager.java create mode 100644 quarkus-test-cli/src/main/java/io/quarkus/test/util/QuarkusCLIUtils.java create mode 100644 quarkus-test-cli/src/main/java/io/quarkus/test/util/YamlPropertiesHandler.java create mode 100644 quarkus-test-cli/src/test/java/io/quarkus/test/YamlPropertiesHandlerTest.java create mode 100644 quarkus-test-cli/src/test/resources/example.yaml diff --git a/examples/quarkus-cli/src/test/java/io/quarkus/qe/QuarkusCliClientIT.java b/examples/quarkus-cli/src/test/java/io/quarkus/qe/QuarkusCliClientIT.java index 0f574228a..f4175cec2 100644 --- a/examples/quarkus-cli/src/test/java/io/quarkus/qe/QuarkusCliClientIT.java +++ b/examples/quarkus-cli/src/test/java/io/quarkus/qe/QuarkusCliClientIT.java @@ -1,9 +1,15 @@ package io.quarkus.qe; +import static io.quarkus.test.bootstrap.QuarkusCliClient.CreateApplicationRequest.defaults; import static io.quarkus.test.utils.AwaitilityUtils.untilAsserted; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.File; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.file.Path; +import java.nio.file.Paths; import java.time.Duration; import java.util.List; import java.util.concurrent.TimeUnit; @@ -11,7 +17,9 @@ import jakarta.inject.Inject; +import org.apache.commons.io.FileUtils; import org.apache.http.HttpStatus; +import org.awaitility.Awaitility; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -19,6 +27,7 @@ import io.quarkus.test.bootstrap.QuarkusCliDefaultService; import io.quarkus.test.bootstrap.QuarkusCliRestService; import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.services.quarkus.CliDevModeVersionLessQuarkusApplicationManagedResource; @Tag("quarkus-cli") @QuarkusScenario @@ -35,7 +44,7 @@ public class QuarkusCliClientIT { public void shouldCreateApplicationOnJvm() { // Create application QuarkusCliRestService app = cliClient.createApplication("app", - QuarkusCliClient.CreateApplicationRequest.defaults().withStream("3.8")); + defaults().withStream("3.8")); // Should build on Jvm QuarkusCliClient.Result result = app.buildOnJvm(); @@ -60,7 +69,7 @@ public void shouldCreateExtension() { @Test public void shouldCreateApplicationUsingArtifactId() { QuarkusCliRestService app = cliClient.createApplication("com.mycompany:my-app", - QuarkusCliClient.CreateApplicationRequest.defaults().withStream("3.8")); + defaults().withStream("3.8")); assertEquals("my-app", app.getServiceFolder().getFileName().toString(), "The application directory differs."); QuarkusCliClient.Result result = app.buildOnJvm(); @@ -71,7 +80,7 @@ public void shouldCreateApplicationUsingArtifactId() { public void shouldAddAndRemoveExtensions() throws InterruptedException { // Create application QuarkusCliRestService app = cliClient.createApplication("app", - QuarkusCliClient.CreateApplicationRequest.defaults().withStream("3.8")); + defaults().withStream("3.8")); // By default, it installs only "quarkus-resteasy-reactive" assertInstalledExtensions(app, RESTEASY_REACTIVE_EXTENSION); @@ -108,4 +117,46 @@ private void startAfter(QuarkusCliRestService app, Duration duration) throws Int TimeUnit.SECONDS.sleep(duration.getSeconds()); app.start(); } + + @Test + public void shouldRunApplicationWithoutOverwritingVersion() { + QuarkusCliRestService app = cliClient.createApplication("versionFull:app", defaults() + .withStream("3.8") + .withPlatformBom(null) + .withManagedResourceCreator((serviceContext, + quarkusCliClient) -> managedResBuilder -> new CliDevModeVersionLessQuarkusApplicationManagedResource( + serviceContext, quarkusCliClient))); + + app.start(); + String response = app.given().get().getBody().asString(); + // check that app was indeed running with quarkus 3.8 (it was not overwritten) + // version is printed on welcome screen + assertTrue(response.contains("3.8"), "Quarkus is not running on 3.8"); + } + + @Test + public void shouldUpdateApplication() throws IOException { + // Create application + QuarkusCliRestService app = cliClient.createApplication("app", defaults() + // force CLI to omit platform BOM + .withPlatformBom(null) + .withStream("3.2")); + + // Update application + QuarkusCliClient.Result result = app + .update(QuarkusCliClient.UpdateApplicationRequest.defaultUpdate().withStream("3.8")); + File pom = app.getFileFromApplication("pom.xml"); + assertTrue(FileUtils.readFileToString(pom, Charset.defaultCharset()).contains("3.8"), + "Quarkus was not updated to 3.8 stream: " + result.getOutput()); + } + + @Test + public void testCreateApplicationFromExistingSources() { + Path srcPath = Paths.get("src/test/resources/existingSourcesApp"); + QuarkusCliRestService app = cliClient.createApplicationFromExistingSources("app", null, srcPath); + + app.start(); + Awaitility.await().timeout(15, TimeUnit.SECONDS) + .untilAsserted(() -> app.given().get("/hello").then().statusCode(HttpStatus.SC_OK)); + } } diff --git a/examples/quarkus-cli/src/test/java/io/quarkus/qe/QuarkusCliUtilsExampleIT.java b/examples/quarkus-cli/src/test/java/io/quarkus/qe/QuarkusCliUtilsExampleIT.java new file mode 100644 index 000000000..95a8422d8 --- /dev/null +++ b/examples/quarkus-cli/src/test/java/io/quarkus/qe/QuarkusCliUtilsExampleIT.java @@ -0,0 +1,48 @@ +package io.quarkus.qe; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import jakarta.inject.Inject; + +import org.apache.maven.artifact.versioning.DefaultArtifactVersion; +import org.apache.maven.model.Dependency; +import org.codehaus.plexus.util.xml.pull.XmlPullParserException; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.bootstrap.QuarkusCliClient; +import io.quarkus.test.scenarios.QuarkusScenario; +import io.quarkus.test.util.DefaultQuarkusCLIAppManager; +import io.quarkus.test.util.IQuarkusCLIAppManager; +import io.quarkus.test.util.QuarkusCLIUtils; + +/** + * Example how {@link QuarkusCLIUtils#checkDependenciesUpdate} and {@link IQuarkusCLIAppManager} can be used. + * This class actually creates a quarkus app on stream 3.2, updates it to 3.8 and does an example test. + */ +@Tag("quarkus-cli") +@QuarkusScenario +public class QuarkusCliUtilsExampleIT { + private static final DefaultArtifactVersion oldVersion = new DefaultArtifactVersion("3.2"); + private static final DefaultArtifactVersion newVersion = new DefaultArtifactVersion("3.8"); + private final IQuarkusCLIAppManager appManager; + @Inject + static QuarkusCliClient cliClient; + + public QuarkusCliUtilsExampleIT() { + this.appManager = new DefaultQuarkusCLIAppManager(cliClient, oldVersion, newVersion); + } + + @Test + public void exampleDependencyUpdateTest() throws XmlPullParserException, IOException { + List oldDependencies = new ArrayList<>(); + oldDependencies.add(new QuarkusCLIUtils.QuarkusDependency("io.quarkus:quarkus-rest-client")); + + List newDependencies = new ArrayList<>(); + newDependencies.add(new QuarkusCLIUtils.QuarkusDependency("io.quarkus:quarkus-resteasy-client")); + + QuarkusCLIUtils.checkDependenciesUpdate(appManager, oldDependencies, newDependencies); + } +} diff --git a/examples/quarkus-cli/src/test/resources/existingSourcesApp/pom.xml b/examples/quarkus-cli/src/test/resources/existingSourcesApp/pom.xml new file mode 100644 index 000000000..be9923140 --- /dev/null +++ b/examples/quarkus-cli/src/test/resources/existingSourcesApp/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + org.acme + existingSourcesApp + 1.0.0-SNAPSHOT + + + 3.13.0 + 17 + UTF-8 + UTF-8 + quarkus-bom + io.quarkus.platform + 3.12.3 + + + + + + ${quarkus.platform.group-id} + ${quarkus.platform.artifact-id} + ${quarkus.platform.version} + pom + import + + + + + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-rest + + + + + + + ${quarkus.platform.group-id} + quarkus-maven-plugin + ${quarkus.platform.version} + true + + + + build + generate-code + generate-code-tests + native-image-agent + + + + + + maven-compiler-plugin + ${compiler-plugin.version} + + + -parameters + + + + + + diff --git a/examples/quarkus-cli/src/test/resources/existingSourcesApp/src/main/java/org/acme/GreetingResource.java b/examples/quarkus-cli/src/test/resources/existingSourcesApp/src/main/java/org/acme/GreetingResource.java new file mode 100644 index 000000000..244f29426 --- /dev/null +++ b/examples/quarkus-cli/src/test/resources/existingSourcesApp/src/main/java/org/acme/GreetingResource.java @@ -0,0 +1,16 @@ +package org.acme; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; + +@Path("/hello") +public class GreetingResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public String hello() { + return "Hello from Quarkus REST"; + } +} diff --git a/quarkus-test-cli/pom.xml b/quarkus-test-cli/pom.xml index 109364f8d..8b194ff4b 100644 --- a/quarkus-test-cli/pom.xml +++ b/quarkus-test-cli/pom.xml @@ -18,5 +18,13 @@ quarkus-builder provided + + org.apache.maven + maven-core + + + org.yaml + snakeyaml + diff --git a/quarkus-test-cli/src/main/java/io/quarkus/test/bootstrap/QuarkusCliClient.java b/quarkus-test-cli/src/main/java/io/quarkus/test/bootstrap/QuarkusCliClient.java index 0553ac1c1..e5f8168b8 100644 --- a/quarkus-test-cli/src/main/java/io/quarkus/test/bootstrap/QuarkusCliClient.java +++ b/quarkus-test-cli/src/main/java/io/quarkus/test/bootstrap/QuarkusCliClient.java @@ -18,6 +18,7 @@ import io.quarkus.test.logging.FileLoggingHandler; import io.quarkus.test.logging.Log; import io.quarkus.test.services.quarkus.CliDevModeLocalhostQuarkusApplicationManagedResource; +import io.quarkus.test.services.quarkus.CliDevModeVersionLessQuarkusApplicationManagedResource; import io.quarkus.test.services.quarkus.model.QuarkusProperties; import io.quarkus.test.utils.FileUtils; import io.quarkus.test.utils.ProcessBuilderProvider; @@ -77,7 +78,7 @@ public QuarkusCliRestService createApplication(String name, CreateApplicationReq QuarkusCliRestService service = new QuarkusCliRestService(this); ServiceContext serviceContext = service.register(name, context); - service.init(s -> new CliDevModeLocalhostQuarkusApplicationManagedResource(serviceContext, this)); + service.init(request.managedResourceCreator.initBuilder(serviceContext, this)); // We need the service folder to be emptied before generating the project FileUtils.deletePath(serviceContext.getServiceFolder()); @@ -108,6 +109,46 @@ public QuarkusCliRestService createApplication(String name, CreateApplicationReq return service; } + public QuarkusCliRestService createApplicationFromExistingSources(String name, String targetFolderName, Path sourcesDir) { + return createApplicationFromExistingSources(name, targetFolderName, sourcesDir, + ((serviceContext, + quarkusCliClient) -> managedResCreator -> new CliDevModeVersionLessQuarkusApplicationManagedResource( + serviceContext, quarkusCliClient))); + } + + public QuarkusCliRestService createApplicationFromExistingSources(String name, String targetFolderName, Path sourcesDir, + ManagedResourceCreator managedResourceCreator) { + QuarkusCliRestService service = new QuarkusCliRestService(this); + ServiceContext serviceContext = service.register(name, context); + + service.init(managedResourceCreator.initBuilder(serviceContext, this)); + + // We need the service folder to be emptied before generating the project + FileUtils.deletePath(serviceContext.getServiceFolder()); + + FileUtils.copyDirectoryTo(sourcesDir, serviceContext.getServiceFolder()); + + return service; + } + + public Result updateApplication(UpdateApplicationRequest request, Path serviceFolder) { + List args = new ArrayList<>(List.of("update")); + + // stream + if (isNotEmpty(request.stream)) { + args.add("--stream=" + request.stream); + } + + // platform-version + if (isNotEmpty(request.platformVersion)) { + args.add("--platform-version=" + request.platformVersion); + } + + Result result = runCliAndWait(serviceFolder, args.toArray(new String[0])); + assertTrue(result.isSuccessful(), "The application was not updated. Output: " + result.getOutput()); + return result; + } + private static boolean isNotEmpty(String str) { return str != null && !str.isEmpty(); } @@ -194,6 +235,9 @@ public static class CreateApplicationRequest { private String stream; private String[] extensions; private String[] extraArgs; + private ManagedResourceCreator managedResourceCreator = (serviceContext, + quarkusCliClient) -> managedResourceBuilder -> new CliDevModeLocalhostQuarkusApplicationManagedResource( + serviceContext, quarkusCliClient); public CreateApplicationRequest withPlatformBom(String platformBom) { this.platformBom = platformBom; @@ -215,11 +259,39 @@ public CreateApplicationRequest withExtraArgs(String... extraArgs) { return this; } + public CreateApplicationRequest withManagedResourceCreator(ManagedResourceCreator managedResourceCreator) { + this.managedResourceCreator = managedResourceCreator; + return this; + } + public static CreateApplicationRequest defaults() { return new CreateApplicationRequest(); } } + public interface ManagedResourceCreator { + ManagedResourceBuilder initBuilder(ServiceContext serviceContext, QuarkusCliClient quarkusCliClient); + } + + public static class UpdateApplicationRequest { + private String stream; + private String platformVersion; + + public UpdateApplicationRequest withStream(String stream) { + this.stream = stream; + return this; + } + + public UpdateApplicationRequest withPlatformVersion(String platformVersion) { + this.platformVersion = platformVersion; + return this; + } + + public static UpdateApplicationRequest defaultUpdate() { + return new UpdateApplicationRequest(); + } + } + public static class CreateExtensionRequest { private String platformBom; private String stream; diff --git a/quarkus-test-cli/src/main/java/io/quarkus/test/bootstrap/QuarkusCliRestService.java b/quarkus-test-cli/src/main/java/io/quarkus/test/bootstrap/QuarkusCliRestService.java index 2096dcfde..98b76fce0 100644 --- a/quarkus-test-cli/src/main/java/io/quarkus/test/bootstrap/QuarkusCliRestService.java +++ b/quarkus-test-cli/src/main/java/io/quarkus/test/bootstrap/QuarkusCliRestService.java @@ -2,7 +2,11 @@ import static org.junit.jupiter.api.Assertions.assertTrue; +import java.io.File; +import java.nio.file.Path; +import java.util.Arrays; import java.util.List; +import java.util.Objects; import java.util.stream.Collectors; public class QuarkusCliRestService extends RestService { @@ -29,6 +33,14 @@ public QuarkusCliClient.Result removeExtension(String extension) { return cliClient.run(getServiceFolder(), "extension", "remove", extension); } + public QuarkusCliClient.Result update() { + return update(QuarkusCliClient.UpdateApplicationRequest.defaultUpdate()); + } + + public QuarkusCliClient.Result update(QuarkusCliClient.UpdateApplicationRequest request) { + return cliClient.updateApplication(request, getServiceFolder()); + } + public List getInstalledExtensions() { QuarkusCliClient.Result result = cliClient.run(getServiceFolder(), "extension", "list", "--id"); assertTrue(result.isSuccessful(), "Extension list failed"); @@ -36,4 +48,20 @@ public List getInstalledExtensions() { .map(line -> line.replace("✬ ", "")).collect(Collectors.toList()); } + public File getFileFromApplication(String fileName) { + // get file from the service folder + return getFileFromApplication("", fileName); + } + + public File getFileFromApplication(String subFolder, String fileName) { + Path fileFolderPath = getServiceFolder(); + if (subFolder != null && !subFolder.isEmpty()) { + fileFolderPath = Path.of(fileFolderPath.toString(), subFolder); + } + + return Arrays.stream(Objects.requireNonNull(fileFolderPath.toFile().listFiles())) + .filter(f -> f.getName().equalsIgnoreCase(fileName)) + .findFirst() + .orElseThrow(() -> new RuntimeException(fileName + " not found.")); + } } diff --git a/quarkus-test-cli/src/main/java/io/quarkus/test/services/quarkus/CliDevModeLocalhostQuarkusApplicationManagedResource.java b/quarkus-test-cli/src/main/java/io/quarkus/test/services/quarkus/CliDevModeLocalhostQuarkusApplicationManagedResource.java index 85bbd61b0..4474d5041 100644 --- a/quarkus-test-cli/src/main/java/io/quarkus/test/services/quarkus/CliDevModeLocalhostQuarkusApplicationManagedResource.java +++ b/quarkus-test-cli/src/main/java/io/quarkus/test/services/quarkus/CliDevModeLocalhostQuarkusApplicationManagedResource.java @@ -22,17 +22,17 @@ public class CliDevModeLocalhostQuarkusApplicationManagedResource extends QuarkusManagedResource { - private static final String QUARKUS_HTTP_PORT_PROPERTY = "quarkus.http.port"; - private static final String QUARKUS_PLATFORM_ARTIFACT_ID = "quarkus.platform.artifact-id"; - private static final String QUARKUS_PLATFORM_ARTIFACT_ID_VALUE = "quarkus-bom"; - private static final String QUARKUS_PLATFORM_VERSION = "quarkus.platform.version"; + protected static final String QUARKUS_HTTP_PORT_PROPERTY = "quarkus.http.port"; + protected static final String QUARKUS_PLATFORM_ARTIFACT_ID = "quarkus.platform.artifact-id"; + protected static final String QUARKUS_PLATFORM_ARTIFACT_ID_VALUE = "quarkus-bom"; + protected static final String QUARKUS_PLATFORM_VERSION = "quarkus.platform.version"; - private final ServiceContext serviceContext; - private final QuarkusCliClient client; + protected final ServiceContext serviceContext; + protected final QuarkusCliClient client; + protected int assignedHttpPort; private Process process; private LoggingHandler loggingHandler; - private int assignedHttpPort; public CliDevModeLocalhostQuarkusApplicationManagedResource(ServiceContext serviceContext, QuarkusCliClient client) { @@ -101,7 +101,7 @@ protected LoggingHandler getLoggingHandler() { return loggingHandler; } - private Map getPropertiesForCommand() { + protected Map getPropertiesForCommand() { Map runtimeProperties = new HashMap<>(serviceContext.getOwner().getProperties()); runtimeProperties.putIfAbsent(QUARKUS_HTTP_PORT_PROPERTY, "" + assignedHttpPort); runtimeProperties.putIfAbsent(QUARKUS_PLATFORM_VERSION, QuarkusProperties.getVersion()); diff --git a/quarkus-test-cli/src/main/java/io/quarkus/test/services/quarkus/CliDevModeVersionLessQuarkusApplicationManagedResource.java b/quarkus-test-cli/src/main/java/io/quarkus/test/services/quarkus/CliDevModeVersionLessQuarkusApplicationManagedResource.java new file mode 100644 index 000000000..723e2bcea --- /dev/null +++ b/quarkus-test-cli/src/main/java/io/quarkus/test/services/quarkus/CliDevModeVersionLessQuarkusApplicationManagedResource.java @@ -0,0 +1,21 @@ +package io.quarkus.test.services.quarkus; + +import java.util.HashMap; +import java.util.Map; + +import io.quarkus.test.bootstrap.QuarkusCliClient; +import io.quarkus.test.bootstrap.ServiceContext; + +public class CliDevModeVersionLessQuarkusApplicationManagedResource + extends CliDevModeLocalhostQuarkusApplicationManagedResource { + public CliDevModeVersionLessQuarkusApplicationManagedResource(ServiceContext serviceContext, QuarkusCliClient client) { + super(serviceContext, client); + } + + @Override + protected Map getPropertiesForCommand() { + Map runtimeProperties = new HashMap<>(serviceContext.getOwner().getProperties()); + runtimeProperties.putIfAbsent(QUARKUS_HTTP_PORT_PROPERTY, "" + assignedHttpPort); + return runtimeProperties; + } +} diff --git a/quarkus-test-cli/src/main/java/io/quarkus/test/util/DefaultQuarkusCLIAppManager.java b/quarkus-test-cli/src/main/java/io/quarkus/test/util/DefaultQuarkusCLIAppManager.java new file mode 100644 index 000000000..104204956 --- /dev/null +++ b/quarkus-test-cli/src/main/java/io/quarkus/test/util/DefaultQuarkusCLIAppManager.java @@ -0,0 +1,70 @@ +package io.quarkus.test.util; + +import static io.quarkus.test.bootstrap.QuarkusCliClient.CreateApplicationRequest.defaults; +import static io.quarkus.test.bootstrap.QuarkusCliClient.UpdateApplicationRequest.defaultUpdate; + +import java.util.Arrays; + +import org.apache.maven.artifact.versioning.DefaultArtifactVersion; + +import io.quarkus.test.bootstrap.QuarkusCliClient; +import io.quarkus.test.bootstrap.QuarkusCliRestService; +import io.quarkus.test.logging.Log; +import io.quarkus.test.services.quarkus.CliDevModeVersionLessQuarkusApplicationManagedResource; + +public class DefaultQuarkusCLIAppManager implements IQuarkusCLIAppManager { + private final QuarkusCliClient cliClient; + private final DefaultArtifactVersion oldVersion; + private final DefaultArtifactVersion newVersion; + + public DefaultQuarkusCLIAppManager(QuarkusCliClient cliClient, + DefaultArtifactVersion oldVersion, DefaultArtifactVersion newVersion) { + this.cliClient = cliClient; + this.oldVersion = oldVersion; + this.newVersion = newVersion; + } + + @Override + public void updateApp(QuarkusCliRestService app) { + Log.info("Updating app to version stream: " + newVersion); + app.update(defaultUpdate().withStream(newVersion.toString())); + } + + @Override + public QuarkusCliRestService createApplication() { + Log.info("Creating app with version stream: " + oldVersion); + return cliClient.createApplication("app", defaults() + .withPlatformBom(null) + .withStream(oldVersion.toString()) + // overwrite managedResource to use quarkus version defined in pom.xml and not overwrite it in CLI command + .withManagedResourceCreator((serviceContext, + quarkusCliClient) -> managedResBuilder -> new CliDevModeVersionLessQuarkusApplicationManagedResource( + serviceContext, quarkusCliClient))); + } + + @Override + public QuarkusCliRestService createApplicationWithExtensions(String... extensions) { + Log.info("Creating app with version stream: " + oldVersion + " and extensions " + Arrays.toString(extensions)); + return cliClient.createApplication("app", defaults() + .withPlatformBom(null) + .withExtensions(extensions) + .withStream(oldVersion.toString()) + // overwrite managedResource to use quarkus version defined in pom.xml and not overwrite it in CLI command + .withManagedResourceCreator((serviceContext, + quarkusCliClient) -> managedResBuilder -> new CliDevModeVersionLessQuarkusApplicationManagedResource( + serviceContext, quarkusCliClient))); + } + + @Override + public QuarkusCliRestService createApplicationWithExtraArgs(String... extraArgs) { + Log.info("Creating app with version stream: " + oldVersion + " and extraArgs " + Arrays.toString(extraArgs)); + return cliClient.createApplication("app", defaults() + .withPlatformBom(null) + .withExtraArgs(extraArgs) + .withStream(oldVersion.toString()) + // overwrite managedResource to use quarkus version defined in pom.xml and not overwrite it in CLI command + .withManagedResourceCreator((serviceContext, + quarkusCliClient) -> managedResBuilder -> new CliDevModeVersionLessQuarkusApplicationManagedResource( + serviceContext, quarkusCliClient))); + } +} diff --git a/quarkus-test-cli/src/main/java/io/quarkus/test/util/IQuarkusCLIAppManager.java b/quarkus-test-cli/src/main/java/io/quarkus/test/util/IQuarkusCLIAppManager.java new file mode 100644 index 000000000..102416724 --- /dev/null +++ b/quarkus-test-cli/src/main/java/io/quarkus/test/util/IQuarkusCLIAppManager.java @@ -0,0 +1,28 @@ +package io.quarkus.test.util; + +import io.quarkus.test.bootstrap.QuarkusCliRestService; + +/** + * Producer of QuarkusCliRestService apps for tests in {@link QuarkusCLIUtils}. + */ +public interface IQuarkusCLIAppManager { + /** + * Create an app which can be updated. + */ + default QuarkusCliRestService createApplication() { + return createApplicationWithExtensions((String) null); + } + + /** + * @param extensions Pass this parameter to + * {@link io.quarkus.test.bootstrap.QuarkusCliClient} createApplication.withExtensions + */ + QuarkusCliRestService createApplicationWithExtensions(String... extensions); + + QuarkusCliRestService createApplicationWithExtraArgs(String... extraArgs); + + /** + * Update app to new quarkus version. + */ + void updateApp(QuarkusCliRestService app); +} diff --git a/quarkus-test-cli/src/main/java/io/quarkus/test/util/QuarkusCLIUtils.java b/quarkus-test-cli/src/main/java/io/quarkus/test/util/QuarkusCLIUtils.java new file mode 100644 index 000000000..92bf743c2 --- /dev/null +++ b/quarkus-test-cli/src/main/java/io/quarkus/test/util/QuarkusCLIUtils.java @@ -0,0 +1,378 @@ +package io.quarkus.test.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import org.apache.maven.artifact.versioning.DefaultArtifactVersion; +import org.apache.maven.model.Dependency; +import org.apache.maven.model.Model; +import org.apache.maven.model.Plugin; +import org.apache.maven.model.io.xpp3.MavenXpp3Reader; +import org.apache.maven.model.io.xpp3.MavenXpp3Writer; +import org.codehaus.plexus.util.xml.XmlStreamReader; +import org.codehaus.plexus.util.xml.pull.XmlPullParserException; + +import io.quarkus.test.bootstrap.QuarkusCliRestService; + +public abstract class QuarkusCLIUtils { + public static final String RESOURCES_DIR = Paths.get("src", "main", "resources").toString(); + public static final String PROPERTIES_FILE = "application.properties"; + public static final String PROPERTIES_YAML_FILE = "application.yml"; + public static final String POM_FILE = "pom.xml"; + + /** + * This constant stands for number of fields in groupId:artifactId:version string, when separated via ":". + * Checkstyle doesn't allow to have a number directly in a code, so this needs to be a constant. + */ + private static final int GAV_FIELDS_LENGTH = 3; + + /** + * Create app, put properties into application.properties file, + * then update the app and verify that properties are the expected ones. + * + * @param appManager Manager to produce and update app + * @param oldProperties Properties to put into application before update. + * These properties should not be present after update. + * @param expectedNewProperties Properties which should be in app after update. + */ + public static void checkPropertiesUpdate(IQuarkusCLIAppManager appManager, + Properties oldProperties, Properties expectedNewProperties) throws IOException { + QuarkusCliRestService app = appManager.createApplication(); + writePropertiesToPropertiesFile(app, oldProperties); + + appManager.updateApp(app); + Properties newProperties = readPropertiesFile(app); + + verifyProperties(newProperties, oldProperties, expectedNewProperties); + } + + /** + * Create app, put properties into application.yml file, + * then update the app and verify that properties are the expected ones. + * + * @param appManager Manager to produce and update app + * @param oldProperties Properties to put into application before update. + * These properties should not be present after update. + * @param expectedNewProperties Properties which should be in app after update. + */ + public static void checkYamlPropertiesUpdate(IQuarkusCLIAppManager appManager, + Properties oldProperties, + Properties expectedNewProperties) throws IOException { + // create app with yaml extension + QuarkusCliRestService app = appManager.createApplicationWithExtensions("quarkus-config-yaml"); + // write properties to yaml + writePropertiesToYamlFile(app, oldProperties); + + appManager.updateApp(app); + + Properties properties = readPropertiesYamlFile(app); + verifyProperties(properties, oldProperties, expectedNewProperties); + } + + private static void verifyProperties(Properties actualProperties, + Properties oldProperties, Properties expectedNewProperties) { + for (Map.Entry entry : expectedNewProperties.entrySet()) { + assertTrue(actualProperties.containsKey(entry.getKey()), + "Properties after update does not contain " + entry.getKey()); + assertEquals(entry.getValue(), actualProperties.get(entry.getKey()), + "Property " + entry.getKey() + " does not match after update"); + } + + for (Map.Entry entry : oldProperties.entrySet()) { + assertFalse(actualProperties.containsKey(entry.getKey()), + "Properties after update should not contain " + entry.getKey()); + } + } + + /** + * Create app, put dependencies into it, update it and check new dependencies are present. + * Use {@link QuarkusDependency} it has .equals method properly set. + * + * @param oldDependencies Dependencies to put into app before update. Use {@link QuarkusDependency}. + * These dependencies are expected to not be in app after update. + * @param newDependencies Dependencies to expect after update. Use {@link QuarkusDependency}. + */ + public static void checkDependenciesUpdate(IQuarkusCLIAppManager appManager, + List oldDependencies, List newDependencies) + throws XmlPullParserException, IOException { + QuarkusCliRestService app = appManager.createApplication(); + addDependenciesToPom(app, oldDependencies); + + appManager.updateApp(app); + + List actualDependencies = getDependencies(app); + oldDependencies.forEach(dependency -> assertFalse(actualDependencies.contains(dependency), + "Pom.xml after update should not contain dependency: " + dependency)); + newDependencies.forEach(dependency -> assertTrue(actualDependencies.contains(dependency), + "Pom.xml after update should contain dependency: " + dependency)); + } + + /** + * Create app, put plugins into it, update it and check new plugins are present. + * Use {@link QuarkusPlugin} it has .equals method properly set. + * + * @param oldPlugins Plugin to put into app before update. Use {@link QuarkusPlugin}. + * These plugins are expected to not be in app after update + * @param newPlugins Plguins to expect after update. Use {@link QuarkusPlugin}. + */ + public static void checkPluginUpdate(IQuarkusCLIAppManager appManager, + List oldPlugins, List newPlugins) + throws XmlPullParserException, IOException { + QuarkusCliRestService app = appManager.createApplication(); + addPluginsToPom(app, oldPlugins); + + appManager.updateApp(app); + + List actualPlugins = getPlugins(app); + oldPlugins.forEach(plugin -> assertFalse(actualPlugins.contains(plugin), + "Pom.xml after update should not contain plugin " + plugin)); + newPlugins.forEach(plugin -> assertTrue(actualPlugins.contains(plugin), + "Pom.xml after update should contain plugin " + plugin)); + } + + /** + * Write properties into app's application.properties file. + */ + public static void writePropertiesToPropertiesFile(QuarkusCliRestService app, Properties properties) throws IOException { + File propertiesFile = getPropertiesFile(app); + BufferedWriter writer = new BufferedWriter(new FileWriter(propertiesFile)); + + for (Map.Entry entry : properties.entrySet()) { + writer.append(entry.getKey().toString()); + writer.append("="); + writer.append(entry.getValue().toString()); + writer.append("\n"); + } + writer.close(); + } + + /** + * Write properties into app's application.yml file. + */ + public static void writePropertiesToYamlFile(QuarkusCliRestService app, Properties properties) throws IOException { + File yaml = getPropertiesYamlFile(app); + YamlPropertiesHandler.writePropertiesIntoYaml(yaml, properties); + } + + public static Properties readPropertiesFile(QuarkusCliRestService app) throws IOException { + return loadPropertiesFromFile(getPropertiesFile(app)); + } + + public static Properties readPropertiesYamlFile(QuarkusCliRestService app) throws IOException { + File yamlFile = getPropertiesYamlFile(app); + return YamlPropertiesHandler.readYamlFileIntoProperties(yamlFile); + } + + public static Properties loadPropertiesFromFile(File file) throws IOException { + Properties properties = new Properties(); + properties.load(new FileInputStream(file)); + return properties; + } + + public static File getPropertiesFile(QuarkusCliRestService app) { + return app.getFileFromApplication(RESOURCES_DIR, PROPERTIES_FILE); + } + + public static File getPropertiesYamlFile(QuarkusCliRestService app) { + return app.getFileFromApplication(RESOURCES_DIR, PROPERTIES_YAML_FILE); + } + + /** + * Takes content of a file and check that in it's content all keys are renamed to values. + * It's supposed for quarkus update testing of stuff like rename method names or package imports, + * where we can easily check that e.g. import javax.security.cert was renamed to java.security.cert. + * + * @param file File to check content of + * @param renames map of renames string. Asserts that map keys should be renamed to their corresponding values. + */ + public static void checkRenamesInFile(File file, Map renames) throws IOException { + String content = Files.readString(file.toPath()); + + for (Map.Entry entry : renames.entrySet()) { + String failMessage = entry.getKey() + " should be renamed to " + entry.getValue(); + assertFalse(content.contains(entry.getKey()), failMessage); + assertTrue(content.contains(entry.getValue()), failMessage); + } + } + + /** + * Reads quarkus version from app's pom. + */ + public static DefaultArtifactVersion getQuarkusAppVersion(QuarkusCliRestService app) + throws IOException, XmlPullParserException { + return new DefaultArtifactVersion(getPom(app).getProperties().getProperty("quarkus.platform.version")); + } + + public static void addDependenciesToPom(QuarkusCliRestService app, List dependencies) + throws XmlPullParserException, IOException { + Model pom = getPom(app); + dependencies.forEach(pom::addDependency); + savePom(app, pom); + } + + public static void addPluginsToPom(QuarkusCliRestService app, List plugins) + throws XmlPullParserException, IOException { + Model pom = getPom(app); + plugins.forEach(plugin -> pom.getBuild().addPlugin(plugin)); + savePom(app, pom); + } + + /** + * Get plugins from app's pom. + * Does not read pluginManagement. + */ + public static List getPlugins(QuarkusCliRestService app) throws XmlPullParserException, IOException { + return getPom(app).getBuild().getPlugins(); + } + + /** + * Get dependencies from app's pom. + * Does not read dependencyManagement. + */ + public static List getDependencies(QuarkusCliRestService app) throws XmlPullParserException, IOException { + return getPom(app).getDependencies(); + } + + /** + * Get properties defined in app's pom. + */ + public static Properties getProperties(QuarkusCliRestService app) throws XmlPullParserException, IOException { + return getPom(app).getProperties(); + } + + /** + * Get main pom of the application (the one in root dir). + */ + public static Model getPom(QuarkusCliRestService app) throws XmlPullParserException, IOException { + return getPom(app, ""); + } + + /** + * Get pom of the application. + * Specifying subdir param can be used to get pom of nested module (in case of multi-module apps). + */ + public static Model getPom(QuarkusCliRestService app, String subdir) throws IOException, XmlPullParserException { + File pomfile = app.getFileFromApplication(subdir, POM_FILE); + MavenXpp3Reader mavenReader = new MavenXpp3Reader(); + XmlStreamReader streamReader = new XmlStreamReader(pomfile); + return mavenReader.read(streamReader); + } + + public static void savePom(QuarkusCliRestService app, Model model) throws IOException { + OutputStream output = new FileOutputStream(app.getFileFromApplication(POM_FILE)); + new MavenXpp3Writer().write(output, model); + } + + /** + * Extend upstream Dependency class to better suit our needs. + */ + public static class QuarkusDependency extends Dependency { + + /** + * Constructor, which parses groupId:ArtifactId:Version into class. + * Version part is optional. + * Argument can be e.g. "org.graalvm.nativeimage:svm:24.0.1" or "io.quarkus:quarkus-rest-client" + */ + public QuarkusDependency(String groupArtifactVersion) { + String[] fields = groupArtifactVersion.split(":"); + setGroupId(fields[0]); + setArtifactId(fields[1]); + if (fields.length == GAV_FIELDS_LENGTH) { + setVersion(fields[2]); + } + } + + /** + * Original Dependency class does not implement "equals" method, so we cannot easily check if some dependency is + * contained somewhere + * Implementing this method so we can easily detect dependencies in collections etc. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Dependency dependency)) { + return false; + } + if (!(dependency.getArtifactId().equals(this.getArtifactId()) + && dependency.getGroupId().equals(this.getGroupId()))) { + return false; + } + if (this.getVersion() != null && dependency.getVersion() != null) { + return dependency.getVersion().equals(this.getVersion()); + } + return this.getVersion() == null && dependency.getVersion() == null; + } + + /** + * Overriding hash code is required by checkStyle if .equals is overridden. + */ + @Override + public int hashCode() { + return super.hashCode(); + } + } + + public static class QuarkusPlugin extends Plugin { + /** + * Constructor, which parses groupId:ArtifactId:Version into class. + * Version part is optional. + * Argument can be e.g. "org.apache.maven.plugins:maven-compiler-plugin:3.10.0" + */ + public QuarkusPlugin(String groupArtifactVersion) { + String[] fields = groupArtifactVersion.split(":"); + setGroupId(fields[0]); + setArtifactId(fields[1]); + if (fields.length == GAV_FIELDS_LENGTH) { + setVersion(fields[2]); + } + } + + /** + * Parent Plugin class only compares groupId and artifactId in .equals. + * Implementing this method so we can easily detect and distinguish plugins in collections etc. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Plugin plugin)) { + return false; + } + if (!(plugin.getArtifactId().equals(this.getArtifactId()) + && plugin.getGroupId().equals(this.getGroupId()))) { + return false; + } + if (this.getVersion() != null && plugin.getVersion() != null) { + return plugin.getVersion().equals(this.getVersion()); + } + return this.getVersion() == null && plugin.getVersion() == null; + } + + /** + * Overriding hash code is required by checkStyle if .equals is overridden. + */ + @Override + public int hashCode() { + return super.hashCode(); + } + + /** + * Override toString to also print version, parent class is not doing that. + */ + @Override + public String toString() { + return "Plugin {groupId=" + getGroupId() + ", artifactId=" + getArtifactId() + ", version=" + getVersion() + "}"; + } + } +} diff --git a/quarkus-test-cli/src/main/java/io/quarkus/test/util/YamlPropertiesHandler.java b/quarkus-test-cli/src/main/java/io/quarkus/test/util/YamlPropertiesHandler.java new file mode 100644 index 000000000..705a144aa --- /dev/null +++ b/quarkus-test-cli/src/main/java/io/quarkus/test/util/YamlPropertiesHandler.java @@ -0,0 +1,60 @@ +package io.quarkus.test.util; + +import static java.util.Collections.singletonMap; +import static org.apache.maven.surefire.shared.lang3.StringUtils.isBlank; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileWriter; +import java.io.IOException; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Properties; + +import org.yaml.snakeyaml.Yaml; + +public abstract class YamlPropertiesHandler { + + public static void writePropertiesIntoYaml(File yamlFile, Properties properties) throws IOException { + Yaml yaml = new Yaml(); + yaml.dump(properties, new FileWriter(yamlFile)); + } + + public static Properties readYamlFileIntoProperties(File yamlFile) throws FileNotFoundException { + Yaml yaml = new Yaml(); + Map obj = yaml.load(new FileInputStream(yamlFile)); + + Properties properties = new Properties(); + properties.putAll(getFlattenedMap(obj)); + + return properties; + } + + private static Map getFlattenedMap(Map source) { + Map result = new LinkedHashMap<>(); + buildFlattenedMap(result, source, null); + return result; + } + + private static void buildFlattenedMap(Map result, Map source, String path) { + source.forEach((key, value) -> { + if (!isBlank(path)) { + key = path + (key.startsWith("[") ? key : '.' + key); + } + if (value instanceof String) { + result.put(key, value); + } else if (value instanceof Map) { + buildFlattenedMap(result, (Map) value, key); + } else if (value instanceof Collection) { + int count = 0; + for (Object object : (Collection) value) { + buildFlattenedMap(result, singletonMap("[" + (count++) + "]", object), key); + } + } else { + result.put(key, value != null ? "" + value : ""); + } + }); + } +} diff --git a/quarkus-test-cli/src/test/java/io/quarkus/test/YamlPropertiesHandlerTest.java b/quarkus-test-cli/src/test/java/io/quarkus/test/YamlPropertiesHandlerTest.java new file mode 100644 index 000000000..8876334a5 --- /dev/null +++ b/quarkus-test-cli/src/test/java/io/quarkus/test/YamlPropertiesHandlerTest.java @@ -0,0 +1,40 @@ +package io.quarkus.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Properties; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.util.YamlPropertiesHandler; + +public class YamlPropertiesHandlerTest { + + @Test + public void testYamlParsing() throws URISyntaxException, IOException { + File yamlFile = new File(getClass().getClassLoader().getResource("example.yaml").toURI()); + Properties properties = YamlPropertiesHandler.readYamlFileIntoProperties(yamlFile); + + assertEquals("http://localhost:8180/auth/realms/quarkus", properties.getProperty("quarkus.oidc.auth-server-url"), + "Loaded property should have the value from yaml"); + } + + @Test + public void testYamlWriteAndRead() throws IOException { + File tempYamlFile = File.createTempFile("_yamlTest", ".yaml"); + tempYamlFile.deleteOnExit(); + + Properties properties = new Properties(); + properties.put("quarkus.hibernate-search-orm.automatic-indexing.synchronization.strategy", "sync"); + properties.put("quarkus.hibernate-search-orm.quarkusQE.automatic-indexing.synchronization.strategy", "sync"); + + YamlPropertiesHandler.writePropertiesIntoYaml(tempYamlFile, properties); + + Properties parsedProperties = YamlPropertiesHandler.readYamlFileIntoProperties(tempYamlFile); + + assertEquals(properties, parsedProperties, "Parsed properties should be the same, as those written to the file"); + } +} diff --git a/quarkus-test-cli/src/test/resources/example.yaml b/quarkus-test-cli/src/test/resources/example.yaml new file mode 100644 index 000000000..e9f73d5f0 --- /dev/null +++ b/quarkus-test-cli/src/test/resources/example.yaml @@ -0,0 +1,19 @@ +quarkus: + datasource: + jdbc: + url: jdbc:postgresql://localhost:5432/quarkus_test + + hibernate-orm: + database: + generation: drop-and-create + + oidc: + enabled: true + auth-server-url: http://localhost:8180/auth/realms/quarkus + client-id: app + +app: + frontend: + oidc-realm: quarkus + oidc-app: app + oidc-server: http://localhost:8180/auth