From 886f587a2563d1d45d7ef75a2cfeee42a5622276 Mon Sep 17 00:00:00 2001 From: Stuart Meeks Date: Wed, 19 Aug 2026 14:34:17 +0000 Subject: [PATCH] test: migrate to xUnit v3 and run the Execution suite in CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two changes that both land in the test stack. xUnit v3. The old `xunit` package ID is the feature-frozen v2 line (2.9.3 is its latest); v3 ships as `xunit.v3`, currently 4.0.0. v3 self-hosts on Microsoft Testing Platform rather than VSTest, so the whole VSTest stack comes out: Microsoft.NET.Test.Sdk, xunit.runner.visualstudio, and coverlet.collector (a VSTest data collector, inert under MTP) are replaced by xunit.v3 plus Microsoft.Testing.Extensions.CodeCoverage. Test projects are now executables, `dotnet test` opts into the MTP runner via a repo-root global.json (hence `--project `), and TRX comes from `--report-xunit-trx`, not `--logger trx`. The v3 analyzers (2.0.0) added two rules that TreatWarningsAsErrors turns into build failures, so the test code moved with them rather than suppressing: xUnit1051 (208 sites) — CancellationToken-accepting calls now pass TestContext.Current.CancellationToken, so a hung await is actually interruptible. Applied via the analyzer's own code fix. xUnit2033 (12 sites) — use the value Assert.Single returns instead of re-deriving it with [0]. Hand-applied; the fixer has no fix-all. CI gap. Snipdeck.Execution.Tests was built but never run by any job, so its 61 tests only ever executed locally. It now runs alongside Core on ubuntu, and also on Windows — Execution wraps ConPTY and process launching, so Windows is the platform that actually matters for it. Acknowledgements (README and the in-app list) swap Coverlet for Microsoft Testing Platform to match what is now referenced. All 372 tests pass on both Linux and Windows, and the full solution builds clean with no warnings on the Windows head. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/ci.yml | 22 +++++-- .github/workflows/release.yml | 2 +- CHANGELOG.md | 8 +++ CONTRIBUTING.md | 11 +++- Directory.Packages.props | 12 ++-- README.md | 7 +- global.json | 5 ++ .../ViewModels/SettingsViewModel.cs | 2 +- .../Engine/SubstitutionEngineTests.cs | 4 +- .../Services/BackupServiceTests.cs | 38 +++++------ .../Services/ExamplesSeedTests.cs | 4 +- .../Services/IconAssetStorageTests.cs | 4 +- .../Services/JsonSettingsStoreTests.cs | 20 +++--- .../Services/JsonSnipStoreTests.cs | 28 ++++---- .../Services/SnipFilterTests.cs | 12 ++-- .../Snipdeck.Core.Tests.csproj | 7 +- .../ViewModels/SettingsViewModelTests.cs | 14 ++-- .../ViewModels/ShellViewModelCommandsTests.cs | 18 ++--- .../ViewModels/ShellViewModelNavTests.cs | 2 +- .../ViewModels/ShellViewModelTests.cs | 14 ++-- .../ViewModels/SnipEditorViewModelTests.cs | 8 +-- .../SqliteCommandHistoryStoreTests.cs | 66 +++++++++---------- .../Snipdeck.Execution.Tests.csproj | 7 +- .../ViewModels/HistoryViewModelTests.cs | 14 ++-- .../ParameterSharerTests.cs | 8 +-- .../Snipdeck.Importer.Tests.csproj | 7 +- .../StoreMergerTests.cs | 4 +- 27 files changed, 190 insertions(+), 158 deletions(-) create mode 100644 global.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8018ad..b75da6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ env: jobs: core-tests: - name: Core build + tests (ubuntu) + name: Core + Execution build + tests (ubuntu) runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -39,13 +39,20 @@ jobs: - name: Restore Core projects run: | dotnet restore src/Snipdeck.Core/Snipdeck.Core.csproj + dotnet restore src/Snipdeck.Execution/Snipdeck.Execution.csproj dotnet restore tests/Snipdeck.Core.Tests/Snipdeck.Core.Tests.csproj + dotnet restore tests/Snipdeck.Execution.Tests/Snipdeck.Execution.Tests.csproj - name: Build Core - run: dotnet build src/Snipdeck.Core/Snipdeck.Core.csproj --configuration Release --no-restore + run: | + dotnet build src/Snipdeck.Core/Snipdeck.Core.csproj --configuration Release --no-restore + dotnet build src/Snipdeck.Execution/Snipdeck.Execution.csproj --configuration Release --no-restore - name: Build + run Core tests - run: dotnet test tests/Snipdeck.Core.Tests/Snipdeck.Core.Tests.csproj --configuration Release --no-restore --logger "trx;LogFileName=core-tests.trx" --results-directory TestResults + run: dotnet test --project tests/Snipdeck.Core.Tests/Snipdeck.Core.Tests.csproj --configuration Release --no-restore --report-xunit-trx --report-xunit-trx-filename core-tests.trx --results-directory TestResults + + - name: Build + run Execution tests + run: dotnet test --project tests/Snipdeck.Execution.Tests/Snipdeck.Execution.Tests.csproj --configuration Release --no-restore --report-xunit-trx --report-xunit-trx-filename execution-tests.trx --results-directory TestResults - name: Upload test results if: always() @@ -80,7 +87,7 @@ jobs: run: dotnet build tools/Snipdeck.Importer/Snipdeck.Importer.csproj --configuration Release --no-restore - name: Build + run importer tests - run: dotnet test tools/Snipdeck.Importer.Tests/Snipdeck.Importer.Tests.csproj --configuration Release --no-restore --logger "trx;LogFileName=importer-tests.trx" --results-directory TestResults + run: dotnet test --project tools/Snipdeck.Importer.Tests/Snipdeck.Importer.Tests.csproj --configuration Release --no-restore --report-xunit-trx --report-xunit-trx-filename importer-tests.trx --results-directory TestResults - name: Upload test results if: always() @@ -113,7 +120,12 @@ jobs: run: dotnet build --configuration Release --no-restore - name: Run Core tests (sanity check on Windows) - run: dotnet test tests/Snipdeck.Core.Tests/Snipdeck.Core.Tests.csproj --configuration Release --no-build --logger "trx;LogFileName=core-tests-windows.trx" --results-directory TestResults + run: dotnet test --project tests/Snipdeck.Core.Tests/Snipdeck.Core.Tests.csproj --configuration Release --no-build --report-xunit-trx --report-xunit-trx-filename core-tests-windows.trx --results-directory TestResults + + # Execution wraps ConPTY and process launching, so Windows is the platform + # that actually matters for it — the ubuntu run only proves it is portable. + - name: Run Execution tests (ConPTY + process launching are platform-specific) + run: dotnet test --project tests/Snipdeck.Execution.Tests/Snipdeck.Execution.Tests.csproj --configuration Release --no-build --report-xunit-trx --report-xunit-trx-filename execution-tests-windows.trx --results-directory TestResults - name: Upload test results if: always() diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 85f9fe5..3f4ac2d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -87,7 +87,7 @@ jobs: run: dotnet restore - name: Run Core tests - run: dotnet test tests/Snipdeck.Core.Tests/Snipdeck.Core.Tests.csproj --configuration Release --no-restore + run: dotnet test --project tests/Snipdeck.Core.Tests/Snipdeck.Core.Tests.csproj --configuration Release --no-restore - name: Publish Snipdeck.App (win-x64) # No -p:Version here — Nerdbank.GitVersioning stamps the assemblies at diff --git a/CHANGELOG.md b/CHANGELOG.md index 01ad917..36751ab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed +- Migrated the test suites to xUnit v3 on Microsoft Testing Platform, replacing + the VSTest stack. Development-only; the shipped app is unaffected. + +### Fixed +- CI now runs the `Snipdeck.Execution` test suite, on both Ubuntu and Windows. + It was built but never executed, so 61 tests were only ever run locally. + ### Changed - Refreshed dependencies to their latest stable releases, including the Windows App SDK (2.4.0), WebView2, the Windows SDK build tools, SQLite, the dependency diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index cead4cf..04b12a4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -21,10 +21,17 @@ dotnet build # Build Core only (works on Linux / macOS) dotnet build src/Snipdeck.Core -# Run Core tests -dotnet test tests/Snipdeck.Core.Tests +# Run the tests (Core, Execution, importer) +dotnet test --project tests/Snipdeck.Core.Tests +dotnet test --project tests/Snipdeck.Execution.Tests +dotnet test --project tools/Snipdeck.Importer.Tests ``` +The test projects are **xUnit v3**, which runs on Microsoft Testing Platform +rather than VSTest. The repo-root `global.json` opts `dotnet test` into that +runner, which is why the project is passed as `--project ` rather than as a +bare argument. TRX reports come from `--report-xunit-trx`, not `--logger trx`. + On a non-Windows machine, restoring the `Snipdeck.App` project requires `EnableWindowsTargeting=true`: diff --git a/Directory.Packages.props b/Directory.Packages.props index d8e6811..a959e19 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -42,12 +42,14 @@ - + - - - - + + diff --git a/README.md b/README.md index d7b13b3..ae1e47d 100644 --- a/README.md +++ b/README.md @@ -114,8 +114,9 @@ Requirements: # Restore + build everything dotnet build -# Run Core unit tests -dotnet test tests/Snipdeck.Core.Tests +# Run the unit tests +dotnet test --project tests/Snipdeck.Core.Tests +dotnet test --project tests/Snipdeck.Execution.Tests ``` The `Snipdeck.Core` project targets `net10.0` and is fully portable, so @@ -137,10 +138,10 @@ Snipdeck is built on the work of these open-source projects, with thanks to their authors and maintainers: - [.NET Community Toolkit](https://github.com/CommunityToolkit/dotnet) — MVVM source generators and helpers (MIT) -- [Coverlet](https://github.com/coverlet-coverage/coverlet) — code-coverage collection (MIT) - [H.NotifyIcon](https://github.com/HavenDV/H.NotifyIcon) — system-tray icon and menu (MIT) - [Jdenticon](https://github.com/dmester/jdenticon-net) — identicons for CLIs without a custom icon (MIT) - [Markdig](https://github.com/xoofx/markdig) — Markdown rendering for Snip descriptions (BSD-2-Clause) +- [Microsoft Testing Platform](https://github.com/microsoft/testfx) — runs the test suites and collects code coverage (MIT) - [Microsoft.Extensions.DependencyInjection](https://github.com/dotnet/runtime) — dependency injection (MIT) - [Nerdbank.GitVersioning](https://github.com/dotnet/Nerdbank.GitVersioning) — git-derived versioning (MIT) - [Porta.Pty](https://github.com/tomlm/Porta.Pty) — cross-platform pseudo-terminal (ConPTY) for running commands (MIT) diff --git a/global.json b/global.json new file mode 100644 index 0000000..3140116 --- /dev/null +++ b/global.json @@ -0,0 +1,5 @@ +{ + "test": { + "runner": "Microsoft.Testing.Platform" + } +} diff --git a/src/Snipdeck.Core/ViewModels/SettingsViewModel.cs b/src/Snipdeck.Core/ViewModels/SettingsViewModel.cs index c90a011..100ecd4 100644 --- a/src/Snipdeck.Core/ViewModels/SettingsViewModel.cs +++ b/src/Snipdeck.Core/ViewModels/SettingsViewModel.cs @@ -117,7 +117,7 @@ public SettingsViewModel( new("Spectre.Console", "Console UI for the SnipCommand import tool.", new Uri("https://github.com/spectreconsole/spectre.console"), "MIT"), new("Nerdbank.GitVersioning", "Derives the version from git history.", new Uri("https://github.com/dotnet/Nerdbank.GitVersioning"), "MIT"), new("xUnit", "Unit-testing framework.", new Uri("https://github.com/xunit/xunit"), "Apache-2.0"), - new("Coverlet", "Code-coverage collection for tests.", new Uri("https://github.com/coverlet-coverage/coverlet"), "MIT"), + new("Microsoft Testing Platform", "Runs the test suites and collects code coverage.", new Uri("https://github.com/microsoft/testfx"), "MIT"), ]; [ObservableProperty] diff --git a/tests/Snipdeck.Core.Tests/Engine/SubstitutionEngineTests.cs b/tests/Snipdeck.Core.Tests/Engine/SubstitutionEngineTests.cs index cb48a52..9d1c918 100644 --- a/tests/Snipdeck.Core.Tests/Engine/SubstitutionEngineTests.cs +++ b/tests/Snipdeck.Core.Tests/Engine/SubstitutionEngineTests.cs @@ -81,8 +81,8 @@ public void Repeated_missing_token_is_listed_once_only() new Dictionary()); Assert.Equal("{missing} and {missing} again", result.Text); - Assert.Single(result.UnresolvedTokens); - Assert.Equal("missing", result.UnresolvedTokens[0]); + var token = Assert.Single(result.UnresolvedTokens); + Assert.Equal("missing", token); } [Fact] diff --git a/tests/Snipdeck.Core.Tests/Services/BackupServiceTests.cs b/tests/Snipdeck.Core.Tests/Services/BackupServiceTests.cs index 0f6d1e7..5fc8160 100644 --- a/tests/Snipdeck.Core.Tests/Services/BackupServiceTests.cs +++ b/tests/Snipdeck.Core.Tests/Services/BackupServiceTests.cs @@ -54,7 +54,7 @@ public async Task CreateBackupAsync_returns_null_when_source_missing() var clock = new FakeClock(new DateTimeOffset(2026, 5, 29, 12, 0, 0, TimeSpan.Zero)); var service = BuildService(clock); - var info = await service.CreateBackupAsync(); + var info = await service.CreateBackupAsync(TestContext.Current.CancellationToken); Assert.Null(info); Assert.False(Directory.Exists(_backupDirectory) && Directory.EnumerateFiles(_backupDirectory).Any()); @@ -67,12 +67,12 @@ public async Task CreateBackupAsync_copies_source_to_timestamped_destination() var clock = new FakeClock(new DateTimeOffset(2026, 5, 29, 12, 34, 56, 789, TimeSpan.Zero)); var service = BuildService(clock); - var info = await service.CreateBackupAsync(); + var info = await service.CreateBackupAsync(TestContext.Current.CancellationToken); Assert.NotNull(info); Assert.True(File.Exists(info!.FilePath)); Assert.EndsWith("snipstore_20260529_123456789.json", info.FilePath); - Assert.Equal("hello", await File.ReadAllTextAsync(info.FilePath)); + Assert.Equal("hello", await File.ReadAllTextAsync(info.FilePath, TestContext.Current.CancellationToken)); Assert.Equal(5, info.SizeBytes); Assert.Equal(clock.UtcNow, info.CreatedAtUtc); } @@ -84,7 +84,7 @@ public async Task CreateBackupAsync_creates_missing_backup_directory() Assert.False(Directory.Exists(_backupDirectory)); var clock = new FakeClock(new DateTimeOffset(2026, 5, 29, 12, 0, 0, TimeSpan.Zero)); - await BuildService(clock).CreateBackupAsync(); + await BuildService(clock).CreateBackupAsync(TestContext.Current.CancellationToken); Assert.True(Directory.Exists(_backupDirectory)); } @@ -97,9 +97,9 @@ public async Task CreateBackupAsync_appends_collision_suffix_when_clock_repeats( var clock = new FakeClock(fixedTime); var service = BuildService(clock); - var first = await service.CreateBackupAsync(); - var second = await service.CreateBackupAsync(); - var third = await service.CreateBackupAsync(); + var first = await service.CreateBackupAsync(TestContext.Current.CancellationToken); + var second = await service.CreateBackupAsync(TestContext.Current.CancellationToken); + var third = await service.CreateBackupAsync(TestContext.Current.CancellationToken); Assert.NotEqual(first!.FilePath, second!.FilePath); Assert.NotEqual(second.FilePath, third!.FilePath); @@ -117,7 +117,7 @@ public async Task CreateBackupAsync_prunes_backups_beyond_retention() for (var i = 0; i < 5; i++) { - await service.CreateBackupAsync(); + await service.CreateBackupAsync(TestContext.Current.CancellationToken); clock.Advance(TimeSpan.FromSeconds(1)); } @@ -143,14 +143,14 @@ public async Task CreateBackupAsync_reads_retention_lazily_from_provider() // Fill up under the initial retention of 5. for (var i = 0; i < 5; i++) { - await service.CreateBackupAsync(); + await service.CreateBackupAsync(TestContext.Current.CancellationToken); clock.Advance(TimeSpan.FromSeconds(1)); } Assert.Equal(5, Directory.GetFiles(_backupDirectory, "snipstore_*.json").Length); // Tighten retention and back up again: the next prune honours the new value. retention = 2; - await service.CreateBackupAsync(); + await service.CreateBackupAsync(TestContext.Current.CancellationToken); Assert.Equal(2, Directory.GetFiles(_backupDirectory, "snipstore_*.json").Length); } @@ -164,7 +164,7 @@ public async Task CreateBackupAsync_clamps_non_positive_provider_value_to_one() for (var i = 0; i < 4; i++) { - await service.CreateBackupAsync(); + await service.CreateBackupAsync(TestContext.Current.CancellationToken); clock.Advance(TimeSpan.FromSeconds(1)); } @@ -186,19 +186,19 @@ public async Task PruneStep_does_not_touch_unrelated_files_in_backup_directory() WriteSource(); Directory.CreateDirectory(_backupDirectory); var sibling = Path.Combine(_backupDirectory, "not-a-backup.txt"); - await File.WriteAllTextAsync(sibling, "untouched"); + await File.WriteAllTextAsync(sibling, "untouched", TestContext.Current.CancellationToken); var clock = new FakeClock(new DateTimeOffset(2026, 5, 29, 12, 0, 0, TimeSpan.Zero)); var service = BuildService(clock, retention: 1); for (var i = 0; i < 5; i++) { - await service.CreateBackupAsync(); + await service.CreateBackupAsync(TestContext.Current.CancellationToken); clock.Advance(TimeSpan.FromSeconds(1)); } Assert.True(File.Exists(sibling)); - Assert.Equal("untouched", await File.ReadAllTextAsync(sibling)); + Assert.Equal("untouched", await File.ReadAllTextAsync(sibling, TestContext.Current.CancellationToken)); } [Fact] @@ -208,13 +208,13 @@ public async Task ListBackupsAsync_returns_newest_first_with_parsed_timestamps() var clock = new FakeClock(new DateTimeOffset(2026, 5, 29, 12, 0, 0, TimeSpan.Zero)); var service = BuildService(clock); - await service.CreateBackupAsync(); + await service.CreateBackupAsync(TestContext.Current.CancellationToken); clock.Advance(TimeSpan.FromSeconds(5)); - await service.CreateBackupAsync(); + await service.CreateBackupAsync(TestContext.Current.CancellationToken); clock.Advance(TimeSpan.FromSeconds(5)); - await service.CreateBackupAsync(); + await service.CreateBackupAsync(TestContext.Current.CancellationToken); - var list = await service.ListBackupsAsync(); + var list = await service.ListBackupsAsync(TestContext.Current.CancellationToken); Assert.Equal(3, list.Count); Assert.True(list[0].CreatedAtUtc > list[1].CreatedAtUtc); @@ -227,7 +227,7 @@ public async Task ListBackupsAsync_returns_empty_when_directory_missing() var clock = new FakeClock(DateTimeOffset.UtcNow); var service = BuildService(clock); - var list = await service.ListBackupsAsync(); + var list = await service.ListBackupsAsync(TestContext.Current.CancellationToken); Assert.Empty(list); } diff --git a/tests/Snipdeck.Core.Tests/Services/ExamplesSeedTests.cs b/tests/Snipdeck.Core.Tests/Services/ExamplesSeedTests.cs index 3a7fa58..eb72849 100644 --- a/tests/Snipdeck.Core.Tests/Services/ExamplesSeedTests.cs +++ b/tests/Snipdeck.Core.Tests/Services/ExamplesSeedTests.cs @@ -95,8 +95,8 @@ public async Task Seed_round_trips_through_the_json_store() var store = new JsonSnipStore(Path.Combine(tempDir, "store.json")); var original = ExamplesSeed.Build(); - await store.SaveAsync(original); - var loaded = await store.LoadAsync(); + await store.SaveAsync(original, TestContext.Current.CancellationToken); + var loaded = await store.LoadAsync(TestContext.Current.CancellationToken); Assert.Equal(original.Clis.Count, loaded.Clis.Count); Assert.Equal(original.Snips.Count, loaded.Snips.Count); diff --git a/tests/Snipdeck.Core.Tests/Services/IconAssetStorageTests.cs b/tests/Snipdeck.Core.Tests/Services/IconAssetStorageTests.cs index c26ee68..45247e1 100644 --- a/tests/Snipdeck.Core.Tests/Services/IconAssetStorageTests.cs +++ b/tests/Snipdeck.Core.Tests/Services/IconAssetStorageTests.cs @@ -51,7 +51,7 @@ public async Task DeleteIconAsync_ignores_parent_directory_traversal() { // A file outside the icons directory that a malformed IconRef tries to reach. var victim = Path.Combine(_baseDirectory, "important.txt"); - await File.WriteAllTextAsync(victim, "keep me"); + await File.WriteAllTextAsync(victim, "keep me", TestContext.Current.CancellationToken); await _storage.DeleteIconAsync("../important.txt"); @@ -62,7 +62,7 @@ public async Task DeleteIconAsync_ignores_parent_directory_traversal() public async Task DeleteIconAsync_ignores_absolute_paths() { var victim = Path.Combine(_baseDirectory, "outside.txt"); - await File.WriteAllTextAsync(victim, "keep me"); + await File.WriteAllTextAsync(victim, "keep me", TestContext.Current.CancellationToken); // Path.Combine(base, absolute) discards base — an unguarded delete would hit this. await _storage.DeleteIconAsync(victim); diff --git a/tests/Snipdeck.Core.Tests/Services/JsonSettingsStoreTests.cs b/tests/Snipdeck.Core.Tests/Services/JsonSettingsStoreTests.cs index 3486b78..68bdbf1 100644 --- a/tests/Snipdeck.Core.Tests/Services/JsonSettingsStoreTests.cs +++ b/tests/Snipdeck.Core.Tests/Services/JsonSettingsStoreTests.cs @@ -30,7 +30,7 @@ public async Task LoadAsync_returns_defaults_when_file_missing() { var store = new JsonSettingsStore(PathIn("settings.json")); - var config = await store.LoadAsync(); + var config = await store.LoadAsync(TestContext.Current.CancellationToken); Assert.Equal(AppConfig.CurrentSchemaVersion, config.SchemaVersion); Assert.Null(config.StoragePath); @@ -58,9 +58,9 @@ await store.SaveAsync(new AppConfig Modifiers = HotkeyModifiers.Control | HotkeyModifiers.Shift, Key = "Space", }, - }); + }, TestContext.Current.CancellationToken); - var loaded = await store.LoadAsync(); + var loaded = await store.LoadAsync(TestContext.Current.CancellationToken); Assert.Equal("/data/store", loaded.StoragePath); Assert.Equal("/data/backups", loaded.BackupDirectory); @@ -76,7 +76,7 @@ public async Task SaveAsync_creates_missing_parent_directory() var nested = PathIn("a/b/settings.json"); var store = new JsonSettingsStore(nested); - await store.SaveAsync(new AppConfig()); + await store.SaveAsync(new AppConfig(), TestContext.Current.CancellationToken); Assert.True(File.Exists(nested)); } @@ -87,7 +87,7 @@ public async Task SaveAsync_does_not_leave_tmp_file_behind_on_success() var path = PathIn("settings.json"); var store = new JsonSettingsStore(path); - await store.SaveAsync(new AppConfig()); + await store.SaveAsync(new AppConfig(), TestContext.Current.CancellationToken); Assert.False(File.Exists(path + ".tmp")); Assert.True(File.Exists(path)); @@ -100,21 +100,21 @@ public async Task LoadAsync_throws_when_schema_version_is_newer_than_supported() var futureJson = $$""" { "schemaVersion": {{AppConfig.CurrentSchemaVersion + 1}} } """; - await File.WriteAllTextAsync(path, futureJson); + await File.WriteAllTextAsync(path, futureJson, TestContext.Current.CancellationToken); var store = new JsonSettingsStore(path); - await Assert.ThrowsAsync(() => store.LoadAsync()); + await Assert.ThrowsAsync(() => store.LoadAsync(TestContext.Current.CancellationToken)); } [Fact] public async Task LoadAsync_repairs_a_missing_hotkey_with_the_default() { var path = PathIn("settings.json"); - await File.WriteAllTextAsync(path, "{}"); + await File.WriteAllTextAsync(path, "{}", TestContext.Current.CancellationToken); var store = new JsonSettingsStore(path); - var loaded = await store.LoadAsync(); + var loaded = await store.LoadAsync(TestContext.Current.CancellationToken); Assert.NotNull(loaded.Hotkey); Assert.Equal(HotkeyModifiers.Control | HotkeyModifiers.Alt, loaded.Hotkey.Modifiers); @@ -126,7 +126,7 @@ public async Task SaveAsync_throws_on_null_config() { var store = new JsonSettingsStore(PathIn("settings.json")); - await Assert.ThrowsAsync(() => store.SaveAsync(null!)); + await Assert.ThrowsAsync(() => store.SaveAsync(null!, TestContext.Current.CancellationToken)); } } } diff --git a/tests/Snipdeck.Core.Tests/Services/JsonSnipStoreTests.cs b/tests/Snipdeck.Core.Tests/Services/JsonSnipStoreTests.cs index 39aa2fa..b964596 100644 --- a/tests/Snipdeck.Core.Tests/Services/JsonSnipStoreTests.cs +++ b/tests/Snipdeck.Core.Tests/Services/JsonSnipStoreTests.cs @@ -38,7 +38,7 @@ public async Task LoadAsync_returns_empty_document_when_file_missing() { var store = new JsonSnipStore(PathIn("store.json")); - var document = await store.LoadAsync(); + var document = await store.LoadAsync(TestContext.Current.CancellationToken); Assert.Equal(SnipStoreDocument.CurrentSchemaVersion, document.SchemaVersion); Assert.Empty(document.Clis); @@ -88,8 +88,8 @@ public async Task SaveAsync_then_LoadAsync_round_trips_full_document() }, }; - await store.SaveAsync(original); - var loaded = await store.LoadAsync(); + await store.SaveAsync(original, TestContext.Current.CancellationToken); + var loaded = await store.LoadAsync(TestContext.Current.CancellationToken); Assert.Equal(SnipStoreDocument.CurrentSchemaVersion, loaded.SchemaVersion); @@ -123,7 +123,7 @@ public async Task SaveAsync_creates_missing_parent_directory() var nested = PathIn("a/b/c/store.json"); var store = new JsonSnipStore(nested); - await store.SaveAsync(new SnipStoreDocument()); + await store.SaveAsync(new SnipStoreDocument(), TestContext.Current.CancellationToken); Assert.True(File.Exists(nested)); } @@ -134,7 +134,7 @@ public async Task SaveAsync_does_not_leave_tmp_file_behind_on_success() var path = PathIn("store.json"); var store = new JsonSnipStore(path); - await store.SaveAsync(new SnipStoreDocument()); + await store.SaveAsync(new SnipStoreDocument(), TestContext.Current.CancellationToken); Assert.False(File.Exists(path + ".tmp")); Assert.True(File.Exists(path)); @@ -149,14 +149,14 @@ public async Task SaveAsync_overwrites_existing_file() await store.SaveAsync(new SnipStoreDocument { Clis = { new Cli { Name = "first" } }, - }); + }, TestContext.Current.CancellationToken); await store.SaveAsync(new SnipStoreDocument { Clis = { new Cli { Name = "second" } }, - }); + }, TestContext.Current.CancellationToken); - var loaded = await store.LoadAsync(); + var loaded = await store.LoadAsync(TestContext.Current.CancellationToken); var cli = Assert.Single(loaded.Clis); Assert.Equal("second", cli.Name); } @@ -172,11 +172,11 @@ public async Task LoadAsync_throws_when_schema_version_is_newer_than_supported() "snips": [] } """; - await File.WriteAllTextAsync(path, futureJson); + await File.WriteAllTextAsync(path, futureJson, TestContext.Current.CancellationToken); var store = new JsonSnipStore(path); - await Assert.ThrowsAsync(() => store.LoadAsync()); + await Assert.ThrowsAsync(() => store.LoadAsync(TestContext.Current.CancellationToken)); } [Fact] @@ -184,7 +184,7 @@ public async Task SaveAsync_throws_on_null_document() { var store = new JsonSnipStore(PathIn("store.json")); - await Assert.ThrowsAsync(() => store.SaveAsync(null!)); + await Assert.ThrowsAsync(() => store.SaveAsync(null!, TestContext.Current.CancellationToken)); } [Fact] @@ -200,7 +200,7 @@ public async Task Concurrent_saves_leave_a_consistent_well_formed_file() await Task.WhenAll(tasks); - var loaded = await store.LoadAsync(); + var loaded = await store.LoadAsync(TestContext.Current.CancellationToken); var cli = Assert.Single(loaded.Clis); Assert.StartsWith("cli-", cli.Name); Assert.False(File.Exists(path + ".tmp")); @@ -224,9 +224,9 @@ await store.SaveAsync(new SnipStoreDocument }, }, }, - }); + }, TestContext.Current.CancellationToken); - var json = await File.ReadAllTextAsync(path); + var json = await File.ReadAllTextAsync(path, TestContext.Current.CancellationToken); Assert.Contains("\"type\": \"choice\"", json); } } diff --git a/tests/Snipdeck.Core.Tests/Services/SnipFilterTests.cs b/tests/Snipdeck.Core.Tests/Services/SnipFilterTests.cs index bb0eb04..695718a 100644 --- a/tests/Snipdeck.Core.Tests/Services/SnipFilterTests.cs +++ b/tests/Snipdeck.Core.Tests/Services/SnipFilterTests.cs @@ -72,8 +72,8 @@ public void Search_matches_command_template() var result = SnipFilter.Apply(snips, "users", null).ToList(); - Assert.Single(result); - Assert.Equal("b", result[0].Title); + var match = Assert.Single(result); + Assert.Equal("b", match.Title); } [Fact] @@ -87,8 +87,8 @@ public void Search_matches_tag() var result = SnipFilter.Apply(snips, "deploy", null).ToList(); - Assert.Single(result); - Assert.Equal("a", result[0].Title); + var match = Assert.Single(result); + Assert.Equal("a", match.Title); } [Fact] @@ -120,8 +120,8 @@ public void Search_and_tag_apply_together_as_an_AND_filter() var result = SnipFilter.Apply(snips, "logs", "read").ToList(); - Assert.Single(result); - Assert.Equal("Read logs", result[0].Title); + var match = Assert.Single(result); + Assert.Equal("Read logs", match.Title); } [Fact] diff --git a/tests/Snipdeck.Core.Tests/Snipdeck.Core.Tests.csproj b/tests/Snipdeck.Core.Tests/Snipdeck.Core.Tests.csproj index 9276705..7cbb623 100644 --- a/tests/Snipdeck.Core.Tests/Snipdeck.Core.Tests.csproj +++ b/tests/Snipdeck.Core.Tests/Snipdeck.Core.Tests.csproj @@ -2,6 +2,7 @@ net10.0 + Exe false