Skip to content
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
4 changes: 2 additions & 2 deletions backend/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -102,8 +102,8 @@
<PackageVersion Include="SIL.Chorus.Mercurial" Version="6.5.1.43" />
<PackageVersion Include="SIL.ChorusPlugin.LfMergeBridge" Version="4.3.0-beta0048" />
<PackageVersion Include="SIL.Core" Version="17.0.0" />
<PackageVersion Include="SIL.LCModel" Version="11.0.0-beta0162" />
<PackageVersion Include="SIL.LCModel.FixData" Version="11.0.0-beta0162" />
<PackageVersion Include="SIL.LCModel" Version="11.0.0-beta0165" />
<PackageVersion Include="SIL.LCModel.FixData" Version="11.0.0-beta0165" />
<PackageVersion Include="SIL.WritingSystems" Version="17.0.0" />
<!-- Only included so it can be excluded. See FwLiteMaui.csproj. -->
<PackageVersion Include="Mono.Unix" Version="7.1.0-final.1.21458.1" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
using FwDataMiniLcmBridge.Tests.Fixtures;
using MiniLcm.Models;

namespace FwDataMiniLcmBridge.Tests.MiniLcmTests;

// FwData always treats HN=0 as "auto-assign"
// and auto-corrects anything problematic (duplicates, out-of-range)
[Collection(ProjectLoaderFixture.Name)]
public class HomographNumberTests(ProjectLoaderFixture fixture) : HomographNumberTestsBase
{
protected override Task<IMiniLcmApi> NewApi()
{
return Task.FromResult<IMiniLcmApi>(fixture.NewProjectApi("homograph-number-test", "en", "en"));
}

[Fact]
public async Task CreateEntry_HomographNumberOutOfRange_IsClampedToMaxPlusOne()
{
const string form = "createOutOfRangeHnTest";
var a = await Api.CreateEntry(new Entry { LexemeForm = { ["en"] = form } });
var b = await Api.CreateEntry(new Entry { LexemeForm = { ["en"] = form } });
(await Api.GetEntry(a.Id))!.HomographNumber.Should().Be(1);
(await Api.GetEntry(b.Id))!.HomographNumber.Should().Be(2);

// Request HN=10 with only 2 existing homographs — should be corrected to current-max + 1 = 3.
var c = await Api.CreateEntry(new Entry { LexemeForm = { ["en"] = form }, HomographNumber = 10 });

(await Api.GetEntry(a.Id))!.HomographNumber.Should().Be(1);
(await Api.GetEntry(b.Id))!.HomographNumber.Should().Be(2);
(await Api.GetEntry(c.Id))!.HomographNumber.Should().Be(3, "out-of-range HN is clamped to max + 1");
}

[Fact]
public async Task CreateEntry_DuplicateHomographNumber_IsDeduplicated()
{
const string form = "createDuplicateHnTest";
var a = await Api.CreateEntry(new Entry { LexemeForm = { ["en"] = form } });
var b = await Api.CreateEntry(new Entry { LexemeForm = { ["en"] = form } });
var c = await Api.CreateEntry(new Entry { LexemeForm = { ["en"] = form } });
(await Api.GetEntry(a.Id))!.HomographNumber.Should().Be(1);
(await Api.GetEntry(b.Id))!.HomographNumber.Should().Be(2);
(await Api.GetEntry(c.Id))!.HomographNumber.Should().Be(3);

// Request HN=2, colliding with B.
var d = await Api.CreateEntry(new Entry { LexemeForm = { ["en"] = form }, HomographNumber = 2 });

(await Api.GetEntry(a.Id))!.HomographNumber.Should().Be(1);
(await Api.GetEntry(b.Id))!.HomographNumber.Should().Be(2, "older HN=2 keeps its slot");
(await Api.GetEntry(d.Id))!.HomographNumber.Should().Be(3, "duplicate slots in after the senior holder");
(await Api.GetEntry(c.Id))!.HomographNumber.Should().Be(4, "C is shoved up to keep the sequence valid");
}

[Fact]
public async Task UpdateEntry_HomographNumberOutOfRange_IsClampedBackIntoRange()
{
const string form = "updateOutOfRangeHnTest";
var a = await Api.CreateEntry(new Entry { LexemeForm = { ["en"] = form } });
var b = await Api.CreateEntry(new Entry { LexemeForm = { ["en"] = form } });
var c = await Api.CreateEntry(new Entry { LexemeForm = { ["en"] = form } });
// baseline: A=1, B=2, C=3

// Update the newest (C) to an out-of-range HN; renumber sorts it back to the end.
await Api.UpdateEntry(c, c with { HomographNumber = 10 });

(await Api.GetEntry(a.Id))!.HomographNumber.Should().Be(1);
(await Api.GetEntry(b.Id))!.HomographNumber.Should().Be(2);
(await Api.GetEntry(c.Id))!.HomographNumber.Should().Be(3, "out-of-range update is clamped back into range");
}

[Fact]
public async Task UpdateEntry_HomographNumberZero_BackfillsIntoTheGap()
{
const string form = "updateZeroHnTest";
var a = await Api.CreateEntry(new Entry { LexemeForm = { ["en"] = form } });
var b = await Api.CreateEntry(new Entry { LexemeForm = { ["en"] = form } });
var c = await Api.CreateEntry(new Entry { LexemeForm = { ["en"] = form } });
// baseline: A=1, B=2, C=3

// Setting C's HN to 0 leaves a gap at 3; renumber fills C back in.
await Api.UpdateEntry(c, c with { HomographNumber = 0 });

(await Api.GetEntry(a.Id))!.HomographNumber.Should().Be(1);
(await Api.GetEntry(b.Id))!.HomographNumber.Should().Be(2);
(await Api.GetEntry(c.Id))!.HomographNumber.Should().Be(3, "HN=0 is backfilled into the gap it left behind");
}

[Fact]
public async Task UpdateEntry_DuplicateHomographNumber_IsDeduplicated()
{
const string form = "updateDuplicateHnTest";
var a = await Api.CreateEntry(new Entry { LexemeForm = { ["en"] = form } });
var b = await Api.CreateEntry(new Entry { LexemeForm = { ["en"] = form } });
var c = await Api.CreateEntry(new Entry { LexemeForm = { ["en"] = form } });
// baseline: A=1, B=2, C=3

// Update C (newest) to duplicate A's HN=1.
await Api.UpdateEntry(c, c with { HomographNumber = 1 });

(await Api.GetEntry(a.Id))!.HomographNumber.Should().Be(1, "older HN=1 keeps its slot");
(await Api.GetEntry(c.Id))!.HomographNumber.Should().Be(2, "duplicate slots in after the senior holder");
(await Api.GetEntry(b.Id))!.HomographNumber.Should().Be(3, "B is shoved up to keep the sequence valid");
}
}
3 changes: 3 additions & 0 deletions backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,7 @@ private Entry FromLexEntry(ILexEntry entry)
CitationForm = FromLcmMultiString(entry.CitationForm),
LiteralMeaning = FromLcmMultiString(entry.LiteralMeaning),
MorphType = LcmHelpers.FromLcmMorphType(entry.PrimaryMorphType), // TODO: Decide what to do about entries with *mixed* morph types
HomographNumber = entry.HomographNumber,
Senses = [.. entry.AllSenses.Select(FromLexSense)],
ComplexFormTypes = ToComplexFormTypes(entry),
Components = [.. ToComplexFormComponents(entry)],
Expand Down Expand Up @@ -989,6 +990,8 @@ public async Task<Entry> CreateEntry(Entry entry, CreateEntryOptions? options =
UpdateLcmMultiString(lexEntry.CitationForm, entry.CitationForm);
UpdateLcmMultiString(lexEntry.LiteralMeaning, entry.LiteralMeaning);
UpdateLcmMultiString(lexEntry.Comment, entry.Note);
lexEntry.HomographNumber = entry.HomographNumber;
EntriesRepository.CorrectHomographNumbers(lexEntry);

foreach (var sense in entry.Senses)
{
Expand Down
8 changes: 4 additions & 4 deletions backend/FwLite/FwDataMiniLcmBridge/Api/Sorting.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ public static IEnumerable<ILexEntry> ApplyHeadwordOrder(this IEnumerable<ILexEnt
return entries
.OrderBy(e => e.LexEntryHeadword(sortWsHandle, applyMorphTokens: false))
.ThenBy(e => e.PrimaryMorphType?.SecondaryOrder ?? stemSecondaryOrder)
// .ThenBy(e => e.HomographNumber)
.ThenBy(e => e.HomographNumber)
.ThenBy(e => e.Id.Guid);
}
else
{
return entries
.OrderByDescending(e => e.LexEntryHeadword(sortWsHandle, applyMorphTokens: false))
.ThenByDescending(e => e.PrimaryMorphType?.SecondaryOrder ?? stemSecondaryOrder)
// .ThenByDescending(e => e.HomographNumber)
.ThenByDescending(e => e.HomographNumber)
.ThenByDescending(e => e.Id.Guid);
}
}
Expand All @@ -46,7 +46,7 @@ public static IEnumerable<ILexEntry> ApplyRoughBestMatchOrder(this IEnumerable<I
.ThenBy(x => x.Headword?.Length ?? 0)
.ThenBy(x => x.Headword)
.ThenBy(x => x.Entry.PrimaryMorphType?.SecondaryOrder ?? stemSecondaryOrder)
// .ThenBy(x => x.Entry.HomographNumber)
.ThenBy(x => x.Entry.HomographNumber)
.ThenBy(x => x.Entry.Id.Guid)
.Select(x => x.Entry);
}
Expand All @@ -58,7 +58,7 @@ public static IEnumerable<ILexEntry> ApplyRoughBestMatchOrder(this IEnumerable<I
.ThenByDescending(x => x.Headword?.Length ?? 0)
.ThenByDescending(x => x.Headword)
.ThenByDescending(x => x.Entry.PrimaryMorphType?.SecondaryOrder ?? stemSecondaryOrder)
// .ThenByDescending(x => x.Entry.HomographNumber)
.ThenByDescending(x => x.Entry.HomographNumber)
.ThenByDescending(x => x.Entry.Id.Guid)
.Select(x => x.Entry);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ public override RichMultiString Note
get => new UpdateRichMultiStringProxy(_lcmEntry.Comment, _lexboxLcmApi);
set => throw new NotImplementedException();
}

public override int HomographNumber
{
get => _lcmEntry.HomographNumber;
set
{
_lcmEntry.HomographNumber = value;
_lexboxLcmApi.EntriesRepository.CorrectHomographNumbers(_lcmEntry);
}
}
}

public class UpdateMultiStringProxy(ITsMultiString multiString, FwDataMiniLcmApi lexboxLcmApi) : MultiString
Expand Down
28 changes: 20 additions & 8 deletions backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,9 @@ public async Task CanSyncRandomEntries(ApiType? roundTripApiType)
{
// does not support changing MorphType yet (see UpdateEntryProxy.MorphType)
options = options.Excluding(e => e.MorphType);
// FwData re-renumbers via ILexEntryRepository.CorrectHomographNumbers, so a randomly
// generated HomographNumber won't survive round-trip.
options = options.Excluding(e => e.HomographNumber);
}
return options;
});
Expand Down Expand Up @@ -420,6 +423,7 @@ public async Task CanSyncNewEntryReferencedByExistingEntry()
var actualNewEntry = await Api.GetEntry(newEntry.Id);
actualNewEntry.Should().BeEquivalentTo(newEntry, options => options
.Excluding(e => e.ComplexFormTypes) // LibLcm automatically creates a complex form type. Should we?
.Excluding(e => e.HomographNumber) // auto-assigned/updated by API
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.Components).Exclude(c => c.Order));
}
Expand Down Expand Up @@ -487,14 +491,18 @@ public async Task SyncWithoutComplexFormsAndComponents_CorrectlySyncsUpdatedEntr

// assert
var actualComponent = await Api.GetEntry(componentAfter.Id);
actualComponent.Should().BeEquivalentTo(componentAfter,
options => options.Excluding(e => e.ComplexForms));
actualComponent.Should().BeEquivalentTo(componentAfter, options => options
.Excluding(e => e.ComplexForms)
.Excluding(e => e.HomographNumber) // auto-assigned/updated by API
);
actualComponent.ComplexForms.Should().BeEmpty();

var actualComplexForm = await Api.GetEntry(complexForm.Id);
addedComplexForm.Should().BeEquivalentTo(actualComplexForm);
actualComplexForm.Should().BeEquivalentTo(complexForm,
options => options.Excluding(e => e.Components));
actualComplexForm.Should().BeEquivalentTo(complexForm, options => options
.Excluding(e => e.Components)
.Excluding(e => e.HomographNumber) // auto-assigned/updated by API
);
actualComplexForm.Components.Should().BeEmpty();
}

Expand Down Expand Up @@ -526,14 +534,18 @@ public async Task SyncWithoutComplexFormsAndComponents_CorrectlySyncsAddedEntrie
// assert
var actualComponent = await Api.GetEntry(component.Id);
addedComponent.Should().BeEquivalentTo(actualComponent);
actualComponent.Should().BeEquivalentTo(component,
options => options.Excluding(e => e.ComplexForms));
actualComponent.Should().BeEquivalentTo(component, options => options
.Excluding(e => e.ComplexForms)
.Excluding(e => e.HomographNumber) // auto-assigned/updated by API
);
actualComponent.ComplexForms.Should().BeEmpty();

var actualComplexForm = await Api.GetEntry(complexForm.Id);
addedComplexForm.Should().BeEquivalentTo(actualComplexForm);
actualComplexForm.Should().BeEquivalentTo(complexForm,
options => options.Excluding(e => e.Components));
actualComplexForm.Should().BeEquivalentTo(complexForm, options => options
.Excluding(e => e.Components)
.Excluding(e => e.HomographNumber) // auto-assigned/updated by API
);
actualComplexForm.Components.Should().BeEmpty();
}

Expand Down
Loading
Loading