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 5df38cd33451 CAMEL-23865: Re-apply TUI chart throughput scaling lost 
during refactoring
5df38cd33451 is described below

commit 5df38cd334518c87f1447142a48a5091778c6f9c
Author: Guillaume Nodet <[email protected]>
AuthorDate: Wed Jul 8 07:06:33 2026 +0200

    CAMEL-23865: Re-apply TUI chart throughput scaling lost during refactoring
    
    Re-applies the TUI chart-side throughput scaling lost when commit 
065fd8314d9
    (mouse support refactoring) rewrote MetricsCollector, OverviewTab, and
    EndpointsTab. The backend EWMA calculation survived but the TUI rendering
    regressed to integer division, causing sub-1.0 msg/s workloads to show an
    empty chart. Fixes: scale throughput by 100x to preserve fractional rates,
    add formatThroughput()/niceMax() helpers, clamp failed to total in chart
    title, and add render tests to prevent future regressions.
    
    Closes #24502
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../dsl/jbang/core/commands/tui/EndpointsTab.java  |   8 +-
 .../jbang/core/commands/tui/MetricsCollector.java  | 102 +++++++++--
 .../dsl/jbang/core/commands/tui/OverviewTab.java   |  43 +++--
 .../tui/MetricsCollectorThroughputTest.java        | 114 +++++++++++++
 .../tui/OverviewTabThroughputRenderTest.java       | 186 +++++++++++++++++++++
 5 files changed, 412 insertions(+), 41 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
index 53680458b40b..605b1473c2b9 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/EndpointsTab.java
@@ -524,9 +524,9 @@ class EndpointsTab extends AbstractTableTab {
 
         Line chartTitle = Line.from(
                 Span.styled("▬", 
Style.EMPTY.fg(Color.ansi(AnsiColor.BRIGHT_GREEN))),
-                Span.raw(String.format(" in:%-4d ", curIn)),
+                Span.raw(String.format(" in:%-4s ", 
MetricsCollector.formatThroughput(curIn))),
                 Span.styled("▬", Style.EMPTY.fg(Color.CYAN)),
-                Span.raw(String.format(" out:%-4d msg/s", curOut)));
+                Span.raw(String.format(" out:%-4s msg/s", 
MetricsCollector.formatThroughput(curOut))));
 
         Rect rightArea = hParts.get(1);
         frame.renderWidget(DualSparkline.builder()
@@ -637,9 +637,9 @@ class EndpointsTab extends AbstractTableTab {
                 Span.styled(uriLabel, Style.EMPTY.fg(Color.YELLOW)),
                 Span.raw("] "),
                 Span.styled("▬", 
Style.EMPTY.fg(Color.ansi(AnsiColor.BRIGHT_GREEN))),
-                Span.raw(String.format(" in:%-4d ", curIn)),
+                Span.raw(String.format(" in:%-4s ", 
MetricsCollector.formatThroughput(curIn))),
                 Span.styled("▬", Style.EMPTY.fg(Color.CYAN)),
-                Span.raw(String.format(" out:%-4d msg/s", curOut)));
+                Span.raw(String.format(" out:%-4s msg/s", 
MetricsCollector.formatThroughput(curOut))));
 
         frame.renderWidget(DualSparkline.builder()
                 .topData(inArr)
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
index 9670b766d22f..fe752fd47f42 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollector.java
@@ -19,6 +19,7 @@ package org.apache.camel.dsl.jbang.core.commands.tui;
 import java.time.Duration;
 import java.util.LinkedHashMap;
 import java.util.LinkedList;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.concurrent.ConcurrentHashMap;
@@ -33,6 +34,8 @@ class MetricsCollector {
     static final int MAX_ENDPOINT_CHART_POINTS = 300;
     static final int MAX_HEAP_HISTORY_POINTS = 120;
     static final long HEAP_SAMPLE_INTERVAL_MS = 5000;
+    // Throughput values are stored scaled by this factor so sub-1.0 msg/s 
rates are preserved as longs
+    static final long THROUGHPUT_SCALE = 100;
 
     // Throughput history per PID (one point per second)
     private final Map<String, LinkedList<Long>> throughputHistory = new 
ConcurrentHashMap<>();
@@ -152,33 +155,43 @@ class MetricsCollector {
     // --- Update methods ---
 
     void updateThroughputHistory(IntegrationInfo info) {
-        long currentTotal = info.exchangesTotal;
         long currentFailed = info.failed;
         long now = System.currentTimeMillis();
 
         String pid = info.pid;
         LinkedList<long[]> samples = throughputSamples.computeIfAbsent(pid, k 
-> new LinkedList<>());
-        samples.add(new long[] { now, currentTotal, currentFailed });
+        samples.add(new long[] { now, info.exchangesTotal, currentFailed });
 
         while (!samples.isEmpty() && now - samples.get(0)[0] > 2000) {
             samples.remove(0);
         }
 
+        // Use the EWMA throughput from the status JSON (already smoothed in 
camel-core)
+        // and store scaled by THROUGHPUT_SCALE so sub-1.0 rates (e.g. 0.20 
msg/s) are preserved as longs
+        long tp = 0;
+        if (info.throughput != null) {
+            try {
+                tp = Math.round(Double.parseDouble(info.throughput) * 
THROUGHPUT_SCALE);
+            } catch (NumberFormatException e) {
+                // ignore
+            }
+        }
+
+        // Failed throughput still computed from delta (no EWMA source for 
failed-only)
+        long fp = 0;
         if (samples.size() >= 2) {
             long[] oldest = samples.get(0);
             long[] newest = samples.get(samples.size() - 1);
-            long deltaTotal = newest[1] - oldest[1];
             long deltaFailed = newest[2] - oldest[2];
             long deltaTimeMs = newest[0] - oldest[0];
-            long tp = deltaTimeMs > 0 ? (deltaTotal * 1000) / deltaTimeMs : 0;
-            long fp = deltaTimeMs > 0 ? (deltaFailed * 1000) / deltaTimeMs : 0;
+            fp = deltaTimeMs > 0 ? (deltaFailed * 1000 * THROUGHPUT_SCALE) / 
deltaTimeMs : 0;
+        }
 
-            Long lastTime = previousExchangesTime.get(pid);
-            if (lastTime == null || now - lastTime >= 1000) {
-                previousExchangesTime.put(pid, now);
-                addToHistory(throughputHistory, pid, tp, MAX_SPARKLINE_POINTS);
-                addToHistory(failedHistory, pid, fp, MAX_SPARKLINE_POINTS);
-            }
+        Long lastTime = previousExchangesTime.get(pid);
+        if (lastTime == null || now - lastTime >= 1000) {
+            previousExchangesTime.put(pid, now);
+            addToHistory(throughputHistory, pid, tp, MAX_SPARKLINE_POINTS);
+            addToHistory(failedHistory, pid, fp, MAX_SPARKLINE_POINTS);
         }
     }
 
@@ -253,6 +266,15 @@ class MetricsCollector {
             String pid, long now, long inTotal, long outTotal,
             Map<String, LinkedList<long[]>> samplesMap, Map<String, Long> 
prevTimeMap,
             Map<String, LinkedList<Long>> inHistMap, Map<String, 
LinkedList<Long>> outHistMap) {
+        recordEndpointSample(pid, now, inTotal, outTotal,
+                samplesMap, prevTimeMap, inHistMap, outHistMap, 
THROUGHPUT_SCALE);
+    }
+
+    private void recordEndpointSample(
+            String pid, long now, long inTotal, long outTotal,
+            Map<String, LinkedList<long[]>> samplesMap, Map<String, Long> 
prevTimeMap,
+            Map<String, LinkedList<Long>> inHistMap, Map<String, 
LinkedList<Long>> outHistMap,
+            long scale) {
         LinkedList<long[]> samples = samplesMap.computeIfAbsent(pid, k -> new 
LinkedList<>());
         samples.add(new long[] { now, inTotal, outTotal });
         while (!samples.isEmpty() && now - samples.get(0)[0] > 2000) {
@@ -262,8 +284,8 @@ class MetricsCollector {
             long[] oldest = samples.get(0);
             long[] newest = samples.get(samples.size() - 1);
             long deltaMs = newest[0] - oldest[0];
-            long inRate = deltaMs > 0 ? (newest[1] - oldest[1]) * 1000 / 
deltaMs : 0;
-            long outRate = deltaMs > 0 ? (newest[2] - oldest[2]) * 1000 / 
deltaMs : 0;
+            long inRate = deltaMs > 0 ? (newest[1] - oldest[1]) * 1000 * scale 
/ deltaMs : 0;
+            long outRate = deltaMs > 0 ? (newest[2] - oldest[2]) * 1000 * 
scale / deltaMs : 0;
             Long lastTime = prevTimeMap.get(pid);
             if (lastTime == null || now - lastTime >= 1000) {
                 prevTimeMap.put(pid, now);
@@ -282,8 +304,10 @@ class MetricsCollector {
             String key = info.pid + "/" + cb.id;
             long success = cb.successfulCalls;
             long failed = cb.failedCalls;
+            // Circuit breaker history stays unscaled (scale=1) because 
CircuitBreakerTab
+            // formats values as plain integers, not via formatThroughput()
             recordEndpointSample(key, now, success, failed,
-                    cbThroughputSamples, previousCbTime, cbSuccessHistory, 
cbFailHistory);
+                    cbThroughputSamples, previousCbTime, cbSuccessHistory, 
cbFailHistory, 1);
         }
     }
 
@@ -403,4 +427,54 @@ class MetricsCollector {
             map.keySet().removeIf(k -> k.startsWith(prefix));
         }
     }
+
+    // --- Shared throughput formatting utilities ---
+
+    /**
+     * Round a scaled throughput value up to a nice chart-axis maximum. 
Returns values that are multiples of 1, 2, or 5
+     * at the appropriate magnitude, ensuring the Y-axis labels are 
human-readable.
+     */
+    static long niceMax(long rawMax) {
+        if (rawMax <= 0) {
+            return THROUGHPUT_SCALE;
+        }
+        int[] steps = { 1, 2, 5 };
+        long multiplier = THROUGHPUT_SCALE;
+        while (multiplier > 0) {
+            for (int s : steps) {
+                long candidate = s * multiplier;
+                if (candidate < 0) {
+                    // overflow — fall back to rawMax
+                    return rawMax;
+                }
+                if (candidate >= rawMax) {
+                    return candidate;
+                }
+            }
+            long next = multiplier * 10;
+            if (next / 10 != multiplier) {
+                // overflow — fall back to rawMax
+                return rawMax;
+            }
+            multiplier = next;
+        }
+        return rawMax;
+    }
+
+    /**
+     * Format a scaled throughput value for display. Values >= 10 are shown as 
integers, values >= 1 with one decimal,
+     * and sub-1 values with two decimals.
+     */
+    static String formatThroughput(long scaledValue) {
+        double v = scaledValue / (double) THROUGHPUT_SCALE;
+        if (v >= 10) {
+            return String.valueOf(Math.round(v));
+        } else if (v >= 1) {
+            return String.format(Locale.US, "%.1f", v);
+        } else if (scaledValue > 0) {
+            return String.format(Locale.US, "%.2f", v);
+        } else {
+            return "0";
+        }
+    }
 }
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
index 55914b8cd547..b3c6195f9d03 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTab.java
@@ -504,11 +504,18 @@ class OverviewTab extends AbstractTab {
             for (long v : mergedTotal) {
                 rawMax = Math.max(rawMax, v);
             }
-            long maxTp = roundUpNice(rawMax);
+            long maxTp = MetricsCollector.niceMax(rawMax);
             long curTp = mergedTotal[renderPoints - 1];
-            long curFailed = mergedFailed[renderPoints - 1];
+            // Clamp failed to total so the title matches the chart bars 
(total comes from
+            // EWMA while failed is still delta-based, so failed can 
momentarily exceed total)
+            long curFailed = Math.min(mergedFailed[renderPoints - 1], curTp);
             long curOk = Math.max(0, curTp - curFailed);
 
+            // Format throughput values unscaled for display
+            String curTpFmt = MetricsCollector.formatThroughput(curTp);
+            String curOkFmt = MetricsCollector.formatThroughput(curOk);
+            String curFailFmt = MetricsCollector.formatThroughput(curFailed);
+
             Line titleLine;
             if (chartMode == CHART_SINGLE && ctx.selectedPid != null) {
                 IntegrationInfo chartSel = ctx.findSelectedIntegration();
@@ -517,18 +524,18 @@ class OverviewTab extends AbstractTab {
                 titleLine = Line.from(
                         Span.raw(" ["),
                         Span.styled(chartName, Style.EMPTY.fg(Color.YELLOW)),
-                        Span.raw(String.format("] Throughput: %d msg/s  ", 
curTp)),
+                        Span.raw(String.format("] Throughput: %s msg/s  ", 
curTpFmt)),
                         Span.styled("■", 
Style.EMPTY.fg(Color.ansi(AnsiColor.BRIGHT_GREEN))),
-                        Span.raw(String.format(" ok:%d  ", curOk)),
+                        Span.raw(String.format(" ok:%s  ", curOkFmt)),
                         Span.styled("■", Style.EMPTY.fg(Color.RED)),
-                        Span.raw(String.format(" fail:%d ", curFailed)));
+                        Span.raw(String.format(" fail:%s ", curFailFmt)));
             } else {
                 titleLine = Line.from(
-                        Span.raw(String.format(" [All] Throughput: %d msg/s  
", curTp)),
+                        Span.raw(String.format(" [All] Throughput: %s msg/s  
", curTpFmt)),
                         Span.styled("■", 
Style.EMPTY.fg(Color.ansi(AnsiColor.BRIGHT_GREEN))),
-                        Span.raw(String.format(" ok:%d  ", curOk)),
+                        Span.raw(String.format(" ok:%s  ", curOkFmt)),
                         Span.styled("■", Style.EMPTY.fg(Color.RED)),
-                        Span.raw(String.format(" fail:%d ", curFailed)));
+                        Span.raw(String.format(" fail:%s ", curFailFmt)));
             }
 
             Block chartBlock = 
Block.builder().borderType(BorderType.ROUNDED).borders(Borders.ALL)
@@ -547,7 +554,7 @@ class OverviewTab extends AbstractTab {
 
             BarChart barChart = BarChart.builder()
                     .data(groups)
-                    .max(maxTp > 0 ? maxTp + 2 : 2)
+                    .max(maxTp)
                     .barWidth(1)
                     .barGap(0)
                     .groupGap(0)
@@ -560,9 +567,11 @@ class OverviewTab extends AbstractTab {
             Style dimStyle = Style.EMPTY.dim();
             for (int row = 0; row < barRows; row++) {
                 if (row == 0) {
-                    yLines.add(Line.from(Span.styled(String.format("%3d", 
maxTp), dimStyle)));
+                    yLines.add(
+                            Line.from(Span.styled(String.format("%3s", 
MetricsCollector.formatThroughput(maxTp)), dimStyle)));
                 } else if (barRows > 4 && row == barRows / 2) {
-                    yLines.add(Line.from(Span.styled(String.format("%3d", 
maxTp / 2), dimStyle)));
+                    yLines.add(Line
+                            .from(Span.styled(String.format("%3s", 
MetricsCollector.formatThroughput(maxTp / 2)), dimStyle)));
                 } else if (row == barRows - 1) {
                     yLines.add(Line.from(Span.styled("  0", dimStyle)));
                 } else {
@@ -897,18 +906,6 @@ class OverviewTab extends AbstractTab {
         return sortStyle(column, sort);
     }
 
-    private static long roundUpNice(long value) {
-        if (value <= 10) {
-            return 10;
-        }
-        long step = (long) Math.pow(10, Math.floor(Math.log10(value)));
-        long rounded = ((value + step - 1) / step) * step;
-        if (rounded % 2 != 0) {
-            rounded += step;
-        }
-        return rounded;
-    }
-
     private static boolean hasReadmeInSourceDir(IntegrationInfo info) {
         java.nio.file.Path srcDir = FilesBrowser.resolveSourceDirectory(info);
         if (srcDir != null) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollectorThroughputTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollectorThroughputTest.java
new file mode 100644
index 000000000000..27c536c73279
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/MetricsCollectorThroughputTest.java
@@ -0,0 +1,114 @@
+/*
+ * 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 org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for {@link MetricsCollector} throughput formatting and scaling 
utilities. Verifies that sub-1.0 msg/s rates
+ * (common in development workloads) are correctly preserved and displayed.
+ */
+class MetricsCollectorThroughputTest {
+
+    @Test
+    void formatThroughputZero() {
+        assertEquals("0", MetricsCollector.formatThroughput(0));
+    }
+
+    @Test
+    void formatThroughputSubOne() {
+        // 0.20 msg/s = 20 when scaled by 100
+        assertEquals("0.20", MetricsCollector.formatThroughput(20));
+        // 0.05 msg/s = 5 when scaled by 100
+        assertEquals("0.05", MetricsCollector.formatThroughput(5));
+    }
+
+    @Test
+    void formatThroughputBetweenOneAndTen() {
+        // 1.0 msg/s = 100 when scaled by 100
+        assertEquals("1.0", MetricsCollector.formatThroughput(100));
+        // 5.5 msg/s = 550 when scaled by 100
+        assertEquals("5.5", MetricsCollector.formatThroughput(550));
+    }
+
+    @Test
+    void formatThroughputAboveTen() {
+        // 10 msg/s = 1000 when scaled by 100
+        assertEquals("10", MetricsCollector.formatThroughput(1000));
+        // 42 msg/s = 4200 when scaled by 100
+        assertEquals("42", MetricsCollector.formatThroughput(4200));
+    }
+
+    @Test
+    void niceMaxReturnsScaleForZero() {
+        assertEquals(MetricsCollector.THROUGHPUT_SCALE, 
MetricsCollector.niceMax(0));
+    }
+
+    @Test
+    void niceMaxReturnsScaleForSmallValues() {
+        // 0.20 msg/s scaled = 20 -> niceMax should return 100 (= 1.0 msg/s)
+        long result = MetricsCollector.niceMax(20);
+        assertEquals(100, result);
+    }
+
+    @Test
+    void niceMaxRoundsUpToNiceNumber() {
+        // 3.5 msg/s scaled = 350 -> niceMax should return 500 (= 5.0 msg/s)
+        long result = MetricsCollector.niceMax(350);
+        assertEquals(500, result);
+    }
+
+    @Test
+    void niceMaxForLargeValues() {
+        // 75 msg/s scaled = 7500 -> niceMax should return 10000 (= 100 msg/s)
+        long result = MetricsCollector.niceMax(7500);
+        assertEquals(10000, result);
+    }
+
+    @Test
+    void niceMaxAlwaysGreaterOrEqual() {
+        // niceMax should always return a value >= the input
+        for (long v : new long[] { 1, 10, 50, 99, 100, 150, 200, 500, 1000, 
5000, 10000 }) {
+            assertTrue(MetricsCollector.niceMax(v) >= v,
+                    "niceMax(" + v + ") = " + MetricsCollector.niceMax(v) + " 
should be >= " + v);
+        }
+    }
+
+    @Test
+    void throughputScaleIsHundred() {
+        // Verify the scale factor — tests and display logic depend on this 
value
+        assertEquals(100, MetricsCollector.THROUGHPUT_SCALE);
+    }
+
+    @Test
+    void niceMaxDoesNotOverflowForLargeInput() {
+        // Very large rawMax should not cause infinite loop or negative values
+        long result = MetricsCollector.niceMax(Long.MAX_VALUE / 2);
+        assertTrue(result >= Long.MAX_VALUE / 2,
+                "niceMax should return at least rawMax for extreme values, got 
" + result);
+    }
+
+    @Test
+    void niceMaxHandlesMaxLong() {
+        // Long.MAX_VALUE itself should not hang
+        long result = MetricsCollector.niceMax(Long.MAX_VALUE);
+        assertTrue(result > 0, "niceMax(Long.MAX_VALUE) should return a 
positive value, got " + result);
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabThroughputRenderTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabThroughputRenderTest.java
new file mode 100644
index 000000000000..f0c348aa2cb8
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/OverviewTabThroughputRenderTest.java
@@ -0,0 +1,186 @@
+/*
+ * 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.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Screen-capture render tests for the OverviewTab throughput chart. Verifies 
that sub-1.0 msg/s rates are displayed
+ * correctly in the bar chart title, preventing future regressions where 
integer truncation would cause the chart to
+ * show "0 msg/s" instead of e.g. "0.20 msg/s".
+ *
+ * @see <a 
href="https://issues.apache.org/jira/browse/CAMEL-23865";>CAMEL-23865</a>
+ */
+class OverviewTabThroughputRenderTest {
+
+    private MonitorContext ctx;
+    private MetricsCollector metrics;
+    private IntegrationInfo info;
+
+    @BeforeEach
+    void setUp() {
+        Theme.resetForTesting();
+
+        info = new IntegrationInfo();
+        info.pid = "1234";
+        info.name = "test-app";
+        info.state = 5; // started
+        info.exchangesTotal = 10;
+        info.throughput = "0.20"; // 0.20 msg/s (e.g. timer with 5s period)
+
+        AtomicReference<List<IntegrationInfo>> data = new 
AtomicReference<>(List.of(info));
+        AtomicReference<List<InfraInfo>> infraData = new 
AtomicReference<>(List.of());
+        ctx = new MonitorContext(data, infraData);
+        ctx.selectedPid = "1234";
+
+        metrics = new MetricsCollector();
+    }
+
+    @AfterEach
+    void tearDown() {
+        Theme.resetForTesting();
+    }
+
+    /**
+     * Verifies that a sub-1.0 msg/s throughput rate is visible in the chart 
title when throughput history contains
+     * scaled values from the EWMA backend.
+     */
+    @Test
+    void chartShowsFractionalThroughputInTitle() {
+        // Populate throughput history with scaled 0.20 msg/s values
+        // (0.20 * THROUGHPUT_SCALE = 20)
+        Map<String, LinkedList<Long>> throughputHistory = 
metrics.getThroughputHistory();
+        LinkedList<Long> history = new LinkedList<>();
+        for (int i = 0; i < 30; i++) {
+            history.add(20L); // 0.20 msg/s scaled by 100
+        }
+        throughputHistory.put("1234", history);
+
+        Map<String, LinkedList<Long>> failedHistory = 
metrics.getFailedHistory();
+        LinkedList<Long> fHistory = new LinkedList<>();
+        for (int i = 0; i < 30; i++) {
+            fHistory.add(0L);
+        }
+        failedHistory.put("1234", fHistory);
+
+        OverviewTab tab = new OverviewTab(ctx, metrics, new HashSet<>(), () -> 
{
+        });
+        tab.chartMode = OverviewTab.CHART_ALL;
+
+        String rendered = TuiTestHelper.renderToString(tab, 160, 40);
+
+        assertTrue(rendered.contains("0.20 msg/s"),
+                "Chart title should show '0.20 msg/s' for sub-1.0 throughput, 
but got:\n" + rendered);
+    }
+
+    /**
+     * Verifies that zero throughput displays as "0 msg/s", not "0.00 msg/s".
+     */
+    @Test
+    void chartShowsZeroThroughput() {
+        Map<String, LinkedList<Long>> throughputHistory = 
metrics.getThroughputHistory();
+        LinkedList<Long> history = new LinkedList<>();
+        for (int i = 0; i < 30; i++) {
+            history.add(0L);
+        }
+        throughputHistory.put("1234", history);
+
+        Map<String, LinkedList<Long>> failedHistory = 
metrics.getFailedHistory();
+        failedHistory.put("1234", new LinkedList<>(history));
+
+        OverviewTab tab = new OverviewTab(ctx, metrics, new HashSet<>(), () -> 
{
+        });
+        tab.chartMode = OverviewTab.CHART_ALL;
+
+        String rendered = TuiTestHelper.renderToString(tab, 160, 40);
+
+        assertTrue(rendered.contains("0 msg/s"),
+                "Chart title should show '0 msg/s' for zero throughput, but 
got:\n" + rendered);
+    }
+
+    /**
+     * Verifies that high throughput (>= 10 msg/s) displays as integer without 
decimals.
+     */
+    @Test
+    void chartShowsIntegerThroughputForHighRates() {
+        Map<String, LinkedList<Long>> throughputHistory = 
metrics.getThroughputHistory();
+        LinkedList<Long> history = new LinkedList<>();
+        for (int i = 0; i < 30; i++) {
+            history.add(4200L); // 42 msg/s scaled by 100
+        }
+        throughputHistory.put("1234", history);
+
+        Map<String, LinkedList<Long>> failedHistory = 
metrics.getFailedHistory();
+        LinkedList<Long> fHistory = new LinkedList<>();
+        for (int i = 0; i < 30; i++) {
+            fHistory.add(0L);
+        }
+        failedHistory.put("1234", fHistory);
+
+        OverviewTab tab = new OverviewTab(ctx, metrics, new HashSet<>(), () -> 
{
+        });
+        tab.chartMode = OverviewTab.CHART_ALL;
+
+        String rendered = TuiTestHelper.renderToString(tab, 160, 40);
+
+        assertTrue(rendered.contains("42 msg/s"),
+                "Chart title should show '42 msg/s' for high throughput, but 
got:\n" + rendered);
+    }
+
+    /**
+     * Verifies that the chart with sub-1 throughput data does NOT show the 
integer "0 msg/s" — this was the original
+     * bug where integer truncation made the chart appear empty.
+     */
+    @Test
+    void chartDoesNotTruncateSubOneRateToZero() {
+        Map<String, LinkedList<Long>> throughputHistory = 
metrics.getThroughputHistory();
+        LinkedList<Long> history = new LinkedList<>();
+        for (int i = 0; i < 30; i++) {
+            history.add(20L); // 0.20 msg/s
+        }
+        throughputHistory.put("1234", history);
+
+        Map<String, LinkedList<Long>> failedHistory = 
metrics.getFailedHistory();
+        LinkedList<Long> fHistory = new LinkedList<>();
+        for (int i = 0; i < 30; i++) {
+            fHistory.add(0L);
+        }
+        failedHistory.put("1234", fHistory);
+
+        OverviewTab tab = new OverviewTab(ctx, metrics, new HashSet<>(), () -> 
{
+        });
+        tab.chartMode = OverviewTab.CHART_ALL;
+
+        String rendered = TuiTestHelper.renderToString(tab, 160, 40);
+
+        // The old bug would show "0 msg/s" here due to integer truncation
+        assertFalse(rendered.contains("Throughput: 0 msg/s"),
+                "Chart should NOT truncate 0.20 msg/s to '0 msg/s' — integer 
truncation regression detected:\n"
+                                                              + rendered);
+    }
+}

Reply via email to