diff --git a/gclo.Engine.Tests/ActionLoggingTests.cs b/gclo.Engine.Tests/ActionLoggingTests.cs
index af269af..9cd02cb 100644
--- a/gclo.Engine.Tests/ActionLoggingTests.cs
+++ b/gclo.Engine.Tests/ActionLoggingTests.cs
@@ -37,9 +37,23 @@ public sealed class ActionLoggingTests : IDisposable
+ ///
+ /// Synchronous progress: the handler runs inline on the reporting thread, one
+ /// report at a time. The view model's handler belongs to the UI thread and is
+ /// never entered twice at once there; several workers reporting at the same
+ /// moment must not enter it together here either.
+ ///
private sealed class SyncProgress(Action handler) : IProgress
{
- public void Report(RepoProgress value) => handler(value);
+ private readonly Lock _oneAtATime = new();
+
+ public void Report(RepoProgress value)
+ {
+ lock(_oneAtATime)
+ {
+ handler(value);
+ }
+ }
}
diff --git a/gclo.Engine.Tests/AppSettingsTests.cs b/gclo.Engine.Tests/AppSettingsTests.cs
index 012ab3d..fe7eb1e 100644
--- a/gclo.Engine.Tests/AppSettingsTests.cs
+++ b/gclo.Engine.Tests/AppSettingsTests.cs
@@ -10,7 +10,6 @@ namespace gclo.Engine.Tests;
/// Pins defaults and load-time sanitizing of .
-[Collection("GCLO_DATA_DIR")] // sets a process-wide variable: never beside another class that does
public sealed class AppSettingsTests
{
[Fact]
@@ -30,7 +29,6 @@ public sealed class AppSettingsTests
// A pre-splash settings.json has no SplashMilliseconds field; it must load
// as the 5s default, not as 0 clamped to the minimum.
string dir = Path.Combine(Path.GetTempPath(), "gclo-tests", Guid.NewGuid().ToString("N"));
- Environment.SetEnvironmentVariable("GCLO_DATA_DIR", dir);
try
{
Directory.CreateDirectory(dir);
@@ -38,7 +36,7 @@ public sealed class AppSettingsTests
Path.Combine(dir, "settings.json"),
"""{ "DefaultTargetFolder": "C:\\src", "DefaultMaxConcurrency": 8, "Theme": "Dark" }""");
- var loaded = AppSettings.Load();
+ var loaded = AppSettings.Load(directory: dir);
Assert.Equal(AppSettings.DefaultSplashMilliseconds, loaded.SplashMilliseconds);
Assert.True(loaded.ShowSplashScreen);
@@ -46,7 +44,6 @@ public sealed class AppSettingsTests
}
finally
{
- Environment.SetEnvironmentVariable("GCLO_DATA_DIR", null);
try
{
Directory.Delete(dir, recursive: true);
@@ -63,25 +60,23 @@ public sealed class AppSettingsTests
public void Save_ClampsSplashDuration()
{
string dir = Path.Combine(Path.GetTempPath(), "gclo-tests", Guid.NewGuid().ToString("N"));
- Environment.SetEnvironmentVariable("GCLO_DATA_DIR", dir);
try
{
var settings = new AppSettings { SplashMilliseconds = 999_999 };
- settings.Save();
+ settings.Save(directory: dir);
Assert.Equal(AppSettings.MaxSplashMilliseconds, settings.SplashMilliseconds);
settings.SplashMilliseconds = 1;
- settings.Save();
+ settings.Save(directory: dir);
Assert.Equal(AppSettings.MinSplashMilliseconds, settings.SplashMilliseconds);
// Round trip: the persisted values load back as saved.
- var loaded = AppSettings.Load();
+ var loaded = AppSettings.Load(directory: dir);
Assert.Equal(AppSettings.MinSplashMilliseconds, loaded.SplashMilliseconds);
Assert.True(loaded.ShowSplashScreen);
}
finally
{
- Environment.SetEnvironmentVariable("GCLO_DATA_DIR", null);
try
{
Directory.Delete(dir, recursive: true);
@@ -100,14 +95,12 @@ public sealed class AppSettingsTests
{
string dir = Path.Combine(Path.GetTempPath(), "gclo-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);
- Environment.SetEnvironmentVariable("GCLO_DATA_DIR", dir);
try
{
test(dir);
}
finally
{
- Environment.SetEnvironmentVariable("GCLO_DATA_DIR", null);
GitTestHelpers.TryDeleteDirectory(dir);
}
}
@@ -120,11 +113,11 @@ public sealed class AppSettingsTests
{
var log = new RecordingActivityLog();
- bool saved = new AppSettings { Theme = "Dark" }.Save(log);
+ bool saved = new AppSettings { Theme = "Dark" }.Save(log, dir);
Assert.True(saved);
Assert.Equal(["settings.json"], Directory.GetFiles(dir).Select(Path.GetFileName), StringComparer.Ordinal);
- Assert.Equal("Dark", AppSettings.Load(log).Theme);
+ Assert.Equal("Dark", AppSettings.Load(log, dir).Theme);
Assert.Empty(log.Entries);
});
@@ -139,20 +132,20 @@ public sealed class AppSettingsTests
return; // elsewhere a file that is open can still be replaced
}
var log = new RecordingActivityLog();
- Assert.True(new AppSettings { Theme = "Dark" }.Save());
+ Assert.True(new AppSettings { Theme = "Dark" }.Save(directory: dir));
string path = Path.Combine(dir, "settings.json");
bool saved;
using(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read))
{
- saved = new AppSettings { Theme = "Light" }.Save(log, TimeSpan.FromMilliseconds(100));
+ saved = new AppSettings { Theme = "Light" }.Save(log, TimeSpan.FromMilliseconds(100), dir);
}
Assert.False(saved);
(string level, string message) = Assert.Single(log.Entries);
Assert.Equal("ERROR", level);
Assert.StartsWith("Settings could not be saved (", message);
- Assert.Equal("Dark", AppSettings.Load().Theme); // not half a file, not the new one: the old one
+ Assert.Equal("Dark", AppSettings.Load(directory: dir).Theme); // not half a file, not the new one: the old one
Assert.Equal(["settings.json"], Directory.GetFiles(dir).Select(Path.GetFileName), StringComparer.Ordinal);
});
@@ -167,24 +160,22 @@ public sealed class AppSettingsTests
}
string dir = Path.Combine(Path.GetTempPath(), "gclo-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);
- Environment.SetEnvironmentVariable("GCLO_DATA_DIR", dir);
try
{
- Assert.True(new AppSettings { Theme = "Dark" }.Save());
+ Assert.True(new AppSettings { Theme = "Dark" }.Save(directory: dir));
Task save;
using(new FileStream(Path.Combine(dir, "settings.json"), FileMode.Open, FileAccess.Read, FileShare.Read))
{
- save = Task.Run(() => new AppSettings { Theme = "Light" }.Save(log: null, TimeSpan.FromSeconds(60)));
+ save = Task.Run(() => new AppSettings { Theme = "Light" }.Save(log: null, TimeSpan.FromSeconds(60), dir));
Task finished = await Task.WhenAny(save, Task.Delay(TimeSpan.FromMilliseconds(200)));
Assert.NotSame(save, finished); // still trying
}
Assert.True(await save.WaitAsync(TimeSpan.FromSeconds(60)));
- Assert.Equal("Light", AppSettings.Load().Theme);
+ Assert.Equal("Light", AppSettings.Load(directory: dir).Theme);
}
finally
{
- Environment.SetEnvironmentVariable("GCLO_DATA_DIR", null);
GitTestHelpers.TryDeleteDirectory(dir);
}
}
@@ -195,9 +186,9 @@ public sealed class AppSettingsTests
public void Save_ByTwoAtOnce_NeverLeavesHalfAFile_OrALeftover()
=> WithDataDirectory(dir =>
{
- Parallel.For(0, 40, i => new AppSettings { Theme = i % 2 == 0 ? "Dark" : "Light" }.Save());
+ Parallel.For(0, 40, i => new AppSettings { Theme = i % 2 == 0 ? "Dark" : "Light" }.Save(directory: dir));
- Assert.Contains(AppSettings.Load().Theme, new[] { "Dark", "Light" }, StringComparer.Ordinal);
+ Assert.Contains(AppSettings.Load(directory: dir).Theme, new[] { "Dark", "Light" }, StringComparer.Ordinal);
Assert.Equal(["settings.json"], Directory.GetFiles(dir).Select(Path.GetFileName), StringComparer.Ordinal);
});
@@ -210,7 +201,7 @@ public sealed class AppSettingsTests
var log = new RecordingActivityLog();
File.WriteAllText(Path.Combine(dir, "settings.json"), "{ not json");
- var loaded = AppSettings.Load(log);
+ var loaded = AppSettings.Load(log, dir);
Assert.Equal("System", loaded.Theme);
(string level, string message) = Assert.Single(log.Entries);
@@ -226,6 +217,6 @@ public sealed class AppSettingsTests
{
File.WriteAllText(Path.Combine(dir, "settings.json"), "{ \"SplashMilliseconds\": 0 }");
- Assert.Equal(AppSettings.MinSplashMilliseconds, AppSettings.Load().SplashMilliseconds);
+ Assert.Equal(AppSettings.MinSplashMilliseconds, AppSettings.Load(directory: dir).SplashMilliseconds);
});
}
diff --git a/gclo.Engine.Tests/CoverageResidueTests.cs b/gclo.Engine.Tests/CoverageResidueTests.cs
index 65fd395..e86724e 100644
--- a/gclo.Engine.Tests/CoverageResidueTests.cs
+++ b/gclo.Engine.Tests/CoverageResidueTests.cs
@@ -14,7 +14,6 @@ namespace gclo.Engine.Tests;
/// branches, settings failure fallbacks, store corruption handling, and the
/// remaining status-text arms. Part of the 100%-coverage contract.
///
-[Collection("GCLO_DATA_DIR")] // sets a process-wide variable: never beside another class that does
public sealed class CoverageResidueTests
{
// ------------------------------------------------- WindowsPathValidator
@@ -55,7 +54,6 @@ public sealed class CoverageResidueTests
{
string dir = Path.Combine(Path.GetTempPath(), "gclo-tests", Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(dir);
- Environment.SetEnvironmentVariable("GCLO_DATA_DIR", dir);
return (dir, new EnvScope(dir));
}
@@ -65,7 +63,6 @@ public sealed class CoverageResidueTests
{
public void Dispose()
{
- Environment.SetEnvironmentVariable("GCLO_DATA_DIR", null);
try
{
Directory.Delete(dir, recursive: true);
@@ -86,7 +83,7 @@ public sealed class CoverageResidueTests
{
File.WriteAllText(Path.Combine(dir, "settings.json"), "{ not json");
- var settings = AppSettings.Load();
+ var settings = AppSettings.Load(directory: dir);
Assert.Equal("System", settings.Theme);
Assert.Equal(AppSettings.DefaultConcurrency, settings.DefaultMaxConcurrency);
@@ -103,7 +100,7 @@ public sealed class CoverageResidueTests
{
File.WriteAllText(Path.Combine(dir, "settings.json"), "null");
- Assert.Equal("System", AppSettings.Load().Theme);
+ Assert.Equal("System", AppSettings.Load(directory: dir).Theme);
}
}
@@ -118,7 +115,7 @@ public sealed class CoverageResidueTests
File.WriteAllText(
Path.Combine(dir, "settings.json"), """{ "Theme": "Hotdog" }""");
- Assert.Equal("System", AppSettings.Load().Theme);
+ Assert.Equal("System", AppSettings.Load(directory: dir).Theme);
}
}
@@ -134,7 +131,10 @@ public sealed class CoverageResidueTests
using var exclusiveLock = new FileStream(
path, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
- new AppSettings().Save(); // must not throw
+ // Must not throw, and must say that nothing was written.
+ bool saved = new AppSettings().Save(log: null, TimeSpan.FromMilliseconds(50), dir);
+
+ Assert.False(saved);
}
}
diff --git a/gclo.Engine.Tests/GcloPathsTests.cs b/gclo.Engine.Tests/GcloPathsTests.cs
index a9af527..38fd8b1 100644
--- a/gclo.Engine.Tests/GcloPathsTests.cs
+++ b/gclo.Engine.Tests/GcloPathsTests.cs
@@ -77,4 +77,29 @@ public sealed class GcloPathsTests
Assert.Equal(Path.Combine(GcloPaths.DefaultDataRoot, "logs"), GcloPaths.LogDirectory);
}
+
+
+
+ ///
+ /// Settings named no folder go to the data folder. Checked here, in the one class
+ /// that moves the data folder for the process, so no settings test has to.
+ ///
+ [Fact]
+ public void Settings_SavedAndLoadedWithoutAFolder_UseTheDataRoot()
+ {
+ string moved = Path.Combine(Path.GetTempPath(), "gclo-tests", Guid.NewGuid().ToString("N"));
+ Environment.SetEnvironmentVariable("GCLO_DATA_DIR", moved);
+ try
+ {
+ Assert.True(new AppSettings { Theme = "Dark" }.Save());
+
+ Assert.True(File.Exists(Path.Combine(moved, "settings.json")));
+ Assert.Equal("Dark", AppSettings.Load().Theme);
+ }
+ finally
+ {
+ Environment.SetEnvironmentVariable("GCLO_DATA_DIR", null);
+ GitTestHelpers.TryDeleteDirectory(moved);
+ }
+ }
}
diff --git a/gclo.Engine.Tests/SyncAllCoordinatorTests.cs b/gclo.Engine.Tests/SyncAllCoordinatorTests.cs
index bd3f2d7..5ac4ff6 100644
--- a/gclo.Engine.Tests/SyncAllCoordinatorTests.cs
+++ b/gclo.Engine.Tests/SyncAllCoordinatorTests.cs
@@ -43,10 +43,23 @@ public sealed class SyncAllCoordinatorTests : IDisposable
- /// Synchronous progress: handler runs inline on the reporting thread.
+ ///
+ /// Synchronous progress: the handler runs inline on the reporting thread, one
+ /// report at a time. The view model's handler belongs to the UI thread and is
+ /// never entered twice at once there; several workers reporting at the same
+ /// moment must not enter it together here either.
+ ///
private sealed class SyncProgress(Action handler) : IProgress
{
- public void Report(RepoProgress value) => handler(value);
+ private readonly Lock _oneAtATime = new();
+
+ public void Report(RepoProgress value)
+ {
+ lock(_oneAtATime)
+ {
+ handler(value);
+ }
+ }
}
@@ -213,11 +226,11 @@ public sealed class SyncAllCoordinatorTests : IDisposable
Task run = _coordinator.RunAsync(new[] { alpha, beta }, CancellationToken.None);
- await entered.Task; // alpha's sync is in flight...
+ await entered.Task.WaitAsync(TimeSpan.FromSeconds(30)); // alpha's sync is in flight...
Assert.Equal(new[] { "org-alpha" }, _lister.Organizations); // ...and beta has not started
gate.SetResult();
- SyncAllResult result = await run;
+ SyncAllResult result = await run.WaitAsync(TimeSpan.FromSeconds(30));
Assert.Equal(new SyncAllResult(Ran: 2, Skipped: 0, WasCanceled: false), result);
Assert.Equal(new[] { "org-alpha", "org-beta" }, _lister.Organizations);
@@ -336,10 +349,10 @@ public sealed class SyncAllCoordinatorTests : IDisposable
using var cts = new CancellationTokenSource();
Task run = _coordinator.RunAsync(new[] { alpha, beta }, cts.Token);
- await entered.Task;
+ await entered.Task.WaitAsync(TimeSpan.FromSeconds(30));
cts.Cancel(); // requested while alpha's sync is in flight...
gate.SetResult(); // ...which must still run to completion
- SyncAllResult result = await run;
+ SyncAllResult result = await run.WaitAsync(TimeSpan.FromSeconds(30));
Assert.Equal(new SyncAllResult(Ran: 1, Skipped: 0, WasCanceled: true), result);
Assert.Equal(SyncStatus.Done, alpha.Repos[0].Status); // finished, not canceled
diff --git a/gclo.Engine.Tests/VaultEdgeTests.cs b/gclo.Engine.Tests/VaultEdgeTests.cs
index 9d3fbab..ae70cc5 100644
--- a/gclo.Engine.Tests/VaultEdgeTests.cs
+++ b/gclo.Engine.Tests/VaultEdgeTests.cs
@@ -98,6 +98,28 @@ public sealed class VaultEdgeTests
+ /// A vault whose entries are named for this test run alone, never in the namespace the app uses.
+ private static CredentialManagerVault NewTestVault()
+ => new(isWindows: true, scope: "tests-" + Guid.NewGuid().ToString("N")[..12]);
+
+
+
+ [Fact]
+ public void DefaultConstructor_NamesItsEntriesForTheDataFolder_AndFindsNothingForAnAccountNobodySaved()
+ {
+ if(!OperatingSystem.IsWindows())
+ {
+ return;
+ }
+
+ // The one test that uses the namespace of the real data folder, and it only reads.
+ var vault = new CredentialManagerVault();
+
+ Assert.Null(vault.TryRetrieve(Guid.NewGuid()));
+ }
+
+
+
[Fact]
public void Store_TokenLargerThanCredManBlobLimit_SurfacesWin32Error()
{
@@ -107,7 +129,7 @@ public sealed class VaultEdgeTests
}
// CRED_MAX_CREDENTIAL_BLOB_SIZE is 2560 bytes; UTF-16 doubles the length.
- var vault = new CredentialManagerVault();
+ CredentialManagerVault vault = NewTestVault();
string oversized = new('x', 4000);
Win32Exception ex = Assert.Throws(() => vault.Store(Guid.NewGuid(), oversized));
@@ -125,7 +147,7 @@ public sealed class VaultEdgeTests
return;
}
- var vault = new CredentialManagerVault();
+ CredentialManagerVault vault = NewTestVault();
string invalidTarget = new('x', 40_000); // far past CRED_MAX_GENERIC_TARGET_NAME_LENGTH
Win32Exception ex = Assert.Throws(() => vault.TryRetrieve(invalidTarget, Guid.NewGuid()));
@@ -143,7 +165,7 @@ public sealed class VaultEdgeTests
return;
}
- var vault = new CredentialManagerVault();
+ CredentialManagerVault vault = NewTestVault();
string invalidTarget = new('x', 40_000);
Win32Exception ex = Assert.Throws(() => vault.Delete(invalidTarget, Guid.NewGuid()));
diff --git a/gclo.Engine.Tests/VaultTests.cs b/gclo.Engine.Tests/VaultTests.cs
index c5a73f4..36b7293 100644
--- a/gclo.Engine.Tests/VaultTests.cs
+++ b/gclo.Engine.Tests/VaultTests.cs
@@ -69,6 +69,17 @@ public sealed class VaultTests
// ---------------------------------------------------------------- CredentialManagerVault
+ ///
+ /// A vault over the real Credential Manager whose entries are named for this test
+ /// run alone ("gclo:tests-...:account:..."), never in the namespace the installed
+ /// app and the CLI use: whatever a test writes or leaves behind is not an account's token.
+ ///
+ [System.Runtime.Versioning.SupportedOSPlatform("windows")]
+ private static CredentialManagerVault NewTestVault()
+ => new(isWindows: true, scope: "tests-" + Guid.NewGuid().ToString("N")[..12]);
+
+
+
[Fact]
public void CredentialManager_RoundTrips_AgainstTheRealStore()
{
@@ -79,7 +90,7 @@ public sealed class VaultTests
return;
}
- var vault = new CredentialManagerVault();
+ CredentialManagerVault vault = NewTestVault();
var id = Guid.NewGuid();
try
{
diff --git a/gclo.Engine.Tests/WorkspaceViewModelTests.cs b/gclo.Engine.Tests/WorkspaceViewModelTests.cs
index c1b0710..7134d4f 100644
--- a/gclo.Engine.Tests/WorkspaceViewModelTests.cs
+++ b/gclo.Engine.Tests/WorkspaceViewModelTests.cs
@@ -30,6 +30,11 @@ public sealed class WorkspaceViewModelTests : IDisposable
+ /// Every temporary folder a test asked for; removed after it, whatever the test did with it.
+ private readonly List _folders = new();
+
+
+
public void Dispose()
{
foreach(WorkspaceViewModel vm in _created)
@@ -40,14 +45,45 @@ public sealed class WorkspaceViewModelTests : IDisposable
TryDeleteDirectory(vm.TargetFolder);
}
}
+ foreach(string folder in _folders)
+ {
+ TryDeleteDirectory(folder);
+ }
}
- /// Synchronous progress: handler runs inline on the reporting thread.
+ ///
+ /// A folder name of this test's own under the temp folder. Remembered, so it is
+ /// removed afterwards also when the view model that synced into it was pointed
+ /// elsewhere later, or was never tracked.
+ ///
+ private string NewTempFolder()
+ {
+ string folder = Path.Combine(Path.GetTempPath(), "gclo-tests", Guid.NewGuid().ToString("N"));
+ _folders.Add(folder);
+ return folder;
+ }
+
+
+
+ ///
+ /// Synchronous progress: the handler runs inline on the reporting thread, one
+ /// report at a time. The view model's handler belongs to the UI thread and is
+ /// never entered twice at once there; several workers reporting at the same
+ /// moment must not enter it together here either.
+ ///
private sealed class SyncProgress(Action handler) : IProgress
{
- public void Report(RepoProgress value) => handler(value);
+ private readonly Lock _oneAtATime = new();
+
+ public void Report(RepoProgress value)
+ {
+ lock(_oneAtATime)
+ {
+ handler(value);
+ }
+ }
}
@@ -100,7 +136,7 @@ public sealed class WorkspaceViewModelTests : IDisposable
vm.Organization = "acme";
vm.Token = "token-1234567890";
await WaitUntilAsync(() => vm.StatusText.Length > 0, "org lookup to settle");
- vm.TargetFolder = Path.Combine(Path.GetTempPath(), "gclo-tests", Guid.NewGuid().ToString("N"));
+ vm.TargetFolder = NewTempFolder();
vm.MaxConcurrencyValue = 1; // serialize progress: the sync fake reports inline across threads
await vm.LoadReposCommand.ExecuteAsync(null);
@@ -236,7 +272,7 @@ public sealed class WorkspaceViewModelTests : IDisposable
vm.Organization = "acme";
vm.Token = "token-1234567890";
await WaitUntilAsync(() => vm.StatusText.Length > 0, "org lookup to settle");
- vm.TargetFolder = Path.Combine(Path.GetTempPath(), "gclo-tests", Guid.NewGuid().ToString("N"));
+ vm.TargetFolder = NewTempFolder();
vm.MaxConcurrencyValue = 1;
Assert.True(vm.CanEditInputs);
@@ -1388,7 +1424,7 @@ public sealed class WorkspaceViewModelTests : IDisposable
vm.Organization = "acme";
vm.Token = "token-1234567890";
await WaitUntilAsync(() => vm.StatusText.Length > 0, "org lookup to settle");
- vm.TargetFolder = Path.Combine(Path.GetTempPath(), "gclo-tests", Guid.NewGuid().ToString("N"));
+ vm.TargetFolder = NewTempFolder();
await vm.LoadReposCommand.ExecuteAsync(null);
int reportingThread = -1;
_git.CloneHandler = (_, _, _, _, _) =>
@@ -1854,7 +1890,7 @@ public sealed class WorkspaceViewModelTests : IDisposable
Id = Guid.NewGuid(),
Name = "Work",
Organization = "acme",
- TargetRoot = Path.Combine(Path.GetTempPath(), "gclo-tests", Guid.NewGuid().ToString("N")),
+ TargetRoot = NewTempFolder(),
};
var vault = new InMemoryVault();
vault.Store(account.Id, "vault-token-1234567890");
@@ -2000,7 +2036,7 @@ public sealed class WorkspaceViewModelTests : IDisposable
[Fact]
public async Task Sync_AccountWorkspace_RecordsTheResultOnTheStore()
{
- string root = Path.Combine(Path.GetTempPath(), "gclo-tests", Guid.NewGuid().ToString("N"));
+ string root = NewTempFolder();
try
{
var vault = new InMemoryVault();
@@ -2033,7 +2069,7 @@ public sealed class WorkspaceViewModelTests : IDisposable
[Fact]
public async Task Sync_QuickSync_DoesNotRecordOnTheStore()
{
- string root = Path.Combine(Path.GetTempPath(), "gclo-tests", Guid.NewGuid().ToString("N"));
+ string root = NewTempFolder();
try
{
var vault = new InMemoryVault();
@@ -2068,7 +2104,7 @@ public sealed class WorkspaceViewModelTests : IDisposable
[Fact]
public async Task Sync_RecordSyncResultFailure_StillCompletesTheRun()
{
- string root = Path.Combine(Path.GetTempPath(), "gclo-tests", Guid.NewGuid().ToString("N"));
+ string root = NewTempFolder();
string storeDir = Path.Combine(root, "store");
try
{
@@ -2221,7 +2257,7 @@ public sealed class WorkspaceViewModelTests : IDisposable
vm.Organization = "acme";
vm.Token = "token-1234567890";
await WaitUntilAsync(() => vm.StatusText.Length > 0, "org lookup to settle");
- vm.TargetFolder = Path.Combine(Path.GetTempPath(), "gclo-tests", Guid.NewGuid().ToString("N"));
+ vm.TargetFolder = NewTempFolder();
Task firstLoad = vm.LoadReposCommand.ExecuteAsync(null);
first.SetResult([Repo("alpha")]);
@@ -2382,7 +2418,7 @@ public sealed class WorkspaceViewModelTests : IDisposable
vm.Organization = "acme";
vm.Token = "token-1234567890";
await WaitUntilAsync(() => vm.StatusText.Length > 0, "org lookup to settle");
- vm.TargetFolder = Path.Combine(Path.GetTempPath(), "gclo-tests", Guid.NewGuid().ToString("N"));
+ vm.TargetFolder = NewTempFolder();
await vm.LoadReposCommand.ExecuteAsync(null);
await vm.SyncCommand.ExecuteAsync(null); // captures the handler
@@ -2401,8 +2437,8 @@ public sealed class WorkspaceViewModelTests : IDisposable
- /// A folder of this test's own; the view model's target folder is removed when the test ends.
- private static string NewTargetRoot() => Path.Combine(Path.GetTempPath(), "gclo-tests", Guid.NewGuid().ToString("N"));
+ /// A folder of this test's own, removed when the test ends.
+ private string NewTargetRoot() => NewTempFolder();
diff --git a/gclo.UiTests/SmokeTests.cs b/gclo.UiTests/SmokeTests.cs
index 9485164..f29cc2b 100644
--- a/gclo.UiTests/SmokeTests.cs
+++ b/gclo.UiTests/SmokeTests.cs
@@ -162,14 +162,16 @@ public sealed partial class SmokeTests : UiTestBase
- // (7) Typing into the token box (which kicks the offline org-listing path) does
- // not crash the app, and the org combo stays present. No token validation
- // is asserted — these tests run without network access or credentials.
+ // (7) Typing into the token box does not crash the app, and the org combo
+ // stays present. The text is too short to be taken for a token, so nothing
+ // is looked up: these tests run without network access or credentials, and
+ // a longer made-up token would be sent to the real API to be refused. The
+ // listing itself is exercised offline, with the fixture token, in RepoTableTests.
[Fact]
public void ConnectTokenBox_TypingText_KeepsAppAliveAndOrgBoxPresent()
{
AutomationElement tokenBox = Session.WaitForElement("ConnectTokenBox");
- Session.EnterText(tokenBox, "ghp_uitests_offline_dummy_token");
+ Session.EnterText(tokenBox, "ghp_short");
Assert.NotNull(Session.WaitForElement("ConnectOrgBox"));
Assert.False(Session.App.HasExited);
diff --git a/gclo.ViewModels/AppSettings.cs b/gclo.ViewModels/AppSettings.cs
index 320b08a..1d15691 100644
--- a/gclo.ViewModels/AppSettings.cs
+++ b/gclo.ViewModels/AppSettings.cs
@@ -54,7 +54,8 @@ public sealed class AppSettings
- private static string SettingsPath => Path.Combine(GcloPaths.DataRoot, "settings.json");
+ /// The settings file in , or in the data folder when none is named.
+ private static string SettingsPath(string? directory) => Path.Combine(directory ?? GcloPaths.DataRoot, "settings.json");
@@ -63,11 +64,13 @@ public sealed class AppSettings
/// yields defaults, and loaded values are sanitized (concurrency clamped, theme validated).
/// A file that is there but cannot be used is reported to .
///
- public static AppSettings Load(IActivityLog? log = null)
+ /// Where a file that cannot be used is reported.
+ /// The folder that holds settings.json; the data folder () when null.
+ public static AppSettings Load(IActivityLog? log = null, string? directory = null)
{
try
{
- string path = SettingsPath;
+ string path = SettingsPath(directory);
if(File.Exists(path))
{
AppSettings? loaded = JsonSerializer.Deserialize(
@@ -104,22 +107,24 @@ public sealed class AppSettings
/// because losing a preference write must never crash the app; they are reported
/// to and in the result.
///
+ /// Where a failure is reported.
+ /// The folder that holds settings.json; the data folder () when null.
/// True when the settings are on disk.
- public bool Save(IActivityLog? log = null) => Save(log, s_savePatience);
+ public bool Save(IActivityLog? log = null, string? directory = null) => Save(log, s_savePatience, directory);
/// Saves with its own patience for a file that is in use, for tests.
- internal bool Save(IActivityLog? log, TimeSpan patience)
+ internal bool Save(IActivityLog? log, TimeSpan patience, string? directory = null)
{
try
{
Sanitize();
- string path = SettingsPath;
- string? directory = Path.GetDirectoryName(path);
- if(!string.IsNullOrEmpty(directory))
+ string path = SettingsPath(directory);
+ string? folder = Path.GetDirectoryName(path);
+ if(!string.IsNullOrEmpty(folder))
{
- Directory.CreateDirectory(directory);
+ Directory.CreateDirectory(folder);
}
// A name of its own: two processes saving at once do not write one file.