Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
37 changes: 37 additions & 0 deletions backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,4 +224,41 @@ public async Task CanSyncNewEntryReferencedByExistingEntry()
.For(e => e.Components).Exclude(c => c.Id)
.For(e => e.Components).Exclude(c => c.Order));
}

[Fact]
public async Task CanSyncNewComplexFormComponentReferencingNewSense()
{
// arrange
// - before
var complexFormEntryBefore = await Api.CreateEntry(new() { LexemeForm = { { "en", "complex-form" } } });
var componentEntryBefore = await Api.CreateEntry(new() { LexemeForm = { { "en", "component" } } });

// - after
var complexFormEntryAfter = complexFormEntryBefore.Copy();
var componentEntryAfter = componentEntryBefore.Copy();
var senseId = Guid.NewGuid();
componentEntryAfter.Senses = [new Sense() { Id = senseId, EntryId = componentEntryAfter.Id }];

var component = ComplexFormComponent.FromEntries(complexFormEntryAfter, componentEntryAfter, senseId);
complexFormEntryAfter.Components.Add(component);
componentEntryAfter.ComplexForms.Add(component);

// act
await EntrySync.Sync(
// note: the entry with the added sense is at the end of the list
[complexFormEntryBefore, componentEntryBefore],
[complexFormEntryAfter, componentEntryAfter],
Api);

// assert
var actualComplexFormEntry = await Api.GetEntry(complexFormEntryAfter.Id);
actualComplexFormEntry.Should().BeEquivalentTo(complexFormEntryAfter,
options => SyncTests.SyncExclusions(options)
.Excluding(e => e.ComplexFormTypes) // LibLcm automatically creates a complex form type. Should we?
.WithStrictOrdering());

var actualComponentEntry = await Api.GetEntry(componentEntryAfter.Id);
actualComponentEntry.Should().BeEquivalentTo(componentEntryAfter,
options => SyncTests.SyncExclusions(options).WithStrictOrdering());
}
}
27 changes: 18 additions & 9 deletions backend/FwLite/MiniLcm.Tests/DiffCollectionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ public async Task Diff_AddThenUpdate_CallsAddForNewRecords()
var entry = new Entry(Guid.NewGuid(), "test");
await DiffCollection.DiffAddThenUpdate([], [entry], _fakeApi);
_fakeApi.VerifyCalls(
new FakeDiffApi.MethodCall(entry, nameof(FakeDiffApi.AddAndGet)),
new FakeDiffApi.MethodCall(entry, nameof(FakeDiffApi.AddWithoutReferencesAndGet)),
new FakeDiffApi.MethodCall((entry, entry), nameof(FakeDiffApi.Replace))
);
}
Expand All @@ -250,22 +250,25 @@ public async Task DiffAddThenUpdate_CallsReplaceForMatchingRecords()
var entry = new Entry(Guid.NewGuid(), "test");
var updated = entry with { Word = "new" };
await DiffCollection.DiffAddThenUpdate([entry], [updated], _fakeApi);
_fakeApi.VerifyCalls(new FakeDiffApi.MethodCall((entry, updated), nameof(FakeDiffApi.Replace)));
_fakeApi.VerifyCalls(
new FakeDiffApi.MethodCall((entry, updated), nameof(FakeDiffApi.ReplaceWithoutReferencesAndGet)),
new FakeDiffApi.MethodCall((updated, updated), nameof(FakeDiffApi.Replace)));
}

[Fact]
public async Task DiffAddThenUpdate_AddAlwaysBeforeReplace()
public async Task DiffAddThenUpdate_WithoutReferencesAlwaysFirst()
{
var newEntry = new Entry(Guid.NewGuid(), "new");
var oldEntry = new Entry(Guid.NewGuid(), "test");
var updated = oldEntry with { Word = "new" };
await DiffCollection.DiffAddThenUpdate([oldEntry], [updated, newEntry], _fakeApi);
//this order is required because the new entry must be created before the updated entry is modified.
//the updated entry might reference the newEntry and so must be updated after the new entry is created.
//the order that the replace calls are made is unimportant.
//this order is required because new entries (and/or new senses etc.) must be created first
//updated entries might reference the newEntry (or a new sense) and so must be updated after the new entry is created.
//the order of the "simple" Replace calls is unimportant.
_fakeApi.VerifyCalls(
new FakeDiffApi.MethodCall(newEntry, nameof(FakeDiffApi.AddAndGet)),
new FakeDiffApi.MethodCall((oldEntry, updated), nameof(FakeDiffApi.Replace)),
new FakeDiffApi.MethodCall((oldEntry, updated), nameof(FakeDiffApi.ReplaceWithoutReferencesAndGet)),
new FakeDiffApi.MethodCall(newEntry, nameof(FakeDiffApi.AddWithoutReferencesAndGet)),
new FakeDiffApi.MethodCall((updated, updated), nameof(FakeDiffApi.Replace)),
new FakeDiffApi.MethodCall((newEntry, newEntry), nameof(FakeDiffApi.Replace))
);
}
Expand Down Expand Up @@ -293,12 +296,18 @@ public override Task<int> Replace(Entry before, Entry after)
return Task.FromResult(1);
}

public override Task<(int, Entry)> AddAndGet(Entry value)
public override Task<(int, Entry)> AddWithoutReferencesAndGet(Entry value)
{
Calls.Add(new(value));
return Task.FromResult((1, value));
}

public override Task<(int, Entry)> ReplaceWithoutReferencesAndGet(Entry before, Entry after)
{
Calls.Add(new((before, after)));
return Task.FromResult((1, after));
}

public override Guid GetId(Entry value)
{
return value.Id;
Expand Down
5 changes: 5 additions & 0 deletions backend/FwLite/MiniLcm/Models/Entry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ public Entry WithoutEntryRefs()
{
return this with { Components = [], ComplexForms = [] };
}

public Entry WithEntryRefsFrom(Entry from)
{
return this with { Components = from.Components, ComplexForms = from.ComplexForms };
}
}

public class Variants
Expand Down
17 changes: 13 additions & 4 deletions backend/FwLite/MiniLcm/SyncHelpers/DiffCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,16 @@ namespace MiniLcm.SyncHelpers;
public abstract class CollectionDiffApi<T, TId> where TId : notnull
{
public abstract Task<int> Add(T value);
public virtual async Task<(int, T)> AddAndGet(T value)
public virtual async Task<(int, T)> AddWithoutReferencesAndGet(T value)
{
var changes = await Add(value);
return (changes, value);
}
public virtual async Task<(int, T)> ReplaceWithoutReferencesAndGet(T before, T after)
{
var changes = await Replace(before, after);
return (changes, after);
}
public abstract Task<int> Remove(T value);
public abstract Task<int> Replace(T before, T after);
public abstract TId GetId(T value);
Expand Down Expand Up @@ -60,13 +65,17 @@ public static async Task<int> DiffAddThenUpdate<T, TId>(
{
if (beforeEntriesDict.Remove(diffApi.GetId(afterEntry), out var beforeEntry))
{
postAddUpdates.Add((beforeEntry, afterEntry)); // defer updating existing entry
// ensure all children that might be referenced are created
var (changed, replacedEntry) = await diffApi.ReplaceWithoutReferencesAndGet(beforeEntry, afterEntry);
changes += changed;
postAddUpdates.Add((replacedEntry, afterEntry)); // defer full update
}
else
{
var (change, created) = await diffApi.AddAndGet(afterEntry); // create new entry
// create new entry with children that might be referenced
var (change, created) = await diffApi.AddWithoutReferencesAndGet(afterEntry);
changes += change;
postAddUpdates.Add((created, afterEntry)); // defer updating new entry
postAddUpdates.Add((created, afterEntry)); // defer full update
}
}

Expand Down
16 changes: 14 additions & 2 deletions backend/FwLite/MiniLcm/SyncHelpers/EntrySync.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,15 +97,27 @@ private static async Task<int> SensesSync(Guid entryId,

private class EntriesDiffApi(IMiniLcmApi api) : ObjectWithIdCollectionDiffApi<Entry>
{
public override async Task<(int, Entry)> AddAndGet(Entry afterEntry)
public override async Task<(int, Entry)> AddWithoutReferencesAndGet(Entry afterEntry)
{
//create each entry without components.
//create each entry (and its hierarchy: senses, example sentence etc.) in isolation (e.g. without components)
//After each entry is created, then replace will be called to create those components
var entryWithoutEntryRefs = afterEntry.WithoutEntryRefs();
var changes = await Add(entryWithoutEntryRefs);
return (changes, entryWithoutEntryRefs);
}

public override async Task<(int, Entry)> ReplaceWithoutReferencesAndGet(Entry beforeEntry, Entry afterEntry)
{
//same as AddAndGet, but for already existing entries, because they
//might have new entities (e.g. senses) in their hierarchy that other entries reference
var beforeEntryWithoutEntryRefs = beforeEntry.WithoutEntryRefs();
var afterEntryWithoutEntryRefs = afterEntry.WithoutEntryRefs();
var changes = await Sync(beforeEntryWithoutEntryRefs, afterEntryWithoutEntryRefs, api);
//We've synced everything except the refs
var updatedBeforeEntry = afterEntry.WithEntryRefsFrom(beforeEntry);
return (changes, updatedBeforeEntry);
}

public override async Task<int> Add(Entry afterEntry)
{
await api.CreateEntry(afterEntry);
Expand Down
Loading