This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new b7e00e8bbcef CAMEL-25364: camel-jbang - a failed run or restart
returns its cause, and camel_run does not start a second copy of a running app
(#27399)
b7e00e8bbcef is described below
commit b7e00e8bbcefc755efbbd3238d7ec51923569dd7
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 6 12:52:03 2026 +0200
CAMEL-25364: camel-jbang - a failed run or restart returns its cause, and
camel_run does not start a second copy of a running app (#27399)
* CAMEL-25364: camel-jbang - a failed run or restart returns its cause, and
camel_run does not start a second copy of a running app
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
* CAMEL-25364: address review feedback
- the failure output keeps the exception lines of a record (the exception,
its causes and a
message over several lines, such as the diagnostics of a Java class that
does not compile) and
only leaves out the stack frames; output without log records is returned
as it is, its end kept
- the console output is decoded leniently, so a byte that is not UTF-8 does
not empty it
- the guard finds the running integration through a symlinked directory
(Files.isSameFile), and
also a copy this launcher started that is still starting, or started by a
concurrent call
- the guard says what dev mode and camel_control restart do by how the
integration was started:
with --source-dir and --dev, without dev mode, or with its own files
- the running result has the log file
- docs: the camel_run row; tests for the guard (also through a symlink) and
the output
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---------
Signed-off-by: Claus Ibsen <[email protected]>
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../modules/ROOT/pages/camel-jbang-mcp.adoc | 8 +-
.../core/commands/ai/IntegrationLauncher.java | 207 +++++++++++++++++++--
.../commands/ai/IntegrationLauncherGuardTest.java | 91 +++++++++
.../core/commands/ai/IntegrationLauncherTest.java | 95 ++++++++++
4 files changed, 383 insertions(+), 18 deletions(-)
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index 609b1a3658bd..0346a95592a3 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -352,7 +352,13 @@ project `directory` as an argument, the runtime tools take
the integration `name
| Starts an integration from a project directory with `camel run --source-dir`
in a separate process, in dev
mode by default: the directory is watched, so a changed or added file (a
route, a bean file, a Java class) is
reloaded. Name files to run only those, for a directory that holds several
apps. Returns the pid, name and
- log file once the integration is up.
+ log file once the integration is up. When an integration already runs from
the directory, or one started
+ earlier is still starting, no second copy is started: the answer is that one
(`status: running` with its
+ pid, name and log file, or `status: starting`), with what dev mode and
`camel_control restart` do for it.
+ When the start fails, the answer has the cause from the console output: each
log record with its exception
+ lines (also a message over several lines, such as the diagnostics of a Java
class that does not compile) and
+ the first line of the user's code, without the stack frames. A failed
`camel_control restart` answers the
+ same way.
| `camel_control`
| Controls a running integration: `stop`, `kill`, `restart` (picks up edited
files without dev mode),
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
index 7d64a6322d95..ef81b0aff447 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncher.java
@@ -21,14 +21,19 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Map;
import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
import org.apache.camel.dsl.jbang.core.common.LauncherHelper;
import org.apache.camel.dsl.jbang.core.common.RuntimeHelper;
+import org.apache.camel.util.json.JsonArray;
import org.apache.camel.util.json.JsonObject;
/**
@@ -41,6 +46,18 @@ public final class IntegrationLauncher {
/** How long a started integration is given to publish its status file
before the call returns without a pid. */
static final long STARTUP_TIMEOUT_MS = 30_000;
+ /** The last log records of a failed start that are returned. */
+ static final int FAILURE_RECORDS = 15;
+
+ /** A stack frame, or the count of the frames a cause shares with the
exception it caused. */
+ private static final Pattern FRAME = Pattern.compile("^\\s*(at
\\S.*|\\.\\.\\. \\d+ (more|common frames omitted))$");
+
+ /**
+ * The processes this JVM started from a directory with --source-dir, so a
copy that has not written its status file
+ * yet (still starting, or started by a concurrent call) is seen by the
guard too.
+ */
+ private static final Map<Path, ProcessHandle> LAUNCHED = new
ConcurrentHashMap<>();
+
private IntegrationLauncher() {
}
@@ -72,22 +89,33 @@ public final class IntegrationLauncher {
return result;
}
result.put("directory", directory.toString());
- result.put("command", String.join(" ", cmd));
Path output;
Process process;
- try {
- Files.createDirectories(CommandLineHelper.getCamelDir());
- output = Files.createTempFile(CommandLineHelper.getCamelDir(),
"camel-launch-", ".log");
- output.toFile().deleteOnExit();
- ProcessBuilder pb = new ProcessBuilder(cmd);
- pb.directory(directory.toFile());
- pb.redirectErrorStream(true);
- pb.redirectOutput(output.toFile());
- process = pb.start();
- } catch (IOException e) {
- result.put("status", "failed");
- result.put("error", "Cannot start camel run: " + e.getMessage());
- return result;
+ // one check and start at a time, so two calls in flight do not both
start a copy
+ synchronized (LAUNCHED) {
+ if (sourceDir && alreadyRunning(directory, result)) {
+ // the whole directory is the app: one already running from it
is that app, and a second copy only
+ // fights it for ports and files and makes its name ambiguous
to the other tools
+ return result;
+ }
+ result.put("command", String.join(" ", cmd));
+ try {
+ Files.createDirectories(CommandLineHelper.getCamelDir());
+ output = Files.createTempFile(CommandLineHelper.getCamelDir(),
"camel-launch-", ".log");
+ output.toFile().deleteOnExit();
+ ProcessBuilder pb = new ProcessBuilder(cmd);
+ pb.directory(directory.toFile());
+ pb.redirectErrorStream(true);
+ pb.redirectOutput(output.toFile());
+ process = pb.start();
+ } catch (IOException e) {
+ result.put("status", "failed");
+ result.put("error", "Cannot start camel run: " +
e.getMessage());
+ return result;
+ }
+ if (sourceDir) {
+ LAUNCHED.put(key(directory), process.toHandle());
+ }
}
long pid = process.pid();
long deadline = System.currentTimeMillis() + STARTUP_TIMEOUT_MS;
@@ -95,7 +123,7 @@ public final class IntegrationLauncher {
if (!process.isAlive()) {
result.put("status", "failed");
result.put("exitCode", process.exitValue());
- result.put("output", readTail(output));
+ result.put("output", failureOutput(output));
result.put("message", "camel run exited before the integration
started; the output has the reason");
return result;
}
@@ -218,9 +246,153 @@ public final class IntegrationLauncher {
return null;
}
+ /**
+ * The console output of a run that failed to start, as camel_get_log
shows a log (CAMEL-25364): each record keeps
+ * its message, the lines of its exceptions (the exception, its causes,
and a message over several lines such as the
+ * diagnostics of a Java class that does not compile) and the first line
of the user's code, and the stack frames
+ * are left out. The end of the raw output is mostly frames of the
runtime, and the line that says what went wrong
+ * is above them. This is the only copy the agent gets, as the console
capture is deleted. Output without log
+ * records (a launcher that fails before logging) is returned as it is,
its end kept.
+ */
+ static String failureOutput(Path output) {
+ List<String> lines;
+ try {
+ lines = readText(output).lines().toList();
+ } catch (IOException e) {
+ return readTail(output).strip();
+ }
+ if (lines.size() > 5000) {
+ lines = lines.subList(lines.size() - 5000, lines.size());
+ }
+ lines = lines.stream().filter(l -> !l.isBlank()).toList();
+ // folded for the first line of the user's code, and once more without
the frames for the exception lines
+ JsonArray rows = (JsonArray) LogFileReader.build(lines,
FAILURE_RECORDS, null, null, false, new JsonObject())
+ .get("lines");
+ if (rows == null || rows.isEmpty()
+ || rows.stream().allMatch(r -> ((JsonObject)
r).getStringOrDefault("time", "").isEmpty())) {
+ // no log records: a launcher that failed before logging prints
its error at the end
+ return readTail(output).strip();
+ }
+ List<String> withoutFrames = lines.stream().filter(l ->
!FRAME.matcher(l).matches()).toList();
+ JsonArray exceptions = (JsonArray) LogFileReader
+ .build(withoutFrames, FAILURE_RECORDS, null, null, true, new
JsonObject()).get("lines");
+ boolean aligned = exceptions != null && exceptions.size() ==
rows.size();
+ StringBuilder sb = new StringBuilder();
+ // newest first in the log tool; oldest first here, so it reads as the
console did
+ for (int i = rows.size() - 1; i >= 0; i--) {
+ JsonObject r = (JsonObject) rows.get(i);
+ String level = r.getStringOrDefault("time", "").isEmpty() ? "" :
r.getStringOrDefault("level", "") + " ";
+ sb.append(level).append(r.getStringOrDefault("message", ""));
+ if (r.get("detailLines") != null) {
+ String detail = aligned ? ((JsonObject)
exceptions.get(i)).getString("detail") : null;
+ if (detail != null) {
+ // the exception, its causes and a message over several
lines (compiler diagnostics)
+ detail.lines().forEach(l -> sb.append("\n
").append(l.strip()));
+ } else if (r.get("cause") != null) {
+ sb.append("\n cause: ").append(r.getString("cause"));
+ }
+ if (r.get("at") != null) {
+ sb.append("\n at: ").append(r.getString("at"));
+ }
+ sb.append("\n (stack frames left out)");
+ } else if (r.get("detail") != null) {
+ sb.append("\n").append(r.getString("detail"));
+ }
+ sb.append("\n");
+ }
+ return sb.toString().strip();
+ }
+
+ /**
+ * Whether an integration already runs from the directory, or is still
starting from it, filling in the result for
+ * it: status running or starting, its pid, and what dev mode and a
restart do for it.
+ */
+ static boolean alreadyRunning(Path directory, JsonObject result) {
+ RuntimeHelper.ProcessInfo running = runningFrom(directory);
+ if (running != null) {
+ String runningName = running.contextName() != null &&
!running.contextName().isBlank()
+ ? running.contextName() : running.name();
+ result.put("status", "running");
+ result.put("pid", running.pid());
+ result.put("name", runningName);
+ result.put("log", LogFileReader.logFile(running.pid(),
running.name()).toString());
+ result.put("message", runningMessage(runningName, running.pid()));
+ return true;
+ }
+ ProcessHandle starting = LAUNCHED.get(key(directory));
+ if (starting != null && starting.isAlive()) {
+ result.put("status", "starting");
+ result.put("pid", starting.pid());
+ result.put("message", "pid " + starting.pid() + " started earlier
from this directory is still starting"
+ + " (dependencies may be downloading); a
second copy was not started."
+ + " list_processes shows it once it is up.");
+ return true;
+ }
+ return false;
+ }
+
+ /** What the agent can do with the integration that runs from the
directory, by how it was started. */
+ private static String runningMessage(String name, long pid) {
+ String prefix = name + " (pid " + pid + ") already runs from this
directory; a second copy was not started.";
+ List<String> args = ProcessHandle.of(pid).flatMap(ph ->
ph.info().arguments()).map(Arrays::asList).orElse(null);
+ if (args == null) {
+ return prefix + " camel_control stop stops it, then camel_run
starts the directory again.";
+ }
+ boolean sourceDir = args.stream().anyMatch(a ->
a.startsWith("--source-dir"));
+ boolean dev = args.contains("--dev");
+ if (!sourceDir) {
+ // started with its own files: dev mode watches those only, and a
restart runs the same files again
+ return prefix + " It was started with its own files, not the whole
directory, so a file added to the"
+ + " directory, such as a Java class, is not part of it,
also not after camel_control restart: stop it"
+ + " with camel_control stop, then camel_run runs the whole
directory.";
+ }
+ return prefix + (dev
+ ? " In dev mode a changed or added file is reloaded;
camel_control restart restarts it, for example"
+ + " after adding a Java class."
+ : " It is not in dev mode: camel_control restart picks up a
changed or added file.");
+ }
+
+ /** The integration that runs with this directory as its working
directory, or null. */
+ private static RuntimeHelper.ProcessInfo runningFrom(Path directory) {
+ Path wanted = directory.toAbsolutePath().normalize();
+ for (RuntimeHelper.ProcessInfo p : RuntimeHelper.discoverProcesses()) {
+ Path dir = workingDirectory(p.pid());
+ if (dir != null && sameDirectory(dir, wanted)) {
+ return p;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Whether two paths are the same directory: the process reports its
symlink-resolved working directory, and the
+ * caller may pass a path through a symlink (every directory under /tmp on
macOS).
+ */
+ private static boolean sameDirectory(Path a, Path b) {
+ try {
+ return Files.isSameFile(a, b);
+ } catch (IOException e) {
+ return
a.toAbsolutePath().normalize().equals(b.toAbsolutePath().normalize());
+ }
+ }
+
+ /** The key of a directory in {@link #LAUNCHED}: its real path, the same
through any symlink. */
+ private static Path key(Path directory) {
+ try {
+ return directory.toRealPath();
+ } catch (IOException e) {
+ return directory.toAbsolutePath().normalize();
+ }
+ }
+
+ /** The console output, decoded leniently: console output is not always
UTF-8 (Windows uses the code page). */
+ private static String readText(Path output) throws IOException {
+ return new String(Files.readAllBytes(output), StandardCharsets.UTF_8);
+ }
+
private static String readTail(Path output) {
try {
- String text = Files.readString(output, StandardCharsets.UTF_8);
+ String text = readText(output);
return text.length() > 4000 ? "..." + text.substring(text.length()
- 4000) : text;
} catch (IOException e) {
return "";
@@ -324,7 +496,8 @@ public final class IntegrationLauncher {
}
if (!p.isAlive()) {
return "Restart of " + (name != null ? name : "pid " + pid) +
" failed with exit code "
- + p.exitValue() + ":\n" + readTail(output);
+ + p.exitValue() + ", so it is stopped now: fix the
cause below, then camel_run starts it again.\n"
+ + failureOutput(output);
}
return "Restarting " + (name != null ? name : "pid " + pid) + " as
pid " + p.pid()
+ " (still starting)";
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherGuardTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherGuardTest.java
new file mode 100644
index 000000000000..ae6049078887
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherGuardTest.java
@@ -0,0 +1,91 @@
+/*
+ * 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.ai;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.common.CommandLineHelper;
+import org.apache.camel.util.json.JsonObject;
+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;
+
+/**
+ * CAMEL-25364: camel_run of a directory an integration already runs from
returns that one instead of starting a second
+ * copy. The running integration is this test's own JVM, made visible by a
status file that names the directory.
+ */
+@Isolated
+class IntegrationLauncherGuardTest {
+
+ @TempDir
+ Path home;
+
+ @TempDir
+ Path project;
+
+ private String originalHome;
+
+ @BeforeEach
+ void runningFromTheProject() throws Exception {
+ originalHome = CommandLineHelper.getHomeDir().toString();
+ CommandLineHelper.useHomeDir(home.toString());
+ Files.writeString(project.resolve("route.camel.yaml"), "- from:\n
uri: timer:tick\n steps: []\n");
+ Path camelDir = CommandLineHelper.getCamelDir();
+ Files.createDirectories(camelDir);
+ JsonObject runtime = new JsonObject();
+ // the process reports its working directory resolved, as new
File(".").getAbsolutePath() does
+ runtime.put("directory", project.toRealPath().toString());
+ JsonObject context = new JsonObject();
+ context.put("name", "demo");
+ JsonObject status = new JsonObject();
+ status.put("runtime", runtime);
+ status.put("context", context);
+ Files.writeString(camelDir.resolve(ProcessHandle.current().pid() +
"-status.json"), status.toJson());
+ }
+
+ @AfterEach
+ void restoreHome() {
+ CommandLineHelper.useHomeDir(originalHome);
+ }
+
+ @Test
+ void theRunningIntegrationIsReturned() {
+ JsonObject result = IntegrationLauncher.run(project, List.of(), null,
true, List.of());
+ assertThat(result.getString("status")).isEqualTo("running");
+
assertThat(result.getLong("pid")).isEqualTo(ProcessHandle.current().pid());
+ assertThat(result.getString("name")).isEqualTo("demo");
+ assertThat(result.getString("log")).isNotBlank();
+ assertThat(result.containsKey("command")).isFalse();
+ // this JVM was not started with --source-dir, so it is not said to
reload an added file
+ assertThat(result.getString("message")).contains("a second copy was
not started")
+ .contains("started with its own files").doesNotContain("is
reloaded");
+ }
+
+ @Test
+ void theRunningIntegrationIsFoundThroughASymlink() throws Exception {
+ Path link = Files.createSymbolicLink(home.resolve("project-link"),
project);
+ JsonObject result = IntegrationLauncher.run(link, List.of(), null,
true, List.of());
+ assertThat(result.getString("status")).isEqualTo("running");
+
assertThat(result.getLong("pid")).isEqualTo(ProcessHandle.current().pid());
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherTest.java
index 5d5fca25d53e..b263e6d876d9 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/IntegrationLauncherTest.java
@@ -16,6 +16,7 @@
*/
package org.apache.camel.dsl.jbang.core.commands.ai;
+import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
@@ -61,4 +62,98 @@ class IntegrationLauncherTest {
.containsExactly("run", "a.camel.yaml",
"application.properties", "--dev", "--name=demo",
"--logging-color=false", "--port=9000");
}
+
+ /**
+ * CAMEL-25364: a failed start returns what went wrong, not the last stack
frames of the runtime. The cause and the
+ * first frame of the user's code are kept, the frames are counted.
+ */
+ @Test
+ void failureOutputKeepsTheCauseNotTheFrames(@TempDir Path dir) throws
Exception {
+ StringBuilder out = new StringBuilder();
+ out.append("2026-10-06 01:12:03.101 INFO 81990 --- [ main]
org.apache.camel.main.MainSupport :"
+ + " Apache Camel (JBang) 4.23.0-SNAPSHOT is starting\n");
+ out.append("2026-10-06 01:12:04.552 ERROR 81990 --- [ main]
org.apache.camel.main.MainSupport :"
+ + " Failed to create route: order-generator at: >>>
Bean[ref:orderNumber method:next] <<<\n");
+ out.append("org.apache.camel.FailedToCreateRouteException: Failed to
create route: order-generator\n");
+ for (int i = 0; i < 30; i++) {
+ out.append("\tat
org.apache.camel.impl.engine.AbstractCamelContext.startingRoutes(AbstractCamelContext.java:")
+ .append(1196 + i).append(")\n");
+ }
+ out.append("Caused by: org.apache.camel.NoSuchBeanException: No bean
could be found in the registry for:"
+ + " orderNumber\n");
+ out.append("\tat
org.apache.camel.component.bean.RegistryBean.getBean(RegistryBean.java:94)\n");
+ out.append("\tat
camel.example.OrderNumber.next(OrderNumber.java:12)\n");
+ out.append("\t... 30 more\n");
+ Path output = dir.resolve("camel-launch.log");
+ Files.writeString(output, out.toString());
+
+ String text = IntegrationLauncher.failureOutput(output);
+ assertThat(text).contains("ERROR Failed to create route:
order-generator at: >>> Bean[ref:orderNumber");
+
assertThat(text).contains("org.apache.camel.FailedToCreateRouteException:
Failed to create route: order-generator");
+ assertThat(text).contains("Caused by:
org.apache.camel.NoSuchBeanException: No bean could be found in the"
+ + " registry for: orderNumber");
+ assertThat(text).contains("at:
camel.example.OrderNumber.next(OrderNumber.java:12)");
+ assertThat(text).contains("(stack frames left out)");
+ assertThat(text).doesNotContain("AbstractCamelContext.startingRoutes");
+ // oldest first, as the console printed it
+ assertThat(text.indexOf("is
starting")).isLessThan(text.indexOf("Failed to create route"));
+ }
+
+ /**
+ * CAMEL-25364: an exception message over several lines is kept, such as
the diagnostics of a Java class that does
+ * not compile: they are continuation lines in the same block as the
frames, and this output is the only copy.
+ */
+ @Test
+ void failureOutputKeepsAMessageOverSeveralLines(@TempDir Path dir) throws
Exception {
+ String out = "2026-10-06 01:12:04.552 ERROR 81990 --- [
main] org.apache.camel.main.KameletMain :"
+ + " Error starting Camel: org.joor.ReflectException:
Compilation error:\n"
+ + "org.joor.ReflectException: Compilation error:\n"
+ + "/work/OrderNumber.java:12: error: cannot find symbol\n"
+ + " return prefix + counter.incrementAndGet();\n"
+ + " symbol: variable counter\n"
+ + "\tat org.joor.Compile.compile(Compile.java:178)\n"
+ + "\tat
org.apache.camel.dsl.java.joor.MultiCompile.compileUnit(MultiCompile.java:207)\n"
+ + "\tat
org.apache.camel.main.KameletMain.run(KameletMain.java:512)\n";
+ Path output = dir.resolve("camel-launch.log");
+ Files.writeString(output, out);
+
+ String text = IntegrationLauncher.failureOutput(output);
+ assertThat(text).contains("ERROR Error starting Camel:
org.joor.ReflectException: Compilation error:");
+ assertThat(text).contains("/work/OrderNumber.java:12: error: cannot
find symbol");
+ assertThat(text).contains("symbol: variable counter");
+ assertThat(text).contains("(stack frames left out)");
+ assertThat(text).doesNotContain("MultiCompile.compileUnit(");
+ }
+
+ /**
+ * CAMEL-25364: output with no log records is returned as it is, with its
end, where a launcher prints its error.
+ */
+ @Test
+ void failureOutputWithoutLogRecordsKeepsTheEnd(@TempDir Path dir) throws
Exception {
+ StringBuilder out = new StringBuilder();
+ for (int i = 1; i <= 60; i++) {
+ out.append("Downloading dependency ").append(i).append('\n');
+ }
+ out.append("Cannot find dependency org.example:missing:1.0\n");
+ Path output = dir.resolve("camel-launch.log");
+ Files.writeString(output, out.toString());
+
assertThat(IntegrationLauncher.failureOutput(output)).contains("Downloading
dependency 60")
+ .endsWith("Cannot find dependency org.example:missing:1.0");
+ }
+
+ /** CAMEL-25364: console output that is not UTF-8 (a Windows code page) is
still returned. */
+ @Test
+ void failureOutputThatIsNotUtf8IsKept(@TempDir Path dir) throws Exception {
+ Path output = dir.resolve("camel-launch.log");
+ // C:\Users\Jörg in ISO-8859-1: the ö is a single byte that is not
valid UTF-8
+ Files.write(output, "Cannot read
C:\\Users\\J\u00f6rg\\route.camel.yaml\n".getBytes(StandardCharsets.ISO_8859_1));
+ assertThat(IntegrationLauncher.failureOutput(output)).contains("Cannot
read C:\\Users\\J").contains("rg\\route");
+ }
+
+ @Test
+ void failureOutputOfPlainTextIsKept(@TempDir Path dir) throws Exception {
+ Path output = dir.resolve("camel-launch.log");
+ Files.writeString(output, "Cannot find dependency
org.example:missing:1.0\n");
+
assertThat(IntegrationLauncher.failureOutput(output)).isEqualTo("Cannot find
dependency org.example:missing:1.0");
+ }
}