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

Reply via email to