Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.14-beta
0.0.15-beta
103 changes: 103 additions & 0 deletions apps.Tests/LiveProgressRendererTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using apps.Tests.Fakes;

namespace apps.Tests;

/// <summary>Covers checklist state transitions and per-scanner progress counts.</summary>
public sealed class LiveProgressRendererTests
{
[Test]
public async Task Checklist_TransitionsThroughScanAndCheckStates()
{
var scanner = new FakeScanner { Name = "JetBrains", Kind = AppKind.Extension };
var renderer = new LiveProgressRenderer([scanner]);

renderer.StartScan([scanner]);
await AssertState(renderer, scanner.Name, ChecklistProgressState.Waiting);

renderer.RenderScannerActive(scanner.Name);
renderer.RenderScannerProgress(scanner.Name, 2);
await AssertState(renderer, scanner.Name, ChecklistProgressState.Scanning);

renderer.RenderScannerDone(scanner.Name);
await AssertState(renderer, scanner.Name, ChecklistProgressState.Waiting);

renderer.StartCheck([(scanner, 2)]);
renderer.RenderCheckActive(scanner.Name);
await AssertState(renderer, scanner.Name, ChecklistProgressState.Checking);

renderer.RenderCheckProgress(scanner.Name, updateAvailable: false, failed: false);
var checking = renderer.GetChecklistSnapshot(scanner.Name);
await Assert.That(checking.Checked).IsEqualTo(1);
await Assert.That(checking.State).IsEqualTo(ChecklistProgressState.Checking);

renderer.RenderCheckProgress(scanner.Name, updateAvailable: true, failed: false);
renderer.RenderCheckComplete();

var completed = renderer.GetChecklistSnapshot(scanner.Name);
await Assert.That(completed.State).IsEqualTo(ChecklistProgressState.Completed);
await Assert.That(completed.Discovered).IsEqualTo(2);
await Assert.That(completed.CheckTotal).IsEqualTo(2);
await Assert.That(completed.Checked).IsEqualTo(2);
await Assert.That(completed.Updates).IsEqualTo(1);
await Assert.That(completed.Failures).IsEqualTo(0);
}

[Test]
public async Task Checklist_ScanFailureRemainsFailedAfterCheckStage()
{
var scanner = new FakeScanner { Name = "Broken" };
var renderer = new LiveProgressRenderer([scanner]);

renderer.StartScan([scanner]);
renderer.RenderScannerActive(scanner.Name);
renderer.RenderScannerFailed(scanner.Name);
renderer.StartCheck(Array.Empty<(IScanner Scanner, int Total)>());
renderer.RenderCheckComplete();

await AssertState(renderer, scanner.Name, ChecklistProgressState.Failed);
}

[Test]
public async Task Checklist_CheckErrorCompletesAsFailed()
{
var scanner = new FakeScanner { Name = "Broken" };
var renderer = new LiveProgressRenderer([scanner]);

renderer.StartScan([scanner]);
renderer.RenderScannerProgress(scanner.Name, 1);
renderer.RenderScannerDone(scanner.Name);
renderer.StartCheck([(scanner, 1)]);
renderer.RenderCheckActive(scanner.Name);
renderer.RenderCheckProgress(scanner.Name, updateAvailable: false, failed: true);
renderer.RenderCheckComplete();

var failed = renderer.GetChecklistSnapshot(scanner.Name);
await Assert.That(failed.State).IsEqualTo(ChecklistProgressState.Failed);
await Assert.That(failed.Failures).IsEqualTo(1);
}

[Test]
public async Task Checklist_DryRunCompletesSuccessfulScanners()
{
var scanner = new FakeScanner { Name = "ScanOnly" };
var renderer = new LiveProgressRenderer([scanner]);

renderer.StartScan([scanner]);
renderer.RenderScannerProgress(scanner.Name, 3);
renderer.RenderScannerDone(scanner.Name);
renderer.RenderDryRunComplete();

var completed = renderer.GetChecklistSnapshot(scanner.Name);
await Assert.That(completed.State).IsEqualTo(ChecklistProgressState.Completed);
await Assert.That(completed.Discovered).IsEqualTo(3);
}

private static async Task AssertState(
LiveProgressRenderer renderer,
string scannerName,
ChecklistProgressState expected)
{
var snapshot = renderer.GetChecklistSnapshot(scannerName);
await Assert.That(snapshot.State).IsEqualTo(expected);
}
}
3 changes: 3 additions & 0 deletions apps/AnsiStyle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ internal static class AnsiStyle
/// <summary>Applies red foreground.</summary>
internal static string Red(string s) => IsAnsi ? $"\e[31m{s}\e[0m" : s;

/// <summary>Applies white foreground.</summary>
internal static string White(string s) => IsAnsi ? $"\e[37m{s}\e[0m" : s;

/// <summary>Applies cyan foreground.</summary>
internal static string Cyan(string s) => IsAnsi ? $"\e[36m{s}\e[0m" : s;

Expand Down
13 changes: 7 additions & 6 deletions apps/CheckOrchestrator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,9 @@ public sealed class CheckOrchestrator(IEnumerable<IScanner> scanners, LiveProgre
appGroups.Add((scanner, groupedByScanner));
}

var totalToCheck = appGroups.Sum(g => g.Apps.Length);
renderer.SetCheckTotal(totalToCheck);
renderer.StartCheck(appGroups.Select(g => (g.Scanner, g.Apps.Length)).ToArray());

// Periodic timer to refresh the check progress line with updated elapsed time
// Periodic timer to animate the check checklist and refresh elapsed time.
using var timerCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
var timerTask = renderer.RunCheckTimerAsync(timerCts.Token);

Expand All @@ -66,7 +65,7 @@ public sealed class CheckOrchestrator(IEnumerable<IScanner> scanners, LiveProgre

app.CheckFailed = app.CheckFailed || error;
checkedApps.Add(app);
renderer.RenderCheckActive(total);
renderer.RenderCheckProgress(app.App.Source.Name, app.HasUpdate, app.CheckFailed);
}

await timerCts.CancelAsync().ConfigureAwait(false);
Expand All @@ -80,19 +79,21 @@ public sealed class CheckOrchestrator(IEnumerable<IScanner> scanners, LiveProgre
}

var updates = checkedApps.Count(c => c.HasUpdate);
renderer.RenderCheckComplete(total, updates, errors);
renderer.RenderCheckComplete();
logger.LogInformation(
"Check complete: {Total} checked, {Updates} updates, {Errors} errors",
total, updates, errors);

return (total, updates, errors);
}

private static async Task RunCheckGroupAsync(
private async Task RunCheckGroupAsync(
(IScanner Scanner, AppRecord[] Apps) group,
ChannelWriter<(AppRecord App, bool Error)> writer,
CancellationToken cancellationToken)
{
renderer.RenderCheckActive(group.Scanner.Name);

await foreach (var result in group.Scanner.CheckAsync(group.Apps, cancellationToken).ConfigureAwait(false))
{
await writer.WriteAsync(result, cancellationToken).ConfigureAwait(false);
Expand Down
6 changes: 6 additions & 0 deletions apps/Components/Chrome/ChromeExtScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ public sealed class ChromeExtScanner(IHttpClientFactory httpClientFactory, ILogg
/// <inheritdoc/>
public string DisplayName => "Chrome";

/// <inheritdoc/>
public string ProgressLabel => "Chrome Extensions";

/// <inheritdoc/>
public string ProgressItemNoun => "extension";

public OS SupportedOS => OS.MacOS | OS.Windows;
public AppKind Kind => AppKind.Extension;

Expand Down
6 changes: 6 additions & 0 deletions apps/Components/Docker/DockerImageScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ public sealed class DockerImageScanner(IProcessRunner runner, IHttpClientFactory
/// <inheritdoc/>
public string DisplayName => "Docker";

/// <inheritdoc/>
public string ProgressLabel => "Docker Images";

/// <inheritdoc/>
public string ProgressItemNoun => "image";

public OS SupportedOS => OS.MacOS | OS.Windows;
public AppKind Kind => AppKind.DevTool;

Expand Down
3 changes: 3 additions & 0 deletions apps/Components/Dotnet/DotnetScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ internal sealed record DotnetComponent(string Name, string Version, string Path)
/// <inheritdoc/>
public string DisplayName => ".NET";

/// <inheritdoc/>
public string ProgressLabel => ".NET";

public OS SupportedOS => OS.MacOS | OS.Windows;
public AppKind Kind => AppKind.DevTool;

Expand Down
6 changes: 6 additions & 0 deletions apps/Components/Go/GoScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ public sealed class GoScanner(IProcessRunner runner, IHttpClientFactory httpClie
/// <inheritdoc/>
public string DisplayName => "Go";

/// <inheritdoc/>
public string ProgressLabel => "Go Tools";

/// <inheritdoc/>
public string ProgressItemNoun => "tool";

public OS SupportedOS => OS.MacOS | OS.Windows;
public AppKind Kind => AppKind.DevTool;

Expand Down
6 changes: 6 additions & 0 deletions apps/Components/JetBrains/JetBrainsPluginScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ public sealed class JetBrainsPluginScanner(IHttpClientFactory httpClientFactory,
/// <inheritdoc/>
public string DisplayName => "JetBrains";

/// <inheritdoc/>
public string ProgressLabel => "JetBrains Plugins";

/// <inheritdoc/>
public string ProgressItemNoun => "plugin";

public OS SupportedOS => OS.MacOS | OS.Windows;
public AppKind Kind => AppKind.Extension;

Expand Down
6 changes: 6 additions & 0 deletions apps/Components/MacOs/MacApplicationsScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ public sealed partial class MacApplicationsScanner(
/// <inheritdoc/>
public string DisplayName => "Application";

/// <inheritdoc/>
public string ProgressLabel => "Applications";

/// <inheritdoc/>
public string ProgressItemNoun => "app";

public OS SupportedOS => OS.MacOS;
public AppKind Kind => AppKind.App | AppKind.Extension | AppKind.Package;

Expand Down
3 changes: 3 additions & 0 deletions apps/Components/Node/NodeScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ public sealed class NodeScanner(IProcessRunner runner, IHttpClientFactory httpCl
/// <inheritdoc/>
public string DisplayName => "Node";

/// <inheritdoc/>
public string ProgressLabel => "Node";

public OS SupportedOS => OS.MacOS | OS.Windows;
public AppKind Kind => AppKind.DevTool | AppKind.Package;

Expand Down
6 changes: 6 additions & 0 deletions apps/Components/VsCode/VsCodeExtScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ public sealed class VsCodeExtScanner(IProcessRunner runner, IHttpClientFactory h
/// <inheritdoc/>
public string DisplayName => "VS Code";

/// <inheritdoc/>
public string ProgressLabel => "VS Code Extensions";

/// <inheritdoc/>
public string ProgressItemNoun => "extension";

public OS SupportedOS => OS.MacOS | OS.Windows;
public AppKind Kind => AppKind.Extension;

Expand Down
6 changes: 6 additions & 0 deletions apps/Components/Windows/WindowsApplicationsScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ public sealed class WindowsApplicationsScanner(IProcessRunner runner, ILogger<Wi
/// <inheritdoc/>
public string DisplayName => "Application";

/// <inheritdoc/>
public string ProgressLabel => "Applications";

/// <inheritdoc/>
public string ProgressItemNoun => "app";

public OS SupportedOS => OS.Windows;
public AppKind Kind => AppKind.App;

Expand Down
6 changes: 6 additions & 0 deletions apps/IScanner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ public interface IScanner
/// <summary>Human-readable label shown in the <c>Source</c> output column (e.g. "App Store", ".NET").</summary>
string DisplayName { get; }

/// <summary>Human-readable category label shown in the progress checklist.</summary>
string ProgressLabel => DisplayName;

/// <summary>Singular name for an item reported by this scanner.</summary>
string ProgressItemNoun => "item";

OS SupportedOS { get; }
AppKind Kind { get; }

Expand Down
Loading
Loading