Skip to content

Commit 85eb028

Browse files
committed
Merge remote-tracking branch 'origin/develop'
2 parents d7d531f + 4f68c28 commit 85eb028

File tree

38 files changed

+3035
-2719
lines changed

38 files changed

+3035
-2719
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ msal.cache
2323
artifacts/
2424
.vs/
2525
project-cache.json
26+
localResourcesCache/
2627

2728
#Verify
2829
*.received.*

backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -956,8 +956,9 @@ public IAsyncEnumerable<Entry> SearchEntries(string query, QueryOptions? options
956956
return Task.FromResult<Entry?>(FromLexEntry(EntriesRepository.GetObject(id)));
957957
}
958958

959-
public async Task<Entry> CreateEntry(Entry entry)
959+
public async Task<Entry> CreateEntry(Entry entry, CreateEntryOptions? options = null)
960960
{
961+
options ??= CreateEntryOptions.Everything;
961962
entry.Id = entry.Id == default ? Guid.NewGuid() : entry.Id;
962963
try
963964
{
@@ -983,15 +984,18 @@ public async Task<Entry> CreateEntry(Entry entry)
983984
AddComplexFormType(lexEntry, complexFormType.Id);
984985
}
985986

986-
foreach (var component in entry.Components)
987+
if (options.IncludeComplexFormsAndComponents)
987988
{
988-
AddComplexFormComponent(lexEntry, component);
989-
}
990-
991-
foreach (var complexForm in entry.ComplexForms)
992-
{
993-
var complexLexEntry = EntriesRepository.GetObject(complexForm.ComplexFormEntryId);
994-
AddComplexFormComponent(complexLexEntry, complexForm);
989+
foreach (var component in entry.Components)
990+
{
991+
AddComplexFormComponent(lexEntry, component);
992+
}
993+
994+
foreach (var complexForm in entry.ComplexForms)
995+
{
996+
var complexLexEntry = EntriesRepository.GetObject(complexForm.ComplexFormEntryId);
997+
AddComplexFormComponent(complexLexEntry, complexForm);
998+
}
995999
}
9961000
// Subtract entry.Publications from Publications to get the publications that the entry should not be published in
9971001
var doNotPublishIn = Publications.PossibilitiesOS.Where(p => entry.PublishIn.All(ep => ep.Id != p.Guid));
@@ -1287,7 +1291,7 @@ await Cache.DoUsingNewOrCurrentUOW("Update Entry",
12871291
"Revert entry",
12881292
async () =>
12891293
{
1290-
await EntrySync.Sync(before, after, api ?? this);
1294+
await EntrySync.SyncFull(before, after, api ?? this);
12911295
});
12921296
return await GetEntry(after.Id) ?? throw new NullReferenceException("unable to find entry with id " + after.Id);
12931297
}

backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs

Lines changed: 205 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using MiniLcm.SyncHelpers;
55
using MiniLcm.Tests.AutoFakerHelpers;
66
using Soenneker.Utils.AutoBogus;
7-
using Soenneker.Utils.AutoBogus.Config;
87

98
namespace FwLiteProjectSync.Tests;
109

@@ -29,7 +28,7 @@ public async Task CanSyncRandomEntries()
2928
..after.Senses
3029
])];
3130

32-
await EntrySync.Sync(createdEntry, after, Api);
31+
await EntrySync.SyncFull(createdEntry, after, Api);
3332
var actual = await Api.GetEntry(after.Id);
3433
actual.Should().NotBeNull();
3534
actual.Should().BeEquivalentTo(after, options => options
@@ -92,7 +91,7 @@ public async Task CanChangeComplexFormViaSync_Components()
9291
complexFormAfter.Components[0].ComponentEntryId = component2.Id;
9392
complexFormAfter.Components[0].ComponentHeadword = component2.Headword();
9493

95-
await EntrySync.Sync(complexForm, complexFormAfter, Api);
94+
await EntrySync.SyncFull(complexForm, complexFormAfter, Api);
9695

9796
var actual = await Api.GetEntry(complexFormAfter.Id);
9897
actual.Should().NotBeNull();
@@ -125,7 +124,7 @@ public async Task CanChangeComplexFormViaSync_ComplexForms()
125124
componentAter.ComplexForms[0].ComplexFormEntryId = complexForm2.Id;
126125
componentAter.ComplexForms[0].ComplexFormHeadword = complexForm2.Headword();
127126

128-
await EntrySync.Sync(component, componentAter, Api);
127+
await EntrySync.SyncFull(component, componentAter, Api);
129128

130129
var actual = await Api.GetEntry(componentAter.Id);
131130
actual.Should().NotBeNull();
@@ -140,7 +139,7 @@ public async Task CanChangeComplexFormTypeViaSync()
140139
var entry = await Api.CreateEntry(new() { LexemeForm = { { "en", "complexForm1" } } });
141140
var after = entry.Copy();
142141
after.ComplexFormTypes = [complexFormType];
143-
await EntrySync.Sync(entry, after, Api);
142+
await EntrySync.SyncFull(entry, after, Api);
144143

145144
var actual = await Api.GetEntry(after.Id);
146145
actual.Should().NotBeNull();
@@ -172,14 +171,14 @@ public async Task CanInsertComplexFormComponentViaSync(bool componentThenComplex
172171
// this results in 2 crdt changes:
173172
// (1) add complex-form (i.e. implicitly add component)
174173
// (2) move component to the right place
175-
await EntrySync.Sync([newComponentBefore, complexFormBefore], [newComponentAfter, complexFormAfter], Api);
174+
await EntrySync.SyncFull([newComponentBefore, complexFormBefore], [newComponentAfter, complexFormAfter], Api);
176175
}
177176
else
178177
{
179178
// this results in 1 crdt change:
180179
// the component is added in the right place
181180
// (adding the complex-form becomes a no-op, because it already exists and a BetweenPosition is not specified)
182-
await EntrySync.Sync([complexFormBefore, newComponentBefore], [complexFormAfter, newComponentAfter], Api);
181+
await EntrySync.SyncFull([complexFormBefore, newComponentBefore], [complexFormAfter, newComponentAfter], Api);
183182
}
184183

185184
// assert
@@ -210,7 +209,7 @@ public async Task CanSyncNewEntryReferencedByExistingEntry()
210209
newEntry.Components.Add(newComplexFormComponent);
211210

212211
// act
213-
await EntrySync.Sync([existingEntryBefore], [existingEntryAfter, newEntry], Api);
212+
await EntrySync.SyncFull([existingEntryBefore], [existingEntryAfter, newEntry], Api);
214213

215214
// assert
216215
var actualExistingEntry = await Api.GetEntry(existingEntryAfter.Id);
@@ -224,4 +223,202 @@ public async Task CanSyncNewEntryReferencedByExistingEntry()
224223
.For(e => e.Components).Exclude(c => c.Id)
225224
.For(e => e.Components).Exclude(c => c.Order));
226225
}
226+
227+
[Fact]
228+
public async Task CanSyncNewComplexFormComponentReferencingNewSense()
229+
{
230+
// arrange
231+
// - before
232+
var complexFormEntryBefore = await Api.CreateEntry(new() { LexemeForm = { { "en", "complex-form" } } });
233+
var componentEntryBefore = await Api.CreateEntry(new() { LexemeForm = { { "en", "component" } } });
234+
235+
// - after
236+
var complexFormEntryAfter = complexFormEntryBefore.Copy();
237+
var componentEntryAfter = componentEntryBefore.Copy();
238+
var senseId = Guid.NewGuid();
239+
componentEntryAfter.Senses = [new Sense() { Id = senseId, EntryId = componentEntryAfter.Id }];
240+
241+
var component = ComplexFormComponent.FromEntries(complexFormEntryAfter, componentEntryAfter, senseId);
242+
complexFormEntryAfter.Components.Add(component);
243+
componentEntryAfter.ComplexForms.Add(component);
244+
245+
// act
246+
await EntrySync.SyncFull(
247+
// note: the entry with the added sense is at the end of the list
248+
[complexFormEntryBefore, componentEntryBefore],
249+
[complexFormEntryAfter, componentEntryAfter],
250+
Api);
251+
252+
// assert
253+
var actualComplexFormEntry = await Api.GetEntry(complexFormEntryAfter.Id);
254+
actualComplexFormEntry.Should().BeEquivalentTo(complexFormEntryAfter,
255+
options => SyncTests.SyncExclusions(options)
256+
.Excluding(e => e.ComplexFormTypes) // LibLcm automatically creates a complex form type. Should we?
257+
.WithStrictOrdering());
258+
259+
var actualComponentEntry = await Api.GetEntry(componentEntryAfter.Id);
260+
actualComponentEntry.Should().BeEquivalentTo(componentEntryAfter,
261+
options => SyncTests.SyncExclusions(options).WithStrictOrdering());
262+
}
263+
264+
[Fact]
265+
public async Task SyncWithoutComplexFormsAndComponents_CorrectlySyncsUpdatedEntries()
266+
{
267+
// Arrange
268+
// - before
269+
var componentBefore = await Api.CreateEntry(new() { LexemeForm = { { "en", "component" } } });
270+
271+
// - after
272+
var componentAfter = componentBefore.Copy();
273+
componentAfter.LexemeForm["en"] = "component updated";
274+
var complexForm = new Entry()
275+
{
276+
Id = Guid.NewGuid(),
277+
LexemeForm = { { "en", "complex form" } }
278+
};
279+
var complexFormComponent = ComplexFormComponent.FromEntries(complexForm, componentAfter);
280+
componentAfter.ComplexForms.Add(complexFormComponent);
281+
complexForm.Components.Add(complexFormComponent);
282+
283+
// act
284+
var (changes, added) = await EntrySync.SyncWithoutComplexFormsAndComponents([componentBefore], [componentAfter, complexForm], Api);
285+
added.Should().HaveCount(1);
286+
var addedComplexForm = added.First();
287+
288+
// assert
289+
var actualComponent = await Api.GetEntry(componentAfter.Id);
290+
actualComponent.Should().BeEquivalentTo(componentAfter,
291+
options => options.Excluding(e => e.ComplexForms));
292+
actualComponent.ComplexForms.Should().BeEmpty();
293+
294+
var actualComplexForm = await Api.GetEntry(complexForm.Id);
295+
addedComplexForm.Should().BeEquivalentTo(actualComplexForm);
296+
actualComplexForm.Should().BeEquivalentTo(complexForm,
297+
options => options.Excluding(e => e.Components));
298+
actualComplexForm.Components.Should().BeEmpty();
299+
}
300+
301+
[Fact]
302+
public async Task SyncWithoutComplexFormsAndComponents_CorrectlySyncsAddedEntries()
303+
{
304+
// Arrange
305+
// - after
306+
var component = new Entry()
307+
{
308+
Id = Guid.NewGuid(),
309+
LexemeForm = { { "en", "component" } }
310+
};
311+
var complexForm = new Entry()
312+
{
313+
Id = Guid.NewGuid(),
314+
LexemeForm = { { "en", "complex form" } }
315+
};
316+
var complexFormComponent = ComplexFormComponent.FromEntries(complexForm, component);
317+
component.ComplexForms.Add(complexFormComponent);
318+
complexForm.Components.Add(complexFormComponent);
319+
320+
// act
321+
var (_, added) = await EntrySync.SyncWithoutComplexFormsAndComponents([], [component, complexForm], Api);
322+
added.Should().HaveCount(2);
323+
var addedComponent = added.ElementAt(0);
324+
var addedComplexForm = added.ElementAt(1);
325+
326+
// assert
327+
var actualComponent = await Api.GetEntry(component.Id);
328+
addedComponent.Should().BeEquivalentTo(actualComponent);
329+
actualComponent.Should().BeEquivalentTo(component,
330+
options => options.Excluding(e => e.ComplexForms));
331+
actualComponent.ComplexForms.Should().BeEmpty();
332+
333+
var actualComplexForm = await Api.GetEntry(complexForm.Id);
334+
addedComplexForm.Should().BeEquivalentTo(actualComplexForm);
335+
actualComplexForm.Should().BeEquivalentTo(complexForm,
336+
options => options.Excluding(e => e.Components));
337+
actualComplexForm.Components.Should().BeEmpty();
338+
}
339+
340+
[Fact]
341+
public async Task SyncComplexFormsAndComponents_CorrectlySyncsUpdatedEntries()
342+
{
343+
// Arrange
344+
// - before
345+
var componentBefore = await Api.CreateEntry(new() { LexemeForm = { { "en", "component" } } });
346+
var complexFormBefore = await Api.CreateEntry(new() { LexemeForm = { { "en", "complex form" } } });
347+
348+
// - after
349+
var componentAfter = componentBefore.Copy();
350+
componentAfter.LexemeForm["en"] = "component updated";
351+
var complexFormAfter = complexFormBefore.Copy();
352+
complexFormAfter.LexemeForm["en"] = "complex form updated";
353+
var complexFormComponent = ComplexFormComponent.FromEntries(complexFormAfter, componentAfter);
354+
componentAfter.ComplexForms.Add(complexFormComponent);
355+
complexFormAfter.Components.Add(complexFormComponent);
356+
357+
// act
358+
await EntrySync.SyncComplexFormsAndComponents([componentBefore, complexFormBefore], [componentAfter, complexFormAfter], Api);
359+
360+
// assert
361+
var actualComponent = await Api.GetEntry(componentAfter.Id);
362+
actualComponent.Should().NotBeNull();
363+
364+
// complex forms were synced
365+
actualComponent.ComplexForms.Should().NotBeEmpty();
366+
actualComponent.ComplexForms.Should().BeEquivalentTo(componentAfter.ComplexForms, options
367+
=> options.Excluding(c => c.Id)
368+
.Excluding(c => c.Order)
369+
// The lexeme-form/headword wasn't synced so it doesn't match the "after" version
370+
.Excluding(c => c.ComplexFormHeadword)
371+
.Excluding(c => c.ComponentHeadword));
372+
373+
var actualComplexForm = await Api.GetEntry(complexFormAfter.Id);
374+
actualComplexForm.Should().NotBeNull();
375+
// components were synced
376+
actualComplexForm.Components.Should().NotBeEmpty();
377+
actualComplexForm.Components.Should().BeEquivalentTo(complexFormAfter.Components, options
378+
=> options.Excluding(c => c.Id)
379+
.Excluding(c => c.Order)
380+
// The lexeme-form/headword wasn't synced so it doesn't match the "after" version
381+
.Excluding(c => c.ComplexFormHeadword)
382+
.Excluding(c => c.ComponentHeadword));
383+
384+
// Lexeme form was not synced
385+
actualComponent.LexemeForm.Should().BeEquivalentTo(componentBefore.LexemeForm);
386+
actualComponent.LexemeForm.Should().NotBeEquivalentTo(componentAfter.LexemeForm);
387+
actualComplexForm.LexemeForm.Should().BeEquivalentTo(complexFormBefore.LexemeForm);
388+
actualComplexForm.LexemeForm.Should().NotBeEquivalentTo(complexFormAfter.LexemeForm);
389+
}
390+
391+
[Fact]
392+
public async Task SyncComplexFormsAndComponents_ThrowsExceptionIfEntryNotInBefore()
393+
{
394+
// Arrange
395+
var component = new Entry() { Id = Guid.NewGuid() };
396+
var complexForm = new Entry() { Id = Guid.NewGuid() };
397+
var complexFormComponent = ComplexFormComponent.FromEntries(complexForm, component);
398+
component.ComplexForms.Add(complexFormComponent);
399+
complexForm.Components.Add(complexFormComponent);
400+
401+
// Act
402+
var act = () => EntrySync.SyncComplexFormsAndComponents([], [component, complexForm], Api);
403+
404+
// Assert
405+
await act.Should().ThrowAsync<InvalidOperationException>();
406+
}
407+
408+
[Fact]
409+
public async Task SyncComplexFormsAndComponents_ThrowsExceptionIfEntryNotInAfter()
410+
{
411+
// Arrange
412+
var component = new Entry() { Id = Guid.NewGuid() };
413+
var complexForm = new Entry() { Id = Guid.NewGuid() };
414+
var complexFormComponent = ComplexFormComponent.FromEntries(complexForm, component);
415+
component.ComplexForms.Add(complexFormComponent);
416+
complexForm.Components.Add(complexFormComponent);
417+
418+
// Act
419+
var act = () => EntrySync.SyncComplexFormsAndComponents([component, complexForm], [], Api);
420+
421+
// Assert
422+
await act.Should().ThrowAsync<InvalidOperationException>();
423+
}
227424
}

backend/FwLite/FwLiteProjectSync.Tests/Import/ResumableTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,10 +158,10 @@ Task<PartOfSpeech> IMiniLcmWriteApi.CreatePartOfSpeech(PartOfSpeech partOfSpeech
158158
return _api.CreatePartOfSpeech(partOfSpeech);
159159
}
160160

161-
Task<Entry> IMiniLcmWriteApi.CreateEntry(Entry entry)
161+
Task<Entry> IMiniLcmWriteApi.CreateEntry(Entry entry, CreateEntryOptions? options)
162162
{
163163
ResumableTests.MaybeThrowRandom(random, 0.2);
164-
return _api.CreateEntry(entry);
164+
return _api.CreateEntry(entry, options);
165165
}
166166

167167
async Task IMiniLcmWriteApi.BulkCreateEntries(IAsyncEnumerable<Entry> entries)

backend/FwLite/FwLiteProjectSync/CrdtFwdataProjectSyncService.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,10 +89,10 @@ private async Task<SyncResult> Sync(IMiniLcmApi crdtApi, IMiniLcmApi fwdataApi,
8989
fwdataChanges += await ComplexFormTypeSync.Sync(currentFwDataComplexFormTypes, await crdtApi.GetComplexFormTypes().ToArrayAsync(), fwdataApi);
9090

9191
var currentFwDataEntries = await fwdataApi.GetAllEntries().ToArrayAsync();
92-
crdtChanges += await EntrySync.Sync(projectSnapshot.Entries, currentFwDataEntries, crdtApi);
92+
crdtChanges += await EntrySync.SyncFull(projectSnapshot.Entries, currentFwDataEntries, crdtApi);
9393
LogDryRun(crdtApi, "crdt");
9494

95-
fwdataChanges += await EntrySync.Sync(currentFwDataEntries, await crdtApi.GetAllEntries().ToArrayAsync(), fwdataApi);
95+
fwdataChanges += await EntrySync.SyncFull(currentFwDataEntries, await crdtApi.GetAllEntries().ToArrayAsync(), fwdataApi);
9696
LogDryRun(fwdataApi, "fwdata");
9797

9898
//todo push crdt changes to lexbox

backend/FwLite/FwLiteProjectSync/DryRunMiniLcmApi.cs

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,10 +151,15 @@ public Task DeleteMorphTypeData(Guid id)
151151
return Task.CompletedTask;
152152
}
153153

154-
public Task<Entry> CreateEntry(Entry entry)
155-
{
156-
DryRunRecords.Add(new DryRunRecord(nameof(CreateEntry), $"Create entry {entry.Headword()}"));
157-
return Task.FromResult(entry);
154+
public Task<Entry> CreateEntry(Entry entry, CreateEntryOptions? options)
155+
{
156+
options ??= new CreateEntryOptions();
157+
DryRunRecords.Add(new DryRunRecord(nameof(CreateEntry), $"Create entry {entry.Headword()} ({options})"));
158+
// Only return what would have been persisted
159+
if (options.IncludeComplexFormsAndComponents)
160+
return Task.FromResult(entry);
161+
else
162+
return Task.FromResult(entry with { Components = [], ComplexForms = [] });
158163
}
159164

160165
public Task<Entry> UpdateEntry(Guid id, UpdateObjectInput<Entry> update)

backend/FwLite/FwLiteProjectSync/Import/ResumableImportApi.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,9 @@ private async ValueTask<Dictionary<string, object>> EnsureCached(string typeName
4545

4646
// ********** Overrides go here **********
4747

48-
async Task<Entry> IMiniLcmWriteApi.CreateEntry(Entry entry)
48+
async Task<Entry> IMiniLcmWriteApi.CreateEntry(Entry entry, CreateEntryOptions? options)
4949
{
50-
return await HasCreated(entry, _api.GetAllEntries(), () => _api.CreateEntry(entry));
50+
return await HasCreated(entry, _api.GetAllEntries(), () => _api.CreateEntry(entry, options));
5151
}
5252

5353
async Task<PartOfSpeech> IMiniLcmWriteApi.CreatePartOfSpeech(PartOfSpeech partOfSpeech)

0 commit comments

Comments
 (0)