From 117e8959ca0eaba00acab20c65aba9274cafbd8d Mon Sep 17 00:00:00 2001
From: James Maes
diff --git a/gclo.Engine.Tests/SyncAllCoordinatorTests.cs b/gclo.Engine.Tests/SyncAllCoordinatorTests.cs
index 447be17..d84c9cc 100644
--- a/gclo.Engine.Tests/SyncAllCoordinatorTests.cs
+++ b/gclo.Engine.Tests/SyncAllCoordinatorTests.cs
@@ -178,7 +178,7 @@ public sealed class SyncAllCoordinatorTests : IDisposable
[Fact]
- public async Task RunAsync_DefaultTokenAccount_WithoutADefaultToken_IsSkipped_NotRun()
+ public async Task RunAsync_DefaultTokenAccount_WithoutADefaultToken_NeedsAttention_AndIsNotRun()
{
WorkspaceViewModel shared = CreateDefaultTokenWorkspace("shared", withDefaultToken: false);
Assert.True(shared.UsesDefaultToken);
@@ -188,10 +188,9 @@ public sealed class SyncAllCoordinatorTests : IDisposable
SyncAllResult result = await _coordinator.RunAsync([shared], CancellationToken.None);
- Assert.Equal(0, result.Ran);
- Assert.Equal(1, result.Skipped);
- Assert.Equal([SyncAllAccountState.Queued, SyncAllAccountState.Skipped], states);
- Assert.Empty(_lister.Organizations); // never asked GitHub: skipped before loading
+ Assert.Equal(new SyncAllResult(Ran: 0, Skipped: 1, WasCanceled: false, Failed: 1), result);
+ Assert.Equal([SyncAllAccountState.Queued, SyncAllAccountState.Failed], states); // not a success with one skipped (#182)
+ Assert.Empty(_lister.Organizations); // never asked GitHub: stopped before loading
}
@@ -298,7 +297,7 @@ public sealed class SyncAllCoordinatorTests : IDisposable
[Fact]
- public async Task RunAsync_SkipsAWorkspaceWhoseLoadFails_AndMovesOn()
+ public async Task RunAsync_AWorkspaceWhoseLoadFails_NeedsAttention_AndThePassMovesOn()
{
_lister.Handler = org => string.Equals(org, "org-alpha"
, StringComparison.Ordinal) ? throw new InvalidOperationException("org not found")
@@ -309,7 +308,7 @@ public sealed class SyncAllCoordinatorTests : IDisposable
SyncAllResult result = await _coordinator.RunAsync(new[] { alpha, beta }, CancellationToken.None);
- Assert.Equal(new SyncAllResult(Ran: 1, Skipped: 1, WasCanceled: false), result);
+ Assert.Equal(new SyncAllResult(Ran: 1, Skipped: 1, WasCanceled: false, Failed: 1), result);
Assert.False(alpha.HasLoadedRepos);
Assert.Equal(new[] { "org-beta-repo" }, git.ClonedRepoNames);
}
@@ -354,7 +353,7 @@ public sealed class SyncAllCoordinatorTests : IDisposable
gate.SetResult(); // ...which must still run to completion
SyncAllResult result = await run.WaitAsync(TimeSpan.FromSeconds(30));
- Assert.Equal(new SyncAllResult(Ran: 1, Skipped: 0, WasCanceled: true), result);
+ Assert.Equal(new SyncAllResult(Ran: 1, Skipped: 0, WasCanceled: true, NotStarted: 1), result);
Assert.Equal(SyncStatus.Done, alpha.Repos[0].Status); // finished, not canceled
Assert.False(alpha.IsRunning);
Assert.False(beta.HasLoadedRepos); // the second account never started
@@ -374,7 +373,7 @@ public sealed class SyncAllCoordinatorTests : IDisposable
SyncAllResult result = await _coordinator.RunAsync(new[] { workspace }, cts.Token);
- Assert.Equal(new SyncAllResult(Ran: 0, Skipped: 0, WasCanceled: true), result);
+ Assert.Equal(new SyncAllResult(Ran: 0, Skipped: 0, WasCanceled: true, NotStarted: 1), result);
Assert.False(workspace.HasLoadedRepos);
Assert.Empty(git.CloneCalls);
}
@@ -497,15 +496,14 @@ public sealed class SyncAllCoordinatorTests : IDisposable
[Fact]
- public async Task RunAsync_DefaultTokenAccount_WhoseTokenWasRemovedAfterItOpened_IsSkipped()
+ public async Task RunAsync_DefaultTokenAccount_WhoseTokenWasRemovedAfterItOpened_NeedsAttention()
{
WorkspaceViewModel shared = CreateDefaultTokenWorkspace("shared", withDefaultToken: true);
_vault.Delete(AppSettings.DefaultTokenVaultId); // removed in Settings meanwhile
SyncAllResult result = await _coordinator.RunAsync([shared], CancellationToken.None);
- Assert.Equal(0, result.Ran);
- Assert.Equal(1, result.Skipped);
+ Assert.Equal(new SyncAllResult(Ran: 0, Skipped: 1, WasCanceled: false, Failed: 1), result);
Assert.Empty(_lister.Organizations); // GitHub was never asked with the old token
}
@@ -514,7 +512,7 @@ public sealed class SyncAllCoordinatorTests : IDisposable
// ---------------------------------------------------------------- review of #157: no success for a sync that never ran
[Fact]
- public async Task RunAsync_AnAccountWhoseOwnTokenIsGone_IsSkipped_NotReportedAsSucceeded()
+ public async Task RunAsync_AnAccountWhoseOwnTokenIsGone_NeedsAttention_NotReportedAsSucceeded()
{
WorkspaceViewModel workspace = CreateWorkspace("own");
await workspace.LoadReposCommand.ExecuteAsync(null); // a loaded table: the sync command can run without a token
@@ -524,9 +522,8 @@ public sealed class SyncAllCoordinatorTests : IDisposable
SyncAllResult result = await _coordinator.RunAsync([workspace], CancellationToken.None);
- Assert.Equal(0, result.Ran);
- Assert.Equal(1, result.Skipped);
- Assert.Equal([SyncAllAccountState.Queued, SyncAllAccountState.Skipped], states);
+ Assert.Equal(new SyncAllResult(Ran: 0, Skipped: 1, WasCanceled: false, Failed: 1), result);
+ Assert.Equal([SyncAllAccountState.Queued, SyncAllAccountState.Failed], states);
}
@@ -701,7 +698,7 @@ public sealed class SyncAllCoordinatorTests : IDisposable
Assert.True(result.WasCanceled);
Assert.Equal(SyncAllOutcome.Canceled, status.Outcome);
Assert.Equal("Sync all canceled", status.Title);
- Assert.Equal("1 account synced, 0 skipped.", status.Message);
+ Assert.Equal("1 account synced, 0 skipped. 1 account was not started.", status.Message);
Assert.False(status.IsRunning);
// The next pass starts clean.
@@ -752,8 +749,13 @@ public sealed class SyncAllCoordinatorTests : IDisposable
SyncAllResult result = await _coordinator.RunAsync([failing, never], cts.Token);
- Assert.Equal(new SyncAllResult(Ran: 1, Skipped: 0, WasCanceled: true, Failed: 1), result);
- Assert.Equal("1 account synced, 0 skipped. 1 account needs attention; it is marked in the pane.", SyncAllStatus.Describe(result));
+ Assert.Equal(new SyncAllResult(Ran: 1, Skipped: 0, WasCanceled: true, Failed: 1, NotStarted: 1), result);
+ Assert.Equal(
+ "1 account synced, 0 skipped. 1 account needs attention; it is marked in the pane. 1 account was not started.",
+ SyncAllStatus.Describe(result));
+ Assert.Equal(
+ "0 accounts synced, 0 skipped. 3 accounts were not started.",
+ SyncAllStatus.Describe(new SyncAllResult(Ran: 0, Skipped: 0, WasCanceled: true, NotStarted: 3)));
}
@@ -833,4 +835,149 @@ public sealed class SyncAllCoordinatorTests : IDisposable
Assert.Throws
diff --git a/gclo.Engine.Tests/SyncAllCoordinatorTests.cs b/gclo.Engine.Tests/SyncAllCoordinatorTests.cs
index d84c9cc..ceef39f 100644
--- a/gclo.Engine.Tests/SyncAllCoordinatorTests.cs
+++ b/gclo.Engine.Tests/SyncAllCoordinatorTests.cs
@@ -188,7 +188,7 @@ public sealed class SyncAllCoordinatorTests : IDisposable
SyncAllResult result = await _coordinator.RunAsync([shared], CancellationToken.None);
- Assert.Equal(new SyncAllResult(Ran: 0, Skipped: 1, WasCanceled: false, Failed: 1), result);
+ Assert.Equal(new SyncAllResult(Ran: 0, Skipped: 0, WasCanceled: false, Failed: 1, CouldNotSync: 1), result);
Assert.Equal([SyncAllAccountState.Queued, SyncAllAccountState.Failed], states); // not a success with one skipped (#182)
Assert.Empty(_lister.Organizations); // never asked GitHub: stopped before loading
}
@@ -308,7 +308,7 @@ public sealed class SyncAllCoordinatorTests : IDisposable
SyncAllResult result = await _coordinator.RunAsync(new[] { alpha, beta }, CancellationToken.None);
- Assert.Equal(new SyncAllResult(Ran: 1, Skipped: 1, WasCanceled: false, Failed: 1), result);
+ Assert.Equal(new SyncAllResult(Ran: 1, Skipped: 0, WasCanceled: false, Failed: 1, CouldNotSync: 1), result);
Assert.False(alpha.HasLoadedRepos);
Assert.Equal(new[] { "org-beta-repo" }, git.ClonedRepoNames);
}
@@ -503,7 +503,7 @@ public sealed class SyncAllCoordinatorTests : IDisposable
SyncAllResult result = await _coordinator.RunAsync([shared], CancellationToken.None);
- Assert.Equal(new SyncAllResult(Ran: 0, Skipped: 1, WasCanceled: false, Failed: 1), result);
+ Assert.Equal(new SyncAllResult(Ran: 0, Skipped: 0, WasCanceled: false, Failed: 1, CouldNotSync: 1), result);
Assert.Empty(_lister.Organizations); // GitHub was never asked with the old token
}
@@ -522,7 +522,7 @@ public sealed class SyncAllCoordinatorTests : IDisposable
SyncAllResult result = await _coordinator.RunAsync([workspace], CancellationToken.None);
- Assert.Equal(new SyncAllResult(Ran: 0, Skipped: 1, WasCanceled: false, Failed: 1), result);
+ Assert.Equal(new SyncAllResult(Ran: 0, Skipped: 0, WasCanceled: false, Failed: 1, CouldNotSync: 1), result);
Assert.Equal([SyncAllAccountState.Queued, SyncAllAccountState.Failed], states);
}
@@ -541,8 +541,7 @@ public sealed class SyncAllCoordinatorTests : IDisposable
SyncAllResult result = await _coordinator.RunAsync([workspace], CancellationToken.None);
Assert.Equal(RunResultKind.Error, workspace.ResultKind);
- Assert.Equal(0, result.Ran);
- Assert.Equal(1, result.Skipped);
+ Assert.Equal(new SyncAllResult(Ran: 0, Skipped: 0, WasCanceled: false, Failed: 1, CouldNotSync: 1), result);
Assert.Equal([SyncAllAccountState.Queued, SyncAllAccountState.Running, SyncAllAccountState.Failed], states);
}
@@ -607,7 +606,7 @@ public sealed class SyncAllCoordinatorTests : IDisposable
Assert.True(status.IsOpen); // the result stays until it is closed
Assert.Equal(SyncAllOutcome.Finished, status.Outcome);
Assert.Equal("Sync all finished", status.Title);
- Assert.Equal("2 accounts synced, 0 skipped.", status.Message);
+ Assert.Equal("2 of 2 accounts synced.", status.Message);
Assert.Null(status.CurrentAccountId);
Assert.Null(status.Fraction);
@@ -627,14 +626,14 @@ public sealed class SyncAllCoordinatorTests : IDisposable
"beta: 0 of 1 repository.",
"beta: 1 of 1 repository.",
"beta: starting.",
- "2 accounts synced, 0 skipped.",
+ "2 of 2 accounts synced.",
],
said);
// The pass is over: what the accounts do next is no longer the bar's business.
status.IsOpen = false;
await alpha.SyncCommand.ExecuteAsync(null);
- Assert.Equal("2 accounts synced, 0 skipped.", status.Message);
+ Assert.Equal("2 of 2 accounts synced.", status.Message);
Assert.False(status.IsOpen);
}
@@ -666,7 +665,7 @@ public sealed class SyncAllCoordinatorTests : IDisposable
await run.WaitAsync(TimeSpan.FromSeconds(30));
Assert.Contains(0.5, fractions);
- Assert.Equal("1 account synced, 0 skipped.", status.Message);
+ Assert.Equal("1 of 1 account synced.", status.Message);
}
@@ -698,7 +697,7 @@ public sealed class SyncAllCoordinatorTests : IDisposable
Assert.True(result.WasCanceled);
Assert.Equal(SyncAllOutcome.Canceled, status.Outcome);
Assert.Equal("Sync all canceled", status.Title);
- Assert.Equal("1 account synced, 0 skipped. 1 account was not started.", status.Message);
+ Assert.Equal("1 of 2 accounts synced. 1 account was not started.", status.Message);
Assert.False(status.IsRunning);
// The next pass starts clean.
@@ -724,10 +723,10 @@ public sealed class SyncAllCoordinatorTests : IDisposable
SyncAllResult result = await _coordinator.RunAsync([failing, blocked, fine], CancellationToken.None);
- Assert.Equal(new SyncAllResult(Ran: 2, Skipped: 1, WasCanceled: false, Failed: 2), result);
+ Assert.Equal(new SyncAllResult(Ran: 2, Skipped: 0, WasCanceled: false, Failed: 2, CouldNotSync: 1), result);
Assert.Equal(SyncAllOutcome.FinishedWithFailures, status.Outcome);
Assert.Equal("Sync all finished", status.Title);
- Assert.Equal("2 accounts synced, 1 skipped. 2 accounts need attention; they are marked in the pane.", status.Message);
+ Assert.Equal("2 of 3 accounts synced. 2 accounts need attention; they are marked in the pane.", status.Message);
}
@@ -751,10 +750,10 @@ public sealed class SyncAllCoordinatorTests : IDisposable
Assert.Equal(new SyncAllResult(Ran: 1, Skipped: 0, WasCanceled: true, Failed: 1, NotStarted: 1), result);
Assert.Equal(
- "1 account synced, 0 skipped. 1 account needs attention; it is marked in the pane. 1 account was not started.",
+ "1 of 2 accounts synced. 1 account needs attention; it is marked in the pane. 1 account was not started.",
SyncAllStatus.Describe(result));
Assert.Equal(
- "0 accounts synced, 0 skipped. 3 accounts were not started.",
+ "0 of 3 accounts synced. 3 accounts were not started.",
SyncAllStatus.Describe(new SyncAllResult(Ran: 0, Skipped: 0, WasCanceled: true, NotStarted: 3)));
}
@@ -851,7 +850,7 @@ public sealed class SyncAllCoordinatorTests : IDisposable
await _coordinator.RunAsync([shared], CancellationToken.None);
Assert.Equal(SyncAllOutcome.FinishedWithFailures, status.Outcome);
- Assert.Equal("0 accounts synced, 1 skipped. 1 account needs attention; it is marked in the pane.", status.Message);
+ Assert.Equal("0 of 1 account synced. 1 account needs attention; it is marked in the pane.", status.Message);
}
@@ -895,14 +894,18 @@ public sealed class SyncAllCoordinatorTests : IDisposable
SyncAllResult result = await run.WaitAsync(TimeSpan.FromSeconds(30));
Assert.Equal(RunResultKind.Canceled, alpha.ResultKind);
- Assert.Equal(new SyncAllResult(Ran: 1, Skipped: 1, WasCanceled: false, Failed: afterAFailure ? 1 : 0), result); // beta ran
+ Assert.Equal(
+ afterAFailure
+ ? new SyncAllResult(Ran: 1, Skipped: 0, WasCanceled: false, Failed: 1, CouldNotSync: 1)
+ : new SyncAllResult(Ran: 1, Skipped: 1, WasCanceled: false),
+ result); // beta ran
Assert.Equal(
[SyncAllAccountState.Queued, SyncAllAccountState.Running, afterAFailure ? SyncAllAccountState.Failed : SyncAllAccountState.Skipped],
states);
Assert.Equal(
afterAFailure
- ? "1 account synced, 1 skipped. 1 account needs attention; it is marked in the pane."
- : "1 account synced, 1 skipped.",
+ ? "1 of 2 accounts synced. 1 account needs attention; it is marked in the pane."
+ : "1 of 2 accounts synced. 1 account was skipped (busy, canceled, or nothing selected).",
status.Message);
}
@@ -911,7 +914,7 @@ public sealed class SyncAllCoordinatorTests : IDisposable
[Theory]
[InlineData(true)]
[InlineData(false)] // one repository was not selected: it is not selected afterwards either
- public async Task RetryFailed_GivesTheSelectionBack_SoTheNextSyncCoversWhatItCoveredBefore(bool allSelected)
+ public async Task RetryFailed_LeavesTheSelectionAlone_SoTheNextSyncCoversWhatItCoveredBefore(bool allSelected)
{
_lister.Handler = _ => [Repo("one"), Repo("two"), Repo("three")];
bool failTwo = true;
@@ -935,7 +938,7 @@ public sealed class SyncAllCoordinatorTests : IDisposable
await alpha.RetryFailedCommand.ExecuteAsync(null);
Assert.False(alpha.HasFailedRepos);
- Assert.Equal(selected, alpha.SelectedCount); // the retry ran one repository; the selection is the user's again
+ Assert.Equal(selected, alpha.SelectedCount); // the retry ran one repository; the selection is still the user's
Assert.Equal(selected, alpha.Repos.Count(repo => repo.IsSelected));
Assert.Equal(allSelected, alpha.Repos.Single(repo => string.Equals(repo.Name, "three", StringComparison.Ordinal)).IsSelected);
@@ -960,7 +963,45 @@ public sealed class SyncAllCoordinatorTests : IDisposable
shared.RefreshAccountToken();
Assert.Null(shared.MissingTokenDescription);
+ Assert.Null(shared.MissingTokenAdvice);
Assert.Contains(changed, name => string.Equals(name, nameof(WorkspaceViewModel.MissingTokenDescription), StringComparison.Ordinal));
+ Assert.Contains(changed, name => string.Equals(name, nameof(WorkspaceViewModel.MissingTokenAdvice), StringComparison.Ordinal));
+ }
+
+
+
+ [Fact]
+ public void AnAccountsPage_SaysWhereItsTokenIsSaved_InTheWordsOfTheWindow()
+ {
+ WorkspaceViewModel shared = CreateDefaultTokenWorkspace("shared", withDefaultToken: false);
+ WorkspaceViewModel own = CreateWorkspace("own");
+ _vault.Delete(own.AccountId!.Value);
+ own.RefreshAccountToken();
+
+ Assert.Equal(
+ "No default token is saved. Save one in File > Settings, or give this account its own token with Edit account…",
+ shared.MissingTokenAdvice);
+ Assert.Equal("No token is saved for this account. Choose Edit account… and enter one.", own.MissingTokenAdvice);
+ Assert.Null(CreateWorkspace("fine").MissingTokenAdvice);
+ }
+
+
+
+ [Fact]
+ public async Task SeveralAccountsThatWereSkipped_AreCountedOnce_AndNotAsNeedingAttention()
+ {
+ WorkspaceViewModel first = CreateWorkspace("first");
+ WorkspaceViewModel second = CreateWorkspace("second");
+ _lister.Handler = _ => []; // nothing to sync in either
+ var status = new SyncAllStatus();
+ _coordinator.Status = status;
+
+ SyncAllResult result = await _coordinator.RunAsync([first, second], CancellationToken.None);
+
+ Assert.Equal(new SyncAllResult(Ran: 0, Skipped: 2, WasCanceled: false), result);
+ Assert.Equal(2, result.Total);
+ Assert.Equal(SyncAllOutcome.Finished, status.Outcome);
+ Assert.Equal("0 of 2 accounts synced. 2 accounts were skipped (busy, canceled, or nothing selected).", status.Message);
}
@@ -980,4 +1021,51 @@ public sealed class SyncAllCoordinatorTests : IDisposable
gate.SetResult();
await sync.WaitAsync(TimeSpan.FromSeconds(30));
}
+
+
+
+ [Fact]
+ public async Task RetryFailed_RunsTheFailedRows_AlsoWhenTheyAreNotSelected_AndASelectionChangeMadeMeanwhileStays()
+ {
+ _lister.Handler = _ => [Repo("one"), Repo("two"), Repo("three")];
+ bool failTwo = true;
+ var entered = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var gate = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
+ var git = new FakeGitClient
+ {
+ CloneHandler = (_, path, _, _, _) =>
+ {
+ if(!path.EndsWith("two", StringComparison.Ordinal))
+ {
+ return Task.CompletedTask;
+ }
+ if(failTwo)
+ {
+ return Task.FromException(new InvalidOperationException("boom"));
+ }
+ entered.TrySetResult();
+ return gate.Task;
+ },
+ };
+ WorkspaceViewModel alpha = CreateWorkspace("alpha", git);
+ await alpha.LoadReposCommand.ExecuteAsync(null);
+ await alpha.SyncCommand.ExecuteAsync(null);
+ RepoItemViewModel Row(string name) => alpha.Repos.Single(repo => string.Equals(repo.Name, name, StringComparison.Ordinal));
+ Row("two").IsSelected = false; // the failed row is taken out of the selection: the retry is for it all the same
+ int before = git.CloneCalls.Count;
+
+ failTwo = false;
+ Task retry = alpha.RetryFailedCommand.ExecuteAsync(null);
+ await entered.Task.WaitAsync(TimeSpan.FromSeconds(30));
+ Row("one").IsSelected = false; // the checkboxes stay enabled while a run is going on
+ gate.SetResult();
+ await retry.WaitAsync(TimeSpan.FromSeconds(30));
+
+ Assert.Equal(SyncStatus.Done, Row("two").Status);
+ Assert.Equal(1, git.CloneCalls.Count - before); // the failed row, and only it
+ Assert.False(Row("one").IsSelected);
+ Assert.False(Row("two").IsSelected);
+ Assert.True(Row("three").IsSelected);
+ Assert.Equal(1, alpha.SelectedCount);
+ }
}
diff --git a/gclo.Engine.Tests/WorkspaceViewModelTests.cs b/gclo.Engine.Tests/WorkspaceViewModelTests.cs
index 3fe9f82..e46876c 100644
--- a/gclo.Engine.Tests/WorkspaceViewModelTests.cs
+++ b/gclo.Engine.Tests/WorkspaceViewModelTests.cs
@@ -641,7 +641,7 @@ public sealed class WorkspaceViewModelTests : IDisposable
Assert.Equal(SyncStatus.Done, byName["bravo"].Status);
Assert.Null(byName["bravo"].Error);
Assert.Equal(SyncStatus.Done, byName["alpha"].Status); // untouched by the retry
- Assert.True(byName["bravo"].IsSelected); // the retry ran exactly the failure, and the selection is what it was (#182)
+ Assert.True(byName["bravo"].IsSelected); // the retry ran exactly the failure, and the selection was not touched (#182)
Assert.True(byName["alpha"].IsSelected);
Assert.True(byName["charlie"].IsSelected);
Assert.Equal(1, vm.TotalCount); // run-scoped (#22): the retry run was just bravo
diff --git a/gclo.ViewModels/SyncAllCoordinator.cs b/gclo.ViewModels/SyncAllCoordinator.cs
index 26562e7..b6d96af 100644
--- a/gclo.ViewModels/SyncAllCoordinator.cs
+++ b/gclo.ViewModels/SyncAllCoordinator.cs
@@ -42,10 +42,10 @@ public sealed class SyncAllCoordinator(IActivityLog log)
/// loading its repositories first when needed. A workspace is skipped (counted,
/// logged, never fatal) when it is already busy or has nothing to sync. One that
/// cannot be synced (it has no token, its repositories cannot be loaded, its sync
- /// cannot start) is skipped too, and counted and announced as failed: the pass
- /// must not read as a success when an account was not synced (#182). Canceling
- ///