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 9692de5beecf CAMEL-25897: camel-jbang - the source view shows the body
types each step has been reached with at the end of its line (#28234)
9692de5beecf is described below
commit 9692de5beecfa8b08c6323a73e6ff4631750f6d3
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Oct 24 11:13:08 2026 +0200
CAMEL-25897: camel-jbang - the source view shows the body types each step
has been reached with at the end of its line (#28234)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../images/jbang/camel-tui-source-body-types.png | Bin 0 -> 366120 bytes
.../ROOT/pages/camel-jbang-tui-diagram.adoc | 3 +-
.../ROOT/pages/camel-jbang-tui-source-editor.adoc | 21 +-
.../dsl/jbang/core/commands/tui/LiveRunLines.java | 43 ++-
.../jbang/core/commands/tui/SourceLiveColumn.java | 136 ++++++++-
.../jbang/core/commands/tui/SourceRunState.java | 131 ++++++---
.../dsl/jbang/core/commands/tui/SourceTab.java | 23 +-
.../dsl/jbang/core/commands/tui/SourceViewer.java | 8 +-
.../src/main/resources/tui/help/source.md | 15 +
.../core/commands/tui/SourceBodyTypesTest.java | 303 +++++++++++++++++++++
.../core/commands/tui/SourceRunStateTest.java | 25 +-
11 files changed, 656 insertions(+), 52 deletions(-)
diff --git
a/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-body-types.png
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-body-types.png
new file mode 100644
index 000000000000..8a30469ce191
Binary files /dev/null and
b/docs/user-manual/modules/ROOT/images/jbang/camel-tui-source-body-types.png
differ
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
index c50d9a84a421..6fc27a5b1c34 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-diagram.adoc
@@ -213,7 +213,8 @@ The arrows of a branch are side by side, so the types
beside them are cut to the
many more (`ByteArrayInputStreamCache +2`). The Info panel of the selected
step lists them all with their kind. In the
topology, the Info panel of a route says how many of its steps were reached
with more than one type
(`⚠ 1 mixed · 2 varied`), and the Routes tab has the same in its BODY TYPES
column, with the types of each step in the
-table of the processors of the selected route.
+table of the processors of the selected route. The source view of the Source
tab has them at the end of the line of each
+step (see xref:camel-jbang-tui-source-editor.adoc#_body_types[Body Types]).
image::jbang/camel-tui-routes-body-types.png[The Routes tab: the orders route
has four steps reached with bodies of more than one kind, and its processors
list the types of each step]
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
index b020d717ec43..be04781198d1 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-tui-source-editor.adoc
@@ -8,7 +8,7 @@ routes.
== Top Features
* <<_live_run_data,*Live run data*>> -- the exchanges and failures of each
line of the running integration, next to
- the code.
+ the code, and the body types each step has been reached with at the end of
its line.
* <<_running_as_saved,*Running as saved*>> -- whether the integration runs the
file as it is saved, or needs a restart.
* <<_problems_as_you_type,*Problems as you type*>> -- endpoint options, Simple
expressions and more are checked as soon
as a file opens and while you type, in YAML, Java and XML routes; a YAML or
XML file with problems is not saved.
@@ -55,6 +55,23 @@ In the example the timer fired 227 times; the `filter` let
137 of those through
The column keeps its width, so the code does not move as the numbers grow. It
works for YAML, Java and XML routes, as
`camel run` records the source line of every processor.
+=== Body Types
+
+With the message history on, such as with `camel run --dev`, the body types
each step has been reached with go at the
+end of its line, dimmed and lined up. A step reached with bodies of more than
one kind (`null`, `payload`, `data` or
+`collection`) needs attention, as its body depends on the path a message
takes: it has a `⚠` in the gutter, and its
+types and kinds are in the warning color. The quick documentation of the line
lists all the types, which the end of the
+line cuts to the room there is.
+
+image::jbang/camel-tui-source-body-types.png[The orders route in the source
view: the steps after the choice are reached with a map and a stream, as only
one branch marshals, so they have a warning in the gutter, and the quick
documentation of the selected line says why]
+
+The same body types are in the Routes and Diagram tabs (see
+xref:camel-jbang-tui-diagram.adoc#_body_types[Body Types]).
+
+The live run data and the body types are of the version of the file the
integration runs. While it does not run the
+file as saved (see <<_running_as_saved,Running as Saved>>), the numbers are
dimmed and the body types are left out, as
+the lines of the version that runs may be others.
+
== Running as Saved
The bottom border of the source says whether the integration runs the routes
of the file as it is saved:
@@ -64,6 +81,8 @@ The bottom border of the source says whether the integration
runs the routes of
* `○ not running yet` -- the file was saved after the integration loaded it.
An integration that runs without `--dev`
does not reload a file when it is saved: restart it. The log of one in dev
mode says why it did not reload the file.
* `⚠ the reload failed` -- the integration runs the routes it ran before the
reload, until a reload succeeds.
+* `○ the routes of this file are stopped` -- stopped (or suspended) by hand.
The live run data and the body types still
+ show when the integration loaded the file as saved, which is not known for a
route stopped after a reload.
image::jbang/camel-tui-source-running-as-saved.png[The bottom border of an
edited route file: the integration runs without --dev, so the saved file is not
running yet]
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LiveRunLines.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LiveRunLines.java
index a65e5aec6a92..fafddd111129 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LiveRunLines.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/LiveRunLines.java
@@ -17,8 +17,10 @@
package org.apache.camel.dsl.jbang.core.commands.tui;
import java.util.HashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
+import java.util.Set;
/**
* The live run data of a source file: what the processors of a running
integration on each line of it did, read from
@@ -33,10 +35,12 @@ final class LiveRunLines {
/**
* @param routes the routes of the running integration, with their
processors
* @param filePath the file shown
- * @return per 0-based line of the file, the exchanges, failures
and mean time of the processors on it
+ * @return per 0-based line of the file, the exchanges, failures
and mean time of the processors on it, and
+ * the body types they have been reached with
*/
static Map<Integer, SourceLiveColumn.LiveLine> of(List<RouteInfo> routes,
String filePath) {
Map<Integer, long[]> sums = new HashMap<>();
+ Map<Integer, LineTypes> types = new HashMap<>();
String name = nameOf(filePath);
if (routes == null || name == null) {
return Map.of();
@@ -46,13 +50,48 @@ final class LiveRunLines {
add(sums, name, r.source, r.total, r.failed, r.meanTime);
for (ProcessorInfo p : r.processors) {
add(sums, name, p.source, p.total, p.failed, p.meanTime);
+ addBodyTypes(types, name, p);
}
}
Map<Integer, SourceLiveColumn.LiveLine> answer = new HashMap<>();
- sums.forEach((line, s) -> answer.put(line, new
SourceLiveColumn.LiveLine(s[0], s[1], s[0] > 0 ? s[2] / s[0] : 0)));
+ sums.forEach((line, s) -> {
+ LineTypes t = types.get(line);
+ answer.put(line, t == null
+ ? new SourceLiveColumn.LiveLine(s[0], s[1], s[0] > 0 ?
s[2] / s[0] : 0)
+ : new SourceLiveColumn.LiveLine(
+ s[0], s[1], s[0] > 0 ? s[2] / s[0] : 0,
List.copyOf(t.types), List.copyOf(t.kinds), t.mixed));
+ });
return answer;
}
+ /** The body types of the processors on a line, and whether one of them
was reached with more than one kind. */
+ private static final class LineTypes {
+ final Set<String> types = new LinkedHashSet<>();
+ final Set<String> kinds = new LinkedHashSet<>();
+ boolean mixed;
+ }
+
+ /**
+ * Adds the body types a processor has been reached with to its line
(CAMEL-25897): the types and the kinds of all
+ * the processors on the line, which is mixed when one of them was reached
with bodies of more than one kind, not
+ * when two of them each had their own kind.
+ */
+ private static void addBodyTypes(Map<Integer, LineTypes> types, String
name, ProcessorInfo p) {
+ if (p.bodyTypes == null || p.bodyTypes.isEmpty()) {
+ return;
+ }
+ int line = lineOf(p.source, name);
+ if (line < 0) {
+ return;
+ }
+ LineTypes t = types.computeIfAbsent(line, k -> new LineTypes());
+ t.types.addAll(p.bodyTypes);
+ if (p.bodyKinds != null) {
+ t.kinds.addAll(p.bodyKinds);
+ }
+ t.mixed |= BodyTypes.mixed(p.bodyKinds);
+ }
+
/** Adds what a processor did to its line: totals and failures summed, the
mean time weighted by the exchanges. */
private static void add(Map<Integer, long[]> sums, String name, String
source, long total, long failed, long mean) {
int line = lineOf(source, name);
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceLiveColumn.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceLiveColumn.java
index 580a435ddd34..d5eb29c645e2 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceLiveColumn.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceLiveColumn.java
@@ -24,20 +24,37 @@ import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
import dev.tamboui.style.Style;
+import dev.tamboui.text.CharWidth;
import dev.tamboui.text.Span;
/**
* The live run data of the running integration in the source view ({@link
SourceViewer}), a column at the end of the
* line numbers: how many exchanges the processors on a line handled, how many
failed, and their mean time (a heat map
* of the source while it runs). A line that failed is what Shift+F8 asks the
AI to fix when it has no problem.
+ * <p/>
+ * The body types the steps of a line have been reached with go at the end of
the line, lined up, with a ⚠ in the gutter
+ * of a step reached with bodies of more than one kind (CAMEL-25897). They
only hold while the view has the version of
+ * the file that runs: once the file is saved and the integration did not load
it, the live run data is dimmed and the
+ * body types are left out.
*/
class SourceLiveColumn {
+ /**
+ * The most a line (its number and code) counts for lining up the body
types: a long one does not push them away.
+ */
+ private static final int MAX_BODY_TYPES_ALIGN = 72;
+
/**
* What a line of a running integration's source did: the exchanges of the
processors on it, the failed ones and the
- * mean processing time.
+ * mean processing time; and the body types they have been reached with,
mixed when one of them was reached with
+ * bodies of more than one kind.
*/
- record LiveLine(long total, long failed, long meanMillis) {
+ record LiveLine(long total, long failed, long meanMillis, List<String>
bodyTypes, List<String> bodyKinds,
+ boolean mixed) {
+
+ LiveLine(long total, long failed, long meanMillis) {
+ this(total, failed, meanMillis, List.of(), List.of(), false);
+ }
}
/** The live run data of a file by its path: per 0-based line, what the
processors on it did. */
@@ -54,6 +71,10 @@ class SourceLiveColumn {
private final Supplier<String> loadedFile;
private final BooleanSupplier dirty;
+ private final Supplier<List<String>> lines;
+ private BooleanSupplier stale = () -> false;
+ // the column the body types of the lines line up at, from the lines that
have them
+ private int bodyTypesAlign;
private LiveRunData liveRunData;
private LineFailures lineFailures;
private Map<Integer, LiveLine> liveLines = Collections.emptyMap();
@@ -62,14 +83,32 @@ class SourceLiveColumn {
private int liveFailedWidth;
private int liveMeanWidth;
private String liveWidthsFile;
+ // the file the live run data was read for: another one is read at once,
not after a second
+ private String liveLinesFile;
/**
* @param loadedFile the path of the file shown, whose live run data is
read; null when none is
* @param dirty whether the file is edited and not saved, so the live
run data is not of what is shown
+ * @param lines the lines shown (the line number, then the code), to
line up the body types after them
*/
- SourceLiveColumn(Supplier<String> loadedFile, BooleanSupplier dirty) {
+ SourceLiveColumn(Supplier<String> loadedFile, BooleanSupplier dirty,
Supplier<List<String>> lines) {
this.loadedFile = loadedFile;
this.dirty = dirty;
+ this.lines = lines;
+ }
+
+ /**
+ * Whether the integration runs another version of the file than the one
shown, such as one saved since it loaded
+ * the file (CAMEL-25771): the live run data is of other lines then, so it
is dimmed and the body types are left
+ * out.
+ */
+ void setStale(BooleanSupplier stale) {
+ this.stale = stale;
+ }
+
+ /** Whether what the integration did is of the lines shown: the file is
not edited, and it runs as shown. */
+ private boolean current() {
+ return !dirty.getAsBoolean() && !stale.getAsBoolean();
}
/**
@@ -105,7 +144,7 @@ class SourceLiveColumn {
* of each frame asks it; the exception is only read on Shift+F8).
*/
boolean failsAtRuntime(int row) {
- if (dirty.getAsBoolean() || row < 0) {
+ if (!current() || row < 0) {
return false;
}
LiveLine live = liveLines().get(row);
@@ -130,6 +169,11 @@ class SourceLiveColumn {
Style totalStyle = data ? Theme.label() : Style.EMPTY;
Style failedStyle = Theme.error().bold();
Style dim = Style.EMPTY.dim();
+ if (stale.getAsBoolean()) {
+ // of the version of the file that runs, whose lines may be others
+ totalStyle = dim;
+ failedStyle = dim;
+ }
if (selBg != null) {
totalStyle = totalStyle.patch(selBg);
failedStyle = failedStyle.patch(selBg);
@@ -145,6 +189,61 @@ class SourceLiveColumn {
spans.add(Span.styled(" │", dim));
}
+ /**
+ * The gutter of a line: a ⚠ when a step on it has been reached with
bodies of more than one kind, which needs
+ * attention (CAMEL-25897); blank otherwise.
+ */
+ Span gutter(int lineIndex) {
+ LiveLine live = current() ? liveLines().get(lineIndex) : null;
+ return live != null && live.mixed() ? Span.styled(" " + BodyTypes.MARK
+ " ", Theme.warning()) : Span.raw(" ");
+ }
+
+ /**
+ * The body types the steps of a line have been reached with, at the end
of the line, dimmed (a warning when they
+ * are of more than one kind), lined up with those of the other lines, and
cut to the room up to the right edge
+ * (CAMEL-25897). Nothing in the plain mode, or when the view does not
have the version of the file that runs.
+ *
+ * @param spans the spans of the line so far: the gutter, the line
number, the live run data and the code
+ * @param rightEdge the column of the right edge of the view, in the
columns of the line
+ */
+ void addBodyTypes(List<Span> spans, int lineIndex, boolean plain, Style
selBg, int rightEdge) {
+ LiveLine live = plain || !current() ? null :
liveLines().get(lineIndex);
+ if (live == null || live.bodyTypes().isEmpty()) {
+ return;
+ }
+ int width = 0;
+ for (Span span : spans) {
+ width += CharWidth.of(span.content());
+ }
+ int pad = Math.max(2, bodyTypesAlign - width);
+ String text = BodyTypes.fit(live.bodyTypes(), live.mixed() ?
live.bodyKinds() : List.of(), rightEdge - width - pad);
+ if (text.isEmpty()) {
+ return;
+ }
+ Style style = live.mixed() ? Theme.warning() : Theme.muted();
+ spans.add(Span.styled(" ".repeat(pad), selBg != null ? selBg :
Style.EMPTY));
+ spans.add(Span.styled(text, selBg != null ? style.patch(selBg) :
style));
+ }
+
+ /**
+ * The body types of the steps of a line for the quick documentation
(CAMEL-25897), all of them, and why a mix of
+ * kinds needs attention; null when the line has none, or the editor does
not have the version of the file that
+ * runs.
+ */
+ SourceViewer.DocEntry bodyTypesDoc(int row) {
+ LiveLine live = current() ? liveLines().get(row) : null;
+ if (live == null || live.bodyTypes().isEmpty()) {
+ return null;
+ }
+ if (live.mixed()) {
+ return SourceViewer.DocEntry.withTitle("Body types",
BodyTypes.names(live.bodyTypes()) + " " + BodyTypes.MARK
+ + " " +
BodyTypes.kinds(live.bodyKinds())
+ + ": the body
here depends on the path a message takes");
+ }
+ String title = live.bodyKinds().size() == 1 ? "Body types (" +
live.bodyKinds().get(0) + ")" : "Body types";
+ return SourceViewer.DocEntry.withTitle(title,
BodyTypes.names(live.bodyTypes()));
+ }
+
/** The live run data of the loaded file, read again at most once a second
(the status is polled about as often). */
private Map<Integer, LiveLine> liveLines() {
String loadedFilePath = loadedFile.get();
@@ -152,7 +251,7 @@ class SourceLiveColumn {
return Collections.emptyMap();
}
long now = MonotonicClock.millis();
- if (now - liveLinesTime > 1000) {
+ if (now - liveLinesTime > 1000 ||
!loadedFilePath.equals(liveLinesFile)) {
refreshLiveLines(now);
}
return liveLines;
@@ -178,6 +277,7 @@ class SourceLiveColumn {
Map<Integer, LiveLine> lines = liveRunData.lines(loadedFilePath);
liveLines = lines != null ? lines : Collections.emptyMap();
liveLinesTime = now;
+ liveLinesFile = loadedFilePath;
// the widths of the column, for all the lines alike; they only
grow while the same file is shown (from
// a minimum), so the column does not jitter when a number crosses
99 -> 100 and back (a mean time going
// up and down, a reset of the statistics); another file, or the
integration stopping, starts afresh
@@ -198,6 +298,32 @@ class SourceLiveColumn {
}
}
}
+ bodyTypesAlign = bodyTypesAlign();
+ }
+ }
+
+ /**
+ * The column the body types line up at: after the longest of the lines
that have them (the gutter, the line number,
+ * the live run data and the code), and at most {@link
#MAX_BODY_TYPES_ALIGN} into the code, so a long line does not
+ * push the body types of the others off screen.
+ */
+ private int bodyTypesAlign() {
+ List<String> shown = lines.get();
+ int longest = 0;
+ for (Map.Entry<Integer, LiveLine> e : liveLines.entrySet()) {
+ int i = e.getKey();
+ if (!e.getValue().bodyTypes().isEmpty() && shown != null && i >= 0
&& i < shown.size()) {
+ longest = Math.max(longest,
Math.min(CharWidth.of(shown.get(i)), MAX_BODY_TYPES_ALIGN));
+ }
+ }
+ if (longest == 0) {
+ return 0;
}
+ // the gutter before the line number, and the live run data after it
+ int live = liveTotalWidth == 0
+ ? 0
+ : 1 + liveTotalWidth + (liveFailedWidth > 0 ? 1 +
liveFailedWidth : 0)
+ + (liveMeanWidth > 0 ? 1 + liveMeanWidth : 0) + 2;
+ return 3 + longest + live + 3;
}
}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunState.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunState.java
index 76ee51aaed46..e038144d3741 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunState.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunState.java
@@ -21,6 +21,8 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicLong;
import dev.tamboui.layout.Rect;
import dev.tamboui.terminal.Frame;
@@ -45,7 +47,9 @@ import org.apache.camel.util.json.JsonObject;
* <li>after a reload, a route started after the save runs what is saved. The
integration does not say when it loaded a
* route, so a route stopped and started again by hand after a reload is taken
as loaded then.</li>
* </ul>
- * A failed reload puts back the routes that ran before it, so until a reload
succeeds the state is the reload error.
+ * A failed reload puts back the routes that ran before it, so until a reload
succeeds the state is the reload error. A
+ * file whose routes are all stopped is shown as such; the facts of its lines
still hold when the integration loaded it
+ * as saved, which is known without a reload, but not for a route stopped
after a reload.
*/
final class SourceRunState {
@@ -60,13 +64,15 @@ final class SourceRunState {
/** the last reload failed, so the routes that ran before it run */
RELOAD_FAILED,
/** saved after the routes were loaded, and the integration did not
load it again */
- NOT_RUNNING
+ NOT_RUNNING,
+ /** the routes the integration has from the file are all stopped (or
suspended) */
+ STOPPED
}
/**
* @param kind the state
* @param routes the routes the integration has from the file
- * @param stale how many of them were loaded before the file was saved
+ * @param stale how many of them were loaded before the file was saved,
or at a time not known
* @param detail why not running, or the reload error; null when running
*/
record State(Kind kind, int routes, int stale, String detail) {
@@ -90,21 +96,23 @@ final class SourceRunState {
// an integration that has not reloaded loaded its routes when it
started; after a reload, when each started
boolean reloaded = info.reloaded > 0 || info.reloadError != null ||
info.uptime <= 0;
int routes = 0;
+ int started = 0;
int stale = 0;
+ int unknown = 0;
for (RouteInfo r : info.routes) {
- if (!"Started".equals(r.state) || LiveRunLines.lineOf(r.source,
name) < 0) {
+ if (LiveRunLines.lineOf(r.source, name) < 0) {
continue;
}
- long loaded = info.uptime;
- if (reloaded) {
- long uptime = uptimeMillis(r.uptime);
- if (uptime < 0) {
- continue;
- }
- loaded = Math.max(info.uptime, now - uptime);
- }
routes++;
- if (loaded < fileModified) {
+ boolean running = "Started".equals(r.state);
+ if (running) {
+ started++;
+ }
+ long uptime = running ? uptimeMillis(r.uptime) : -1;
+ if (reloaded && uptime < 0) {
+ // a stopped route after a reload: when it was loaded is not
known, so it may be of another version
+ unknown++;
+ } else if ((reloaded ? Math.max(info.uptime, now - uptime) :
info.uptime) < fileModified) {
stale++;
}
}
@@ -112,11 +120,13 @@ final class SourceRunState {
return null;
}
if (info.reloadError != null) {
- return new State(Kind.RELOAD_FAILED, routes, stale,
info.reloadError);
+ return new State(Kind.RELOAD_FAILED, routes, stale + unknown,
info.reloadError);
}
if (stale == 0) {
- return new State(Kind.RUNNING, routes, 0, null);
+ // what a route stopped by hand says of the lines holds when it is
the version that was loaded
+ return new State(started == 0 ? Kind.STOPPED : Kind.RUNNING,
routes, unknown, null);
}
+ stale += unknown;
if (info.devMode) {
if (now - fileModified < RELOAD_GRACE_MILLIS) {
return new State(Kind.RELOADING, routes, stale, null);
@@ -150,6 +160,7 @@ final class SourceRunState {
case NOT_RUNNING -> Line.from(
Span.styled(TuiIcons.IDLE + " not running yet",
Theme.warning()),
Span.styled(": " + state.detail(), Theme.muted()));
+ case STOPPED -> Line.from(Span.styled(TuiIcons.IDLE + " the routes
of this file are stopped", Theme.muted()));
};
}
@@ -161,6 +172,7 @@ final class SourceRunState {
case RELOADING -> "reloading";
case RELOAD_FAILED -> "reloadFailed";
case NOT_RUNNING -> "notRunning";
+ case STOPPED -> "stopped";
});
jo.put("routes", state.routes());
jo.put("routesLoadedBeforeSave", state.stale());
@@ -176,44 +188,101 @@ final class SourceRunState {
*/
static final class Tracker {
+ /** The state computed for a file: null when the integration has no
route from it. */
private record Computed(String file, State state) {
}
private volatile String file;
private volatile Computed computed;
+ // a save in the editor makes the state unknown until a refresh after
it computes it again (written by the UI
+ // thread only)
+ private volatile int saves;
+ private volatile int computedSaves;
+ private boolean wasDirty;
+ // the refreshes in the order they started: a later one wins over one
that ends after it (guarded by this)
+ private final AtomicLong refreshes = new AtomicLong();
+ private long written;
- /** The file the source panel shows, on the UI thread when it draws;
null when none is. */
- void shown(String file) {
+ /**
+ * The file the source panel shows, on the UI thread when it draws;
null when none is. Edits that are no longer
+ * unsaved were saved (or discarded): the state is unknown until the
next refresh.
+ *
+ * @return whether the state is to be computed now: another file is
shown, or the file was saved
+ */
+ boolean shown(String file, boolean dirty) {
+ boolean saved = wasDirty && !dirty;
+ if (saved) {
+ saves++;
+ }
+ wasDirty = dirty;
+ boolean other = !Objects.equals(file, this.file);
this.file = file;
+ return file != null && (saved || other);
}
- /** Computes the state of the file shown, on the background thread of
the data refresh. */
+ /**
+ * Computes the state of the file shown, off the UI thread: on the
data refresh, when another file is shown or
+ * the file is saved, and when tui_get_table reads the tab.
+ */
void refresh(IntegrationInfo info) {
+ long order = refreshes.incrementAndGet();
+ int at = saves;
String shown = file;
- if (shown == null || info == null) {
- computed = null;
- return;
+ Computed result = null;
+ if (shown != null && info != null) {
+ try {
+ // the file system keeps the time of the save by the wall
clock, so it is compared with the wall clock
+ long modified =
Files.getLastModifiedTime(Path.of(shown)).toMillis();
+ result = new Computed(shown, of(info, shown, modified,
System.currentTimeMillis()));
+ } catch (IOException | RuntimeException e) {
+ // not known
+ }
}
- try {
- // the file system keeps the time of the save by the wall
clock, so it is compared with the wall clock
- long modified =
Files.getLastModifiedTime(Path.of(shown)).toMillis();
- State state = of(info, shown, modified,
System.currentTimeMillis());
- computed = state != null ? new Computed(shown, state) : null;
- } catch (IOException | RuntimeException e) {
- computed = null;
+ synchronized (this) {
+ if (order > written) {
+ written = order;
+ computed = result;
+ computedSaves = at;
+ }
}
}
void reset() {
file = null;
- computed = null;
+ synchronized (this) {
+ // a refresh that started before does not write after
+ written = refreshes.incrementAndGet();
+ computed = null;
+ computedSaves = saves;
+ }
}
- /** The state of the file shown; null when unknown, or when it was
computed for another file. */
+ /**
+ * The state of the file shown; null when unknown: computed for
another file, or before the last save in the
+ * editor.
+ */
State current() {
Computed c = computed;
String shown = file;
- return c != null && c.file().equals(shown) ? c.state() : null;
+ return c != null && c.file().equals(shown) && computedSaves ==
saves ? c.state() : null;
+ }
+
+ /**
+ * Whether the integration may run another version of the file than
the one shown, so the live run data is of
+ * other lines (CAMEL-25897): not known yet for the file shown (just
opened, or saved in the editor since),
+ * saved after the integration loaded it, a failed reload, or a route
stopped after a reload.
+ */
+ boolean stale() {
+ Computed c = computed;
+ String shown = file;
+ if (shown == null) {
+ return false;
+ }
+ if (c == null || !c.file().equals(shown) || computedSaves !=
saves) {
+ return true;
+ }
+ State state = c.state();
+ return state != null && (state.kind() == Kind.RELOAD_FAILED ||
state.stale() > 0);
}
/** The state on the bottom border of the source panel, left of the
cursor position of the editor. */
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
index 35109301f038..1031a67a5b69 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceTab.java
@@ -165,11 +165,13 @@ class SourceTab extends AbstractTab {
}
});
sourceViewer.setOnFileCreated(this::refreshFiles);
- // the live run data of the selected integration at the end of the
lines of its source
+ // the live run data of the selected integration at the end of the
lines of its source, and the body types of
+ // its steps; dimmed and left out while it runs another version of the
file (CAMEL-25897)
sourceViewer.liveColumn().setLiveRunData(path -> {
IntegrationInfo info = ctx.findSelectedIntegration();
return info != null ? LiveRunLines.of(info.routes, path) :
Map.of();
});
+ sourceViewer.liveColumn().setStale(runState::stale);
// the exception of the last failure on a line, for fix with AI
(Shift+F8) on a line that fails at runtime
sourceViewer.liveColumn().setLineFailures((path, line) -> {
IntegrationInfo info = ctx.findSelectedIntegration();
@@ -1208,10 +1210,13 @@ class SourceTab extends AbstractTab {
sourceViewer.setFocused(focusOnViewer);
sourceViewer.render(frame, area);
// whether the integration runs the file shown as it is saved, in
the view and in the editor (CAMEL-25771)
- runState.shown(sourceViewer.getCurrentFilePath());
+ if (runState.shown(sourceViewer.getCurrentFilePath(),
sourceViewer.isDirty())) {
+ // another file, or a save: its run state now, rather than at
the next refresh
+ ctx.backgroundExecutor.execute(() ->
runState.refresh(ctx.findSelectedIntegration()));
+ }
runState.render(frame, area);
} else {
- runState.shown(null);
+ runState.shown(null, false);
List<Line> lines = emptySourceLines(routeIndex, rootDir);
frame.renderWidget(
@@ -1641,8 +1646,8 @@ class SourceTab extends AbstractTab {
}
/**
- * The quick doc of a line with what the project knows about it put first:
the values of its property placeholders
- * and where the bean it refers to is declared.
+ * The quick doc of a line with what the project knows about it put first:
the body types its steps have been
+ * reached with, the values of its property placeholders and where the
bean it refers to is declared.
*/
private SourceViewer.EditQuickDocProvider
withProjectDocs(SourceViewer.EditQuickDocProvider provider) {
return (lines, row) -> {
@@ -1650,7 +1655,13 @@ class SourceTab extends AbstractTab {
if (row < 0 || row >= lines.size()) {
return base;
}
- List<SourceViewer.DocEntry> extra = new
ArrayList<>(assist.placeholderDocs(lines.get(row)));
+ List<SourceViewer.DocEntry> extra = new ArrayList<>();
+ // the body types the steps of the line have been reached with,
while it runs as shown (CAMEL-25897)
+ SourceViewer.DocEntry bodyTypes =
sourceViewer.liveColumn().bodyTypesDoc(row);
+ if (bodyTypes != null) {
+ extra.add(bodyTypes);
+ }
+ extra.addAll(assist.placeholderDocs(lines.get(row)));
ProjectBeans.Location bean = projectBeans().refOn(lines.get(row));
if (bean != null) {
extra.add(new
SourceViewer.DocEntry(ProjectBeans.describe(bean), false, "Bean"));
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
index 0a06ded5bda5..f696cd38d0de 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceViewer.java
@@ -196,7 +196,8 @@ class SourceViewer {
dirty = true;
}
});
- private final SourceLiveColumn liveColumn = new SourceLiveColumn(() ->
this.loadedFilePath, () -> this.dirty);
+ private final SourceLiveColumn liveColumn
+ = new SourceLiveColumn(() -> this.loadedFilePath, () ->
this.dirty, () -> this.lines);
private final SourceRefactorActions refactor = new
SourceRefactorActions(editState, new SourceRefactorActions.Editor() {
@Override
public Path editableFile() {
@@ -2829,9 +2830,7 @@ class SourceViewer {
? Span.styled(" ⚠ ", Theme.warning().bold())
: Span.styled(" ✗ ", Theme.error().bold()));
} else {
- spans.add(isDeprecated
- ? Span.styled(" ⚠ ", Theme.warning())
- : Span.raw(" "));
+ spans.add(isDeprecated ? Span.styled(" ⚠ ", Theme.warning()) :
liveColumn.gutter(lineIndex));
}
if (!prefix.isEmpty()) {
spans.add(Span.styled(prefix, Style.EMPTY.dim()));
@@ -2848,6 +2847,7 @@ class SourceViewer {
}
spans.add(Span.styled(" ↵ " + jl.routeId(), linkStyle));
}
+ liveColumn.addBodyTypes(spans, lineIndex, plainMode, isSelected ?
selBg : null, viewportWidth - 1 + hSkip);
Line full = Line.from(spans);
if (hSkip > 0) {
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
index 20faa055005f..340c3775f65c 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/resources/tui/help/source.md
@@ -51,6 +51,18 @@ processors on each line do: the exchanges they handled
(`79`), how many failed
its width, so the code does not move as the numbers grow. The source reads as
a heat map of the route: where messages go, where they fail.
+With the message history on (such as `camel run --dev`), the body types each
+step has been reached with go at the end of its line, dimmed and lined up
+(`ByteArrayInputStreamCache`, `String | byte[]`). A step reached with bodies of
+more than one kind (null, payload, data or collection) needs attention, as its
+body depends on the path a message takes: it has a `⚠` in the gutter and its
+types in the warning color (`⚠ byte[] | LinkedHashMap (payload and data)`).
The quick
+documentation of the line lists them all.
+
+The live run data and the body types are of the version of the file the
+integration runs: while it is not running as saved (see below), the numbers
+are dimmed and the body types are left out, as the lines may be others.
+
## Running as Saved
The bottom border of the source says whether the integration runs the routes
of the file as it is saved:
@@ -61,6 +73,9 @@ of the file as it is saved:
reload the file
- `⚠ the reload failed` — the integration runs what it ran before the reload,
until a reload succeeds
+- `○ the routes of this file are stopped` — stopped (or suspended) by hand: the
+ live run data and the body types still show when the integration loaded the
+ file as saved, which is not known for a route stopped after a reload
It compares when the file was saved with when the integration loaded the
routes it has from it, and shows nothing for a file it has no route from.
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceBodyTypesTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceBodyTypesTest.java
new file mode 100644
index 000000000000..3487ad7eee8e
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceBodyTypesTest.java
@@ -0,0 +1,303 @@
+/*
+ * 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.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.FileTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import dev.tamboui.buffer.Buffer;
+import dev.tamboui.layout.Rect;
+import dev.tamboui.terminal.Frame;
+import dev.tamboui.tui.event.KeyEvent;
+import org.apache.camel.dsl.jbang.core.commands.tui.SourceLiveColumn.LiveLine;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The body types each step has been reached with, at the end of its line in
the source view, with a ⚠ in the gutter of
+ * a step reached with bodies of more than one kind (CAMEL-25897).
+ */
+class SourceBodyTypesTest {
+
+ private static final String ROUTE = """
+ - route:
+ id: orders
+ from:
+ uri: timer:orders
+ steps:
+ - unmarshal:
+ json: {}
+ - choice:
+ when:
+ - simple: "${body[priority]} == 1"
+ steps:
+ - marshal:
+ json: {}
+ - to:
+ uri: log:outbox
+ """;
+
+ private static final int UNMARSHAL = 5;
+ private static final int CHOICE = 7;
+ private static final int TO = 13;
+
+ @TempDir
+ Path dir;
+
+ private final Map<Integer, LiveLine> data = new HashMap<>();
+ private SourceViewer viewer;
+ private Path file;
+
+ @BeforeEach
+ void open() throws Exception {
+ Theme.resetForTesting();
+ file = Files.writeString(dir.resolve("orders.camel.yaml"), ROUTE);
+ viewer = new SourceViewer();
+ viewer.loadFile(file);
+ viewer.liveColumn().setLiveRunData(path -> data);
+ data.put(UNMARSHAL,
line(List.of("org.apache.camel.converter.stream.ByteArrayInputStreamCache"),
List.of("payload"),
+ false));
+ data.put(CHOICE, line(List.of("java.util.LinkedHashMap"),
List.of("data"), false));
+ data.put(TO, line(List.of("byte[]", "java.util.LinkedHashMap"),
List.of("payload", "data"), true));
+ viewer.liveColumn().refreshLiveLinesForTesting();
+ }
+
+ @Test
+ void theBodyTypesOfAStepAtTheEndOfItsLineLinedUp() {
+ String[] rows = render(120).split("\n");
+
+ String unmarshal = row(rows, "- unmarshal:");
+ String choice = row(rows, "- choice:");
+ String to = row(rows, "- to:");
+ assertThat(unmarshal).contains("ByteArrayInputStreamCache");
+ assertThat(choice).contains("LinkedHashMap");
+ assertThat(to).contains(BodyTypes.MARK + " byte[] | LinkedHashMap
(payload and data)");
+ // in a column of their own
+ assertThat(unmarshal.indexOf("ByteArrayInputStreamCache"))
+ .isEqualTo(choice.indexOf("LinkedHashMap"))
+ .isEqualTo(to.indexOf(BodyTypes.MARK + " byte[]"));
+ // the step reached with bodies of more than one kind has a ⚠ in the
gutter, the others not
+ assertThat(to.substring(0, 4)).contains(BodyTypes.MARK);
+ assertThat(unmarshal.substring(0, 4)).doesNotContain(BodyTypes.MARK);
+ // a line without a step has nothing
+ assertThat(row(rows, "json:
{}")).doesNotContain("ByteArrayInputStreamCache").doesNotContain("LinkedHashMap");
+ }
+
+ @Test
+ void theTypesAreCutToTheRoomThereIs() {
+ String to = row(render(56).split("\n"), "- to:");
+
+ assertThat(to).contains(BodyTypes.MARK + "
byte[]").doesNotContain("(payload and data)");
+ }
+
+ @Test
+ void aCutNameKeepsItsEllipsisAndTheCountItsDigits() {
+ // the last column of the view is the scrollbar's: what is cut ends
before it
+ boolean cut = false;
+ for (int width = 44; width <= 72; width++) {
+ String[] rows = render(width).split("\n");
+ String unmarshal = row(rows, "- unmarshal:");
+ if (unmarshal.contains("ByteArr") &&
!unmarshal.contains("ByteArrayInputStreamCache")) {
+ assertThat(unmarshal).as("width %d", width).contains("…");
+ cut = true;
+ }
+ String to = row(rows, "- to:");
+ if (to.contains(" +")) {
+ assertThat(to).as("width %d", width).contains(" +1");
+ }
+ }
+ assertThat(cut).isTrue();
+ }
+
+ @Test
+ void anotherFileHasItsOwnLiveRunDataAtOnce() throws Exception {
+ Path other = Files.writeString(dir.resolve("stock.camel.yaml"), "-
from:\n uri: timer:stock\n");
+ Map<String, Map<Integer, LiveLine>> byFile = Map.of(
+ file.toString(), Map.of(0, new LiveLine(120, 0, 0)),
+ other.toString(), Map.of(0, new LiveLine(7, 0, 0)));
+ viewer.liveColumn().setLiveRunData(byFile::get);
+ viewer.liveColumn().refreshLiveLinesForTesting();
+
assertThat(viewer.liveColumn().liveColumnForTesting(0)).contains("120");
+
+ // not the numbers of the file shown before for up to a second
+ viewer.loadFile(other);
+
assertThat(viewer.liveColumn().liveColumnForTesting(0)).contains("7").doesNotContain("120");
+ }
+
+ @Test
+ void anotherFileIsStaleUntilItsStateIsKnown() throws Exception {
+ Path other = Files.writeString(dir.resolve("stock.camel.yaml"), "-
from:\n uri: timer:stock\n");
+ long now = System.currentTimeMillis();
+ Files.setLastModifiedTime(file, FileTime.fromMillis(now - 3_600_000));
+ IntegrationInfo info = new IntegrationInfo();
+ info.uptime = now - 60_000;
+ RouteInfo route = new RouteInfo();
+ route.routeId = "orders";
+ route.source = "orders.camel.yaml:1";
+ route.state = "Started";
+ route.uptime = "59s";
+ info.routes.add(route);
+ SourceRunState.Tracker tracker = new SourceRunState.Tracker();
+
+ // shown: computed at once
+ assertThat(tracker.shown(file.toString(), false)).isTrue();
+ assertThat(tracker.stale()).isTrue();
+ tracker.refresh(info);
+ assertThat(tracker.stale()).isFalse();
+ assertThat(tracker.shown(file.toString(), false)).isFalse();
+
+ // another file: not known until computed, then known to have no route
of the integration
+ assertThat(tracker.shown(other.toString(), false)).isTrue();
+ assertThat(tracker.stale()).isTrue();
+ tracker.refresh(info);
+ assertThat(tracker.current()).isNull();
+ assertThat(tracker.stale()).isFalse();
+
+ // a save is computed at once too
+ assertThat(tracker.shown(other.toString(), true)).isFalse();
+ assertThat(tracker.shown(other.toString(), false)).isTrue();
+ }
+
+ @Test
+ void nothingInThePlainMode() {
+ viewer.handleKeyEvent(KeyEvent.ofChar('p'));
+
+ String rendered = render(120);
+
+ assertThat(rendered).contains("-
unmarshal:").doesNotContain("ByteArrayInputStreamCache");
+ }
+
+ @Test
+ void nothingWhileTheIntegrationRunsAnotherVersionOfTheFile() {
+ data.put(TO, new LiveLine(10, 2, 0, List.of("byte[]",
"java.util.LinkedHashMap"), List.of("payload", "data"), true));
+ viewer.liveColumn().refreshLiveLinesForTesting();
+ assertThat(viewer.liveColumn().failsAtRuntime(TO)).isTrue();
+
+ // saved after the integration loaded it: the lines it ran may be
others
+ viewer.liveColumn().setStale(() -> true);
+ String[] rows = render(120).split("\n");
+
+ assertThat(row(rows, "-
unmarshal:")).doesNotContain("ByteArrayInputStreamCache");
+ String to = row(rows, "- to:");
+ assertThat(to).doesNotContain("byte[]");
+ assertThat(to.substring(0, 4)).doesNotContain(BodyTypes.MARK);
+ // the live run data stays, dimmed, and no line is offered to fix as
failing at runtime
+ assertThat(to).contains("10");
+ assertThat(viewer.liveColumn().failsAtRuntime(TO)).isFalse();
+ assertThat(viewer.liveColumn().bodyTypesDoc(TO)).isNull();
+ }
+
+ @Test
+ void theQuickDocumentationHasThemAll() {
+ SourceViewer.DocEntry to = viewer.liveColumn().bodyTypesDoc(TO);
+ assertThat(to.title()).isEqualTo("Body types");
+ assertThat(to.text()).isEqualTo("byte[] | LinkedHashMap " +
BodyTypes.MARK
+ + " payload and data: the body here
depends on the path a message takes");
+
+ SourceViewer.DocEntry unmarshal =
viewer.liveColumn().bodyTypesDoc(UNMARSHAL);
+ assertThat(unmarshal.title()).isEqualTo("Body types (payload)");
+ assertThat(unmarshal.text()).isEqualTo("ByteArrayInputStreamCache");
+
+ assertThat(viewer.liveColumn().bodyTypesDoc(UNMARSHAL + 1)).isNull();
+ }
+
+ @Test
+ void theStepsOfALineTogether() {
+ RouteInfo route = new RouteInfo();
+ route.source = "MyRoute.java:10";
+ // two steps on one line, each with a kind of its own: the line is not
mixed
+ route.processors.add(processor("MyRoute.java:12",
List.of("java.lang.String"), List.of("payload")));
+ route.processors.add(processor("MyRoute.java:12",
List.of("java.util.LinkedHashMap"), List.of("data")));
+ route.processors.add(processor("MyRoute.java:14", List.of("byte[]",
"java.util.LinkedHashMap"),
+ List.of("payload", "data")));
+ route.processors.add(processor("MyRoute.java:15", List.of(),
List.of()));
+
+ Map<Integer, LiveLine> lines = LiveRunLines.of(List.of(route),
"/work/MyRoute.java");
+
+
assertThat(lines.get(11).bodyTypes()).containsExactly("java.lang.String",
"java.util.LinkedHashMap");
+ assertThat(lines.get(11).mixed()).isFalse();
+ assertThat(lines.get(13).mixed()).isTrue();
+ assertThat(lines.get(14).bodyTypes()).isEmpty();
+ assertThat(lines.get(14)).isEqualTo(new LiveLine(0, 0, 0));
+ }
+
+ @Test
+ void aSaveInTheEditorLeavesThemOutUntilTheIntegrationIsSeenToLoadIt()
throws Exception {
+ long now = System.currentTimeMillis();
+ Files.setLastModifiedTime(file, FileTime.fromMillis(now - 3_600_000));
+ IntegrationInfo info = new IntegrationInfo();
+ info.uptime = now - 60_000;
+ RouteInfo route = new RouteInfo();
+ route.routeId = "orders";
+ route.source = "orders.camel.yaml:1";
+ route.state = "Started";
+ route.uptime = "59s";
+ info.routes.add(route);
+ SourceRunState.Tracker tracker = new SourceRunState.Tracker();
+ tracker.shown(file.toString(), false);
+ tracker.refresh(info);
+ assertThat(tracker.stale()).isFalse();
+
assertThat(tracker.current().kind()).isEqualTo(SourceRunState.Kind.RUNNING);
+
+ // edited, then saved
+ tracker.shown(file.toString(), true);
+ tracker.shown(file.toString(), false);
+ assertThat(tracker.stale()).isTrue();
+ assertThat(tracker.current()).isNull();
+
+ // the next refresh knows again: saved now, after the integration
loaded the file
+ Files.setLastModifiedTime(file, FileTime.fromMillis(now));
+ tracker.refresh(info);
+
assertThat(tracker.current().kind()).isEqualTo(SourceRunState.Kind.NOT_RUNNING);
+ assertThat(tracker.stale()).isTrue();
+ }
+
+ private String render(int width) {
+ Rect area = new Rect(0, 0, width, 24);
+ Buffer buffer = Buffer.empty(area);
+ viewer.render(Frame.forTesting(buffer), area);
+ return TuiTestHelper.bufferToString(buffer);
+ }
+
+ private static String row(String[] rows, String code) {
+ for (String row : rows) {
+ if (row.contains(code)) {
+ return row;
+ }
+ }
+ throw new AssertionError("No row with " + code);
+ }
+
+ private static LiveLine line(List<String> types, List<String> kinds,
boolean mixed) {
+ return new LiveLine(10, 0, 0, types, kinds, mixed);
+ }
+
+ private static ProcessorInfo processor(String source, List<String>
bodyTypes, List<String> bodyKinds) {
+ ProcessorInfo proc = new ProcessorInfo();
+ proc.source = source;
+ proc.bodyTypes = bodyTypes;
+ proc.bodyKinds = bodyKinds;
+ return proc;
+ }
+}
diff --git
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunStateTest.java
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunStateTest.java
index 9539dea0cc03..983dd5d9ba97 100644
---
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunStateTest.java
+++
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/SourceRunStateTest.java
@@ -132,9 +132,8 @@ class SourceRunStateTest {
}
@Test
- void onlyTheStartedRoutesOfTheFile() {
+ void onlyTheRoutesOfTheFile() {
route("stock", "stock.camel.yaml:3", "59s");
- route("orders", "orders.camel.yaml:3", "59s").state = "Stopped";
assertThat(SourceRunState.of(info, FILE, NOW - 10_000, NOW)).isNull();
@@ -142,6 +141,28 @@ class SourceRunStateTest {
assertThat(SourceRunState.of(info, "/work/README.md", NOW - 10_000,
NOW)).isNull();
}
+ @Test
+ void aFileWhoseRoutesAreStopped() {
+ RouteInfo orders = route("orders", "orders.camel.yaml:3", "");
+ orders.state = "Stopped";
+
+ // loaded when the integration started, as saved: what its lines say
holds
+ SourceRunState.State state = SourceRunState.of(info, FILE, NOW -
3_600_000, NOW);
+ assertThat(state.kind()).isEqualTo(SourceRunState.Kind.STOPPED);
+ assertThat(state.stale()).isZero();
+ assertThat(text(SourceRunState.line(state))).isEqualTo(TuiIcons.IDLE +
" the routes of this file are stopped");
+
assertThat(SourceRunState.toJson(state).getString("state")).isEqualTo("stopped");
+
+ // saved since: the saved file is not running, stopped or not
+ assertThat(SourceRunState.of(info, FILE, NOW - 10_000,
NOW).kind()).isEqualTo(SourceRunState.Kind.NOT_RUNNING);
+
+ // stopped after a reload: when it was loaded is not known
+ info.reloaded = 1;
+ state = SourceRunState.of(info, FILE, NOW - 3_600_000, NOW);
+ assertThat(state.kind()).isEqualTo(SourceRunState.Kind.STOPPED);
+ assertThat(state.stale()).isEqualTo(1);
+ }
+
@Test
void theUptimeOfARoute() {
assertThat(SourceRunState.uptimeMillis("0s")).isZero();