From 3c5814c8e43b82f1082873c70e8dbabaa1167d43 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 1 Nov 2026 02:25:37 +0000 Subject: [PATCH] Report a throwing status getter in its own row instead of crashing --status [patch] StatusReport.Build read each detail's getter directly, and RunAsync invoked the State getter at the call site, so one throwing getter aborted the process (exit 134) and lost every row of the report. Both now go through StatusReport.Read, which absorbs non-fatal exceptions the same way the rest of the library does and prints "error: " in that row. Fixes ktsu-dev/TrayApp#62 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW4brDwN4YAzA6oCKV6 --- TrayApp.Test/StatusReportTests.cs | 22 ++++++++++++++++++++ TrayApp.Test/TrayAppBuilderTests.cs | 31 +++++++++++++++++++++++++++++ TrayApp/StatusReport.cs | 24 +++++++++++++++++++++- TrayApp/TrayAppBuilder.cs | 2 +- 4 files changed, 77 insertions(+), 2 deletions(-) diff --git a/TrayApp.Test/StatusReportTests.cs b/TrayApp.Test/StatusReportTests.cs index bac994a..c8c756c 100644 --- a/TrayApp.Test/StatusReportTests.cs +++ b/TrayApp.Test/StatusReportTests.cs @@ -47,6 +47,28 @@ public void Build_WithAStatusAndDetails_AppendsThemInOrder() Assert.Contains("systemd-inhibit", report, StringComparison.Ordinal); } + [TestMethod] + public void Build_WithAThrowingDetail_ReportsTheErrorInItsRowAndKeepsTheRest() + { + string report = StatusReport.Build( + "demo", + "holding", + [ + new StatusDetail("Inhibitor", () => throw new InvalidOperationException("systemd-inhibit was not found on PATH")), + new StatusDetail("Helper", () => "present"), + ]); + + Assert.Contains("Platform:", report, StringComparison.Ordinal); + Assert.Contains("Tray icon:", report, StringComparison.Ordinal); + Assert.Contains("Inhibitor: error: systemd-inhibit was not found on PATH", report, StringComparison.Ordinal); + Assert.Contains("Helper:", report, StringComparison.Ordinal); + Assert.Contains("present", report, StringComparison.Ordinal); + } + + [TestMethod] + public void Read_WithAThrowingGetter_ReturnsTheError() => + Assert.AreEqual("error: boom", StatusReport.Read(() => throw new InvalidOperationException("boom"))); + [TestMethod] public void Build_EndsWithoutATrailingNewline() { diff --git a/TrayApp.Test/TrayAppBuilderTests.cs b/TrayApp.Test/TrayAppBuilderTests.cs index 44607aa..d0a875a 100644 --- a/TrayApp.Test/TrayAppBuilderTests.cs +++ b/TrayApp.Test/TrayAppBuilderTests.cs @@ -4,6 +4,7 @@ namespace ktsu.TrayApp.Test; using System; using System.Collections.Generic; +using System.IO; using System.Threading.Tasks; using ktsu.Essentials.PersistenceProviders.InMemory; using ktsu.TrayApp; @@ -351,6 +352,36 @@ public async Task RunAsync_WithStatus_ReportsWithoutStarting() Assert.IsFalse(started); } + [TestMethod] + [DoNotParallelize] + public async Task RunAsync_WithStatusAndThrowingGetters_PrintsEveryRowAndExitsZero() + { + TextWriter original = Console.Out; + using StringWriter output = new(); + Console.SetOut(output); + int exitCode; + try + { + exitCode = await TrayAppBuilder.Create("demo") + .Status(() => throw new InvalidOperationException("state unknown")) + .Status("Inhibitor", () => throw new InvalidOperationException("systemd-inhibit was not found on PATH")) + .Status("Helper", () => "present") + .RunAsync(["--no-tray", "--status"]) + .ConfigureAwait(false); + } + finally + { + Console.SetOut(original); + } + + string report = output.ToString(); + Assert.AreEqual(0, exitCode); + Assert.Contains("Platform:", report, StringComparison.Ordinal); + Assert.Contains("State: error: state unknown", report, StringComparison.Ordinal); + Assert.Contains("Inhibitor: error: systemd-inhibit was not found on PATH", report, StringComparison.Ordinal); + Assert.Contains("Helper:", report, StringComparison.Ordinal); + } + [TestMethod] public async Task RunAsync_WithStatusAndARememberedToggle_RunsNoSetterAndReportsTheLiveState() { diff --git a/TrayApp/StatusReport.cs b/TrayApp/StatusReport.cs index 039d9d5..c34a138 100644 --- a/TrayApp/StatusReport.cs +++ b/TrayApp/StatusReport.cs @@ -45,7 +45,7 @@ public static string Build(string name, string? status, IReadOnlyList + /// Reads one of the tool's status getters for the report. + /// + /// + /// The report exists to explain a tool that cannot do its job, and a getter is likely to throw for the + /// same reason (a missing helper, say), so a throwing getter becomes its own row's text rather than + /// losing every row the library and the other getters would have printed. + /// + /// The tool's getter. + /// What the getter returned, or error: and the message of what it threw. + internal static string Read(Func getter) + { + try + { + return getter(); + } + catch (Exception ex) when (FatalError.IsNotFatal(ex)) + { + return "error: " + ex.Message; + } + } + private static void AppendRow(StringBuilder report, string label, string value) => report.AppendLine($"{(label + ':').PadRight(LabelWidth)}{value}"); } diff --git a/TrayApp/TrayAppBuilder.cs b/TrayApp/TrayAppBuilder.cs index 73cf5ca..e7a293c 100644 --- a/TrayApp/TrayAppBuilder.cs +++ b/TrayApp/TrayAppBuilder.cs @@ -437,7 +437,7 @@ public Task RunAsync(string[] args) if (options.ShowStatus) { - Console.WriteLine(StatusReport.Build(app.Name, app.Status?.Invoke(), app.StatusDetails)); + Console.WriteLine(StatusReport.Build(app.Name, app.Status is null ? null : StatusReport.Read(app.Status), app.StatusDetails)); return Task.FromResult(ExitSuccess); }