test: the suites write nothing outside their own folders and send nothing anywhere #162
No files matched your search
@@ -37,9 +37,23 @@ public sealed class ActionLoggingTests : IDisposable
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private sealed class SyncProgress(Action<RepoProgress> handler) : IProgress<RepoProgress>
|
||||
{
|
||||
public void Report(RepoProgress value) => handler(value);
|
||||
private readonly Lock _oneAtATime = new();
|
||||
|
||||
public void Report(RepoProgress value)
|
||||
{
|
||||
lock(_oneAtATime)
|
||||
{
|
||||
handler(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ namespace gclo.Engine.Tests;
|
||||
|
||||
|
||||
/// <summary>Pins defaults and load-time sanitizing of <see cref="AppSettings"/>.</summary>
|
||||
[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<bool> 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);
|
||||
});
|
||||
}
|
||||
@@ -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.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,4 +77,29 @@ public sealed class GcloPathsTests
|
||||
|
||||
Assert.Equal(Path.Combine(GcloPaths.DefaultDataRoot, "logs"), GcloPaths.LogDirectory);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,10 +43,23 @@ public sealed class SyncAllCoordinatorTests : IDisposable
|
||||
|
||||
|
||||
|
||||
/// <summary>Synchronous progress: handler runs inline on the reporting thread.</summary>
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private sealed class SyncProgress(Action<RepoProgress> handler) : IProgress<RepoProgress>
|
||||
{
|
||||
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<SyncAllResult> 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<SyncAllResult> 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
|
||||
|
||||
@@ -98,6 +98,28 @@ public sealed class VaultEdgeTests
|
||||
|
||||
|
||||
|
||||
/// <summary>A vault whose entries are named for this test run alone, never in the namespace the app uses.</summary>
|
||||
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<Win32Exception>(() => 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<Win32Exception>(() => 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<Win32Exception>(() => vault.Delete(invalidTarget, Guid.NewGuid()));
|
||||
|
||||
@@ -69,6 +69,17 @@ public sealed class VaultTests
|
||||
|
||||
// ---------------------------------------------------------------- CredentialManagerVault
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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
|
||||
{
|
||||
|
||||
@@ -30,6 +30,11 @@ public sealed class WorkspaceViewModelTests : IDisposable
|
||||
|
||||
|
||||
|
||||
/// <summary>Every temporary folder a test asked for; removed after it, whatever the test did with it.</summary>
|
||||
private readonly List<string> _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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>Synchronous progress: handler runs inline on the reporting thread.</summary>
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private string NewTempFolder()
|
||||
{
|
||||
string folder = Path.Combine(Path.GetTempPath(), "gclo-tests", Guid.NewGuid().ToString("N"));
|
||||
_folders.Add(folder);
|
||||
return folder;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private sealed class SyncProgress(Action<RepoProgress> handler) : IProgress<RepoProgress>
|
||||
{
|
||||
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
|
||||
|
||||
|
||||
|
||||
/// <summary>A folder of this test's own; the view model's target folder is removed when the test ends.</summary>
|
||||
private static string NewTargetRoot() => Path.Combine(Path.GetTempPath(), "gclo-tests", Guid.NewGuid().ToString("N"));
|
||||
/// <summary>A folder of this test's own, removed when the test ends.</summary>
|
||||
private string NewTargetRoot() => NewTempFolder();
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -54,7 +54,8 @@ public sealed class AppSettings
|
||||
|
||||
|
||||
|
||||
private static string SettingsPath => Path.Combine(GcloPaths.DataRoot, "settings.json");
|
||||
/// <summary>The settings file in <paramref name="directory"/>, or in the data folder when none is named.</summary>
|
||||
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 <paramref name="log"/>.
|
||||
/// </summary>
|
||||
public static AppSettings Load(IActivityLog? log = null)
|
||||
/// <param name="log">Where a file that cannot be used is reported.</param>
|
||||
/// <param name="directory">The folder that holds settings.json; the data folder (<see cref="GcloPaths.DataRoot"/>) when null.</param>
|
||||
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 <paramref name="log"/> and in the result.
|
||||
/// </summary>
|
||||
/// <param name="log">Where a failure is reported.</param>
|
||||
/// <param name="directory">The folder that holds settings.json; the data folder (<see cref="GcloPaths.DataRoot"/>) when null.</param>
|
||||
/// <returns>True when the settings are on disk.</returns>
|
||||
public bool Save(IActivityLog? log = null) => Save(log, s_savePatience);
|
||||
public bool Save(IActivityLog? log = null, string? directory = null) => Save(log, s_savePatience, directory);
|
||||
|
||||
|
||||
|
||||
/// <summary>Saves with its own patience for a file that is in use, for tests.</summary>
|
||||
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.
|
||||
|
||||
Reference in new issue
Block a user