From aa110bf41a3d913b08d72dae6240007c9592bea6 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 12 May 2026 15:12:18 +0700 Subject: [PATCH 1/7] Update to .NET 10, bump NuGet packages All package bumps from running `dotnet package update`. --- Directory.Packages.props | 40 +++++++++++++++++++-------------------- src/Directory.Build.props | 2 +- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 4263bab..84d0f34 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,26 +3,26 @@ true - - - + + + - - - - - - - - + + + + + + + + - - - - - - - - + + + + + + + + - + \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 9f63e20..5e9eda0 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,6 +1,6 @@ - net8.0 + net10.0 enable false enable From a913a851cb05ad8d9d0d8a9c06c57cf37b5666ed Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 12 May 2026 18:12:33 +0700 Subject: [PATCH 2/7] Adjust to breaking changes in new package versions --- Directory.Packages.props | 7 ++++--- src/SIL.Harmony.Linq2db/Linq2dbKernel.cs | 2 +- src/SIL.Harmony.Linq2db/SIL.Harmony.Linq2db.csproj | 2 +- src/SIL.Harmony.Tests/DataModelPerformanceTests.cs | 1 - src/SIL.Harmony.Tests/DataModelReferenceTests.cs | 2 +- src/SIL.Harmony.Tests/DataModelTestBase.cs | 6 +++--- src/SIL.Harmony.Tests/DataQueryTests.cs | 2 +- src/SIL.Harmony.Tests/MultiThreadingTests.cs | 3 +-- src/SIL.Harmony.Tests/RepositoryTests.cs | 4 ++-- src/SIL.Harmony.Tests/SIL.Harmony.Tests.csproj | 6 ++++-- src/SIL.Harmony.Tests/SyncTests.cs | 4 ++-- 11 files changed, 20 insertions(+), 19 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 84d0f34..212d12e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -17,12 +17,13 @@ - - + + - + + \ No newline at end of file diff --git a/src/SIL.Harmony.Linq2db/Linq2dbKernel.cs b/src/SIL.Harmony.Linq2db/Linq2dbKernel.cs index 5c32565..a4d9f8f 100644 --- a/src/SIL.Harmony.Linq2db/Linq2dbKernel.cs +++ b/src/SIL.Harmony.Linq2db/Linq2dbKernel.cs @@ -1,6 +1,6 @@ using SIL.Harmony.Core; -using LinqToDB.AspNet.Logging; using LinqToDB.EntityFrameworkCore; +using LinqToDB.Extensions.Logging; using LinqToDB.Mapping; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; diff --git a/src/SIL.Harmony.Linq2db/SIL.Harmony.Linq2db.csproj b/src/SIL.Harmony.Linq2db/SIL.Harmony.Linq2db.csproj index d530422..1453659 100644 --- a/src/SIL.Harmony.Linq2db/SIL.Harmony.Linq2db.csproj +++ b/src/SIL.Harmony.Linq2db/SIL.Harmony.Linq2db.csproj @@ -10,8 +10,8 @@ - + diff --git a/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs b/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs index 84398e5..f3ba500 100644 --- a/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs +++ b/src/SIL.Harmony.Tests/DataModelPerformanceTests.cs @@ -11,7 +11,6 @@ using SIL.Harmony.Changes; using SIL.Harmony.Db; using SIL.Harmony.Sample.Changes; -using Xunit.Abstractions; namespace SIL.Harmony.Tests; diff --git a/src/SIL.Harmony.Tests/DataModelReferenceTests.cs b/src/SIL.Harmony.Tests/DataModelReferenceTests.cs index cad2545..702ab44 100644 --- a/src/SIL.Harmony.Tests/DataModelReferenceTests.cs +++ b/src/SIL.Harmony.Tests/DataModelReferenceTests.cs @@ -10,7 +10,7 @@ public class DataModelReferenceTests : DataModelTestBase private readonly Guid _word1Id = Guid.NewGuid(); private readonly Guid _word2Id = Guid.NewGuid(); - public override async Task InitializeAsync() + public override async ValueTask InitializeAsync() { await base.InitializeAsync(); await WriteNextChange(SetWord(_word1Id, "entity1")); diff --git a/src/SIL.Harmony.Tests/DataModelTestBase.cs b/src/SIL.Harmony.Tests/DataModelTestBase.cs index 09e5b2c..d195249 100644 --- a/src/SIL.Harmony.Tests/DataModelTestBase.cs +++ b/src/SIL.Harmony.Tests/DataModelTestBase.cs @@ -161,12 +161,12 @@ public IChange NewDefinition(Guid wordId, }; } - public virtual Task InitializeAsync() + public virtual ValueTask InitializeAsync() { - return Task.CompletedTask; + return ValueTask.CompletedTask; } - public async Task DisposeAsync() + public async ValueTask DisposeAsync() { await _services.DisposeAsync(); diff --git a/src/SIL.Harmony.Tests/DataQueryTests.cs b/src/SIL.Harmony.Tests/DataQueryTests.cs index 1144e4b..6d3beb9 100644 --- a/src/SIL.Harmony.Tests/DataQueryTests.cs +++ b/src/SIL.Harmony.Tests/DataQueryTests.cs @@ -6,7 +6,7 @@ namespace SIL.Harmony.Tests; public class DataQueryTests: DataModelTestBase { private readonly Guid _entity1Id = Guid.NewGuid(); - public override async Task InitializeAsync() + public override async ValueTask InitializeAsync() { await base.InitializeAsync(); await WriteNextChange(SetWord(_entity1Id, "entity1")); diff --git a/src/SIL.Harmony.Tests/MultiThreadingTests.cs b/src/SIL.Harmony.Tests/MultiThreadingTests.cs index 6d68314..cd36094 100644 --- a/src/SIL.Harmony.Tests/MultiThreadingTests.cs +++ b/src/SIL.Harmony.Tests/MultiThreadingTests.cs @@ -1,5 +1,4 @@ using Microsoft.Data.Sqlite; -using Xunit.Abstractions; namespace SIL.Harmony.Tests; @@ -17,7 +16,7 @@ public class MultiThreadingTests(ITestOutputHelper output) { var random = new Random(); var fixture = new DataModelTestBase(new SqliteConnection(_connectionString)); - fixture.InitializeAsync().Wait(); + fixture.InitializeAsync().AsTask().Wait(); var id = Guid.NewGuid(); for (var i = 0; i < 100; i++) { diff --git a/src/SIL.Harmony.Tests/RepositoryTests.cs b/src/SIL.Harmony.Tests/RepositoryTests.cs index b3e23ad..9f2b91e 100644 --- a/src/SIL.Harmony.Tests/RepositoryTests.cs +++ b/src/SIL.Harmony.Tests/RepositoryTests.cs @@ -25,14 +25,14 @@ public RepositoryTests() _crdtDbContext = _services.GetRequiredService(); } - public async Task InitializeAsync() + public async ValueTask InitializeAsync() { // Open the connection manually, otherwise it will be closed after each command, wiping out the in memory sqlite db await _crdtDbContext.Database.OpenConnectionAsync(); await _crdtDbContext.Database.EnsureCreatedAsync(); } - public async Task DisposeAsync() + public async ValueTask DisposeAsync() { await _repository.DisposeAsync(); await _services.DisposeAsync(); diff --git a/src/SIL.Harmony.Tests/SIL.Harmony.Tests.csproj b/src/SIL.Harmony.Tests/SIL.Harmony.Tests.csproj index fdfa563..f5c0252 100644 --- a/src/SIL.Harmony.Tests/SIL.Harmony.Tests.csproj +++ b/src/SIL.Harmony.Tests/SIL.Harmony.Tests.csproj @@ -17,8 +17,8 @@ - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -28,6 +28,8 @@ all + + diff --git a/src/SIL.Harmony.Tests/SyncTests.cs b/src/SIL.Harmony.Tests/SyncTests.cs index bc9985a..2ab3254 100644 --- a/src/SIL.Harmony.Tests/SyncTests.cs +++ b/src/SIL.Harmony.Tests/SyncTests.cs @@ -8,13 +8,13 @@ public class SyncTests : IAsyncLifetime private readonly DataModelTestBase _client1 = new(); private readonly DataModelTestBase _client2 = new(); - public async Task InitializeAsync() + public async ValueTask InitializeAsync() { await _client1.InitializeAsync(); await _client2.InitializeAsync(); } - public async Task DisposeAsync() + public async ValueTask DisposeAsync() { await _client1.DisposeAsync(); await _client2.DisposeAsync(); From 7f68b4c448acd9f1d6ff7d2aca250edad02da1a1 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Tue, 12 May 2026 20:48:45 +0700 Subject: [PATCH 3/7] Fix errors from GitHubActionsTestLogger --- Directory.Packages.props | 1 + src/SIL.Harmony.Tests/SIL.Harmony.Tests.csproj | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 212d12e..0fafb5c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,6 +6,7 @@ + diff --git a/src/SIL.Harmony.Tests/SIL.Harmony.Tests.csproj b/src/SIL.Harmony.Tests/SIL.Harmony.Tests.csproj index f5c0252..f542263 100644 --- a/src/SIL.Harmony.Tests/SIL.Harmony.Tests.csproj +++ b/src/SIL.Harmony.Tests/SIL.Harmony.Tests.csproj @@ -9,9 +9,9 @@ + all - runtime; build; native; contentfiles; analyzers; buildtransitive From 743e92c79b08a115876be58a74d7f30995e02a9d Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Wed, 13 May 2026 12:30:28 +0700 Subject: [PATCH 4/7] Update DbContext verified model with 10.0.7 syntax --- .../DbContextTests.VerifyModel.verified.txt | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/src/SIL.Harmony.Tests/DbContextTests.VerifyModel.verified.txt b/src/SIL.Harmony.Tests/DbContextTests.VerifyModel.verified.txt index 79ccc2f..018dda9 100644 --- a/src/SIL.Harmony.Tests/DbContextTests.VerifyModel.verified.txt +++ b/src/SIL.Harmony.Tests/DbContextTests.VerifyModel.verified.txt @@ -24,7 +24,6 @@ Keys: Id PK Annotations: - DiscriminatorProperty: Relational:FunctionName: Relational:Schema: Relational:SqlQuery: @@ -44,7 +43,6 @@ Foreign keys: ChangeEntity {'CommitId'} -> Commit {'Id'} Required Cascade ToDependent: ChangeEntities Annotations: - DiscriminatorProperty: Relational:FunctionName: Relational:Schema: Relational:SqlQuery: @@ -66,7 +64,7 @@ ElementType: Element type: Guid Required TypeName (string) Required Navigations: - Commit (Commit) ToPrincipal Commit Inverse: Snapshots + Commit (Commit) Required ToPrincipal Commit Inverse: Snapshots Keys: Id PK Foreign keys: @@ -75,7 +73,6 @@ EntityId CommitId, EntityId Unique Annotations: - DiscriminatorProperty: Relational:FunctionName: Relational:Schema: Relational:SqlQuery: @@ -89,7 +86,6 @@ Keys: Id PK Annotations: - DiscriminatorProperty: Relational:FunctionName: Relational:Schema: Relational:SqlQuery: @@ -109,7 +105,6 @@ Indexes: SnapshotId Unique Annotations: - DiscriminatorProperty: Relational:FunctionName: Relational:Schema: Relational:SqlQuery: @@ -135,7 +130,6 @@ SnapshotId Unique WordId Annotations: - DiscriminatorProperty: Relational:FunctionName: Relational:Schema: Relational:SqlQuery: @@ -156,7 +150,6 @@ Indexes: SnapshotId Unique Annotations: - DiscriminatorProperty: Relational:FunctionName: Relational:Schema: Relational:SqlQuery: @@ -179,7 +172,6 @@ SnapshotId Unique Text Unique Annotations: - DiscriminatorProperty: Relational:FunctionName: Relational:Schema: Relational:SqlQuery: @@ -196,7 +188,7 @@ SnapshotId (no field, Guid?) Shadow FK Index Text (string) Required Navigations: - Antonym (Word) ToPrincipal Word + Antonym (Word) Optional ToPrincipal Word Skip navigations: Tags (List) CollectionTag Inverse: Word Keys: @@ -208,7 +200,6 @@ AntonymId SnapshotId Unique Annotations: - DiscriminatorProperty: Relational:FunctionName: Relational:Schema: Relational:SqlQuery: @@ -233,7 +224,6 @@ TagId WordId, TagId Unique Annotations: - DiscriminatorProperty: Relational:FunctionName: Relational:Schema: Relational:SqlQuery: @@ -241,4 +231,4 @@ Relational:ViewName: Relational:ViewSchema: Annotations: - ProductVersion: 9.0.4 \ No newline at end of file + ProductVersion: 10.0.7 \ No newline at end of file From 50ed2303a19a2b9aee0e951b8e02b8eea57b0203 Mon Sep 17 00:00:00 2001 From: Robin Munn Date: Wed, 13 May 2026 14:00:16 +0700 Subject: [PATCH 5/7] More efficient MultiThreadingTests awaiting ValueTask.GetAwaiter().GetResult() doesn't allocate a Task. --- src/SIL.Harmony.Tests/MultiThreadingTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SIL.Harmony.Tests/MultiThreadingTests.cs b/src/SIL.Harmony.Tests/MultiThreadingTests.cs index cd36094..6efee0f 100644 --- a/src/SIL.Harmony.Tests/MultiThreadingTests.cs +++ b/src/SIL.Harmony.Tests/MultiThreadingTests.cs @@ -16,7 +16,7 @@ public class MultiThreadingTests(ITestOutputHelper output) { var random = new Random(); var fixture = new DataModelTestBase(new SqliteConnection(_connectionString)); - fixture.InitializeAsync().AsTask().Wait(); + fixture.InitializeAsync().GetAwaiter().GetResult(); var id = Guid.NewGuid(); for (var i = 0; i < 100; i++) { From 8c46b4503666dba8a3b72c4bb546f884964b3212 Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Wed, 13 May 2026 11:56:38 +0200 Subject: [PATCH 6/7] Use TestContext.Current.CancellationToken --- .../Adapter/CustomObjectAdapterTests.cs | 6 +- .../DataModelReferenceTests.cs | 22 +- .../DataModelSimpleChanges.cs | 16 +- src/SIL.Harmony.Tests/DataQueryTests.cs | 2 +- src/SIL.Harmony.Tests/DbContextTests.cs | 180 +++++------ src/SIL.Harmony.Tests/DefinitionTests.cs | 212 ++++++------- src/SIL.Harmony.Tests/DeleteAndCreateTests.cs | 22 +- src/SIL.Harmony.Tests/ModelSnapshotTests.cs | 284 +++++++++--------- .../PersistExtraDataTests.cs | 2 +- src/SIL.Harmony.Tests/RepositoryTests.cs | 22 +- .../ResourceTests/RemoteResourcesTests.cs | 2 +- .../ResourceTests/WordResourceTests.cs | 80 ++--- src/SIL.Harmony.Tests/SnapshotTests.cs | 16 +- src/SIL.Harmony.Tests/SyncTests.cs | 4 +- 14 files changed, 435 insertions(+), 435 deletions(-) diff --git a/src/SIL.Harmony.Tests/Adapter/CustomObjectAdapterTests.cs b/src/SIL.Harmony.Tests/Adapter/CustomObjectAdapterTests.cs index 8014d54..0be8f90 100644 --- a/src/SIL.Harmony.Tests/Adapter/CustomObjectAdapterTests.cs +++ b/src/SIL.Harmony.Tests/Adapter/CustomObjectAdapterTests.cs @@ -158,8 +158,8 @@ public async Task CanAdaptACustomObject() .Add(builder => builder.HasKey(o => o.Identifier)); }).BuildServiceProvider(); var myDbContext = services.GetRequiredService(); - await myDbContext.Database.OpenConnectionAsync(); - await myDbContext.Database.EnsureCreatedAsync(); + await myDbContext.Database.OpenConnectionAsync(TestContext.Current.CancellationToken); + await myDbContext.Database.EnsureCreatedAsync(TestContext.Current.CancellationToken); var dataModel = services.GetRequiredService(); var objectId = Guid.NewGuid(); var objectId2 = Guid.NewGuid(); @@ -194,6 +194,6 @@ await dataModel.AddChange(Guid.NewGuid(), myClass2.MyNumber.Should().Be(123.45m); myClass2.DeletedTime.Should().BeNull(); - dataModel.QueryLatest().ToBlockingEnumerable().Should().NotBeEmpty(); + dataModel.QueryLatest().ToBlockingEnumerable(TestContext.Current.CancellationToken).Should().NotBeEmpty(); } } \ No newline at end of file diff --git a/src/SIL.Harmony.Tests/DataModelReferenceTests.cs b/src/SIL.Harmony.Tests/DataModelReferenceTests.cs index 702ab44..7d77c4f 100644 --- a/src/SIL.Harmony.Tests/DataModelReferenceTests.cs +++ b/src/SIL.Harmony.Tests/DataModelReferenceTests.cs @@ -41,7 +41,7 @@ public async Task AddReferenceWorks(bool includeObjectInSnapshot) // assert - projected entity var entityWord = await DataModel.QueryLatest(w => w.Include(w => w.Antonym)) - .Where(w => w.Id == _word1Id).SingleOrDefaultAsync(); + .Where(w => w.Id == _word1Id).SingleOrDefaultAsync(TestContext.Current.CancellationToken); entityWord.Should().NotBeNull(); entityWord.AntonymId.Should().Be(_word2Id); entityWord.Antonym.Should().NotBeNull(); @@ -80,7 +80,7 @@ await WriteNextChange( // assert - projected entity var entityWord = await DataModel.QueryLatest(w => w.Include(w => w.Antonym)) - .Where(w => w.Id == word3Id).SingleOrDefaultAsync(); + .Where(w => w.Id == word3Id).SingleOrDefaultAsync(TestContext.Current.CancellationToken); entityWord.Should().NotBeNull(); entityWord.AntonymId.Should().Be(_word2Id); entityWord.Antonym.Should().NotBeNull(); @@ -118,7 +118,7 @@ await WriteNextChange(new SetAntonymReferenceChange(word3Id, _word2Id, setObject // assert - projected entity var entityWord = await DataModel.QueryLatest(w => w.Include(w => w.Antonym)) - .Where(w => w.Id == word3Id).SingleOrDefaultAsync(); + .Where(w => w.Id == word3Id).SingleOrDefaultAsync(TestContext.Current.CancellationToken); entityWord.Should().NotBeNull(); entityWord.AntonymId.Should().Be(_word2Id); entityWord.Antonym.Should().NotBeNull(); @@ -157,7 +157,7 @@ await WriteNextChange( // assert - projected entity var entityWord = await DataModel.QueryLatest(w => w.Include(w => w.Antonym)) - .Where(w => w.Id == word3Id).SingleOrDefaultAsync(); + .Where(w => w.Id == word3Id).SingleOrDefaultAsync(TestContext.Current.CancellationToken); entityWord.Should().NotBeNull(); entityWord.Text.Should().Be("entity3"); entityWord.AntonymId.Should().Be(_word1Id); @@ -197,7 +197,7 @@ await WriteNextChange( // assert - projected entity var entityWord = await DataModel.QueryLatest(w => w.Include(w => w.Antonym)) - .Where(w => w.Id == _word1Id).SingleOrDefaultAsync(); + .Where(w => w.Id == _word1Id).SingleOrDefaultAsync(TestContext.Current.CancellationToken); entityWord.Should().NotBeNull(); entityWord.Text.Should().Be("entity1"); entityWord.AntonymId.Should().Be(word3Id); @@ -236,7 +236,7 @@ await WriteNextChange(new SetAntonymReferenceChange(word3Id, _word1Id, setObject // assert - projected entity var entityWord = await DataModel.QueryLatest(w => w.Include(w => w.Antonym)) - .Where(w => w.Id == word3Id).SingleOrDefaultAsync(); + .Where(w => w.Id == word3Id).SingleOrDefaultAsync(TestContext.Current.CancellationToken); entityWord.Should().NotBeNull(); entityWord.Text.Should().Be("entity3"); entityWord.AntonymId.Should().Be(_word1Id); @@ -275,7 +275,7 @@ await WriteNextChange(new SetAntonymReferenceChange(_word1Id, word3Id, setObject // assert - projected entity var entityWord = await DataModel.QueryLatest(w => w.Include(w => w.Antonym)) - .Where(w => w.Id == _word1Id).SingleOrDefaultAsync(); + .Where(w => w.Id == _word1Id).SingleOrDefaultAsync(TestContext.Current.CancellationToken); entityWord.Should().NotBeNull(); entityWord.Text.Should().Be("entity1"); entityWord.AntonymId.Should().Be(word3Id); @@ -341,10 +341,10 @@ await AddCommitsViaSync([ initialWordCommit, deleteWordCommit ]); - var snapshot = await DbContext.Snapshots.SingleAsync(s => s.CommitId == initialWordCommit.Id); + var snapshot = await DbContext.Snapshots.SingleAsync(s => s.CommitId == initialWordCommit.Id, TestContext.Current.CancellationToken); var initialWord = (Word) snapshot.Entity; initialWord.AntonymId.Should().Be(_word1Id); - snapshot = await DbContext.Snapshots.SingleAsync(s => s.CommitId == deleteWordCommit.Id && s.EntityId == wordId); + snapshot = await DbContext.Snapshots.SingleAsync(s => s.CommitId == deleteWordCommit.Id && s.EntityId == wordId, TestContext.Current.CancellationToken); var wordWithoutRef = (Word) snapshot.Entity; wordWithoutRef.AntonymId.Should().BeNull(); } @@ -422,7 +422,7 @@ public async Task CanCreate2TagsWithTheSameNameOutOfOrder() var commitA = await WriteNextChange(SetTag(Guid.NewGuid(), tagText)); //represents someone syncing in a tag with the same name await WriteChangeBefore(commitA, SetTag(Guid.NewGuid(), tagText)); - DataModel.QueryLatest().ToBlockingEnumerable().Where(t => t.Text == tagText).Should().ContainSingle(); + DataModel.QueryLatest().ToBlockingEnumerable(TestContext.Current.CancellationToken).Where(t => t.Text == tagText).Should().ContainSingle(); } [Fact] @@ -434,6 +434,6 @@ public async Task CanUpdateTagWithTheSameNameOutOfOrder() var commitA = await WriteNextChange(SetTag(Guid.NewGuid(), tagText)); //represents someone syncing in a tag with the same name await WriteNextChange(SetTag(renameTagId, tagText)); - DataModel.QueryLatest().ToBlockingEnumerable().Where(t => t.Text == tagText).Should().ContainSingle(); + DataModel.QueryLatest().ToBlockingEnumerable(TestContext.Current.CancellationToken).Where(t => t.Text == tagText).Should().ContainSingle(); } } diff --git a/src/SIL.Harmony.Tests/DataModelSimpleChanges.cs b/src/SIL.Harmony.Tests/DataModelSimpleChanges.cs index 7d989c1..8d92cc1 100644 --- a/src/SIL.Harmony.Tests/DataModelSimpleChanges.cs +++ b/src/SIL.Harmony.Tests/DataModelSimpleChanges.cs @@ -77,7 +77,7 @@ public async Task WriteMultipleCommits() await WriteNextChange(SetWord(Guid.NewGuid(), "change 3")); DbContext.Snapshots.Should().HaveCount(3); - DataModel.QueryLatest().ToBlockingEnumerable().Should().HaveCount(3); + DataModel.QueryLatest().ToBlockingEnumerable(TestContext.Current.CancellationToken).Should().HaveCount(3); } [Fact] @@ -95,7 +95,7 @@ public async Task Writing2ChangesSecondOverwritesFirst() { await WriteNextChange(SetWord(_entity1Id, "first")); await WriteNextChange(SetWord(_entity1Id, "second")); - var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(); + var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(TestContext.Current.CancellationToken); snapshot.Entity.Is().Text.Should().Be("second"); } @@ -107,7 +107,7 @@ public async Task CanWriteChangesWhenAnUnchangedWordExists() await WriteNextChange(SetWord(_entity1Id, "word-1")); await WriteNextChange(SetWord(_entity1Id, "second")); await WriteNextChange(SetWord(_entity1Id, "third")); - var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(); + var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(TestContext.Current.CancellationToken); snapshot.Entity.Is().Text.Should().Be("third"); } @@ -118,7 +118,7 @@ public async Task Writing2ChangesSecondOverwritesFirstWithLocalFirst() var secondDate = DateTimeOffset.UtcNow.AddSeconds(1); await WriteChange(_localClientId, firstDate, SetWord(_entity1Id, "first")); await WriteChange(_localClientId, secondDate, SetWord(_entity1Id, "second")); - var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(); + var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(TestContext.Current.CancellationToken); snapshot.Entity.Is().Text.Should().Be("second"); } @@ -129,7 +129,7 @@ public async Task Writing2ChangesSecondOverwritesFirstWithUtcFirst() var secondDate = DateTimeOffset.Now.AddSeconds(1); await WriteChange(_localClientId, firstDate, SetWord(_entity1Id, "first")); await WriteChange(_localClientId, secondDate, SetWord(_entity1Id, "second")); - var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(); + var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(TestContext.Current.CancellationToken); snapshot.Entity.Is().Text.Should().Be("second"); } @@ -185,7 +185,7 @@ public async Task CanDeleteAnEntry() { await WriteNextChange(SetWord(_entity1Id, "test-value")); var deleteCommit = await WriteNextChange(new DeleteChange(_entity1Id)); - var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(); + var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(TestContext.Current.CancellationToken); snapshot.Entity.DeletedAt.Should().Be(deleteCommit.DateTime); } @@ -198,7 +198,7 @@ public async Task ApplyChange_ModifiesADeletedEntry() newNoteChange.SupportsApplyChange().Should().BeTrue(); newNoteChange.SupportsNewEntity().Should().BeFalse(); // otherwise it will override the delete await WriteNextChange(newNoteChange); - var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(); + var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(TestContext.Current.CancellationToken); var word = snapshot.Entity.Is(); word.Text.Should().Be("test-value"); word.Note.Should().Be("note-after-delete"); @@ -213,7 +213,7 @@ public async Task NewEntity_UndeletesAnEntry() var recreateChange = SetWord(_entity1Id, "after-undo-delete"); recreateChange.SupportsNewEntity().Should().BeTrue(); await WriteNextChange(recreateChange); - var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(); + var snapshot = await DbContext.Snapshots.DefaultOrder().LastAsync(TestContext.Current.CancellationToken); var word = snapshot.Entity.Is(); word.Text.Should().Be("after-undo-delete"); word.DeletedAt.Should().Be(null); diff --git a/src/SIL.Harmony.Tests/DataQueryTests.cs b/src/SIL.Harmony.Tests/DataQueryTests.cs index 6d3beb9..37705b2 100644 --- a/src/SIL.Harmony.Tests/DataQueryTests.cs +++ b/src/SIL.Harmony.Tests/DataQueryTests.cs @@ -15,7 +15,7 @@ public override async ValueTask InitializeAsync() [Fact] public async Task CanQueryLatestData() { - var entries = await DataModel.QueryLatest().ToArrayAsync(); + var entries = await DataModel.QueryLatest().ToArrayAsync(TestContext.Current.CancellationToken); var entry = entries.Should().ContainSingle().Subject; entry.Text.Should().Be("entity1"); } diff --git a/src/SIL.Harmony.Tests/DbContextTests.cs b/src/SIL.Harmony.Tests/DbContextTests.cs index 2cc91ac..3b7677d 100644 --- a/src/SIL.Harmony.Tests/DbContextTests.cs +++ b/src/SIL.Harmony.Tests/DbContextTests.cs @@ -1,90 +1,90 @@ -using LinqToDB; -using LinqToDB.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Infrastructure; - -namespace SIL.Harmony.Tests; - -public class DbContextTests: DataModelTestBase -{ - [Fact] - public async Task VerifyModel() - { - await Verify(DbContext.Model.ToDebugString(MetadataDebugStringOptions.LongDefault)); - } - - [Theory] - [InlineData(0)] - [InlineData(4)] - [InlineData(-4)] - public async Task CanRoundTripDatesFromEf(int offset) - { - var commitId = Guid.NewGuid(); - var expectedDateTime = new DateTimeOffset(2000, 1, 1, 1, 11, 11, TimeSpan.FromHours(offset)); - var commit = new Commit(commitId) - { - ClientId = Guid.NewGuid(), - HybridDateTime = new HybridDateTime(expectedDateTime, 0) - }; - DbContext.Add(commit); - await DbContext.SaveChangesAsync(); - var actualCommit = await DbContext.Commits.AsNoTracking().SingleOrDefaultAsyncEF(c => c.Id == commitId); - actualCommit!.HybridDateTime.DateTime.Should().Be(expectedDateTime, "EF"); - actualCommit = await DbContext.Commits.ToLinqToDB().SingleOrDefaultAsyncLinqToDB(c => c.Id == commitId); - actualCommit!.HybridDateTime.DateTime.Should().Be(expectedDateTime, "LinqToDB"); - } - - - [Theory] - [InlineData(0)] - [InlineData(4)] - [InlineData(-4)] - public async Task CanRoundTripDatesFromLinq2Db(int offset) - { - - var commitId = Guid.NewGuid(); - var expectedDateTime = new DateTimeOffset(2000, 1, 1, 1, 11, 11, TimeSpan.FromHours(offset)); - - await DbContext.Set().ToLinqToDBTable().AsValueInsertable() - .Value(c => c.Id, commitId) - .Value(c => c.ClientId, Guid.NewGuid()) - .Value(c => c.HybridDateTime.DateTime, expectedDateTime) - .Value(c => c.HybridDateTime.Counter, 0) - .Value(c => c.Metadata, new CommitMetadata()) - .Value(c => c.Hash, "") - .Value(c => c.ParentHash, "") - .InsertAsync(); - var actualCommit = await DbContext.Commits.SingleOrDefaultAsyncEF(c => c.Id == commitId); - actualCommit!.HybridDateTime.DateTime.Should().Be(expectedDateTime, "EF"); - actualCommit = await DbContext.Commits.ToLinqToDB().SingleOrDefaultAsyncLinqToDB(c => c.Id == commitId); - actualCommit!.HybridDateTime.DateTime.Should().Be(expectedDateTime, "LinqToDB"); - } - - [Theory] - [InlineData(TimeSpan.TicksPerHour)] - [InlineData(TimeSpan.TicksPerMinute)] - [InlineData(TimeSpan.TicksPerSecond)] - [InlineData(TimeSpan.TicksPerMillisecond)] - [InlineData(TimeSpan.TicksPerMicrosecond)] - [InlineData(1)] - public async Task CanFilterCommitsByDateTime(double scale) - { - var baseDateTime = new DateTimeOffset(2000, 1, 1, 1, 11, 11, TimeSpan.Zero); - for (int i = 0; i < 50; i++) - { - var offset = new TimeSpan((long)(i * scale)); - DbContext.Add(new Commit - { - ClientId = Guid.NewGuid(), - HybridDateTime = new HybridDateTime(baseDateTime.Add(offset), 0) - }); - } - - await DbContext.SaveChangesAsync(); - var commits = await DbContext.Commits - .Where(c => c.HybridDateTime.DateTime > baseDateTime.Add(new TimeSpan((long)(25 * scale)))) - .OrderBy(c => c.HybridDateTime.DateTime) - .ToArrayAsyncEF(); - commits.Should().HaveCount(24); - } -} +using LinqToDB; +using LinqToDB.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; + +namespace SIL.Harmony.Tests; + +public class DbContextTests: DataModelTestBase +{ + [Fact] + public async Task VerifyModel() + { + await Verify(DbContext.Model.ToDebugString(MetadataDebugStringOptions.LongDefault)); + } + + [Theory] + [InlineData(0)] + [InlineData(4)] + [InlineData(-4)] + public async Task CanRoundTripDatesFromEf(int offset) + { + var commitId = Guid.NewGuid(); + var expectedDateTime = new DateTimeOffset(2000, 1, 1, 1, 11, 11, TimeSpan.FromHours(offset)); + var commit = new Commit(commitId) + { + ClientId = Guid.NewGuid(), + HybridDateTime = new HybridDateTime(expectedDateTime, 0) + }; + DbContext.Add(commit); + await DbContext.SaveChangesAsync(TestContext.Current.CancellationToken); + var actualCommit = await DbContext.Commits.AsNoTracking().SingleOrDefaultAsyncEF(c => c.Id == commitId, TestContext.Current.CancellationToken); + actualCommit!.HybridDateTime.DateTime.Should().Be(expectedDateTime, "EF"); + actualCommit = await DbContext.Commits.ToLinqToDB().SingleOrDefaultAsyncLinqToDB(c => c.Id == commitId, TestContext.Current.CancellationToken); + actualCommit!.HybridDateTime.DateTime.Should().Be(expectedDateTime, "LinqToDB"); + } + + + [Theory] + [InlineData(0)] + [InlineData(4)] + [InlineData(-4)] + public async Task CanRoundTripDatesFromLinq2Db(int offset) + { + + var commitId = Guid.NewGuid(); + var expectedDateTime = new DateTimeOffset(2000, 1, 1, 1, 11, 11, TimeSpan.FromHours(offset)); + + await DbContext.Set().ToLinqToDBTable().AsValueInsertable() + .Value(c => c.Id, commitId) + .Value(c => c.ClientId, Guid.NewGuid()) + .Value(c => c.HybridDateTime.DateTime, expectedDateTime) + .Value(c => c.HybridDateTime.Counter, 0) + .Value(c => c.Metadata, new CommitMetadata()) + .Value(c => c.Hash, "") + .Value(c => c.ParentHash, "") + .InsertAsync(TestContext.Current.CancellationToken); + var actualCommit = await DbContext.Commits.SingleOrDefaultAsyncEF(c => c.Id == commitId, TestContext.Current.CancellationToken); + actualCommit!.HybridDateTime.DateTime.Should().Be(expectedDateTime, "EF"); + actualCommit = await DbContext.Commits.ToLinqToDB().SingleOrDefaultAsyncLinqToDB(c => c.Id == commitId, TestContext.Current.CancellationToken); + actualCommit!.HybridDateTime.DateTime.Should().Be(expectedDateTime, "LinqToDB"); + } + + [Theory] + [InlineData(TimeSpan.TicksPerHour)] + [InlineData(TimeSpan.TicksPerMinute)] + [InlineData(TimeSpan.TicksPerSecond)] + [InlineData(TimeSpan.TicksPerMillisecond)] + [InlineData(TimeSpan.TicksPerMicrosecond)] + [InlineData(1)] + public async Task CanFilterCommitsByDateTime(double scale) + { + var baseDateTime = new DateTimeOffset(2000, 1, 1, 1, 11, 11, TimeSpan.Zero); + for (int i = 0; i < 50; i++) + { + var offset = new TimeSpan((long)(i * scale)); + DbContext.Add(new Commit + { + ClientId = Guid.NewGuid(), + HybridDateTime = new HybridDateTime(baseDateTime.Add(offset), 0) + }); + } + + await DbContext.SaveChangesAsync(TestContext.Current.CancellationToken); + var commits = await DbContext.Commits + .Where(c => c.HybridDateTime.DateTime > baseDateTime.Add(new TimeSpan((long)(25 * scale)))) + .OrderBy(c => c.HybridDateTime.DateTime) + .ToArrayAsyncEF(TestContext.Current.CancellationToken); + commits.Should().HaveCount(24); + } +} diff --git a/src/SIL.Harmony.Tests/DefinitionTests.cs b/src/SIL.Harmony.Tests/DefinitionTests.cs index 49e664c..40ceac0 100644 --- a/src/SIL.Harmony.Tests/DefinitionTests.cs +++ b/src/SIL.Harmony.Tests/DefinitionTests.cs @@ -1,107 +1,107 @@ -using SIL.Harmony.Changes; -using SIL.Harmony.Sample.Models; -using Microsoft.EntityFrameworkCore; - -namespace SIL.Harmony.Tests; - -public class DefinitionTests : DataModelTestBase -{ - [Fact] - public async Task CanAddADefinitionToAWord() - { - var wordId = Guid.NewGuid(); - await WriteNextChange(SetWord(wordId, "hello")); - await WriteNextChange(NewDefinition(wordId, "a greeting", "verb")); - var snapshot = await DataModel.GetProjectSnapshot(); - var definitionSnapshot = snapshot.Snapshots.Values.Single(s => s.IsType()); - var definition = await DataModel.GetBySnapshotId(definitionSnapshot.Id); - definition.Text.Should().Be("a greeting"); - definition.WordId.Should().Be(wordId); - } - - [Fact] - public async Task DeletingAWordDeletesTheDefinition() - { - var wordId = Guid.NewGuid(); - await WriteNextChange(SetWord(wordId, "hello")); - await WriteNextChange(NewDefinition(wordId, "a greeting", "verb")); - await WriteNextChange(new DeleteChange(wordId)); - var snapshot = await DataModel.GetProjectSnapshot(); - snapshot.Snapshots.Values.Where(s => !s.EntityIsDeleted).Should().BeEmpty(); - } - - [Fact] - public async Task AddingADefinitionToADeletedWordDeletesIt() - { - var wordId = Guid.NewGuid(); - await WriteNextChange(SetWord(wordId, "hello")); - await WriteNextChange(new DeleteChange(wordId)); - await WriteNextChange(NewDefinition(wordId, "a greeting", "verb")); - var snapshot = await DataModel.GetProjectSnapshot(); - snapshot.Snapshots.Values.Where(s => !s.EntityIsDeleted).Should().BeEmpty(); - } - - - [Fact] - public async Task CanGetInOrder() - { - var wordId = Guid.NewGuid(); - await WriteNextChange(SetWord(wordId, "hello")); - await WriteNextChange(NewDefinition(wordId, "greet someone", "verb", 2)); - await WriteNextChange(NewDefinition(wordId, "a greeting", "noun", 1)); - - var definitions = await DataModel.QueryLatest().ToArrayAsync(); - definitions.Select(d => d.PartOfSpeech).Should().ContainInConsecutiveOrder( - "noun", - "verb" - ); - } - - [Fact] - public async Task CanChangeOrderBetweenExistingDefinitions() - { - var wordId = Guid.NewGuid(); - var definitionAId = Guid.NewGuid(); - var definitionBId = Guid.NewGuid(); - var definitionCId = Guid.NewGuid(); - await WriteNextChange(SetWord(wordId, "hello")); - await WriteNextChange(NewDefinition(wordId, "a greeting", "noun", 1, definitionAId)); - await WriteNextChange(NewDefinition(wordId, "greet someone", "verb", 2, definitionBId)); - await WriteNextChange(NewDefinition(wordId, "used as a greeting", "exclamation", 3, definitionCId)); - - var definitions = await DataModel.QueryLatest().ToArrayAsync(); - definitions.Select(d => d.PartOfSpeech).Should().ContainInConsecutiveOrder( - "noun", - "verb", - "exclamation" - ); - - //change the order of the exclamation to be between the noun and verb - await WriteNextChange(SetOrderChange.Between(definitionCId, definitions[0], definitions[1])); - - definitions = await DataModel.QueryLatest().ToArrayAsync(); - definitions.Select(d => d.PartOfSpeech).Should().ContainInConsecutiveOrder( - "noun", - "exclamation", - "verb" - ); - } - - [Fact] - public async Task ConsistentlySortsItems() - { - var wordId = Guid.NewGuid(); - //these ids are hardcoded to ensure the order is consistent - var definitionAId = new Guid("a40fb7c8-f3b1-441b-b5fb-0261ae32bff1"); - var definitionBId = new Guid("0197b80d-9691-4896-8c9b-277a3455a7f6"); - await WriteNextChange(SetWord(wordId, "hello")); - await WriteNextChange(NewDefinition(wordId, "greet someone", "verb", 1, definitionAId)); - await WriteNextChange(NewDefinition(wordId, "a greeting", "noun", 1, definitionBId)); - - var definitions = await DataModel.QueryLatest().ToArrayAsync(); - definitions.Select(d => d.Id).Should().ContainInConsecutiveOrder( - definitionBId, - definitionAId - ); - } +using SIL.Harmony.Changes; +using SIL.Harmony.Sample.Models; +using Microsoft.EntityFrameworkCore; + +namespace SIL.Harmony.Tests; + +public class DefinitionTests : DataModelTestBase +{ + [Fact] + public async Task CanAddADefinitionToAWord() + { + var wordId = Guid.NewGuid(); + await WriteNextChange(SetWord(wordId, "hello")); + await WriteNextChange(NewDefinition(wordId, "a greeting", "verb")); + var snapshot = await DataModel.GetProjectSnapshot(); + var definitionSnapshot = snapshot.Snapshots.Values.Single(s => s.IsType()); + var definition = await DataModel.GetBySnapshotId(definitionSnapshot.Id); + definition.Text.Should().Be("a greeting"); + definition.WordId.Should().Be(wordId); + } + + [Fact] + public async Task DeletingAWordDeletesTheDefinition() + { + var wordId = Guid.NewGuid(); + await WriteNextChange(SetWord(wordId, "hello")); + await WriteNextChange(NewDefinition(wordId, "a greeting", "verb")); + await WriteNextChange(new DeleteChange(wordId)); + var snapshot = await DataModel.GetProjectSnapshot(); + snapshot.Snapshots.Values.Where(s => !s.EntityIsDeleted).Should().BeEmpty(); + } + + [Fact] + public async Task AddingADefinitionToADeletedWordDeletesIt() + { + var wordId = Guid.NewGuid(); + await WriteNextChange(SetWord(wordId, "hello")); + await WriteNextChange(new DeleteChange(wordId)); + await WriteNextChange(NewDefinition(wordId, "a greeting", "verb")); + var snapshot = await DataModel.GetProjectSnapshot(); + snapshot.Snapshots.Values.Where(s => !s.EntityIsDeleted).Should().BeEmpty(); + } + + + [Fact] + public async Task CanGetInOrder() + { + var wordId = Guid.NewGuid(); + await WriteNextChange(SetWord(wordId, "hello")); + await WriteNextChange(NewDefinition(wordId, "greet someone", "verb", 2)); + await WriteNextChange(NewDefinition(wordId, "a greeting", "noun", 1)); + + var definitions = await DataModel.QueryLatest().ToArrayAsync(TestContext.Current.CancellationToken); + definitions.Select(d => d.PartOfSpeech).Should().ContainInConsecutiveOrder( + "noun", + "verb" + ); + } + + [Fact] + public async Task CanChangeOrderBetweenExistingDefinitions() + { + var wordId = Guid.NewGuid(); + var definitionAId = Guid.NewGuid(); + var definitionBId = Guid.NewGuid(); + var definitionCId = Guid.NewGuid(); + await WriteNextChange(SetWord(wordId, "hello")); + await WriteNextChange(NewDefinition(wordId, "a greeting", "noun", 1, definitionAId)); + await WriteNextChange(NewDefinition(wordId, "greet someone", "verb", 2, definitionBId)); + await WriteNextChange(NewDefinition(wordId, "used as a greeting", "exclamation", 3, definitionCId)); + + var definitions = await DataModel.QueryLatest().ToArrayAsync(TestContext.Current.CancellationToken); + definitions.Select(d => d.PartOfSpeech).Should().ContainInConsecutiveOrder( + "noun", + "verb", + "exclamation" + ); + + //change the order of the exclamation to be between the noun and verb + await WriteNextChange(SetOrderChange.Between(definitionCId, definitions[0], definitions[1])); + + definitions = await DataModel.QueryLatest().ToArrayAsync(TestContext.Current.CancellationToken); + definitions.Select(d => d.PartOfSpeech).Should().ContainInConsecutiveOrder( + "noun", + "exclamation", + "verb" + ); + } + + [Fact] + public async Task ConsistentlySortsItems() + { + var wordId = Guid.NewGuid(); + //these ids are hardcoded to ensure the order is consistent + var definitionAId = new Guid("a40fb7c8-f3b1-441b-b5fb-0261ae32bff1"); + var definitionBId = new Guid("0197b80d-9691-4896-8c9b-277a3455a7f6"); + await WriteNextChange(SetWord(wordId, "hello")); + await WriteNextChange(NewDefinition(wordId, "greet someone", "verb", 1, definitionAId)); + await WriteNextChange(NewDefinition(wordId, "a greeting", "noun", 1, definitionBId)); + + var definitions = await DataModel.QueryLatest().ToArrayAsync(TestContext.Current.CancellationToken); + definitions.Select(d => d.Id).Should().ContainInConsecutiveOrder( + definitionBId, + definitionAId + ); + } } \ No newline at end of file diff --git a/src/SIL.Harmony.Tests/DeleteAndCreateTests.cs b/src/SIL.Harmony.Tests/DeleteAndCreateTests.cs index 409f8bd..0397054 100644 --- a/src/SIL.Harmony.Tests/DeleteAndCreateTests.cs +++ b/src/SIL.Harmony.Tests/DeleteAndCreateTests.cs @@ -25,7 +25,7 @@ await WriteNextChange( word.DeletedAt.Should().BeNull(); word.Text.Should().Be("Undeleted"); - var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(); + var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(TestContext.Current.CancellationToken); entityWord.Should().NotBeNull(); entityWord.DeletedAt.Should().BeNull(); entityWord.Text.Should().Be("Undeleted"); @@ -48,7 +48,7 @@ await WriteNextChange(new NewWordChange(wordId, "Undeleted"), add: false), word.DeletedAt.Should().BeNull(); word.Text.Should().Be("Undeleted"); - var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(); + var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(TestContext.Current.CancellationToken); entityWord.Should().NotBeNull(); entityWord.DeletedAt.Should().BeNull(); entityWord.Text.Should().Be("Undeleted"); @@ -72,7 +72,7 @@ await WriteNextChange([ word.DeletedAt.Should().BeNull(); word.Text.Should().Be("Undeleted"); - var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(); + var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(TestContext.Current.CancellationToken); entityWord.Should().NotBeNull(); entityWord.DeletedAt.Should().BeNull(); entityWord.Text.Should().Be("Undeleted"); @@ -96,7 +96,7 @@ await WriteNextChange(new NewWordChange(wordId, "Undeleted"), add: false), word.DeletedAt.Should().BeNull(); word.Text.Should().Be("Undeleted"); - var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(); + var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(TestContext.Current.CancellationToken); entityWord.Should().NotBeNull(); entityWord.DeletedAt.Should().BeNull(); entityWord.Text.Should().Be("Undeleted"); @@ -119,7 +119,7 @@ await WriteNextChange( word.DeletedAt.Should().BeNull(); word.Text.Should().Be("Undeleted"); - var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(); + var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(TestContext.Current.CancellationToken); entityWord.Should().NotBeNull(); entityWord.DeletedAt.Should().BeNull(); entityWord.Text.Should().Be("Undeleted"); @@ -141,7 +141,7 @@ await WriteNextChange(new NewWordChange(wordId, "Undeleted"), add: false), word.DeletedAt.Should().BeNull(); word.Text.Should().Be("Undeleted"); - var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(); + var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(TestContext.Current.CancellationToken); entityWord.Should().NotBeNull(); entityWord.DeletedAt.Should().BeNull(); entityWord.Text.Should().Be("Undeleted"); @@ -163,7 +163,7 @@ await WriteNextChange( word.DeletedAt.Should().NotBeNull(); word.Text.Should().Be("original"); - var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(); + var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(TestContext.Current.CancellationToken); entityWord.Should().BeNull(); } @@ -182,7 +182,7 @@ await WriteNextChange(new DeleteChange(wordId), add: false), word.DeletedAt.Should().NotBeNull(); word.Text.Should().Be("original"); - var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(); + var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(TestContext.Current.CancellationToken); entityWord.Should().BeNull(); } @@ -192,7 +192,7 @@ public async Task NewEntityOnExistingEntityIsNoOp() var wordId = Guid.NewGuid(); await WriteNextChange(new NewWordChange(wordId, "original")); - var snapshotsBefore = await DbContext.Snapshots.Where(s => s.EntityId == wordId).ToArrayAsync(); + var snapshotsBefore = await DbContext.Snapshots.Where(s => s.EntityId == wordId).ToArrayAsync(TestContext.Current.CancellationToken); await WriteNextChange( [ @@ -204,12 +204,12 @@ await WriteNextChange( word.DeletedAt.Should().BeNull(); word.Text.Should().Be("original"); - var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(); + var entityWord = await DataModel.QueryLatest().Where(w => w.Id == wordId).SingleOrDefaultAsync(TestContext.Current.CancellationToken); entityWord.Should().NotBeNull(); entityWord.DeletedAt.Should().BeNull(); entityWord.Text.Should().Be("original"); - var snapshotsAfter = await DbContext.Snapshots.Where(s => s.EntityId == wordId).ToArrayAsync(); + var snapshotsAfter = await DbContext.Snapshots.Where(s => s.EntityId == wordId).ToArrayAsync(TestContext.Current.CancellationToken); snapshotsAfter.Select(s => s.Id).Should().BeEquivalentTo(snapshotsBefore.Select(s => s.Id)); } } diff --git a/src/SIL.Harmony.Tests/ModelSnapshotTests.cs b/src/SIL.Harmony.Tests/ModelSnapshotTests.cs index 639f40e..d4455f8 100644 --- a/src/SIL.Harmony.Tests/ModelSnapshotTests.cs +++ b/src/SIL.Harmony.Tests/ModelSnapshotTests.cs @@ -1,142 +1,142 @@ -using SIL.Harmony.Sample.Models; -using Microsoft.EntityFrameworkCore; - -namespace SIL.Harmony.Tests; - -public class ModelSnapshotTests : DataModelTestBase -{ - [Fact] - public void CanGetEmptyModelSnapshot() - { - DataModel.GetProjectSnapshot().Should().NotBeNull(); - } - - [Fact] - public async Task CanGetModelSnapshot() - { - await WriteNextChange(SetWord(Guid.NewGuid(), "entity1")); - await WriteNextChange(SetWord(Guid.NewGuid(), "entity2")); - var snapshot = await DataModel.GetProjectSnapshot(); - snapshot.Snapshots.Should().HaveCount(2); - } - - [Fact] - public async Task ModelSnapshotShowsMultipleChanges() - { - var entityId = Guid.NewGuid(); - await WriteNextChange(SetWord(entityId, "first")); - var secondChange = await WriteNextChange(SetWord(entityId, "second")); - var snapshot = await DataModel.GetProjectSnapshot(); - var simpleSnapshot = snapshot.Snapshots.Values.First(); - var entry = await DataModel.GetBySnapshotId(simpleSnapshot.Id); - entry.Text.Should().Be("second"); - snapshot.LastChange.Should().Be(secondChange.DateTime); - } - - [Fact] - public async Task CanGetWordForASpecificCommit() - { - var entityId = Guid.NewGuid(); - var firstCommit = await WriteNextChange(SetWord(entityId, "first")); - var secondCommit = await WriteNextChange(SetWord(entityId, "second")); - var thirdCommit = await WriteNextChange(SetWord(entityId, "third")); - await ClearNonRootSnapshots(); - var firstWord = await DataModel.GetAtCommit(firstCommit.Id, entityId); - firstWord.Should().NotBeNull(); - firstWord.Text.Should().Be("first"); - - var secondWord = await DataModel.GetAtCommit(secondCommit.Id, entityId); - secondWord.Should().NotBeNull(); - secondWord.Text.Should().Be("second"); - - var thirdWord = await DataModel.GetAtCommit(thirdCommit.Id, entityId); - thirdWord.Should().NotBeNull(); - thirdWord.Text.Should().Be("third"); - } - - [Fact] - public async Task CanGetWordForASpecificTime() - { - var entityId = Guid.NewGuid(); - var firstCommit = await WriteNextChange(SetWord(entityId, "first")); - var secondCommit = await WriteNextChange(SetWord(entityId, "second")); - var thirdCommit = await WriteNextChange(SetWord(entityId, "third")); - //ensures that SnapshotWorker.ApplyCommitsToSnapshots will be called when getting the snapshots - await ClearNonRootSnapshots(); - var firstWord = await DataModel.GetAtTime(firstCommit.DateTime.AddMinutes(5), entityId); - firstWord.Should().NotBeNull(); - firstWord.Text.Should().Be("first"); - - var secondWord = await DataModel.GetAtTime(secondCommit.DateTime.AddMinutes(5), entityId); - secondWord.Should().NotBeNull(); - secondWord.Text.Should().Be("second"); - - //just before the 3rd commit should still be second - secondWord = await DataModel.GetAtTime(thirdCommit.DateTime.Subtract(TimeSpan.FromSeconds(5)), entityId); - secondWord.Should().NotBeNull(); - secondWord.Text.Should().Be("second"); - - var thirdWord = await DataModel.GetAtTime(thirdCommit.DateTime.AddMinutes(5), entityId); - thirdWord.Should().NotBeNull(); - thirdWord.Text.Should().Be("third"); - } - - private Task ClearNonRootSnapshots() - { - return DbContext.Snapshots.Where(s => !s.IsRoot).ExecuteDeleteAsync(); - } - - [Theory] - [InlineData(10)] - [InlineData(100)] - // [InlineData(1_000)] - public async Task CanGetSnapshotFromEarlier(int changeCount) - { - var entityId = Guid.NewGuid(); - await WriteNextChange(SetWord(entityId, "first")); - var changes = new List(changeCount); - var addNew = new List(changeCount); - for (var i = 0; i < changeCount; i++) - { - // todo: these commits all have an odd index, so no intermediate snapshots will be persisted i.e. the snapshot count checking is somewhat deceptive - changes.Add(await WriteNextChange(SetWord(entityId, $"change {i}"), false).AsTask()); - addNew.Add(await WriteNextChange(SetWord(Guid.NewGuid(), $"add {i}"), false).AsTask()); - } - - //adding all via sync means there's sparse snapshots - await AddCommitsViaSync(changes.Concat(addNew)); - //there will only be a snapshot for every other commit, but there's change count * 2 commits, plus a first and last change - DbContext.Snapshots.Should().HaveCount(2 + changeCount); - - for (int i = 0; i < changeCount; i++) - { - var snapshots = await DataModel.GetSnapshotsAtCommit(changes[i]); - var entry = snapshots[entityId].Entity.Is(); - entry.Text.Should().Be($"change {i}"); - snapshots.Values.Should().HaveCount(1 + i); - } - } - - [Fact] - public async Task WorstCaseSnapshotReApply() - { - int changeCount = 1_000; - var entityId = Guid.NewGuid(); - await WriteNextChange(SetWord(entityId, "first")); - //adding all in one AddRange means there's sparse snapshots - await AddCommitsViaSync(Enumerable.Range(0, changeCount) - .Select(i => WriteNextChange(SetWord(entityId, $"change {i}"), false).Result)); - - var latestSnapshot = await DataModel.GetLatestSnapshotByObjectId(entityId); - //delete snapshots so when we get at then we need to re-apply - await DbContext.Snapshots.Where(s => !s.IsRoot).ExecuteDeleteAsync(); - - var computedModelSnapshots = await DataModel.GetSnapshotsAtCommit(latestSnapshot.Commit); - - var entitySnapshot = computedModelSnapshots.Should().ContainSingle().Subject.Value; - entitySnapshot.Should().BeEquivalentTo(latestSnapshot, options => options.Excluding(snapshot => snapshot.Id).Excluding(snapshot => snapshot.Commit).Excluding(s => s.Entity.DbObject)); - var latestSnapshotEntry = latestSnapshot.Entity.Is(); - var entitySnapshotEntry = entitySnapshot.Entity.Is(); - entitySnapshotEntry.Text.Should().Be(latestSnapshotEntry.Text); - } -} +using SIL.Harmony.Sample.Models; +using Microsoft.EntityFrameworkCore; + +namespace SIL.Harmony.Tests; + +public class ModelSnapshotTests : DataModelTestBase +{ + [Fact] + public void CanGetEmptyModelSnapshot() + { + DataModel.GetProjectSnapshot().Should().NotBeNull(); + } + + [Fact] + public async Task CanGetModelSnapshot() + { + await WriteNextChange(SetWord(Guid.NewGuid(), "entity1")); + await WriteNextChange(SetWord(Guid.NewGuid(), "entity2")); + var snapshot = await DataModel.GetProjectSnapshot(); + snapshot.Snapshots.Should().HaveCount(2); + } + + [Fact] + public async Task ModelSnapshotShowsMultipleChanges() + { + var entityId = Guid.NewGuid(); + await WriteNextChange(SetWord(entityId, "first")); + var secondChange = await WriteNextChange(SetWord(entityId, "second")); + var snapshot = await DataModel.GetProjectSnapshot(); + var simpleSnapshot = snapshot.Snapshots.Values.First(); + var entry = await DataModel.GetBySnapshotId(simpleSnapshot.Id); + entry.Text.Should().Be("second"); + snapshot.LastChange.Should().Be(secondChange.DateTime); + } + + [Fact] + public async Task CanGetWordForASpecificCommit() + { + var entityId = Guid.NewGuid(); + var firstCommit = await WriteNextChange(SetWord(entityId, "first")); + var secondCommit = await WriteNextChange(SetWord(entityId, "second")); + var thirdCommit = await WriteNextChange(SetWord(entityId, "third")); + await ClearNonRootSnapshots(); + var firstWord = await DataModel.GetAtCommit(firstCommit.Id, entityId); + firstWord.Should().NotBeNull(); + firstWord.Text.Should().Be("first"); + + var secondWord = await DataModel.GetAtCommit(secondCommit.Id, entityId); + secondWord.Should().NotBeNull(); + secondWord.Text.Should().Be("second"); + + var thirdWord = await DataModel.GetAtCommit(thirdCommit.Id, entityId); + thirdWord.Should().NotBeNull(); + thirdWord.Text.Should().Be("third"); + } + + [Fact] + public async Task CanGetWordForASpecificTime() + { + var entityId = Guid.NewGuid(); + var firstCommit = await WriteNextChange(SetWord(entityId, "first")); + var secondCommit = await WriteNextChange(SetWord(entityId, "second")); + var thirdCommit = await WriteNextChange(SetWord(entityId, "third")); + //ensures that SnapshotWorker.ApplyCommitsToSnapshots will be called when getting the snapshots + await ClearNonRootSnapshots(); + var firstWord = await DataModel.GetAtTime(firstCommit.DateTime.AddMinutes(5), entityId); + firstWord.Should().NotBeNull(); + firstWord.Text.Should().Be("first"); + + var secondWord = await DataModel.GetAtTime(secondCommit.DateTime.AddMinutes(5), entityId); + secondWord.Should().NotBeNull(); + secondWord.Text.Should().Be("second"); + + //just before the 3rd commit should still be second + secondWord = await DataModel.GetAtTime(thirdCommit.DateTime.Subtract(TimeSpan.FromSeconds(5)), entityId); + secondWord.Should().NotBeNull(); + secondWord.Text.Should().Be("second"); + + var thirdWord = await DataModel.GetAtTime(thirdCommit.DateTime.AddMinutes(5), entityId); + thirdWord.Should().NotBeNull(); + thirdWord.Text.Should().Be("third"); + } + + private Task ClearNonRootSnapshots() + { + return DbContext.Snapshots.Where(s => !s.IsRoot).ExecuteDeleteAsync(); + } + + [Theory] + [InlineData(10)] + [InlineData(100)] + // [InlineData(1_000)] + public async Task CanGetSnapshotFromEarlier(int changeCount) + { + var entityId = Guid.NewGuid(); + await WriteNextChange(SetWord(entityId, "first")); + var changes = new List(changeCount); + var addNew = new List(changeCount); + for (var i = 0; i < changeCount; i++) + { + // todo: these commits all have an odd index, so no intermediate snapshots will be persisted i.e. the snapshot count checking is somewhat deceptive + changes.Add(await WriteNextChange(SetWord(entityId, $"change {i}"), false).AsTask()); + addNew.Add(await WriteNextChange(SetWord(Guid.NewGuid(), $"add {i}"), false).AsTask()); + } + + //adding all via sync means there's sparse snapshots + await AddCommitsViaSync(changes.Concat(addNew)); + //there will only be a snapshot for every other commit, but there's change count * 2 commits, plus a first and last change + DbContext.Snapshots.Should().HaveCount(2 + changeCount); + + for (int i = 0; i < changeCount; i++) + { + var snapshots = await DataModel.GetSnapshotsAtCommit(changes[i]); + var entry = snapshots[entityId].Entity.Is(); + entry.Text.Should().Be($"change {i}"); + snapshots.Values.Should().HaveCount(1 + i); + } + } + + [Fact] + public async Task WorstCaseSnapshotReApply() + { + int changeCount = 1_000; + var entityId = Guid.NewGuid(); + await WriteNextChange(SetWord(entityId, "first")); + //adding all in one AddRange means there's sparse snapshots + await AddCommitsViaSync(Enumerable.Range(0, changeCount) + .Select(i => WriteNextChange(SetWord(entityId, $"change {i}"), false).Result)); + + var latestSnapshot = await DataModel.GetLatestSnapshotByObjectId(entityId); + //delete snapshots so when we get at then we need to re-apply + await DbContext.Snapshots.Where(s => !s.IsRoot).ExecuteDeleteAsync(TestContext.Current.CancellationToken); + + var computedModelSnapshots = await DataModel.GetSnapshotsAtCommit(latestSnapshot.Commit); + + var entitySnapshot = computedModelSnapshots.Should().ContainSingle().Subject.Value; + entitySnapshot.Should().BeEquivalentTo(latestSnapshot, options => options.Excluding(snapshot => snapshot.Id).Excluding(snapshot => snapshot.Commit).Excluding(s => s.Entity.DbObject)); + var latestSnapshotEntry = latestSnapshot.Entity.Is(); + var entitySnapshotEntry = entitySnapshot.Entity.Is(); + entitySnapshotEntry.Text.Should().Be(latestSnapshotEntry.Text); + } +} diff --git a/src/SIL.Harmony.Tests/PersistExtraDataTests.cs b/src/SIL.Harmony.Tests/PersistExtraDataTests.cs index 35e8815..0de54a8 100644 --- a/src/SIL.Harmony.Tests/PersistExtraDataTests.cs +++ b/src/SIL.Harmony.Tests/PersistExtraDataTests.cs @@ -76,7 +76,7 @@ public async Task CanPersistExtraData() { var entityId = Guid.NewGuid(); var commit = await _dataModelTestBase.WriteNextChange(new CreateExtraDataModelChange(entityId)); - var extraDataModel = _dataModelTestBase.DataModel.QueryLatest().ToBlockingEnumerable().Should().ContainSingle().Subject; + var extraDataModel = _dataModelTestBase.DataModel.QueryLatest().ToBlockingEnumerable(TestContext.Current.CancellationToken).Should().ContainSingle().Subject; extraDataModel.Id.Should().Be(entityId); extraDataModel.CommitId.Should().Be(commit.Id); extraDataModel.DateTime.Should().Be(commit.HybridDateTime.DateTime); diff --git a/src/SIL.Harmony.Tests/RepositoryTests.cs b/src/SIL.Harmony.Tests/RepositoryTests.cs index 9f2b91e..5fac8a6 100644 --- a/src/SIL.Harmony.Tests/RepositoryTests.cs +++ b/src/SIL.Harmony.Tests/RepositoryTests.cs @@ -115,7 +115,7 @@ await _repository.AddCommits([ Commit(Guid.NewGuid(), commit1Time), Commit(Guid.NewGuid(), commit2Time), ]); - var commits = await _repository.CurrentCommits().ToArrayAsync(); + var commits = await _repository.CurrentCommits().ToArrayAsync(TestContext.Current.CancellationToken); commits.Select(c => c.HybridDateTime).Should().ContainInConsecutiveOrder(commit1Time, commit2Time); } @@ -128,7 +128,7 @@ await _repository.AddCommits([ Commit(Guid.NewGuid(), commit1Time), Commit(Guid.NewGuid(), commit2Time), ]); - var commits = await _repository.CurrentCommits().ToArrayAsync(); + var commits = await _repository.CurrentCommits().ToArrayAsync(TestContext.Current.CancellationToken); commits.Select(c => c.HybridDateTime).Should().ContainInConsecutiveOrder(commit1Time, commit2Time); } @@ -141,7 +141,7 @@ await _repository.AddCommits([ Commit(ids[0], commitTime), Commit(ids[1], commitTime), ]); - var commits = await _repository.CurrentCommits().ToArrayAsync(); + var commits = await _repository.CurrentCommits().ToArrayAsync(TestContext.Current.CancellationToken); commits.Select(c => c.Id).Should().ContainInConsecutiveOrder(ids); } @@ -149,7 +149,7 @@ await _repository.AddCommits([ public async Task CurrentSnapshots_Works() { await _repository.AddSnapshots([Snapshot(Guid.NewGuid(), Guid.NewGuid(), Time(1, 0))]); - var snapshots = await _repository.CurrentSnapshots().ToArrayAsync(); + var snapshots = await _repository.CurrentSnapshots().ToArrayAsync(TestContext.Current.CancellationToken); snapshots.Should().ContainSingle(); } @@ -162,7 +162,7 @@ await _repository.AddSnapshots([ Snapshot(entityId, Guid.NewGuid(), Time(1, 0)), Snapshot(entityId, Guid.NewGuid(), expectedTime), ]); - var snapshots = await _repository.CurrentSnapshots().Include(s => s.Commit).ToArrayAsync(); + var snapshots = await _repository.CurrentSnapshots().Include(s => s.Commit).ToArrayAsync(TestContext.Current.CancellationToken); snapshots.Should().ContainSingle().Which.Commit.HybridDateTime.Should().BeEquivalentTo(expectedTime); } @@ -174,7 +174,7 @@ await _repository.AddSnapshots([ Snapshot(entityId, Guid.NewGuid(), Time(1, 0)), Snapshot(entityId, Guid.NewGuid(), Time(1, 1)), ]); - var snapshots = await _repository.CurrentSnapshots().Include(s => s.Commit).ToArrayAsync(); + var snapshots = await _repository.CurrentSnapshots().Include(s => s.Commit).ToArrayAsync(TestContext.Current.CancellationToken); snapshots.Should().ContainSingle().Which.Commit.HybridDateTime.Counter.Should().Be(1); } @@ -188,7 +188,7 @@ await _repository.AddSnapshots([ Snapshot(entityId, ids[0], time), Snapshot(entityId, ids[1], time), ]); - var snapshots = await _repository.CurrentSnapshots().Include(s => s.Commit).ToArrayAsync(); + var snapshots = await _repository.CurrentSnapshots().Include(s => s.Commit).ToArrayAsync(TestContext.Current.CancellationToken); snapshots.Should().ContainSingle().Which.Commit.Id.Should().Be(ids[1]); } @@ -207,12 +207,12 @@ await _repository.AddSnapshots([ snapshot2, ]); - var snapshots = await _repository.CurrentSnapshots().Include(s => s.Commit).ToArrayAsync(); + var snapshots = await _repository.CurrentSnapshots().Include(s => s.Commit).ToArrayAsync(TestContext.Current.CancellationToken); var commit = snapshots.Should().ContainSingle().Subject.Commit; commit.Id.Should().Be(commitIds[2]); snapshots = await _repository.GetScopedRepository(snapshot2.Commit).CurrentSnapshots().Include(s => s.Commit) - .ToArrayAsync(); + .ToArrayAsync(TestContext.Current.CancellationToken); commit = snapshots.Should().ContainSingle().Subject.Commit; commit.Id.Should().Be(commitIds[1], $"commit order: [{string.Join(", ", commitIds)}]"); } @@ -232,11 +232,11 @@ await _repository.AddSnapshots([ snapshot2, ]); - var snapshots = await _repository.CurrentSnapshots().Include(s => s.Commit).ToArrayAsync(); + var snapshots = await _repository.CurrentSnapshots().Include(s => s.Commit).ToArrayAsync(TestContext.Current.CancellationToken); var commit = snapshots.Should().ContainSingle().Subject.Commit; commit.Id.Should().Be(commitIds[2]); - snapshots = await _repository.GetScopedRepository(snapshot2.Commit).CurrentSnapshots().Include(s => s.Commit).ToArrayAsync(); + snapshots = await _repository.GetScopedRepository(snapshot2.Commit).CurrentSnapshots().Include(s => s.Commit).ToArrayAsync(TestContext.Current.CancellationToken); commit = snapshots.Should().ContainSingle().Subject.Commit; commit.Id.Should().Be(commitIds[1], $"commit order: [{string.Join(", ", commitIds)}]"); } diff --git a/src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesTests.cs b/src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesTests.cs index 59e9b84..214761d 100644 --- a/src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesTests.cs +++ b/src/SIL.Harmony.Tests/ResourceTests/RemoteResourcesTests.cs @@ -128,7 +128,7 @@ public async Task CanDownloadFileFromRemote() localResource.Id.Should().Be(resourceId); - var actualFileContents = await File.ReadAllTextAsync(localResource.LocalPath); + var actualFileContents = await File.ReadAllTextAsync(localResource.LocalPath, TestContext.Current.CancellationToken); actualFileContents.Should().Be(fileContents); var pendingDownloads = await _resourceService.ListResourcesPendingDownload(); pendingDownloads.Should().BeEmpty(); diff --git a/src/SIL.Harmony.Tests/ResourceTests/WordResourceTests.cs b/src/SIL.Harmony.Tests/ResourceTests/WordResourceTests.cs index e7fc702..a52e101 100644 --- a/src/SIL.Harmony.Tests/ResourceTests/WordResourceTests.cs +++ b/src/SIL.Harmony.Tests/ResourceTests/WordResourceTests.cs @@ -1,41 +1,41 @@ -using System.Runtime.CompilerServices; -using Microsoft.Extensions.DependencyInjection; -using SIL.Harmony.Sample.Changes; -using SIL.Harmony.Sample.Models; - -namespace SIL.Harmony.Tests.ResourceTests; - -public class WordResourceTests: DataModelTestBase -{ - private RemoteServiceMock _remoteServiceMock = new(); - private ResourceService _resourceService => _services.GetRequiredService(); - private readonly Guid _entity1Id = Guid.NewGuid(); - - private string CreateFile(string contents, [CallerMemberName] string fileName = "") - { - var filePath = Path.GetFullPath(fileName + ".txt"); - File.WriteAllText(filePath, contents); - return filePath; - } - - [Fact] - public async Task CanReferenceAResourceFromAWord() - { - await WriteNextChange(SetWord(_entity1Id, "test-value")); - var imageFile = CreateFile("not image data"); - //set commit date for add local resource - MockTimeProvider.SetNextDateTime(NextDate()); - var resource = await _resourceService.AddLocalResource(imageFile, Guid.NewGuid(), resourceService: _remoteServiceMock); - await WriteNextChange(new AddWordImageChange(_entity1Id, resource.Id)); - - var word = await DataModel.GetLatest(_entity1Id); - word.Should().NotBeNull(); - word!.ImageResourceId.Should().Be(resource.Id); - - - var localResource = await _resourceService.GetLocalResource(word.ImageResourceId!.Value); - localResource.Should().NotBeNull(); - localResource!.LocalPath.Should().Be(imageFile); - (await File.ReadAllTextAsync(localResource.LocalPath)).Should().Be("not image data"); - } +using System.Runtime.CompilerServices; +using Microsoft.Extensions.DependencyInjection; +using SIL.Harmony.Sample.Changes; +using SIL.Harmony.Sample.Models; + +namespace SIL.Harmony.Tests.ResourceTests; + +public class WordResourceTests: DataModelTestBase +{ + private RemoteServiceMock _remoteServiceMock = new(); + private ResourceService _resourceService => _services.GetRequiredService(); + private readonly Guid _entity1Id = Guid.NewGuid(); + + private string CreateFile(string contents, [CallerMemberName] string fileName = "") + { + var filePath = Path.GetFullPath(fileName + ".txt"); + File.WriteAllText(filePath, contents); + return filePath; + } + + [Fact] + public async Task CanReferenceAResourceFromAWord() + { + await WriteNextChange(SetWord(_entity1Id, "test-value")); + var imageFile = CreateFile("not image data"); + //set commit date for add local resource + MockTimeProvider.SetNextDateTime(NextDate()); + var resource = await _resourceService.AddLocalResource(imageFile, Guid.NewGuid(), resourceService: _remoteServiceMock); + await WriteNextChange(new AddWordImageChange(_entity1Id, resource.Id)); + + var word = await DataModel.GetLatest(_entity1Id); + word.Should().NotBeNull(); + word!.ImageResourceId.Should().Be(resource.Id); + + + var localResource = await _resourceService.GetLocalResource(word.ImageResourceId!.Value); + localResource.Should().NotBeNull(); + localResource!.LocalPath.Should().Be(imageFile); + (await File.ReadAllTextAsync(localResource.LocalPath, TestContext.Current.CancellationToken)).Should().Be("not image data"); + } } \ No newline at end of file diff --git a/src/SIL.Harmony.Tests/SnapshotTests.cs b/src/SIL.Harmony.Tests/SnapshotTests.cs index 1a4ff0e..3a9028b 100644 --- a/src/SIL.Harmony.Tests/SnapshotTests.cs +++ b/src/SIL.Harmony.Tests/SnapshotTests.cs @@ -31,7 +31,7 @@ public async Task MultipleChangesPreservesRootSnapshot() await AddCommitsViaSync(commits); - var snapshots = await DbContext.Snapshots.ToArrayAsync(); + var snapshots = await DbContext.Snapshots.ToArrayAsync(TestContext.Current.CancellationToken); snapshots.Should().HaveCountGreaterThan(1); snapshots.Should().ContainSingle(s => s.IsRoot); } @@ -52,7 +52,7 @@ public async Task MultipleChangesPreservesSomeIntermediateSnapshots() await AddCommitsViaSync(commits); var latestSnapshot = await DataModel.GetLatestSnapshotByObjectId(entityId); - var snapshots = await DbContext.Snapshots.ToArrayAsync(); + var snapshots = await DbContext.Snapshots.ToArrayAsync(TestContext.Current.CancellationToken); snapshots.Should().HaveCountGreaterThan(2); snapshots.Should().ContainSingle(s => s.Id == latestSnapshot.Id); snapshots.Should().ContainSingle(s => s.IsRoot); @@ -119,7 +119,7 @@ await WriteNextChange( ]); var word = await DataModel.QueryLatest(q => q.Include(w => w.Tags) - .Where(w => w.Id == wordId)).FirstOrDefaultAsync(); + .Where(w => w.Id == wordId)).FirstOrDefaultAsync(TestContext.Current.CancellationToken); word.Should().NotBeNull(); word.Tags.Should().BeEquivalentTo([new Tag { Id = tagId, Text = "tag-1" }]); } @@ -138,7 +138,7 @@ await WriteNextChange( await WriteChangeBefore(tagCreation, TagWord(wordId, tagId)); var word = await DataModel.QueryLatest(q => q.Include(w => w.Tags) - .Where(w => w.Id == wordId)).FirstOrDefaultAsync(); + .Where(w => w.Id == wordId)).FirstOrDefaultAsync(TestContext.Current.CancellationToken); word.Should().NotBeNull(); word.Tags.Should().BeEquivalentTo([new Tag { Id = tagId, Text = "tag-1" }]); } @@ -151,13 +151,13 @@ public async Task RegenerateSnapshots_WillArriveAtTheSameState() await WriteNextChange(SetWord(entityId, "test1")); await WriteNextChange(SetWord(entityId, "test2")); await WriteNextChange(SetWord(entityId, "test3")); - var beforeSnapshotIds = await DbContext.Snapshots.Select(s => s.Id).ToArrayAsync(); - var beforeRegenerate = await DataModel.QueryLatest().ToArrayAsync(); + var beforeSnapshotIds = await DbContext.Snapshots.Select(s => s.Id).ToArrayAsync(TestContext.Current.CancellationToken); + var beforeRegenerate = await DataModel.QueryLatest().ToArrayAsync(TestContext.Current.CancellationToken); await DataModel.RegenerateSnapshots(); - var afterRegenerate = await DataModel.QueryLatest().ToArrayAsync(); - var afterSnapshotsIds = await DbContext.Snapshots.Select(s => s.Id).ToArrayAsync(); + var afterRegenerate = await DataModel.QueryLatest().ToArrayAsync(TestContext.Current.CancellationToken); + var afterSnapshotsIds = await DbContext.Snapshots.Select(s => s.Id).ToArrayAsync(TestContext.Current.CancellationToken); afterRegenerate.Should().BeEquivalentTo(beforeRegenerate); //we probably won't have the same number of snapshots, which is ok. but none of the ids should be the same diff --git a/src/SIL.Harmony.Tests/SyncTests.cs b/src/SIL.Harmony.Tests/SyncTests.cs index 2ab3254..15cf8b5 100644 --- a/src/SIL.Harmony.Tests/SyncTests.cs +++ b/src/SIL.Harmony.Tests/SyncTests.cs @@ -116,7 +116,7 @@ public async Task CanSync_AddDependentWithMultipleChanges() await _client2.DataModel.SyncWith(_client1.DataModel); - _client2.DataModel.QueryLatest().ToBlockingEnumerable().Should() - .BeEquivalentTo(_client1.DataModel.QueryLatest().ToBlockingEnumerable()); + _client2.DataModel.QueryLatest().ToBlockingEnumerable(TestContext.Current.CancellationToken).Should() + .BeEquivalentTo(_client1.DataModel.QueryLatest().ToBlockingEnumerable(TestContext.Current.CancellationToken)); } } \ No newline at end of file From 2e408a0466dfb9d4ef1adbc69cca07e0f2cb203f Mon Sep 17 00:00:00 2001 From: Tim Haasdyk Date: Wed, 13 May 2026 12:30:50 +0200 Subject: [PATCH 7/7] Add commits order index --- Directory.Packages.props | 1 + .../DbContextTests.VerifyModel.verified.txt | 1 + .../Db/EntityConfig/CommitEntityConfig.cs | 76 ++++++++++--------- src/SIL.Harmony/SIL.Harmony.csproj | 59 +++++++------- 4 files changed, 74 insertions(+), 63 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 0fafb5c..a7a4243 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -26,5 +26,6 @@ + \ No newline at end of file diff --git a/src/SIL.Harmony.Tests/DbContextTests.VerifyModel.verified.txt b/src/SIL.Harmony.Tests/DbContextTests.VerifyModel.verified.txt index 018dda9..d02fa1a 100644 --- a/src/SIL.Harmony.Tests/DbContextTests.VerifyModel.verified.txt +++ b/src/SIL.Harmony.Tests/DbContextTests.VerifyModel.verified.txt @@ -24,6 +24,7 @@ Keys: Id PK Annotations: + CustomIndex:CompositeIndexes: [{"paths":["HybridDateTime.DateTime","HybridDateTime.Counter","Id"],"unique":false,"name":"IX_Commits_DateTime_Counter_Id"}] Relational:FunctionName: Relational:Schema: Relational:SqlQuery: diff --git a/src/SIL.Harmony/Db/EntityConfig/CommitEntityConfig.cs b/src/SIL.Harmony/Db/EntityConfig/CommitEntityConfig.cs index 9694bb5..38ebe34 100644 --- a/src/SIL.Harmony/Db/EntityConfig/CommitEntityConfig.cs +++ b/src/SIL.Harmony/Db/EntityConfig/CommitEntityConfig.cs @@ -1,34 +1,42 @@ -using System.Text.Json; -using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Metadata.Builders; - -namespace SIL.Harmony.Db.EntityConfig; - -public class CommitEntityConfig : IEntityTypeConfiguration -{ - public void Configure(EntityTypeBuilder builder) - { - builder.ToTable("Commits"); - builder.HasKey(c => c.Id); - builder.ComplexProperty(c => c.HybridDateTime, - hybridEntity => - { - hybridEntity.Property(h => h.DateTime) - .HasConversion( - d => d.UtcDateTime, - //need to use ticks here because the DateTime is stored as UTC, but the db records it as unspecified - d => new DateTimeOffset(d.Ticks, TimeSpan.Zero)) - .HasColumnName("DateTime"); - hybridEntity.Property(h => h.Counter).HasColumnName("Counter"); - }); - builder.Property(c => c.Metadata) - .HasColumnType("jsonb") - .HasConversion( - m => JsonSerializer.Serialize(m, (JsonSerializerOptions?)null), - json => JsonSerializer.Deserialize(json, (JsonSerializerOptions?)null) ?? new() - ); - builder.HasMany(c => c.ChangeEntities) - .WithOne() - .HasForeignKey(c => c.CommitId); - } -} +using System.Text.Json; +using EFCore.ComplexIndexes; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace SIL.Harmony.Db.EntityConfig; + +public class CommitEntityConfig : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Commits"); + builder.HasKey(c => c.Id); + builder.ComplexProperty(c => c.HybridDateTime, + hybridEntity => + { + hybridEntity.Property(h => h.DateTime) + .HasConversion( + d => d.UtcDateTime, + //need to use ticks here because the DateTime is stored as UTC, but the db records it as unspecified + d => new DateTimeOffset(d.Ticks, TimeSpan.Zero)) + .HasColumnName("DateTime"); + hybridEntity.Property(h => h.Counter).HasColumnName("Counter"); + }); + // Supports Harmony's default ordering (DateTime DESC, Counter DESC, Id DESC). + // EF Core 10 cannot express indexes mixing ComplexProperty members + scalars (efcore#11336, targeted for 11). + // We use EFCore.ComplexIndexes instead. The package doesn't support column direction, + // but an ASC index apparently works equivalently for reverse scans on SQLite, Postgres, and SQL Server. + builder.HasComplexCompositeIndex( + c => new { c.HybridDateTime.DateTime, c.HybridDateTime.Counter, c.Id }, + indexName: "IX_Commits_DateTime_Counter_Id"); + builder.Property(c => c.Metadata) + .HasColumnType("jsonb") + .HasConversion( + m => JsonSerializer.Serialize(m, (JsonSerializerOptions?)null), + json => JsonSerializer.Deserialize(json, (JsonSerializerOptions?)null) ?? new() + ); + builder.HasMany(c => c.ChangeEntities) + .WithOne() + .HasForeignKey(c => c.CommitId); + } +} diff --git a/src/SIL.Harmony/SIL.Harmony.csproj b/src/SIL.Harmony/SIL.Harmony.csproj index d731f07..a2673ae 100644 --- a/src/SIL.Harmony/SIL.Harmony.csproj +++ b/src/SIL.Harmony/SIL.Harmony.csproj @@ -1,29 +1,30 @@ - - - - SIL.Harmony - true - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - + + + + SIL.Harmony + true + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + +