pingtimeout commented on code in PR #18: URL: https://github.com/apache/polaris-tools/pull/18#discussion_r2081853413
########## apprunner/README.md: ########## @@ -0,0 +1,365 @@ +<!-- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +--> + +# Polaris Apprunner Gradle and Maven Plugins + +Gradle and Maven plugins to run a Polaris process and "properly" terminate it for integration testing. + +## Java integration tests + +Tests that run via a Gradle `Test` type task, "decorated" with the Polaris Apprunner plugin, have access to +four system properties. Integration tests using the Maven plugin have access to the same system properties. +The names of the system properties can be changed, if needed. See the [Gradle Kotlin DSL](#kotlin-dsl--all-in-one) +and [Maven](#maven) sections below for a summary of the available options. + +* `quarkus.http.test-port` the port on which the Quarkus server listens for application HTTP requests +* `quarkus.management.test-port` the URL on which the Quarkus server listens for management HTTP requests, this + URL is the one emitted by Quarkus during startup and will contain `0.0.0.0` as the host. +* `quarkus.http.test-url` the port on which the Quarkus server listens for application HTTP requests +* `quarkus.management.test-url` the URL on which the Quarkus server listens for management HTTP requests, this + URL is the one emitted by Quarkus during startup and will contain `0.0.0.0` as the host. + +The preferred way to get the URI/URL for application HTTP requests is to get the `quarkus.http.test-port` system +property and construct the URI against `127.0.0.1` (or `::1` if you prefer). + +```java +public class ITWorksWithPolaris { + static final URI POLARIS_SERVER_URI = + URI.create( + String.format( + "http://127.0.0.1:%s/", + requireNonNull( + System.getProperty("quarkus.http.test-port"), + "Required system property quarkus.http.test-port is not set"))); + + @Test + public void pingNessie() { Review Comment: ```suggestion public void pingPolaris() { ``` ########## apprunner/common/src/main/java/org/apache/polaris/apprunner/common/ProcessHandler.java: ########## @@ -0,0 +1,294 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.apprunner.common; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.LongSupplier; + +/** + * Handles the execution of an external process, focused on running a Quarkus application jar. + * + * <p>Starts the process configured in a {@link ProcessBuilder}, provides a method to get the {@link + * #getListenUrls() Quarkus HTTP listen URL} as Quarkus prints to stdout, and manages process + * lifetime and line-by-line I/O pass-through for stdout + stderr. + * + * <p>Any instance of this class can only be used to start (and stop) one process and cannot be + * reused for another process. + * + * <p>This implementation is not thread-safe. + */ +public class ProcessHandler { + + // intentionally long timeouts - think: slow CI systems + public static final long MILLIS_TO_HTTP_PORT = 30_000L; + public static final long MILLIS_TO_STOP = 15_000L; + + private LongSupplier ticker = System::nanoTime; + + private static final int NOT_STARTED = -1; + private static final int RUNNING = -2; + private static final int ERROR = -3; + private final AtomicInteger exitCode = new AtomicInteger(NOT_STARTED); + + private final AtomicBoolean stopped = new AtomicBoolean(); + + private Process process; + + private long timeToListenUrlMillis = MILLIS_TO_HTTP_PORT; + private long timeStopMillis = MILLIS_TO_STOP; + + private Consumer<String> stdoutTarget = System.out::println; + private ListenUrlWaiter listenUrlWaiter; + + private volatile ExecutorService watchdogExecutor; + private volatile Future<?> watchdogFuture; + private volatile Thread shutdownHook; + + public ProcessHandler() { + // empty + } + + public ProcessHandler setTimeToListenUrlMillis(long timeToListenUrlMillis) { + this.timeToListenUrlMillis = timeToListenUrlMillis; + return this; + } + + public ProcessHandler setTimeStopMillis(long timeStopMillis) { + this.timeStopMillis = timeStopMillis; + return this; + } + + public ProcessHandler setStdoutTarget(Consumer<String> stdoutTarget) { + this.stdoutTarget = stdoutTarget; + return this; + } + + public ProcessHandler setTicker(LongSupplier ticker) { + this.ticker = ticker; + return this; + } + + /** + * Starts the process from the given {@link ProcessBuilder}. + * + * @param processBuilder process to start + * @return instance handling the process' runtime + * @throws IOException usually, if the process fails to start + */ + public ProcessHandler start(ProcessBuilder processBuilder) throws IOException { + if (process != null) { Review Comment: For consistency with the `started` method. ```suggestion if (this.process != null) { ``` ########## apprunner/common/src/main/java/org/apache/polaris/apprunner/common/ListenUrlWaiter.java: ########## @@ -0,0 +1,173 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.apprunner.common; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.Consumer; +import java.util.function.LongSupplier; +import java.util.regex.Pattern; + +/** + * Accepts {@link String}s via it's {@link #accept(String)} method and checks for the {@code + * Listening on: http...} pattern. + */ +final class ListenUrlWaiter implements Consumer<String> { + + private static final Pattern HTTP_PORT_LOG_PATTERN = + Pattern.compile( + "^.*Listening on: (http[s]?://[^ ]*)([.] Management interface listening on (http[s]?://[^ ]*)[.])?$"); + static final String TIMEOUT_MESSAGE = + "Did not get the http(s) listen URL from the console output."; + private static final long MAX_ITER_WAIT_NANOS = TimeUnit.MILLISECONDS.toNanos(50); + public static final String NOTHING_RECEIVED = " No output received from process."; + public static final String CAPTURED_LOG_FOLLOWS = " Captured output follows:\n"; + + private final LongSupplier clock; + private final Consumer<String> stdoutTarget; + private final long deadlineListenUrl; + + private final CompletableFuture<List<String>> listenUrl = new CompletableFuture<>(); + private final List<String> capturedLog = new ArrayList<>(); + + /** + * Construct a new instance to wait for Quarkus' {@code Listening on: ...} message. + * + * @param clock monotonic clock, nanoseconds + * @param timeToListenUrlMillis timeout in millis, the "Listen on: ..." must be received within + * this time (otherwise it will fail) + * @param stdoutTarget "real" target for "stdout" + */ + ListenUrlWaiter(LongSupplier clock, long timeToListenUrlMillis, Consumer<String> stdoutTarget) { + this.clock = clock; + this.stdoutTarget = stdoutTarget; + this.deadlineListenUrl = + clock.getAsLong() + TimeUnit.MILLISECONDS.toNanos(timeToListenUrlMillis); + } + + @Override + public void accept(String line) { + if (!listenUrl.isDone()) { + synchronized (capturedLog) { + capturedLog.add(line); Review Comment: Is it possible that capturing all Quarkus output and keeping it in the heap could become a problem if, say, Polaris was started with `TRACE` logging? It seems to me that capturing all logs was mostly used for debugging purposes? ########## apprunner/README.md: ########## @@ -0,0 +1,365 @@ +<!-- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +--> + +# Polaris Apprunner Gradle and Maven Plugins + +Gradle and Maven plugins to run a Polaris process and "properly" terminate it for integration testing. + +## Java integration tests + +Tests that run via a Gradle `Test` type task, "decorated" with the Polaris Apprunner plugin, have access to +four system properties. Integration tests using the Maven plugin have access to the same system properties. +The names of the system properties can be changed, if needed. See the [Gradle Kotlin DSL](#kotlin-dsl--all-in-one) +and [Maven](#maven) sections below for a summary of the available options. + +* `quarkus.http.test-port` the port on which the Quarkus server listens for application HTTP requests +* `quarkus.management.test-port` the URL on which the Quarkus server listens for management HTTP requests, this + URL is the one emitted by Quarkus during startup and will contain `0.0.0.0` as the host. +* `quarkus.http.test-url` the port on which the Quarkus server listens for application HTTP requests +* `quarkus.management.test-url` the URL on which the Quarkus server listens for management HTTP requests, this + URL is the one emitted by Quarkus during startup and will contain `0.0.0.0` as the host. + +The preferred way to get the URI/URL for application HTTP requests is to get the `quarkus.http.test-port` system +property and construct the URI against `127.0.0.1` (or `::1` if you prefer). + +```java +public class ITWorksWithPolaris { + static final URI POLARIS_SERVER_URI = + URI.create( + String.format( + "http://127.0.0.1:%s/", + requireNonNull( + System.getProperty("quarkus.http.test-port"), + "Required system property quarkus.http.test-port is not set"))); + + @Test + public void pingNessie() { + // Use the POLARIS_SERVER_URI in your tests ... + } +} +``` + +## Gradle + +The Polaris Apprunner Gradle ensures that the Polaris Quarkus Server is up and running if and when the configured +test tasks run. It also ensures, as long as you do not forcibly kill Gradle processes, that the Polaris Quarkus Server +is shutdown after the configured test task has finished. Each configured test task gets its "own" Polaris Quarkus +Server started up. + +It is possible to configure multiple tasks/test-suites within a Gradle project to run with a Polaris Quarkus Server. +Since tasks of the same Gradle project do not run concurrently (as of today), there are should be no conflicts, except +potentially the working directory. + +### Using the plugin in projects in the polaris-tools repository + +1. include the apprunner build in your project by adding the following snippet at the beginning of your + `settings.gradle.kts` file: + ```kotlin + includeBuild("../apprunner") { name = "polaris-apprunner" } + ``` + +### Kotlin DSL / step by step + +`build.gradle.kts` + +1. add the plugin + ```kotlin + plugins { + // Replace the version with a release version of the Polaris Apprunner Plugin. + // Omit the version when using the apprunner plugin as a Gradle include build. + id("org.apache.polaris.apprunner") version "0.0.0" + } + ``` +2. Add the Polaris Quarkus Server as a dependency + ```kotlin + // Gradle configuration to reference the tarball + val polarisTarball by + configurations.creating { description = "Used to reference the distribution tarball" } + + dependencies { + polarisTarball("org.apache.polaris:polaris-quarkus-server:1.0.0-incubating-SNAPSHOT:@tgz") + } + + // Directory where the Polaris tarball is extracted to + val unpackedTarball = project.layout.buildDirectory.dir("polaris-tarball") + + // Extracts the Polaris tarball, truncating the path + val polarisUnpackedTarball by + tasks.registering(Sync::class) { + inputs.files(polarisTarball) + destinationDir = unpackedTarball.get().asFile + from(provider { tarTree(polarisTarball.singleFile) }) + eachFile { + // truncates the path (removes the first path element) + relativePath = RelativePath(true, *relativePath.segments.drop(1).toTypedArray()) + } + includeEmptyDirs = false + } + ``` +3. If necessary, add a separate test suite + ```kotlin + testing { + suites { + val polarisServerTest by registering(JvmTestSuite::class) { + // more test-suite related configurations + } + } + } + + tasks.named<Test>("polarisServerTest") { + // Dependency to have the extracted tarball + dependsOn(polarisUnpackedTarball) + } + ``` +4. Tell the Apprunner plugin which test tasks need the Polaris server + ```kotlin + polarisQuarkusApp { + // Add the name of the test task - usually the same as the name of your test source + includeTask(tasks.named("polarisServerTest")) + // Reference the quarkus-run.jar in the tarball, apprunner plugin will then run that jar + executableJar = provider { unpackedTarball.get().file("quarkus-run.jar") } + } + ``` + +Note: the above also works within the `:polaris-quarkus-server` project, but the test suite must be neither +`test` nor `intTest` nor `integrationTest`. + +### Kotlin DSL / all configuration options + +```kotlin +polarisQuarkusApp { + // Ensure that the `test` task has a Polaris Server available. + includeTask(tasks.named<Test>("polarisServerIntegrationTest")) + // Note: prefer setting up separate `polarisServerIntegrationTest` test suite (the name is up to you!), + // see https://docs.gradle.org/current/userguide/jvm_test_suite_plugin.html + + // Override the default Java version (21) to run the Polaris server / Quarkus. + // Must be at least 21! + javaVersion.set(21) + // Additional environment variables for the Polaris server / Quarkus + // (Added to the Gradle inputs used to determine Gradle's UP-TO-DATE status) + environment.put("MY_ENV_VAR", "value") + // Additional environment variables for the Polaris server / Quarkus + // (NOT a Gradle inputs used to determine Gradle's UP-TO-DATE status) + // Put system specific variables (e.g. local paths) here + environmentNonInput.put("MY_ENV_VAR", "value") + // System properties for the Polaris server / Quarkus + // (Added to the Gradle inputs used to determine Gradle's UP-TO-DATE status) + systemProperties.put("my.sys.prop", "value") + // System properties for the Polaris server / Quarkus + // (NOT a Gradle inputs used to determine Gradle's UP-TO-DATE status) + // Put system specific variables (e.g. local paths) here + systemPropertiesNonInput.put("my.sys.prop", "value") + // JVM arguments for the Polaris server JVM (list of strings) + // (Added to the Gradle inputs used to determine Gradle's UP-TO-DATE status) + jvmArguments.add("some-arg") + // JVM arguments for the Polaris server JVM (list of strings) + // (NOT a Gradle inputs used to determine Gradle's UP-TO-DATE status) + // Put system specific variables (e.g. local paths) here + jvmArgumentsNonInput.add("some-arg") + // Use this (full) path to the executable jar of the Polaris server. + // Note: This option should generally be avoided in build scripts, prefer the 'polarisQuarkusServer' + // configuration mentioned above. + executableJar = file("/my/custom/polars-quarkus-server.jar") + // Override the working directory for the Polaris Quarkus server, defaults to `polaris-quarkus-server/` + // in the Gradle project's `build/` directory. + workingDirectory = file("/i/want/it/to/run/here") + // override the default timeout of 30 seconds to wait for the Polaris Quarkus Server to emit the + // listen URLs. + timeToListenUrlMillis = 30000 + // Override the default timeout of 15 seconds to wait for the Polaris Quarkus Server to stop before + // it is forcefully killed + timeToStopMillis = 15000 + // Arguments for the Polaris server (list of strings) + // (Added to the Gradle inputs used to determine Gradle's UP-TO-DATE status) + arguments.add("some-arg") + // Arguments for the Polaris server (list of strings) + // (NOT a Gradle inputs used to determine Gradle's UP-TO-DATE status) + // Put system specific variables here + argumentsNonInput.add("some-arg") + // The following options can be used to use different property names than described above + // in this README + httpListenPortProperty = "quarkus.http.test-port" + httpListenUrlProperty = "quarkus.http.test-url" + managementListenPortProperty = "quarkus.management.test-port" + managementListenUrlProperty = "quarkus.management.test-url" +} +``` + +### Groovy DSL Review Comment: _We_ don't need Groovy as, indeed, Polaris is using Kotlin. However, I believe this project is a runner to facilitate Polaris users' life. Users can run the integration tests of their Enterprise projects in isolation without having to manually deploy a Polaris server themselves. And we cannot make any assumption as to whether they are using Groovy or Kotlin in their Gradle build. ########## apprunner/README.md: ########## @@ -0,0 +1,365 @@ +<!-- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +--> + +# Polaris Apprunner Gradle and Maven Plugins + +Gradle and Maven plugins to run a Polaris process and "properly" terminate it for integration testing. + +## Java integration tests + +Tests that run via a Gradle `Test` type task, "decorated" with the Polaris Apprunner plugin, have access to +four system properties. Integration tests using the Maven plugin have access to the same system properties. +The names of the system properties can be changed, if needed. See the [Gradle Kotlin DSL](#kotlin-dsl--all-in-one) +and [Maven](#maven) sections below for a summary of the available options. + +* `quarkus.http.test-port` the port on which the Quarkus server listens for application HTTP requests +* `quarkus.management.test-port` the URL on which the Quarkus server listens for management HTTP requests, this + URL is the one emitted by Quarkus during startup and will contain `0.0.0.0` as the host. +* `quarkus.http.test-url` the port on which the Quarkus server listens for application HTTP requests +* `quarkus.management.test-url` the URL on which the Quarkus server listens for management HTTP requests, this + URL is the one emitted by Quarkus during startup and will contain `0.0.0.0` as the host. + +The preferred way to get the URI/URL for application HTTP requests is to get the `quarkus.http.test-port` system +property and construct the URI against `127.0.0.1` (or `::1` if you prefer). + +```java +public class ITWorksWithPolaris { + static final URI POLARIS_SERVER_URI = + URI.create( + String.format( + "http://127.0.0.1:%s/", + requireNonNull( + System.getProperty("quarkus.http.test-port"), + "Required system property quarkus.http.test-port is not set"))); + + @Test + public void pingNessie() { + // Use the POLARIS_SERVER_URI in your tests ... + } +} +``` + +## Gradle + +The Polaris Apprunner Gradle ensures that the Polaris Quarkus Server is up and running if and when the configured +test tasks run. It also ensures, as long as you do not forcibly kill Gradle processes, that the Polaris Quarkus Server +is shutdown after the configured test task has finished. Each configured test task gets its "own" Polaris Quarkus +Server started up. + +It is possible to configure multiple tasks/test-suites within a Gradle project to run with a Polaris Quarkus Server. +Since tasks of the same Gradle project do not run concurrently (as of today), there are should be no conflicts, except +potentially the working directory. + +### Using the plugin in projects in the polaris-tools repository + +1. include the apprunner build in your project by adding the following snippet at the beginning of your + `settings.gradle.kts` file: + ```kotlin + includeBuild("../apprunner") { name = "polaris-apprunner" } + ``` + +### Kotlin DSL / step by step + +`build.gradle.kts` + +1. add the plugin + ```kotlin + plugins { + // Replace the version with a release version of the Polaris Apprunner Plugin. + // Omit the version when using the apprunner plugin as a Gradle include build. + id("org.apache.polaris.apprunner") version "0.0.0" + } + ``` +2. Add the Polaris Quarkus Server as a dependency + ```kotlin + // Gradle configuration to reference the tarball + val polarisTarball by + configurations.creating { description = "Used to reference the distribution tarball" } + + dependencies { + polarisTarball("org.apache.polaris:polaris-quarkus-server:1.0.0-incubating-SNAPSHOT:@tgz") + } + + // Directory where the Polaris tarball is extracted to + val unpackedTarball = project.layout.buildDirectory.dir("polaris-tarball") + + // Extracts the Polaris tarball, truncating the path + val polarisUnpackedTarball by + tasks.registering(Sync::class) { + inputs.files(polarisTarball) + destinationDir = unpackedTarball.get().asFile + from(provider { tarTree(polarisTarball.singleFile) }) + eachFile { + // truncates the path (removes the first path element) + relativePath = RelativePath(true, *relativePath.segments.drop(1).toTypedArray()) + } + includeEmptyDirs = false + } + ``` +3. If necessary, add a separate test suite + ```kotlin + testing { + suites { + val polarisServerTest by registering(JvmTestSuite::class) { + // more test-suite related configurations + } + } + } + + tasks.named<Test>("polarisServerTest") { + // Dependency to have the extracted tarball + dependsOn(polarisUnpackedTarball) + } + ``` +4. Tell the Apprunner plugin which test tasks need the Polaris server + ```kotlin + polarisQuarkusApp { + // Add the name of the test task - usually the same as the name of your test source + includeTask(tasks.named("polarisServerTest")) + // Reference the quarkus-run.jar in the tarball, apprunner plugin will then run that jar + executableJar = provider { unpackedTarball.get().file("quarkus-run.jar") } + } + ``` + +Note: the above also works within the `:polaris-quarkus-server` project, but the test suite must be neither +`test` nor `intTest` nor `integrationTest`. + +### Kotlin DSL / all configuration options + +```kotlin +polarisQuarkusApp { + // Ensure that the `test` task has a Polaris Server available. + includeTask(tasks.named<Test>("polarisServerIntegrationTest")) + // Note: prefer setting up separate `polarisServerIntegrationTest` test suite (the name is up to you!), + // see https://docs.gradle.org/current/userguide/jvm_test_suite_plugin.html + + // Override the default Java version (21) to run the Polaris server / Quarkus. + // Must be at least 21! + javaVersion.set(21) + // Additional environment variables for the Polaris server / Quarkus + // (Added to the Gradle inputs used to determine Gradle's UP-TO-DATE status) + environment.put("MY_ENV_VAR", "value") + // Additional environment variables for the Polaris server / Quarkus + // (NOT a Gradle inputs used to determine Gradle's UP-TO-DATE status) + // Put system specific variables (e.g. local paths) here + environmentNonInput.put("MY_ENV_VAR", "value") + // System properties for the Polaris server / Quarkus + // (Added to the Gradle inputs used to determine Gradle's UP-TO-DATE status) + systemProperties.put("my.sys.prop", "value") + // System properties for the Polaris server / Quarkus + // (NOT a Gradle inputs used to determine Gradle's UP-TO-DATE status) + // Put system specific variables (e.g. local paths) here + systemPropertiesNonInput.put("my.sys.prop", "value") + // JVM arguments for the Polaris server JVM (list of strings) + // (Added to the Gradle inputs used to determine Gradle's UP-TO-DATE status) + jvmArguments.add("some-arg") + // JVM arguments for the Polaris server JVM (list of strings) + // (NOT a Gradle inputs used to determine Gradle's UP-TO-DATE status) + // Put system specific variables (e.g. local paths) here + jvmArgumentsNonInput.add("some-arg") + // Use this (full) path to the executable jar of the Polaris server. + // Note: This option should generally be avoided in build scripts, prefer the 'polarisQuarkusServer' + // configuration mentioned above. + executableJar = file("/my/custom/polars-quarkus-server.jar") + // Override the working directory for the Polaris Quarkus server, defaults to `polaris-quarkus-server/` + // in the Gradle project's `build/` directory. + workingDirectory = file("/i/want/it/to/run/here") + // override the default timeout of 30 seconds to wait for the Polaris Quarkus Server to emit the + // listen URLs. + timeToListenUrlMillis = 30000 + // Override the default timeout of 15 seconds to wait for the Polaris Quarkus Server to stop before + // it is forcefully killed + timeToStopMillis = 15000 + // Arguments for the Polaris server (list of strings) + // (Added to the Gradle inputs used to determine Gradle's UP-TO-DATE status) + arguments.add("some-arg") + // Arguments for the Polaris server (list of strings) + // (NOT a Gradle inputs used to determine Gradle's UP-TO-DATE status) + // Put system specific variables here + argumentsNonInput.add("some-arg") + // The following options can be used to use different property names than described above + // in this README + httpListenPortProperty = "quarkus.http.test-port" + httpListenUrlProperty = "quarkus.http.test-url" + managementListenPortProperty = "quarkus.management.test-port" + managementListenUrlProperty = "quarkus.management.test-url" +} +``` + +### Groovy DSL + +`build.gradle` - note: the version number needs to be replaced with a (not yet existing) binary release of +Apache Polaris. + +```groovy +plugins { + id 'java-library' + id 'org.apache.polaris.apprunner' version "0.0.0" +} + +dependencies { + // specify the GAV of the Polaris Quarkus server runnable (uber-jar) + polarisQuarkusServer "org.apache.polaris:polaris-quarkus-server:0.0.0:runner" +} + +polarisQuarkusApp { + // Ensure that the `test` task has a Polaris Server available when the tests run. + includeTask(tasks.named("test")) + // Note: prefer setting up separate `polarisServerIntegrationTest` test suite (the name is up to you!), + // see https://docs.gradle.org/current/userguide/jvm_test_suite_plugin.html + + // See the Kotlin DSL description above for information about the options +} +``` + +## Maven + +The `org.apache.polaris.apprunner:polaris-apprunner-maven-plugin` Maven plugin should be used together with the +standard `maven-failsafe-plugin` + +`pom.xml` - note: the version number needs to be replaced with a (not yet existing) binary release of +Apache Polaris. + +```xml +<project> + <build> + <plugins> + <plugin> + <!-- The Polaris Apprunner Maven plugin should be used for integration tests --> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-failsafe-plugin</artifactId> + </plugin> + + <plugin> + <groupId>org.apache.polaris.apprunner</groupId> + <artifactId>polaris-apprunner-maven-plugin</artifactId> + <!-- TODO replace the version -->> + <version>0.0.0</version> + <configuration> + <!-- Preferred way, specify the GAV of the Polaris Quarkus server (uber-jar) --> + <!-- TODO replace the version -->> + <appArtifactId>org.apache.polaris:polaris-quarkus-server:jar:runner:0.0.0</appArtifactId> + <!-- The system properties passed to the Polaris server --> + <systemProperties> + <foo>bar</foo> + </systemProperties> + <!-- The environment variables passed to the Polaris server --> + <environment> + <HELLO>world</HELLO> + </environment> + <!-- + More options: + <executableJar> (string) Use this (full) path to the executable jar of the Polaris server. + Note: This option should generally be avoided in build scripts, + prefer the 'appArtifactId' option mentioned above. + <javaVersion> (int) Override the default Java version (21) + <jvmArguments> (list<string>) Additional JVM arguments for the Polaris server JVM + <arguments> (list<string>) Additional application arguments for the Polaris server + <workingDirectory> (string) Path of the working directory of the Polaris server, + defaults to "${build.directory}/polaris-quarkus". + <httpListenPortProperty> Override the default 'quarkus.http.test-port' property name + <httpListenUrlProperty> Override the default 'quarkus.http.test-url' property name + <managementListenPortProperty> Override the default 'quarkus.http.management-port' property name + <managementListenUrlProperty> Override the default 'quarkus.http.management-url' property name + --> + </configuration> + <executions> + <execution> + <!-- Start the Polaris Server before the integration tests start --> + <id>start</id> + <phase>pre-integration-test</phase> + <goals> + <goal>start</goal> + </goals> + </execution> + <execution> + <!-- Stop the Polaris Server after the integration tests finished --> + <id>stop</id> + <phase>post-integration-test</phase> + <goals> + <goal>stop</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> +</project> +``` + +## Implicit Quarkus options + +The plugins always pass the following configuration options as system properties to Quarkus: + +``` +quarkus.http.port=0 +quarkus.management.port=0 +quarkus.log.level=INFO +quarkus.log.console.level=INFO +``` + +Those are meant to let Quarkus bind to a random-ish port, so that the started instances do not conflict with anything +else running on the system and that the necessary log line containing the listen-URLs gets emitted. + +You can explicitly override those via the `systemProperties` options of the Gradle and Maven plugins. + +## Developing the plugins + +The Polaris Apprunner plugins are built via a Gradle "included build" (composite build). As generally with composite +builds, task selection via `gradlew` does _not_ get propagated to included builds. This is especially true for +tasks like `spotlessApply`. + +In other words, running `./gradlew spotlessApply` against the "main" Polaris build will run `spotlessApply` +_only_ in the projects in the "main" Polaris build, but _not_ in the apprunner build. This is also true for other +tasks like `check`. + +This means, the easiest way is to just change the current working directory to `tools/apprunner` and work from there. +Publishing the plugins also has to be done from the `tools/apprunner` directory. This is why `gradlew` & co are +present in `tools/apprunner`. + +## FAQ + +### Does it have to be a Polaris Quarkus server? Review Comment: Q: This FAQ entry makes me think that, eventually, the code from this module could be extracted and donated to Quarkus. Then this module would just become a specialization of a "Quarkus Gradle Runner for IT" project. Is this correct? ########## apprunner/gradle-plugin/src/test/resources/org/apache/polaris/apprunner/plugin/TestSimulatingTestUsingThePlugin.java: ########## @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.apprunner.plugin; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; +import org.projectnessie.client.NessieClientBuilder; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.model.Branch; + +/** + * This is not a test for the plugin itself, this is a test that is run BY the test for the plugin. + */ +class TestSimulatingTestUsingThePlugin { + @Test + void pingNessie() throws Exception { + var port = System.getProperty("quarkus.http.test-port"); + assertNotNull(port); + var url = System.getProperty("quarkus.http.test-url"); + assertNotNull(url); + + var uri = String.format("http://127.0.0.1:%s/api/v2", port); + + var client = NessieClientBuilder.createClientBuilderFromSystemSettings().withUri(uri).build(NessieApiV2.class); + // Just some simple REST request to verify that Polaris is started - no fancy interactions w/ Nessie + var config = client.getConfig(); Review Comment: There are references to Nessie that can most likely be simply renamed (e.g. `pingNessie()`) and others that I am less sure about (Nessie client usage). Is using the Nessie Client intended? There are mentions of Nessie's REST API in code that targets the embedded Polaris server, and that is confusing. What is the rationale for using the Nessie Client here? ########## apprunner/common/src/main/java/org/apache/polaris/apprunner/common/ProcessHandler.java: ########## @@ -0,0 +1,294 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.apprunner.common; + +import java.io.IOException; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.function.LongSupplier; + +/** + * Handles the execution of an external process, focused on running a Quarkus application jar. + * + * <p>Starts the process configured in a {@link ProcessBuilder}, provides a method to get the {@link + * #getListenUrls() Quarkus HTTP listen URL} as Quarkus prints to stdout, and manages process + * lifetime and line-by-line I/O pass-through for stdout + stderr. + * + * <p>Any instance of this class can only be used to start (and stop) one process and cannot be + * reused for another process. + * + * <p>This implementation is not thread-safe. + */ +public class ProcessHandler { + + // intentionally long timeouts - think: slow CI systems + public static final long MILLIS_TO_HTTP_PORT = 30_000L; + public static final long MILLIS_TO_STOP = 15_000L; + + private LongSupplier ticker = System::nanoTime; + + private static final int NOT_STARTED = -1; + private static final int RUNNING = -2; + private static final int ERROR = -3; + private final AtomicInteger exitCode = new AtomicInteger(NOT_STARTED); + + private final AtomicBoolean stopped = new AtomicBoolean(); + + private Process process; + + private long timeToListenUrlMillis = MILLIS_TO_HTTP_PORT; + private long timeStopMillis = MILLIS_TO_STOP; + + private Consumer<String> stdoutTarget = System.out::println; + private ListenUrlWaiter listenUrlWaiter; + + private volatile ExecutorService watchdogExecutor; + private volatile Future<?> watchdogFuture; + private volatile Thread shutdownHook; + + public ProcessHandler() { + // empty + } + + public ProcessHandler setTimeToListenUrlMillis(long timeToListenUrlMillis) { + this.timeToListenUrlMillis = timeToListenUrlMillis; + return this; + } + + public ProcessHandler setTimeStopMillis(long timeStopMillis) { + this.timeStopMillis = timeStopMillis; + return this; + } + + public ProcessHandler setStdoutTarget(Consumer<String> stdoutTarget) { + this.stdoutTarget = stdoutTarget; + return this; + } + + public ProcessHandler setTicker(LongSupplier ticker) { + this.ticker = ticker; + return this; + } + + /** + * Starts the process from the given {@link ProcessBuilder}. + * + * @param processBuilder process to start + * @return instance handling the process' runtime + * @throws IOException usually, if the process fails to start + */ + public ProcessHandler start(ProcessBuilder processBuilder) throws IOException { + if (process != null) { + throw new IllegalStateException("Process already started"); + } + + return started(processBuilder.redirectErrorStream(true).start()); + } + + /** + * Alternative to {@link #start(ProcessBuilder)}, directly configures a running process. + * + * @param process running process + * @return {@code this} + */ + ProcessHandler started(Process process) { + if (this.process != null) { + throw new IllegalStateException("Process already started"); + } + + listenUrlWaiter = new ListenUrlWaiter(ticker, timeToListenUrlMillis, stdoutTarget); + + this.process = process; + exitCode.set(RUNNING); + + shutdownHook = new Thread(this::shutdownHandler); + Runtime.getRuntime().addShutdownHook(shutdownHook); + + watchdogExecutor = Executors.newSingleThreadExecutor(); + watchdogFuture = watchdogExecutor.submit(this::watchdog); + + return this; + } + + /** + * Returns the http(s) listen URL as a string as emitted to stdout by Quarkus. + * + * <p>If the Quarkus process does not emit that URL within the time configured via {@link + * #setTimeToListenUrlMillis(long)}, which defaults to {@value #MILLIS_TO_HTTP_PORT} ms, this + * method will throw an {@link IllegalStateException}. + * + * @return the listen URL, never {@code null}. + * @throws InterruptedException if the current thread was interrupted while waiting for the listen + * URL. + * @throws TimeoutException if the Quarkus process did not write the listen URL to stdout. + */ + public List<String> getListenUrls() throws InterruptedException, TimeoutException { + return listenUrlWaiter.getListenUrls(); + } + + /** + * Stops the process. + * + * <p>Tries to gracefully stop the process via a {@code SIGTERM}. If the process is still alive + * after {@link #setTimeStopMillis(long)}, which defaults to {@value #MILLIS_TO_STOP} ms, the + * process will be killed with a {@code SIGKILL}. + */ + public void stop() { + if (process == null) { + throw new IllegalStateException("No process started"); + } + + doStop("Stopped by plugin"); + + watchdogExitGrace(); + } + + private void shutdownHandler() { + doStop("Stop by shutdown handler"); + } + + private void doStop(String reason) { + if (stopped.compareAndSet(false, true)) { + try { + if (reason != null) { + listenUrlWaiter.stopped(reason); + } else { + listenUrlWaiter.timedOut(); + } + process.destroy(); + try { + if (!process.waitFor(timeStopMillis, TimeUnit.MILLISECONDS)) { + process.destroyForcibly(); + } + } catch (InterruptedException e) { + process.destroyForcibly(); + Thread.currentThread().interrupt(); + } + watchdogExecutor.shutdown(); + } finally { + try { + // Don't remove the shutdown-hook if we're running in the shutdown-hook + Runtime.getRuntime().removeShutdownHook(shutdownHook); + } catch (IllegalStateException e) { + // ignore (might happen, when a JVM shutdown is already in progress) + } + } + } + } + + void watchdogExitGrace() { + try { + // Give the watchdog task/thread some time to finish its work + watchdogFuture.get(timeStopMillis, TimeUnit.MILLISECONDS); + } catch (ExecutionException e) { + throw new RuntimeException("ProcessHandler's watchdog thread failed.", e); + } catch (TimeoutException e) { + throw new IllegalStateException("ProcessHandler's watchdog thread failed to finish in time."); + } catch (InterruptedException e) { + process.destroyForcibly(); + Thread.currentThread().interrupt(); + } + } + + public boolean isAlive() { + return exitCode.get() == RUNNING; + } + + /** + * Retrieves the exit-code of the process, if it terminated or throws a {@link + * IllegalThreadStateException} if it is still alive. + * + * @return the exit code of the process + * @throws IllegalThreadStateException if the process is still alive + */ + public int getExitCode() throws IllegalThreadStateException { + if (isAlive()) { + throw new IllegalThreadStateException(); + } + return exitCode.get(); + } + + long remainingWaitTimeNanos() { + return listenUrlWaiter.remainingNanos(); + } + + private Object watchdog() throws IOException { + try (var out = process.getInputStream()) { + var stdout = new InputBuffer(out, listenUrlWaiter); + try { + + /* + * I/O loop. + * + * Fetches data from stdout + stderr and pushes the read data to the associated `InputBuffer` + * instances. The one for `stdout` listens for the HTTP listen address from Quarkus. + * + * As long as there is data from stdout or stderr, the loop does not wait/sleep to get data + * out as fast as possible. If there's no data available, the loop will "yield" via a + * Thread.sleep(1L), which is good enough. + * + * Note: we cannot do blocking-I/O here, because we have to read from both stdout+stderr. Review Comment: Is this still true? Given that `ProcessBuilder::redirectErrorStream` was called, both stdout and stderr are merged into a single stream. So it seems to me that we _could_ do blocking I/O. ########## apprunner/maven-plugin/src/test/resources-its/org/apache/polaris/apprunner/maven/ITPolarisMavenPlugin/applicationIdSpecified/src/test/java/org/apache/polaris/appruner/maven/mavenit/ITSimulatingTestUsingThePlugin.java: ########## @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.apprunner.maven.mavenit; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; +import org.projectnessie.client.api.NessieApiV1; +import org.projectnessie.client.http.HttpClientBuilder; +import org.projectnessie.model.Branch; + +/** + * This is not a test for the plugin itself, this is a test that is run BY the test for the plugin. + */ +class ITSimulatingTestUsingThePlugin { + @Test + void pingNessie() throws Exception { + String port = System.getProperty("quarkus.http.test-port"); + assertNotNull(port, "quarkus.http.test-port"); + String url = System.getProperty("quarkus.http.test-url"); + assertNotNull(url, "quarkus.http.test-url"); + + String uri = String.format("http://127.0.0.1:%s/api/v1", port); + + NessieApiV1 client = HttpClientBuilder.builder().withUri(uri).build(NessieApiV1.class); + // Just some simple REST request to verify that Nessie is started - no fancy interactions w/ Nessie Review Comment: More mentions of Nessie here ########## apprunner/common/src/main/java/org/apache/polaris/apprunner/common/InputBuffer.java: ########## @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.apprunner.common; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.Charset; +import java.util.function.Consumer; + +/** Captures input from an {@link InputStream} and emits full lines terminated with a {@code LF}. */ +final class InputBuffer { + + private final Reader input; + private final Consumer<String> output; + private final StringBuilder lineBuffer = new StringBuilder(); + private boolean failed; + + InputBuffer(InputStream input, Consumer<String> output) { + this(new BufferedReader(new InputStreamReader(input, Charset.defaultCharset())), output); + } + + InputBuffer(Reader input, Consumer<String> output) { + this.input = input; + this.output = output; + } + + /** + * Drains the input passed to the constructor until there's no more data to read, captures full + * lines terminated with a {@code LF}) and pushes these lines to the consumer passed into the + * constructor. + * + * @return {@code true} if any data has been read from the input stream + */ + boolean io() { + // Note: cannot use BufferedReader.readLine() here, because that would block. + try { + if (failed || !input.ready()) { + return false; + } + + var any = false; + while (input.ready()) { + var c = input.read(); Review Comment: Reading form an input stream byte by byte could result in a lot of syscalls. It seems to me that using a BufferedInputStream would be a lot better. Am I missing something ? Or, is it acceptable because we only use the `InputBuffer` for a small amount of data? ########## apprunner/gradle-plugin/src/test/resources/org/apache/polaris/apprunner/plugin/TestSimulatingTestUsingThePlugin.java: ########## @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.polaris.apprunner.plugin; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import org.junit.jupiter.api.Test; +import org.projectnessie.client.NessieClientBuilder; +import org.projectnessie.client.api.NessieApiV2; +import org.projectnessie.model.Branch; + +/** + * This is not a test for the plugin itself, this is a test that is run BY the test for the plugin. + */ +class TestSimulatingTestUsingThePlugin { + @Test + void pingNessie() throws Exception { Review Comment: ```suggestion void pingPolaris() throws Exception { ``` ########## apprunner/README.md: ########## @@ -0,0 +1,365 @@ +<!-- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +--> + +# Polaris Apprunner Gradle and Maven Plugins + +Gradle and Maven plugins to run a Polaris process and "properly" terminate it for integration testing. + +## Java integration tests + +Tests that run via a Gradle `Test` type task, "decorated" with the Polaris Apprunner plugin, have access to +four system properties. Integration tests using the Maven plugin have access to the same system properties. +The names of the system properties can be changed, if needed. See the [Gradle Kotlin DSL](#kotlin-dsl--all-in-one) +and [Maven](#maven) sections below for a summary of the available options. + +* `quarkus.http.test-port` the port on which the Quarkus server listens for application HTTP requests +* `quarkus.management.test-port` the URL on which the Quarkus server listens for management HTTP requests, this + URL is the one emitted by Quarkus during startup and will contain `0.0.0.0` as the host. +* `quarkus.http.test-url` the port on which the Quarkus server listens for application HTTP requests +* `quarkus.management.test-url` the URL on which the Quarkus server listens for management HTTP requests, this + URL is the one emitted by Quarkus during startup and will contain `0.0.0.0` as the host. + +The preferred way to get the URI/URL for application HTTP requests is to get the `quarkus.http.test-port` system +property and construct the URI against `127.0.0.1` (or `::1` if you prefer). + +```java +public class ITWorksWithPolaris { + static final URI POLARIS_SERVER_URI = + URI.create( + String.format( + "http://127.0.0.1:%s/", + requireNonNull( + System.getProperty("quarkus.http.test-port"), + "Required system property quarkus.http.test-port is not set"))); + + @Test + public void pingNessie() { + // Use the POLARIS_SERVER_URI in your tests ... + } +} +``` + +## Gradle + +The Polaris Apprunner Gradle ensures that the Polaris Quarkus Server is up and running if and when the configured +test tasks run. It also ensures, as long as you do not forcibly kill Gradle processes, that the Polaris Quarkus Server +is shutdown after the configured test task has finished. Each configured test task gets its "own" Polaris Quarkus +Server started up. + +It is possible to configure multiple tasks/test-suites within a Gradle project to run with a Polaris Quarkus Server. +Since tasks of the same Gradle project do not run concurrently (as of today), there are should be no conflicts, except +potentially the working directory. + +### Using the plugin in projects in the polaris-tools repository + +1. include the apprunner build in your project by adding the following snippet at the beginning of your + `settings.gradle.kts` file: + ```kotlin + includeBuild("../apprunner") { name = "polaris-apprunner" } + ``` + +### Kotlin DSL / step by step + +`build.gradle.kts` + +1. add the plugin + ```kotlin + plugins { + // Replace the version with a release version of the Polaris Apprunner Plugin. + // Omit the version when using the apprunner plugin as a Gradle include build. + id("org.apache.polaris.apprunner") version "0.0.0" + } + ``` +2. Add the Polaris Quarkus Server as a dependency + ```kotlin + // Gradle configuration to reference the tarball + val polarisTarball by + configurations.creating { description = "Used to reference the distribution tarball" } + + dependencies { + polarisTarball("org.apache.polaris:polaris-quarkus-server:1.0.0-incubating-SNAPSHOT:@tgz") + } + + // Directory where the Polaris tarball is extracted to + val unpackedTarball = project.layout.buildDirectory.dir("polaris-tarball") + + // Extracts the Polaris tarball, truncating the path + val polarisUnpackedTarball by + tasks.registering(Sync::class) { + inputs.files(polarisTarball) + destinationDir = unpackedTarball.get().asFile + from(provider { tarTree(polarisTarball.singleFile) }) + eachFile { + // truncates the path (removes the first path element) + relativePath = RelativePath(true, *relativePath.segments.drop(1).toTypedArray()) + } + includeEmptyDirs = false + } + ``` +3. If necessary, add a separate test suite + ```kotlin + testing { + suites { + val polarisServerTest by registering(JvmTestSuite::class) { + // more test-suite related configurations + } + } + } + + tasks.named<Test>("polarisServerTest") { + // Dependency to have the extracted tarball + dependsOn(polarisUnpackedTarball) + } + ``` +4. Tell the Apprunner plugin which test tasks need the Polaris server + ```kotlin + polarisQuarkusApp { + // Add the name of the test task - usually the same as the name of your test source + includeTask(tasks.named("polarisServerTest")) + // Reference the quarkus-run.jar in the tarball, apprunner plugin will then run that jar + executableJar = provider { unpackedTarball.get().file("quarkus-run.jar") } + } + ``` + +Note: the above also works within the `:polaris-quarkus-server` project, but the test suite must be neither +`test` nor `intTest` nor `integrationTest`. + +### Kotlin DSL / all configuration options + +```kotlin +polarisQuarkusApp { + // Ensure that the `test` task has a Polaris Server available. + includeTask(tasks.named<Test>("polarisServerIntegrationTest")) + // Note: prefer setting up separate `polarisServerIntegrationTest` test suite (the name is up to you!), + // see https://docs.gradle.org/current/userguide/jvm_test_suite_plugin.html + + // Override the default Java version (21) to run the Polaris server / Quarkus. + // Must be at least 21! + javaVersion.set(21) + // Additional environment variables for the Polaris server / Quarkus + // (Added to the Gradle inputs used to determine Gradle's UP-TO-DATE status) + environment.put("MY_ENV_VAR", "value") + // Additional environment variables for the Polaris server / Quarkus + // (NOT a Gradle inputs used to determine Gradle's UP-TO-DATE status) + // Put system specific variables (e.g. local paths) here + environmentNonInput.put("MY_ENV_VAR", "value") + // System properties for the Polaris server / Quarkus + // (Added to the Gradle inputs used to determine Gradle's UP-TO-DATE status) + systemProperties.put("my.sys.prop", "value") + // System properties for the Polaris server / Quarkus + // (NOT a Gradle inputs used to determine Gradle's UP-TO-DATE status) + // Put system specific variables (e.g. local paths) here + systemPropertiesNonInput.put("my.sys.prop", "value") + // JVM arguments for the Polaris server JVM (list of strings) + // (Added to the Gradle inputs used to determine Gradle's UP-TO-DATE status) + jvmArguments.add("some-arg") + // JVM arguments for the Polaris server JVM (list of strings) + // (NOT a Gradle inputs used to determine Gradle's UP-TO-DATE status) + // Put system specific variables (e.g. local paths) here + jvmArgumentsNonInput.add("some-arg") + // Use this (full) path to the executable jar of the Polaris server. + // Note: This option should generally be avoided in build scripts, prefer the 'polarisQuarkusServer' + // configuration mentioned above. + executableJar = file("/my/custom/polars-quarkus-server.jar") + // Override the working directory for the Polaris Quarkus server, defaults to `polaris-quarkus-server/` + // in the Gradle project's `build/` directory. + workingDirectory = file("/i/want/it/to/run/here") + // override the default timeout of 30 seconds to wait for the Polaris Quarkus Server to emit the + // listen URLs. + timeToListenUrlMillis = 30000 + // Override the default timeout of 15 seconds to wait for the Polaris Quarkus Server to stop before + // it is forcefully killed + timeToStopMillis = 15000 + // Arguments for the Polaris server (list of strings) + // (Added to the Gradle inputs used to determine Gradle's UP-TO-DATE status) + arguments.add("some-arg") + // Arguments for the Polaris server (list of strings) + // (NOT a Gradle inputs used to determine Gradle's UP-TO-DATE status) + // Put system specific variables here + argumentsNonInput.add("some-arg") + // The following options can be used to use different property names than described above + // in this README + httpListenPortProperty = "quarkus.http.test-port" + httpListenUrlProperty = "quarkus.http.test-url" + managementListenPortProperty = "quarkus.management.test-port" + managementListenUrlProperty = "quarkus.management.test-url" +} +``` + +### Groovy DSL + +`build.gradle` - note: the version number needs to be replaced with a (not yet existing) binary release of +Apache Polaris. + +```groovy +plugins { + id 'java-library' + id 'org.apache.polaris.apprunner' version "0.0.0" +} + +dependencies { + // specify the GAV of the Polaris Quarkus server runnable (uber-jar) + polarisQuarkusServer "org.apache.polaris:polaris-quarkus-server:0.0.0:runner" +} + +polarisQuarkusApp { + // Ensure that the `test` task has a Polaris Server available when the tests run. + includeTask(tasks.named("test")) + // Note: prefer setting up separate `polarisServerIntegrationTest` test suite (the name is up to you!), + // see https://docs.gradle.org/current/userguide/jvm_test_suite_plugin.html + + // See the Kotlin DSL description above for information about the options +} +``` + +## Maven Review Comment: Same comment as my previous answer: _we_ don't need Maven as, indeed, Polaris is using Gradle. However, I believe this project is a runner to facilitate Polaris users' life. Users can run the integration tests of their Enterprise projects in isolation without having to manually deploy a Polaris server themselves. Maven is the other mainstream Java build system. So it only makes sense that the Apprunner comes with instructions and code to support Maven too. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: issues-unsubscr...@polaris.apache.org For queries about this service, please contact Infrastructure at: us...@infra.apache.org