From a4c3afef016cca1de89f709aae2bcd28b68207cd Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 14 Oct 2026 08:37:54 +0000 Subject: [PATCH] Show no progress for a queued run, and its whole estimate as the ETA [patch] A pending run counted the time since Started as build time, so a run sitting in the queue drew a growing progress ring, percentage and a shrinking ETA before it had begun. The row's duration, ETA and progress now come from MeasureRun, which treats a pending run as zero elapsed: no progress, and the full estimate as its ETA. Running and finished runs are measured as before. Run.IsOngoing still includes Pending, so polling of queued runs is unchanged. Fixes #364 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GKJwMybTmY6x4mJfMM9RKv --- BuildMonitor.Test/RunProgressTests.cs | 71 +++++++++++++++++++++++++++ BuildMonitor/BuildMonitor.cs | 22 +++++++-- 2 files changed, 90 insertions(+), 3 deletions(-) create mode 100644 BuildMonitor.Test/RunProgressTests.cs diff --git a/BuildMonitor.Test/RunProgressTests.cs b/BuildMonitor.Test/RunProgressTests.cs new file mode 100644 index 0000000..97c2773 --- /dev/null +++ b/BuildMonitor.Test/RunProgressTests.cs @@ -0,0 +1,71 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.BuildMonitor.Test; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +/// +/// Tests for . +/// +/// +/// A pending run is queued rather than building, so the time since it was queued must not show up as +/// progress or shorten its ETA. +/// +[TestClass] +[DoNotParallelize] +public sealed class RunProgressTests +{ + private static readonly DateTimeOffset Now = new(2026, 1, 1, 12, 0, 0, TimeSpan.Zero); + private static readonly TimeSpan Estimate = TimeSpan.FromMinutes(4); + + [TestInitialize] + public void FreezeClock() => Clock.UtcNowOverride = () => Now; + + [TestCleanup] + public void ReleaseClock() => Clock.UtcNowOverride = null; + + [TestMethod] + public void APendingRunShowsNoProgressAndTheWholeEstimateAsItsEta() + { + // Arrange + Run run = new() { Status = RunStatus.Pending, Started = Now.AddSeconds(-40) }; + + // Act + (TimeSpan duration, TimeSpan eta, double progress) = BuildMonitor.MeasureRun(run, Estimate); + + // Assert + Assert.AreEqual(TimeSpan.Zero, duration); + Assert.AreEqual(Estimate, eta); + Assert.AreEqual(0, progress); + } + + [TestMethod] + public void ARunningRunCountsTheTimeSinceItStarted() + { + // Arrange + Run run = new() { Status = RunStatus.Running, Started = Now.AddMinutes(-1) }; + + // Act + (TimeSpan duration, TimeSpan eta, double progress) = BuildMonitor.MeasureRun(run, Estimate); + + // Assert + Assert.AreEqual(TimeSpan.FromMinutes(1), duration); + Assert.AreEqual(TimeSpan.FromMinutes(3), eta); + Assert.AreEqual(0.25, progress, 1e-9); + } + + [TestMethod] + public void AFinishedRunUsesItsRecordedDuration() + { + // Arrange + Run run = new() { Status = RunStatus.Success, Started = Now.AddHours(-1), LastUpdated = Now.AddHours(-1).AddMinutes(2) }; + + // Act + (TimeSpan duration, TimeSpan eta, double progress) = BuildMonitor.MeasureRun(run, Estimate); + + // Assert + Assert.AreEqual(TimeSpan.FromMinutes(2), duration); + Assert.AreEqual(TimeSpan.FromMinutes(2), eta); + Assert.AreEqual(0.5, progress, 1e-9); + } +} diff --git a/BuildMonitor/BuildMonitor.cs b/BuildMonitor/BuildMonitor.cs index adc05de..ed70adf 100644 --- a/BuildMonitor/BuildMonitor.cs +++ b/BuildMonitor/BuildMonitor.cs @@ -887,9 +887,7 @@ private static bool RenderBuildBranchRowColumns(Build build, BranchName branch, bool isOngoing = latestRun.IsOngoing; // Use branch-specific estimation for more accurate estimates TimeSpan estimate = build.CalculateEstimatedDuration(branch); - TimeSpan duration = isOngoing ? Clock.UtcNow - latestRun.Started : latestRun.Duration; - TimeSpan eta = duration < estimate ? estimate - duration : TimeSpan.Zero; - double progress = estimate > TimeSpan.Zero ? duration.TotalSeconds / estimate.TotalSeconds : 0; + (TimeSpan duration, TimeSpan eta, double progress) = MeasureRun(latestRun, estimate); bool shouldOpenContextMenu = false; @@ -912,6 +910,24 @@ private static bool RenderBuildBranchRowColumns(Build build, BranchName branch, return shouldOpenContextMenu; } + /// + /// How long a run has taken, how long it has left and how far through it is, against an estimate. + /// A pending run is still queued and has not started building, so none of the time since + /// counts: it shows no progress and the whole estimate as its ETA. + /// + internal static (TimeSpan Duration, TimeSpan Eta, double Progress) MeasureRun(Run run, TimeSpan estimate) + { + TimeSpan duration = run.Status switch + { + RunStatus.Pending => TimeSpan.Zero, + RunStatus.Running => Clock.UtcNow - run.Started, + _ => run.Duration, + }; + TimeSpan eta = duration < estimate ? estimate - duration : TimeSpan.Zero; + double progress = estimate > TimeSpan.Zero ? duration.TotalSeconds / estimate.TotalSeconds : 0; + return (duration, eta, progress); + } + private static bool RenderStatusColumn(Build build, Run latestRun, bool isOngoing, double progress) { bool shouldOpenContextMenu = false;