Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement update and delete apis for ComplexFormTypes and sync them in CrdtFwdataProjectSyncService #1295

Merged
merged 16 commits into from
Dec 4, 2024
Merged
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
2 changes: 1 addition & 1 deletion .github/workflows/fw-lite.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ on:
jobs:
build-and-test:
name: Build FW Lite and run tests
timeout-minutes: 20
timeout-minutes: 30
runs-on: windows-latest
outputs:
version: ${{ steps.setVersion.outputs.VERSION }}
Expand Down
52 changes: 49 additions & 3 deletions backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,11 @@

public Task<WritingSystem> CreateWritingSystem(WritingSystemType type, WritingSystem writingSystem)
{
var exitingWs = type == WritingSystemType.Analysis ? Cache.ServiceLocator.WritingSystems.AnalysisWritingSystems : Cache.ServiceLocator.WritingSystems.VernacularWritingSystems;
if (exitingWs.Any(ws => ws.Id == writingSystem.WsId))
{
throw new DuplicateObjectException($"Writing system {writingSystem.WsId.Code} already exists");
}
CoreWritingSystemDefinition? ws = null;
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Writing System",
"Remove writing system",
Expand Down Expand Up @@ -201,7 +206,7 @@
}
await Cache.DoUsingNewOrCurrentUOW("Update WritingSystem",
"Revert WritingSystem",
async () =>

Check warning on line 209 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FwHeadless / publish-fw-headless

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 209 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Build FW Lite and run tests

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 209 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 209 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Mac

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 209 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 209 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Linux

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 209 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 209 in backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

View workflow job for this annotation

GitHub Actions / Publish FW Lite app for Windows

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
var updateProxy = new UpdateWritingSystemProxy(lcmWritingSystem, this)
{
Expand Down Expand Up @@ -377,6 +382,14 @@
{
return ComplexFormTypesFlattened.Select(ToComplexFormType).ToAsyncEnumerable();
}

public Task<ComplexFormType?> GetComplexFormType(Guid id)
{
var lexEntryType = ComplexFormTypesFlattened.SingleOrDefault(c => c.Guid == id);
if (lexEntryType is null) return Task.FromResult<ComplexFormType?>(null);
return Task.FromResult<ComplexFormType?>(ToComplexFormType(lexEntryType));
}

private ComplexFormType ToComplexFormType(ILexEntryType t)
{
return new ComplexFormType() { Id = t.Guid, Name = FromLcmMultiString(t.Name) };
Expand All @@ -400,6 +413,40 @@
return ToComplexFormType(ComplexFormTypesFlattened.Single(c => c.Guid == complexFormType.Id));
}

public Task<ComplexFormType> UpdateComplexFormType(Guid id, UpdateObjectInput<ComplexFormType> update)
{
var type = ComplexFormTypesFlattened.SingleOrDefault(c => c.Guid == id);
if (type is null) throw new NullReferenceException($"unable to find complex form type with id {id}");
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Update Complex Form Type",
"Revert Complex Form Type",
Cache.ServiceLocator.ActionHandler,
() =>
{
var updateProxy = new UpdateComplexFormTypeProxy(type, null, this);
update.Apply(updateProxy);
});
return Task.FromResult(ToComplexFormType(type));
}

public async Task<ComplexFormType> UpdateComplexFormType(ComplexFormType before, ComplexFormType after)
{
await ComplexFormTypeSync.Sync(before, after, this);
return ToComplexFormType(ComplexFormTypesFlattened.Single(c => c.Guid == after.Id));
}

public async Task DeleteComplexFormType(Guid id)
{
var type = ComplexFormTypesFlattened.SingleOrDefault(c => c.Guid == id);
if (type is null) return;
await Cache.DoUsingNewOrCurrentUOW("Delete Complex Form Type",
"Revert delete",
() =>
{
type.Delete();
return ValueTask.CompletedTask;
});
}

public IAsyncEnumerable<VariantType> GetVariantTypes()
{
return VariantTypes.PossibilitiesOS
Expand Down Expand Up @@ -591,9 +638,8 @@
string? text = e.CitationForm.get_String(sortWs).Text;
text ??= e.LexemeFormOA.Form.get_String(sortWs).Text;
return text?.Trim(LcmHelpers.WhitespaceChars);
})
.Skip(options.Offset)
.Take(options.Count);
});
entries = options.ApplyPaging(entries);

return entries.ToAsyncEnumerable().Select(FromLexEntry);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,35 @@ namespace FwDataMiniLcmBridge.Api.UpdateProxy;
public record UpdateComplexFormTypeProxy : ComplexFormType
{
private readonly ILexEntryType _lexEntryType;
private readonly ILexEntry _lcmEntry;
private readonly ILexEntry? _lcmEntry;
private readonly FwDataMiniLcmApi _lexboxLcmApi;

[SetsRequiredMembers]
public UpdateComplexFormTypeProxy(ILexEntryType lexEntryType, ILexEntry lcmEntry, FwDataMiniLcmApi lexboxLcmApi)
public UpdateComplexFormTypeProxy(ILexEntryType lexEntryType, ILexEntry? lcmEntry, FwDataMiniLcmApi lexboxLcmApi)
{
_lexEntryType = lexEntryType;
_lcmEntry = lcmEntry;
_lexboxLcmApi = lexboxLcmApi;
Name = new();
Name = base.Name = new();
}

public override Guid Id
{
get => _lexEntryType.Guid;
set
{
if (_lcmEntry is null)
throw new InvalidOperationException("Cannot update complex form type Id on a null entry");
_lexboxLcmApi.RemoveComplexFormType(_lcmEntry, _lexEntryType.Guid);
_lexboxLcmApi.AddComplexFormType(_lcmEntry, value);
}
}

public override required MultiString Name
{
get => new UpdateMultiStringProxy(_lexEntryType.Name, _lexboxLcmApi);
set
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public async Task InitializeAsync()
var crdtProjectsFolder =
rootServiceProvider.GetRequiredService<IOptions<LcmCrdtConfig>>().Value.ProjectPath;
if (Path.Exists(crdtProjectsFolder)) Directory.Delete(crdtProjectsFolder, true);
rootServiceProvider.Dispose();
await rootServiceProvider.DisposeAsync();

Directory.CreateDirectory(crdtProjectsFolder);
await DownloadSena3();
Expand Down
12 changes: 4 additions & 8 deletions backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@
using FwDataMiniLcmBridge;
using FwDataMiniLcmBridge.Api;
using FwDataMiniLcmBridge.LcmUtils;
using FwDataMiniLcmBridge.Tests.Fixtures;
using LcmCrdt;
using LexCore.Utils;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MiniLcm;

namespace FwLiteProjectSync.Tests.Fixtures;

Expand All @@ -23,19 +19,19 @@ public class SyncFixture : IAsyncLifetime
private readonly string _projectName;
private readonly IDisposable _cleanup;

public static SyncFixture Create([CallerMemberName] string projectName = "") => new(projectName);
public static SyncFixture Create([CallerMemberName] string projectName = "", [CallerMemberName] string projectFolder = "") => new(projectName, projectFolder);

private SyncFixture(string projectName)
private SyncFixture(string projectName, string projectFolder)
{
_projectName = projectName;
var crdtServices = new ServiceCollection()
.AddSyncServices(_projectName);
.AddSyncServices(projectFolder);
var rootServiceProvider = crdtServices.BuildServiceProvider();
_cleanup = Defer.Action(() => rootServiceProvider.Dispose());
_services = rootServiceProvider.CreateAsyncScope();
}

public SyncFixture(): this("sena-3_" + Guid.NewGuid().ToString("N"))
public SyncFixture(): this("sena-3_" + Guid.NewGuid().ToString("N"), "FwLiteSyncFixture")
{
}

Expand Down
36 changes: 25 additions & 11 deletions backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public class Sena3SyncTests : IClassFixture<Sena3Fixture>, IAsyncLifetime
private CrdtMiniLcmApi _crdtApi = null!;
private FwDataMiniLcmApi _fwDataApi = null!;
private IDisposable? _cleanup;
private MiniLcmImport _miniLcmImport = null!;


public Sena3SyncTests(Sena3Fixture fixture)
Expand All @@ -29,6 +30,7 @@ public async Task InitializeAsync()
{
(_crdtApi, _fwDataApi, var services, _cleanup) = await _fixture.SetupProjects();
_syncService = services.GetRequiredService<CrdtFwdataProjectSyncService>();
_miniLcmImport = services.GetRequiredService<MiniLcmImport>();
_fwDataApi.EntryCount.Should().BeGreaterThan(100, "project should be loaded and have entries");
}

Expand Down Expand Up @@ -56,9 +58,11 @@ private void ShouldAllBeEquivalentTo(Dictionary<Guid, Entry> crdtEntries, Dictio
}

//by default the first sync is an import, this will skip that so that the sync will actually sync data
private async Task BypassImport()
private async Task BypassImport(bool wsImported = false)
{
await _syncService.SaveProjectSnapshot(_fwDataApi.Project, new ([], [], []));
var snapshot = CrdtFwdataProjectSyncService.ProjectSnapshot.Empty;
if (wsImported) snapshot = snapshot with { WritingSystems = await _fwDataApi.GetWritingSystems() };
await _syncService.SaveProjectSnapshot(_fwDataApi.Project, snapshot);
}

//this lets us query entries when there is no writing system
Expand Down Expand Up @@ -98,28 +102,37 @@ public async Task DryRunSync_MakesNoChanges()
_crdtApi.GetEntries().ToBlockingEnumerable().Should().BeEmpty();
}

[Fact(Skip = "this test is waiting for syncing ComplexFormTypes and WritingSystems")]
public async Task DryRunSync_MakesTheSameChangesAsImport()
[Fact]
public async Task DryRunSync_MakesTheSameChangesAsSync()
{
await BypassImport();
//syncing requires querying entries, which fails if there are no writing systems, so we import those first
await _miniLcmImport.ImportWritingSystems(_crdtApi, _fwDataApi);
await BypassImport(true);

var dryRunSyncResult = await _syncService.SyncDryRun(_crdtApi, _fwDataApi);
var syncResult = await _syncService.Sync(_crdtApi, _fwDataApi);
dryRunSyncResult.Should().BeEquivalentTo(syncResult);
dryRunSyncResult.CrdtChanges.Should().Be(syncResult.CrdtChanges);
//can't test fwdata changes as they will not work correctly since the sync code expects Crdts to contain data from FWData
//this throws off the algorithm and it will try to delete everything in fwdata since there's no data in the crdt since it was a dry run
}

[Fact]
public async Task FirstSena3SyncJustDoesAnSync()
{
_fwDataApi.EntryCount.Should().BeGreaterThan(1000,
"projects with less than 1000 entries don't trip over the default query limit");

var results = await _syncService.Sync(_crdtApi, _fwDataApi);
results.FwdataChanges.Should().Be(0);
results.CrdtChanges.Should().BeGreaterThanOrEqualTo(_fwDataApi.EntryCount);

var crdtEntries = await _crdtApi.GetEntries().ToDictionaryAsync(e => e.Id);
var fwdataEntries = await _fwDataApi.GetEntries().ToDictionaryAsync(e => e.Id);
var crdtEntries = await _crdtApi.GetAllEntries().ToDictionaryAsync(e => e.Id);
var fwdataEntries = await _fwDataApi.GetAllEntries().ToDictionaryAsync(e => e.Id);
fwdataEntries.Count.Should().Be(_fwDataApi.EntryCount);
ShouldAllBeEquivalentTo(crdtEntries, fwdataEntries);
}

[Fact(Skip = "this test is waiting for syncing ComplexFormTypes and WritingSystems")]
[Fact]
public async Task SyncWithoutImport_CrdtShouldMatchFwdata()
{
await BypassImport();
Expand All @@ -128,8 +141,9 @@ public async Task SyncWithoutImport_CrdtShouldMatchFwdata()
results.FwdataChanges.Should().Be(0);
results.CrdtChanges.Should().BeGreaterThan(_fwDataApi.EntryCount);

var crdtEntries = await _crdtApi.GetEntries().ToDictionaryAsync(e => e.Id);
var fwdataEntries = await _fwDataApi.GetEntries().ToDictionaryAsync(e => e.Id);
var crdtEntries = await _crdtApi.GetAllEntries().ToDictionaryAsync(e => e.Id);
var fwdataEntries = await _fwDataApi.GetAllEntries().ToDictionaryAsync(e => e.Id);
fwdataEntries.Count.Should().Be(_fwDataApi.EntryCount);
ShouldAllBeEquivalentTo(crdtEntries, fwdataEntries);
}

Expand Down
52 changes: 32 additions & 20 deletions backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,14 @@ await _fixture.FwDataApi.CreateEntry(new Entry()

public async Task DisposeAsync()
{
await foreach (var entry in _fixture.FwDataApi.GetEntries())
await foreach (var entry in _fixture.FwDataApi.GetAllEntries())
{
await _fixture.FwDataApi.DeleteEntry(entry.Id);
}
foreach (var entry in await _fixture.CrdtApi.GetEntries().ToArrayAsync())
foreach (var entry in await _fixture.CrdtApi.GetAllEntries().ToArrayAsync())
{
await _fixture.CrdtApi.DeleteEntry(entry.Id);
}

_fixture.DeleteSyncSnapshot();
}

public SyncTests(SyncFixture fixture)
Expand All @@ -82,8 +80,8 @@ public async Task FirstSyncJustDoesAnImport()
var fwdataApi = _fixture.FwDataApi;
await _syncService.Sync(crdtApi, fwdataApi);

var crdtEntries = await crdtApi.GetEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync();
var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync();
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
Expand Down Expand Up @@ -144,8 +142,8 @@ await crdtApi.CreateEntry(new Entry()
});
await _syncService.Sync(crdtApi, fwdataApi);

var crdtEntries = await crdtApi.GetEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync();
var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync();
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
Expand Down Expand Up @@ -180,8 +178,8 @@ await crdtApi.CreateEntry(new Entry()
});
await _syncService.SyncDryRun(crdtApi, fwdataApi);

var crdtEntries = await crdtApi.GetEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync();
var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync();
crdtEntries.Select(e => e.Id).Should().NotContain(fwDataEntryId);
fwdataEntries.Select(e => e.Id).Should().NotContain(crdtEntryId);
}
Expand Down Expand Up @@ -222,8 +220,8 @@ public async Task CreatingAComplexEntryInFwDataSyncsWithoutIssue()
hatstand.Components = [component1, component2];
await _syncService.Sync(crdtApi, fwdataApi);

var crdtEntries = await crdtApi.GetEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync();
var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync();
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
Expand Down Expand Up @@ -304,8 +302,8 @@ await crdtApi.CreateEntry(new Entry()
});
await _syncService.Sync(crdtApi, fwdataApi);

var crdtEntries = await crdtApi.GetEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync();
var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync();
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
Expand Down Expand Up @@ -383,8 +381,8 @@ await crdtApi.CreateEntry(new Entry()
});
await _syncService.Sync(crdtApi, fwdataApi);

var crdtEntries = await crdtApi.GetEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync();
var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync();
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
Expand All @@ -406,8 +404,8 @@ public async Task UpdatingAnEntryInEachProjectSyncsAcrossBoth()
results.CrdtChanges.Should().Be(1);
results.FwdataChanges.Should().Be(1);

var crdtEntries = await crdtApi.GetEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync();
var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync();
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options
.For(e => e.Components).Exclude(c => c.Id)
Expand Down Expand Up @@ -474,8 +472,8 @@ public async Task AddingASenseToAnEntryInEachProjectSyncsAcrossBoth()

await _syncService.Sync(crdtApi, fwdataApi);

var crdtEntries = await crdtApi.GetEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetEntries().ToArrayAsync();
var crdtEntries = await crdtApi.GetAllEntries().ToArrayAsync();
var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync();
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
Expand All @@ -500,4 +498,18 @@ public async Task CanCreateAComplexFormAndItsComponentInOneSync()
//one of the entries will be created first, it will try to create the reference to the other but it won't exist yet
await _fixture.SyncService.Sync(_fixture.CrdtApi, _fixture.FwDataApi);
}

[Fact]
public async Task CanCreateAComplexFormTypeAndSyncsIt()
{
//ensure they are synced so a real sync will happen when we want it to
await _fixture.SyncService.Sync(_fixture.CrdtApi, _fixture.FwDataApi);

var complexFormEntry = await _fixture.CrdtApi.CreateComplexFormType(new() { Name = new() { { "en", "complexFormType" } } });

//one of the entries will be created first, it will try to create the reference to the other but it won't exist yet
await _fixture.SyncService.Sync(_fixture.CrdtApi, _fixture.FwDataApi);

_fixture.FwDataApi.GetComplexFormTypes().ToBlockingEnumerable().Should().ContainEquivalentOf(complexFormEntry);
}
}
Loading
Loading