Skip to content

Commit

Permalink
Merge branch 'develop' into feat/blazor-hybrid
Browse files Browse the repository at this point in the history
# Conflicts:
#	backend/FwLite/LcmCrdt/CrdtMiniLcmApi.cs
#	backend/FwLite/MiniLcm/IMiniLcmApi.cs
  • Loading branch information
hahn-kev committed Dec 19, 2024
2 parents aa5067c + d38737a commit 68e5f37
Show file tree
Hide file tree
Showing 48 changed files with 1,330 additions and 500 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ protected override Task<IMiniLcmApi> NewApi()
{
return Task.FromResult<IMiniLcmApi>(fixture.NewProjectApi("update-entry-test", "en", "en"));
}

protected override bool ApiUsesImplicitOrdering => true;
}
78 changes: 68 additions & 10 deletions backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Collections.Frozen;
using System.Globalization;
using System.Reflection;
using System.Text;
using FluentValidation;
using FwDataMiniLcmBridge.Api.UpdateProxy;
Expand Down Expand Up @@ -225,7 +224,7 @@ await Cache.DoUsingNewOrCurrentUOW("Update WritingSystem",
"Revert WritingSystem",
async () =>
{
await WritingSystemSync.Sync(after, before, this);
await WritingSystemSync.Sync(before, after, this);
});
return await GetWritingSystem(after.WsId, after.Type) ?? throw new NullReferenceException($"unable to find {after.Type} writing system with id {after.WsId}");
}
Expand Down Expand Up @@ -649,7 +648,7 @@ public IAsyncEnumerable<Entry> SearchEntries(string query, QueryOptions? options
var entries = GetEntries(e =>
e.CitationForm.SearchValue(query) ||
e.LexemeFormOA.Form.SearchValue(query) ||
e.SensesOS.Any(s => s.Gloss.SearchValue(query)), options);
e.AllSenses.Any(s => s.Gloss.SearchValue(query)), options);
return entries;
}

Expand Down Expand Up @@ -857,7 +856,7 @@ await Cache.DoUsingNewOrCurrentUOW("Update Entry",
"Revert entry",
async () =>
{
await EntrySync.Sync(after, before, this);
await EntrySync.Sync(before, after, this);
});
return await GetEntry(after.Id) ?? throw new NullReferenceException("unable to find entry with id " + after.Id);
}
Expand All @@ -874,9 +873,10 @@ public Task DeleteEntry(Guid id)
return Task.CompletedTask;
}

internal void CreateSense(ILexEntry lexEntry, Sense sense)
internal void CreateSense(ILexEntry lexEntry, Sense sense, BetweenPosition? between = null)
{
var lexSense = LexSenseFactory.Create(sense.Id, lexEntry);
var lexSense = LexSenseFactory.Create(sense.Id);
InsertSense(lexEntry, lexSense, between);
var msa = new SandboxGenericMSA() { MsaType = lexSense.GetDesiredMsaType() };
if (sense.PartOfSpeechId.HasValue && PartOfSpeechRepository.TryGetObject(sense.PartOfSpeechId.Value, out var pos))
{
Expand All @@ -886,6 +886,46 @@ internal void CreateSense(ILexEntry lexEntry, Sense sense)
ApplySenseToLexSense(sense, lexSense);
}

internal void InsertSense(ILexEntry lexEntry, ILexSense lexSense, BetweenPosition? between = null)
{
var previousSenseId = between?.Previous;
var nextSenseId = between?.Next;

var previousSense = previousSenseId.HasValue ? lexEntry.AllSenses.Find(s => s.Guid == previousSenseId) : null;
if (previousSense is not null)
{
if (previousSense.SensesOS.Count > 0)
{
// if the sense has sub-senses, our sense will only come directly after it if it is the first sub-sense
previousSense.SensesOS.Insert(0, lexSense);
}
else
{
// todo the user might have wanted it to be a subsense of previousSense
var allSiblings = previousSense.Owner == lexEntry ? lexEntry.SensesOS
: previousSense.Owner is ILexSense parentSense ? parentSense.SensesOS
: throw new InvalidOperationException("Sense parent is not a sense or the expected entry");
var insertI = allSiblings.IndexOf(previousSense) + 1;
lexEntry.SensesOS.Insert(insertI, lexSense);
}
return;
}

var nextSense = nextSenseId.HasValue ? lexEntry.AllSenses.Find(s => s.Guid == nextSenseId) : null;
if (nextSense is not null)
{
// todo the user might have wanted it to be a subsense of whatever is before nextSense
var allSiblings = nextSense.Owner == lexEntry ? lexEntry.SensesOS
: nextSense.Owner is ILexSense parentSense ? parentSense.SensesOS
: throw new InvalidOperationException("Sense parent is not a sense or the expected entry");
var insertI = allSiblings.IndexOf(nextSense);
lexEntry.SensesOS.Insert(insertI, lexSense);
return;
}

lexEntry.SensesOS.Add(lexSense);
}

private void ApplySenseToLexSense(Sense sense, ILexSense lexSense)
{
if (lexSense.MorphoSyntaxAnalysisRA.GetPartOfSpeech()?.Guid != sense.PartOfSpeechId)
Expand Down Expand Up @@ -917,15 +957,15 @@ private void ApplySenseToLexSense(Sense sense, ILexSense lexSense)
return Task.FromResult(lcmSense is null ? null : FromLexSense(lcmSense));
}

public Task<Sense> CreateSense(Guid entryId, Sense sense)
public Task<Sense> CreateSense(Guid entryId, Sense sense, BetweenPosition? between = null)
{
if (sense.Id == default) sense.Id = Guid.NewGuid();
if (!EntriesRepository.TryGetObject(entryId, out var lexEntry))
throw new InvalidOperationException("Entry not found");
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Sense",
"Remove sense",
Cache.ServiceLocator.ActionHandler,
() => CreateSense(lexEntry, sense));
() => CreateSense(lexEntry, sense, between));
return Task.FromResult(FromLexSense(SenseRepository.GetObject(sense.Id)));
}

Expand All @@ -950,11 +990,29 @@ await Cache.DoUsingNewOrCurrentUOW("Update Sense",
"Revert Sense",
async () =>
{
await SenseSync.Sync(entryId, after, before, this);
await SenseSync.Sync(entryId, before, after, this);
});
return await GetSense(entryId, after.Id) ?? throw new NullReferenceException("unable to find sense with id " + after.Id);
}

public Task MoveSense(Guid entryId, Guid senseId, BetweenPosition between)
{
if (!EntriesRepository.TryGetObject(entryId, out var lexEntry))
throw new InvalidOperationException("Entry not found");
if (!SenseRepository.TryGetObject(senseId, out var lexSense))
throw new InvalidOperationException("Sense not found");

UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Move Sense",
"Move Sense back",
Cache.ServiceLocator.ActionHandler,
() =>
{
// LibLCM treats an insert as a move if the sense is already in the entry
InsertSense(lexEntry, lexSense, between);
});
return Task.CompletedTask;
}

public Task AddSemanticDomainToSense(Guid senseId, SemanticDomain semanticDomain)
{
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Add Semantic Domain to Sense",
Expand Down Expand Up @@ -1048,7 +1106,7 @@ await Cache.DoUsingNewOrCurrentUOW("Update Example Sentence",
"Revert Example Sentence",
async () =>
{
await ExampleSentenceSync.Sync(entryId, senseId, after, before, this);
await ExampleSentenceSync.Sync(entryId, senseId, before, after, this);
});
return await GetExampleSentence(entryId, senseId, after.Id) ?? throw new NullReferenceException("unable to find example sentence with id " + after.Id);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,15 +45,9 @@ public override MultiString LiteralMeaning
set => throw new NotImplementedException();
}

public override IList<Sense> Senses
public override List<Sense> Senses
{
get =>
new UpdateListProxy<Sense>(
sense => _lexboxLcmApi.CreateSense(_lcmEntry, sense),
sense => _lexboxLcmApi.DeleteSense(Id, sense.Id),
i => new UpdateSenseProxy(_lcmEntry.SensesOS[i], _lexboxLcmApi),
_lcmEntry.SensesOS.Count
);
get => throw new NotImplementedException();
set => throw new NotImplementedException();
}

Expand Down
32 changes: 25 additions & 7 deletions backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
using FwLiteProjectSync.Tests.Fixtures;
using MiniLcm.Models;
using MiniLcm.SyncHelpers;
using MiniLcm.Tests;
using MiniLcm.Tests.AutoFakerHelpers;
using Soenneker.Utils.AutoBogus;
using Soenneker.Utils.AutoBogus.Config;

namespace FwLiteProjectSync.Tests;

public class EntrySyncTests : IClassFixture<SyncFixture>
{
private static readonly AutoFaker AutoFaker = new(builder => builder.WithOverride(new MultiStringOverride()).WithOverride(new ObjectWithIdOverride()));
private static readonly AutoFaker AutoFaker = new(new AutoFakerConfig()
{
RepeatCount = 5,
Overrides =
[
new MultiStringOverride(),
new ObjectWithIdOverride(),
new OrderableOverride(),
]
});

public EntrySyncTests(SyncFixture fixture)
{
_fixture = fixture;
Expand All @@ -22,10 +32,18 @@ public async Task CanSyncRandomEntries()
{
var createdEntry = await _fixture.CrdtApi.CreateEntry(await AutoFaker.EntryReadyForCreation(_fixture.CrdtApi));
var after = await AutoFaker.EntryReadyForCreation(_fixture.CrdtApi, entryId: createdEntry.Id);
await EntrySync.Sync(after, createdEntry, _fixture.CrdtApi);

after.Senses = [.. AutoFaker.Faker.Random.Shuffle([
// copy some senses over, so moves happen
..AutoFaker.Faker.Random.ListItems(createdEntry.Senses),
..after.Senses
])];

await EntrySync.Sync(createdEntry, after, _fixture.CrdtApi);
var actual = await _fixture.CrdtApi.GetEntry(after.Id);
actual.Should().NotBeNull();
actual.Should().BeEquivalentTo(after, options => options);
actual.Should().BeEquivalentTo(after, options => options
.For(e => e.Senses).Exclude(s => s.Order));
}

[Fact]
Expand Down Expand Up @@ -53,7 +71,7 @@ public async Task CanChangeComplexFormVisSync_Components()
after.Components[0].ComponentEntryId = component2.Id;
after.Components[0].ComponentHeadword = component2.Headword();

await EntrySync.Sync(after, complexForm, _fixture.CrdtApi);
await EntrySync.Sync(complexForm, after, _fixture.CrdtApi);

var actual = await _fixture.CrdtApi.GetEntry(after.Id);
actual.Should().NotBeNull();
Expand Down Expand Up @@ -85,7 +103,7 @@ public async Task CanChangeComplexFormViaSync_ComplexForms()
after.ComplexForms[0].ComplexFormEntryId = complexForm2.Id;
after.ComplexForms[0].ComplexFormHeadword = complexForm2.Headword();

await EntrySync.Sync(after, component, _fixture.CrdtApi);
await EntrySync.Sync(component, after, _fixture.CrdtApi);

var actual = await _fixture.CrdtApi.GetEntry(after.Id);
actual.Should().NotBeNull();
Expand All @@ -99,7 +117,7 @@ public async Task CanChangeComplexFormTypeViaSync()
var entry = await _fixture.CrdtApi.CreateEntry(new() { LexemeForm = { { "en", "complexForm1" } } });
var after = (Entry) entry.Copy();
after.ComplexFormTypes = [complexFormType];
await EntrySync.Sync(after, entry, _fixture.CrdtApi);
await EntrySync.Sync(entry, after, _fixture.CrdtApi);

var actual = await _fixture.CrdtApi.GetEntry(after.Id);
actual.Should().NotBeNull();
Expand Down
20 changes: 17 additions & 3 deletions backend/FwLite/FwLiteProjectSync.Tests/Fixtures/SyncFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,44 @@ public class SyncFixture : IAsyncLifetime
_services.ServiceProvider.GetRequiredService<CrdtFwdataProjectSyncService>();
public IServiceProvider Services => _services.ServiceProvider;
private readonly string _projectName;
private readonly string _projectFolder;
private readonly IDisposable _cleanup;
private static readonly Lock _preCleanupLock = new();
private static readonly HashSet<string> _preCleanupDone = [];

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

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

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

public async Task InitializeAsync()
{
lock (_preCleanupLock)
{
if (!_preCleanupDone.Contains(_projectFolder))
{
_preCleanupDone.Add(_projectFolder);
if (Path.Exists(_projectFolder))
{
Directory.Delete(_projectFolder, true);
}
}
}

var projectsFolder = _services.ServiceProvider.GetRequiredService<IOptions<FwDataBridgeConfig>>().Value
.ProjectsFolder;
if (Path.Exists(projectsFolder)) Directory.Delete(projectsFolder, true);
Directory.CreateDirectory(projectsFolder);
var fwDataProject = new FwDataProject(_projectName, projectsFolder);
_services.ServiceProvider.GetRequiredService<IProjectLoader>()
Expand All @@ -48,7 +63,6 @@ public async Task InitializeAsync()

var crdtProjectsFolder =
_services.ServiceProvider.GetRequiredService<IOptions<LcmCrdtConfig>>().Value.ProjectPath;
if (Path.Exists(crdtProjectsFolder)) Directory.Delete(crdtProjectsFolder, true);
Directory.CreateDirectory(crdtProjectsFolder);
var crdtProject = await _services.ServiceProvider.GetRequiredService<CrdtProjectsService>()
.CreateProject(new(_projectName, FwProjectId: FwDataApi.ProjectId, SeedNewProjectData: false));
Expand Down
3 changes: 2 additions & 1 deletion backend/FwLite/FwLiteProjectSync.Tests/Sena3SyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ private void ShouldAllBeEquivalentTo(Dictionary<Guid, Entry> crdtEntries, Dictio
crdtEntry.Should().BeEquivalentTo(fwdataEntry,
options => options
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id),
.For(e => e.ComplexForms).Exclude(c => c.Id)
.For(e => e.Senses).Exclude(s => s.Order),
$"CRDT entry {crdtEntry.Id} was synced with FwData");
}
}
Expand Down
25 changes: 19 additions & 6 deletions backend/FwLite/FwLiteProjectSync.Tests/SyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,9 @@ public async Task FirstSyncJustDoesAnImport()
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)
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
}

Expand Down Expand Up @@ -145,7 +147,9 @@ await crdtApi.CreateEntry(new Entry()
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)
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
}

Expand Down Expand Up @@ -223,7 +227,9 @@ public async Task CreatingAComplexEntryInFwDataSyncsWithoutIssue()
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)
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));

// Sync again, ensure no problems or changes
Expand Down Expand Up @@ -305,7 +311,9 @@ await crdtApi.CreateEntry(new Entry()
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)
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
}

Expand Down Expand Up @@ -384,7 +392,9 @@ await crdtApi.CreateEntry(new Entry()
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)
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
}

Expand All @@ -408,6 +418,7 @@ public async Task UpdatingAnEntryInEachProjectSyncsAcrossBoth()
var fwdataEntries = await fwdataApi.GetAllEntries().ToArrayAsync();
crdtEntries.Should().BeEquivalentTo(fwdataEntries,
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
//todo the headword should be changed
.For(e => e.Components).Exclude(c => c.ComponentHeadword)
Expand Down Expand Up @@ -475,7 +486,9 @@ public async Task AddingASenseToAnEntryInEachProjectSyncsAcrossBoth()
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)
options => options
.For(e => e.Senses).Exclude(s => s.Order)
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.ComplexForms).Exclude(c => c.Id));
}

Expand Down
Loading

0 comments on commit 68e5f37

Please sign in to comment.