Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,13 @@ await Cache.DoUsingNewOrCurrentUOW("Delete Complex Form Type",
});
}

public Task<MorphType> CreateMorphType(MorphType morphType)
{
// Morph types are a fixed inventory in FieldWorks (see MoMorphTypeTags), seeded with every project.
// MiniLcm only ever needs to read or update them, never add new ones, so creation isn't supported here.
throw new NotSupportedException("Morph types cannot be created in FieldWorks data; they are a predefined inventory.");
}

public IAsyncEnumerable<MorphType> GetMorphTypes()
{
return
Expand Down
10 changes: 5 additions & 5 deletions backend/FwLite/FwLiteProjectSync.Tests/Import/FullImportTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ public async Task DisposeAsync()
}

/// <summary>
/// Regression: Import creates a CRDT project with SeedNewProjectData: false.
/// Morph types must be seeded unconditionally so MorphTypeSync.Sync doesn't throw
/// when it encounters FwData morph types as "new".
/// Regression: Import creates a CRDT project with SeedNewProjectData: false, so morph types
/// are not pre-seeded. MorphTypeSync must create the source project's morph types during the
/// import instead of throwing when it encounters them as "new".
/// </summary>
[Fact]
public async Task Import_FullPath_SeedsMorphTypesBeforeImport()
public async Task Import_FullPath_CreatesMorphTypesDuringImport()
{
// Arrange: create an FwData project with one entry
var projectName = "import-morph-types-" + Guid.NewGuid().ToString("N")[..8];
Expand All @@ -72,7 +72,7 @@ await fwDataApi.CreateEntry(new Entry
var crdtApi = await Services.OpenCrdtProject((CrdtProject)crdtProject);

var morphTypes = await crdtApi.GetMorphTypes().ToArrayAsync();
morphTypes.Should().NotBeEmpty("morph types should be seeded during project creation");
morphTypes.Should().NotBeEmpty("import should have created the source project's morph types");

var entries = await crdtApi.GetEntries().ToArrayAsync();
entries.Should().ContainSingle(e => e.LexemeForm["en"] == "test");
Expand Down
6 changes: 6 additions & 0 deletions backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ public Task DeleteComplexFormType(Guid id)
return Task.CompletedTask;
}

public Task<MorphType> CreateMorphType(MorphType morphType)
{
DryRunRecords.Add(new DryRunRecord(nameof(CreateMorphType), $"Create morph type {morphType.Kind} ({morphType.Id})"));
return Task.FromResult(morphType);
}

public async Task<MorphType> UpdateMorphType(Guid id, UpdateObjectInput<MorphType> update)
{
DryRunRecords.Add(new DryRunRecord(nameof(UpdateMorphType), $"Update morph type {id}"));
Expand Down
4 changes: 4 additions & 0 deletions backend/FwLite/FwLiteProjectSync/Import/ResumableImportApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ async Task<ComplexFormType> IMiniLcmWriteApi.CreateComplexFormType(ComplexFormTy
{
return await HasCreated(complexFormType, _api.GetComplexFormTypes(), () => _api.CreateComplexFormType(complexFormType));
}
async Task<MorphType> IMiniLcmWriteApi.CreateMorphType(MorphType morphType)
{
return await HasCreated(morphType, _api.GetMorphTypes(), () => _api.CreateMorphType(morphType));
}
async Task<SemanticDomain> IMiniLcmWriteApi.CreateSemanticDomain(SemanticDomain semanticDomain)
{
return await HasCreated(semanticDomain, _api.GetSemanticDomains(), () => _api.CreateSemanticDomain(semanticDomain));
Expand Down
3 changes: 2 additions & 1 deletion backend/FwLite/FwLiteProjectSync/MiniLcmImport.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ public async Task ImportProject(IMiniLcmApi importTo, IMiniLcmApi importFrom, in
logger.LogInformation("Imported complex form type {Id}", complexFormType.Id);
}

// Morph types are created automatically for CRDT projects, so we update them instead of creating them
// Sync morph types from the source: MorphTypeSync creates any canonical morph types the
// target doesn't already have and updates the rest (the target isn't pre-seeded on import).
var importFromMorphTypes = await importFrom.GetMorphTypes().ToArrayAsync();
var existingMorphTypes = await importTo.GetMorphTypes().ToArrayAsync();
await MorphTypeSync.Sync(existingMorphTypes, importFromMorphTypes, importTo);
Expand Down
24 changes: 22 additions & 2 deletions backend/FwLite/LcmCrdt.Tests/Data/DownloadProjectTests.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SIL.Harmony.Core;

namespace LcmCrdt.Tests.Data;

public class DownloadProjectTests : IAsyncLifetime
{
private readonly RegressionTestHelper _helper = new("DownloadProject");
private readonly MiniLcmApiFixture _apiFixture = MiniLcmApiFixture.Create(false);
private static readonly Guid _projectId = new("B467051E-A492-4E5B-9C17-858D7797292C");//internal project Id of v2 project
// Download starts from an empty local project, so don't seed morph types — they should arrive via sync.
private readonly MiniLcmApiFixture _apiFixture = MiniLcmApiFixture.Create(false, _projectId, seedMorphTypes: false);

public async Task InitializeAsync()
{
await _helper.InitializeAsync();
await _helper.InitializeAsync(RegressionTestHelper.RegressionVersion.v2);
//add a change after migration which creates MorphTypes
await _helper.Services.GetRequiredService<IMiniLcmApi>().CreateEntry(new Entry()
{
LexemeForm = {{"en", "test"}}
});
await _apiFixture.InitializeAsync();
}

Expand All @@ -24,6 +33,17 @@ public async Task DisposeAsync()
public async Task CanCreateANewProjectViaSync()
{
var remoteModel = _helper.Services.GetRequiredService<DataModel>();
var remoteDb = await _helper.Services.GetRequiredService<IDbContextFactory<LcmCrdtDbContext>>().CreateDbContextAsync();

await _apiFixture.DataModel.SyncWith(remoteModel);
var localCommits = await _apiFixture.DbContext.Set<Commit>().DefaultOrder().ToListAsync();
var remoteCommits = await remoteDb.Set<Commit>().DefaultOrder().ToListAsync();
localCommits.Count.Should().Be(remoteCommits.Count);
for (var i = localCommits.Count - 1; i >= 0; i--)
{
var localCommit = localCommits[i];
var remoteCommit = remoteCommits[i];
localCommit.Should().BeEquivalentTo(remoteCommit, "commit index {0} should be the same", i);
}
}
}
32 changes: 15 additions & 17 deletions backend/FwLite/LcmCrdt.Tests/Data/MigrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@
namespace LcmCrdt.Tests.Data;

[Collection("MigrationTests")]
public class MigrationTests : IAsyncLifetime
public class MigrationTests
{
private readonly RegressionTestHelper _helper = new("MigrationTest");
private static readonly JsonSerializerOptions IndentedHarmonyJsonOptions = new(TestJsonOptions.Harmony())
{
WriteIndented = true
Expand All @@ -25,21 +24,12 @@ internal static void Init()
VerifierSettings.OmitContentFromException();
}

public Task InitializeAsync()
{
return Task.CompletedTask;
}

public async Task DisposeAsync()
{
await _helper.DisposeAsync();
}

[Theory]
[InlineData(RegressionTestHelper.RegressionVersion.v1)]
[InlineData(RegressionTestHelper.RegressionVersion.v2)]
public async Task GetEntries_WorksAfterMigrationFromScriptedDb(RegressionTestHelper.RegressionVersion regressionVersion)
{
await using RegressionTestHelper _helper = new($"{nameof(GetEntries_WorksAfterMigrationFromScriptedDb)}-{regressionVersion}");
await _helper.InitializeAsync(regressionVersion);
var api = _helper.Services.GetRequiredService<IMiniLcmApi>();
var hasEntries = false;
Expand All @@ -58,25 +48,33 @@ public async Task GetEntries_WorksAfterMigrationFromScriptedDb(RegressionTestHel
[Trait("Category", "Verified")]
public async Task VerifyAfterMigrationFromScriptedDb(RegressionTestHelper.RegressionVersion regressionVersion)
{
await using RegressionTestHelper _helper = new($"{nameof(VerifyAfterMigrationFromScriptedDb)}-{regressionVersion}");
await _helper.InitializeAsync(regressionVersion);
var api = _helper.Services.GetRequiredService<IMiniLcmApi>();
var crdtConfig = _helper.Services.GetRequiredService<IOptions<CrdtConfig>>().Value;

await using var dbContext = await _helper.Services.GetRequiredService<ICrdtDbContextFactory>().CreateDbContextAsync();
var snapshots = await dbContext.Snapshots.AsNoTracking()
var allSnapshots = await dbContext.Snapshots.AsNoTracking()
.OrderBy(s => s.TypeName)
.ThenBy(s => s.EntityId)
.ThenBy(c => c.Commit.HybridDateTime.DateTime)
.ThenBy(c => c.Commit.HybridDateTime.Counter)
.ThenBy(c => c.Commit.Id)
.ToArrayAsync();

var entityIdToTypeName = snapshots
var entityIdToTypeName = allSnapshots
.GroupBy(s => s.EntityId)
.ToDictionary(g => g.Key, g => g.First().TypeName);

// Migration seeds canonical morph types with a random commit id (PreDefinedData.AddPredefinedMorphTypes),
// so their commit/snapshot ids aren't stable and can't be pinned here. Their seeding is verified exactly by
// MorphTypeSeedingTests and structurally by VerifyRegeneratedSnapshotsAfterMigrationFromScriptedDb.
const string morphTypeName = "MorphType";
var snapshots = allSnapshots.Where(s => s.TypeName != morphTypeName).ToArray();
var changes = (await dbContext.Commits.AsNoTracking().Include(c => c.ChangeEntities)
.ToArrayAsync())
.SelectMany(c => c.ChangeEntities.Select(changeEntity => new { ChangeEntity = changeEntity, c.HybridDateTime }))
.Where(c => entityIdToTypeName[c.ChangeEntity.EntityId] != morphTypeName)
.OrderBy(s => entityIdToTypeName[s.ChangeEntity.EntityId])
.ThenBy(c => c.ChangeEntity.EntityId)
.ThenBy(c => c.HybridDateTime.DateTime)
Expand Down Expand Up @@ -110,6 +108,7 @@ await Task.WhenAll(
[Trait("Category", "Verified")]
public async Task VerifyRegeneratedSnapshotsAfterMigrationFromScriptedDb(RegressionTestHelper.RegressionVersion regressionVersion)
{
await using RegressionTestHelper _helper = new($"{nameof(VerifyRegeneratedSnapshotsAfterMigrationFromScriptedDb)}-{regressionVersion}");
await _helper.InitializeAsync(regressionVersion);
var api = _helper.Services.GetRequiredService<IMiniLcmApi>();
var crdtConfig = _helper.Services.GetRequiredService<IOptions<CrdtConfig>>().Value;
Expand Down Expand Up @@ -161,9 +160,8 @@ await api.GetSemanticDomains()
await api.GetComplexFormTypes()
.OrderBy(c => c.Id)
.ToArrayAsync(),
await api.GetMorphTypes()
.OrderBy(m => m.Id)
.ToArrayAsync(),
// Morph types are excluded — migration seeds them with random ids; see VerifyAfterMigrationFromScriptedDb.
[],
await api.GetWritingSystems());
}

Expand Down
9 changes: 4 additions & 5 deletions backend/FwLite/LcmCrdt.Tests/Data/RegressionTestHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

namespace LcmCrdt.Tests.Data;

public class RegressionTestHelper(string dbName): IAsyncLifetime
public class RegressionTestHelper(string dbName): IAsyncDisposable
{
private IHost _host = null!;
private AsyncServiceScope _asyncScope;
Expand Down Expand Up @@ -38,9 +38,8 @@ private async Task InitDbFromScripts(RegressionVersion version)
//need to close the connection, otherwise the collations won't get created, they would normally be created on open or save, so we're closing so they get created when EF opens the connection.
await dbConnection.CloseAsync();

await lcmCrdtDbContext.Database.MigrateAsync();

await projectsService.RefreshProjectData();
//setup again to trigger migrations
await projectsService.SetupProjectContext(crdtProject);
}

public Task InitializeAsync()
Expand All @@ -58,7 +57,7 @@ public async Task InitializeAsync(RegressionVersion version)
await InitDbFromScripts(version);
}

public async Task DisposeAsync()
public async ValueTask DisposeAsync()
{
await _asyncScope.DisposeAsync();
if (_host is IAsyncDisposable asyncDisposable) await asyncDisposable.DisposeAsync();
Expand Down
Loading
Loading