This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch CAMEL-26405-26406-flaky-tui-tests in repository https://gitbox.apache.org/repos/asf/camel.git
commit 5c1be2bc8b510bc71b2c92b4d24fc6eb929a60f9 Author: Claus Ibsen <[email protected]> AuthorDate: Thu Nov 12 17:55:21 2026 +0100 CAMEL-26405, CAMEL-26406: camel-jbang - fix flaky LaunchManagerOutcomeTest and TuiShellHistorySupportTest LaunchManagerInfraTest stopped every sleep and sh child of the test JVM, so it killed the processes of tests running in parallel, such as the sleep of LaunchManagerOutcomeTest. launchInfra now returns the process it started, and the test stops only its own. TuiShellHistorySupportTest moved the home directory of the CLI without being isolated: other tests wrote into its temp dir while it was deleted. It is now @Isolated, and the test of AiPanelTest that moves the home directory is in its own isolated class. Co-Authored-By: Claude Opus 5.5 <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../dsl/jbang/core/commands/tui/LaunchManager.java | 9 ++- .../commands/tui/AiPanelModelPersistenceTest.java | 90 ++++++++++++++++++++++ .../dsl/jbang/core/commands/tui/AiPanelTest.java | 24 ------ .../core/commands/tui/LaunchManagerInfraTest.java | 33 ++++---- .../commands/tui/TuiShellHistorySupportTest.java | 3 + 5 files changed, 116 insertions(+), 43 deletions(-) diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java index b00c5b87ebe9..5ef8d4c709e4 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManager.java @@ -270,8 +270,12 @@ class LaunchManager { launchInfra(alias, cmd); } - /** Runs the command that starts the given infra service, and watches it until the service is up. */ - void launchInfra(String alias, List<String> cmd) throws IOException { + /** + * Runs the command that starts the given infra service, and watches it until the service is up. + * + * @return the started process + */ + Process launchInfra(String alias, List<String> cmd) throws IOException { Path outputFile = createSecureTempFile("camel-infra-", ".log"); outputFile.toFile().deleteOnExit(); ProcessBuilder pb = new ProcessBuilder(cmd); @@ -281,6 +285,7 @@ class LaunchManager { PendingLaunch pl = new PendingLaunch(alias, process, outputFile, MonotonicClock.millis()); pl.infraAlias = serviceOf(alias); pendingLaunches.add(pl); + return process; } /** diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelModelPersistenceTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelModelPersistenceTest.java new file mode 100644 index 000000000000..749d8d3124fa --- /dev/null +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelModelPersistenceTest.java @@ -0,0 +1,90 @@ +/* + * 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.camel.dsl.jbang.core.commands.tui; + +import java.nio.file.Path; +import java.util.List; + +import dev.tamboui.tui.event.KeyCode; +import dev.tamboui.tui.event.KeyEvent; +import dev.tamboui.tui.event.KeyModifiers; +import org.apache.camel.dsl.jbang.core.commands.LlmClient; +import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.api.parallel.Isolated; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * The model chosen with /model is saved in the TUI settings, so it survives a restart. + */ +// it moves the home directory of the CLI, where other tests would look for theirs +@Isolated +class AiPanelModelPersistenceTest { + + @TempDir + Path home; + + private Path previousHome; + + @BeforeEach + void setup() { + previousHome = CommandLineHelper.getHomeDir(); + CommandLineHelper.useHomeDir(home.toString()); + } + + @AfterEach + void restore() { + CommandLineHelper.useHomeDir(previousHome.toString()); + } + + @Test + void modelCommandPersistsSelectionAcrossRestarts() { + AiPanel panel = new AiPanel(); + panel.setClientForTesting(new ModelListingLlmClient(List.of("model-a", "model-b"))); + panel.open(); + + for (char ch : "/model model-b".toCharArray()) { + panel.handleKeyEvent(KeyEvent.ofChar(ch)); + } + panel.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE)); + + assertThat(TuiSettings.load().getAiModel()) + .as("the /model selection must be persisted so it survives a TUI restart") + .isEqualTo("model-b"); + assertThat(panel.conversationForTesting()) + .anyMatch(entry -> entry.text().contains("Switched model to model-b")); + } + + private static final class ModelListingLlmClient extends LlmClient { + + private final List<String> models; + + ModelListingLlmClient(List<String> models) { + this.models = models; + withModel("test-model"); + } + + @Override + public List<String> listModels() { + return models; + } + } +} diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java index 64afd11ddc2c..170dc5ae717a 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AiPanelTest.java @@ -16,7 +16,6 @@ */ package org.apache.camel.dsl.jbang.core.commands.tui; -import java.nio.file.Path; import java.time.Instant; import java.util.ArrayList; import java.util.List; @@ -32,10 +31,8 @@ import dev.tamboui.tui.event.KeyCode; import dev.tamboui.tui.event.KeyEvent; import dev.tamboui.tui.event.KeyModifiers; import org.apache.camel.dsl.jbang.core.commands.LlmClient; -import org.apache.camel.dsl.jbang.core.common.CommandLineHelper; import org.apache.camel.util.json.JsonObject; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; import static org.awaitility.Awaitility.await; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -359,27 +356,6 @@ class AiPanelTest { assertTrue(listThread.get().contains("model-list")); } - @Test - void modelCommandPersistsSelectionAcrossRestarts(@TempDir Path tempDir) { - String originalHome = CommandLineHelper.getHomeDir().toString(); - CommandLineHelper.useHomeDir(tempDir.toString()); - try { - AiPanel panel = new AiPanel(); - panel.setClientForTesting(new ModelListingLlmClient(List.of("model-a", "model-b"))); - panel.open(); - - type(panel, "/model model-b"); - panel.handleKeyEvent(KeyEvent.ofKey(KeyCode.ENTER, KeyModifiers.NONE)); - - assertEquals("model-b", TuiSettings.load().getAiModel(), - "the /model selection must be persisted so it survives a TUI restart"); - assertTrue(panel.conversationForTesting().stream() - .anyMatch(entry -> entry.text().contains("Switched model to model-b"))); - } finally { - CommandLineHelper.useHomeDir(originalHome); - } - } - @Test void unknownCommandRendersHelpHint() { AiPanel panel = new AiPanel(); diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManagerInfraTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManagerInfraTest.java index 7bd8a8bcc9ad..21589f75a28c 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManagerInfraTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/LaunchManagerInfraTest.java @@ -22,6 +22,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.camel.util.json.JsonObject; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -35,6 +36,16 @@ class LaunchManagerInfraTest { private final List<InfraInfo> running = new ArrayList<>(); private final LaunchManager lm = new LaunchManager(() -> running); + // the processes this test started: only these are stopped, as other tests run processes of their own + private final List<Process> processes = new ArrayList<>(); + + @AfterEach + void stopProcesses() { + for (Process p : processes) { + p.descendants().forEach(ProcessHandle::destroy); + p.destroy(); + } + } @Test void theServiceOfAnEntryWithItsImplementation() { @@ -59,7 +70,7 @@ class LaunchManagerInfraTest { lm.setNotificationCallback((msg, error) -> notices.add(msg)); AtomicBoolean launched = new AtomicBoolean(); // a first start pulls the image: the infra takes a while - lm.launchInfra("kafka", List.of("sleep", "30")); + processes.add(lm.launchInfra("kafka", List.of("sleep", "30"))); lm.deferUntilInfra(List.of("kafka"), "kafka-orders", () -> launched.set(true)); lm.tick(MonotonicClock.millis()); @@ -70,16 +81,13 @@ class LaunchManagerInfraTest { lm.tick(MonotonicClock.millis()); assertThat(launched).isTrue(); assertThat(notices).contains("Started: kafka"); - ProcessHandle.current().children() - .filter(p -> p.info().command().map(c -> c.endsWith("sleep")).orElse(false)) - .forEach(ProcessHandle::destroy); } @Test void aLaunchThatNeedsAServiceWithItsImplementationRunsOnceTheServiceIsUp() throws Exception { AtomicBoolean launched = new AtomicBoolean(); // camel infra run aws sqs runs as the aws service - lm.launchInfra("aws sqs", List.of("sleep", "30")); + processes.add(lm.launchInfra("aws sqs", List.of("sleep", "30"))); lm.deferUntilInfra(List.of("aws sqs"), "aws-sqs", () -> launched.set(true)); lm.tick(MonotonicClock.millis()); @@ -88,9 +96,6 @@ class LaunchManagerInfraTest { running.add(infra("aws")); lm.tick(MonotonicClock.millis()); assertThat(launched).isTrue(); - ProcessHandle.current().children() - .filter(p -> p.info().command().map(c -> c.endsWith("sleep")).orElse(false)) - .forEach(ProcessHandle::destroy); } @Test @@ -101,7 +106,7 @@ class LaunchManagerInfraTest { lm.setFailureLogCallback((name, log) -> failures.add(name)); AtomicBoolean launched = new AtomicBoolean(); // the container cannot bind its port: camel infra run ends with an error - lm.launchInfra("mosquitto", List.of("sh", "-c", "echo port is already allocated; exit 1")); + processes.add(lm.launchInfra("mosquitto", List.of("sh", "-c", "echo port is already allocated; exit 1"))); lm.deferUntilInfra(List.of("mosquitto"), "mqtt", () -> launched.set(true)); await().atMost(10, TimeUnit.SECONDS).pollInterval(50, TimeUnit.MILLISECONDS).pollInSameThread().until(() -> { lm.tick(MonotonicClock.millis()); @@ -137,8 +142,8 @@ class LaunchManagerInfraTest { void theProgressOfAPullIsShownWhileTheInfraStarts() throws Exception { List<String> notices = new ArrayList<>(); lm.setNotificationCallback((msg, error) -> notices.add(msg)); - lm.launchInfra("ollama", List.of("sh", "-c", - "echo 'Pulling image layers: 3 pending, 2 downloaded, 0 extracted, (1.2 GB/3.4 GB)'; sleep 30")); + processes.add(lm.launchInfra("ollama", List.of("sh", "-c", + "echo 'Pulling image layers: 3 pending, 2 downloaded, 0 extracted, (1.2 GB/3.4 GB)'; sleep 30"))); await().atMost(10, TimeUnit.SECONDS).pollInterval(50, TimeUnit.MILLISECONDS).pollInSameThread().until(() -> { lm.tick(MonotonicClock.millis()); return !notices.isEmpty(); @@ -148,12 +153,6 @@ class LaunchManagerInfraTest { // the same progress is not shown again lm.tick(MonotonicClock.millis()); assertThat(notices).hasSize(1); - ProcessHandle.current().children() - .filter(p -> p.info().command().map(c -> c.endsWith("sh")).orElse(false)) - .forEach(ph -> { - ph.descendants().forEach(ProcessHandle::destroy); - ph.destroy(); - }); } private static InfraInfo infra(String alias) { diff --git a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiShellHistorySupportTest.java b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiShellHistorySupportTest.java index 4deac3c444b0..30f7d6d3d772 100644 --- a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiShellHistorySupportTest.java +++ b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/TuiShellHistorySupportTest.java @@ -28,9 +28,12 @@ import org.jline.terminal.TerminalBuilder; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.api.parallel.Isolated; import static org.assertj.core.api.Assertions.assertThat; +// it moves the home directory of the CLI: other tests would write their files into its temp dir while it is deleted +@Isolated class TuiShellHistorySupportTest { private String originalHome;
