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

Add test for syncing complex forms twice #1256

Merged
merged 32 commits into from
Nov 28, 2024
Merged
Show file tree
Hide file tree
Changes from 25 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
452120a
Add test that shows bug and also add bugfix
rmunn Nov 19, 2024
622856d
Attempt to create unit test to repro sena-3 issue
rmunn Nov 19, 2024
cfcabd4
write failing test
hahn-kev Nov 22, 2024
429b74b
change Entry.Sync to create entries first without complex forms, then…
hahn-kev Nov 22, 2024
ceb7ade
Add integration test for syncing sena-3
rmunn Nov 22, 2024
01e27b3
get sena 3 sync tests working
hahn-kev Nov 22, 2024
ff49919
prevent duplicating complex forms which are the same/already exist
hahn-kev Nov 25, 2024
eba81b9
catch and throw some exceptions with additional context to help debug…
hahn-kev Nov 25, 2024
4e6dacd
add method to SyncService to expose snapshot path for testing
hahn-kev Nov 25, 2024
98f38d4
move CanCreateAComplexFormAndItsComponentInOneSync out of EntrySyncTe…
hahn-kev Nov 25, 2024
1e56414
expose a dry run sync method which returns the dry run records for in…
hahn-kev Nov 25, 2024
373a5ec
dont track EF queries in miniLcm, should fix weird sync test issues w…
hahn-kev Nov 25, 2024
aab1366
don't seed crdt db when creating to avoid sync issues on second sync
hahn-kev Nov 25, 2024
3bc17fc
change test project vernacular ws and change how ComplexFormComponent…
hahn-kev Nov 25, 2024
a253a8d
fix test failing due to no complex form types existing
hahn-kev Nov 25, 2024
c023425
Merge branch 'chore/fix-sync-entry-reference-ordering' into bug/sync-…
hahn-kev Nov 25, 2024
cc38fb9
remove conflicting test
hahn-kev Nov 26, 2024
1f7d1ab
avoid throwing an error when removing complex form types if the type …
hahn-kev Nov 26, 2024
0db9794
fixed bulk import of semantic domains would not import the predefined…
hahn-kev Nov 26, 2024
68b403f
ensure root service provider is cleaned up in fixtures to avoid issue…
hahn-kev Nov 26, 2024
75bf7a7
ensure headword is stable
hahn-kev Nov 26, 2024
58fc9a4
flatten Complex Form types from FW so we import all of them, not just…
hahn-kev Nov 26, 2024
b501099
ensure that all entries are created before complex forms during impor…
hahn-kev Nov 26, 2024
10b0994
improve how entry assertions are executed to make it simpler to find …
hahn-kev Nov 26, 2024
d275bd2
use the entry code for headwords when creating complex form components
hahn-kev Nov 26, 2024
de1ae59
Merge branch 'develop' into bug/sync-crdt-twice-should-not-throw
hahn-kev Nov 26, 2024
977d2e9
fix broken build due to duplicate type mismatch
hahn-kev Nov 27, 2024
75c5a5f
change error message to follow the expected pattern
hahn-kev Nov 28, 2024
36ca09a
add a number of DryRun tests
hahn-kev Nov 28, 2024
de9f95f
don't try to download sena3 from lexbox, just download the zip from g…
hahn-kev Nov 28, 2024
c3f71aa
isolate sena3 sync tests rather than using the same project for each …
hahn-kev Nov 28, 2024
e00fcfc
use HgRunner instead of trying to locate mercurial manually
hahn-kev Nov 28, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ namespace FwDataMiniLcmBridge.Tests.Fixtures;

public static class FwDataTestsKernel
{
public static IServiceCollection AddTestFwDataBridge(this IServiceCollection services)
public static IServiceCollection AddTestFwDataBridge(this IServiceCollection services, bool mockProjectLoader = true)
{
services.AddFwDataBridge();
services.AddSingleton<IConfiguration>(_ => new ConfigurationRoot([]));
services.AddSingleton<MockFwProjectLoader>();
services.AddSingleton<IProjectLoader>(sp => sp.GetRequiredService<MockFwProjectLoader>());
services.AddSingleton<FieldWorksProjectList, MockFwProjectList>();
if (mockProjectLoader)
{
services.AddSingleton<MockFwProjectLoader>();
services.AddSingleton<IProjectLoader>(sp => sp.GetRequiredService<MockFwProjectLoader>());
services.AddSingleton<FieldWorksProjectList, MockFwProjectList>();
}
return services;
}
}
106 changes: 62 additions & 44 deletions backend/FwLite/FwDataMiniLcmBridge/Api/FwDataMiniLcmApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using FwDataMiniLcmBridge.LcmUtils;
using Microsoft.Extensions.Logging;
using MiniLcm;
using MiniLcm.Exceptions;
using MiniLcm.Models;
using MiniLcm.SyncHelpers;
using SIL.LCModel;
Expand Down Expand Up @@ -37,6 +38,8 @@ public class FwDataMiniLcmApi(Lazy<LcmCache> cacheLazy, bool onCloseSave, ILogge
private ICmTranslationFactory CmTranslationFactory => Cache.ServiceLocator.GetInstance<ICmTranslationFactory>();
private ICmPossibilityRepository CmPossibilityRepository => Cache.ServiceLocator.GetInstance<ICmPossibilityRepository>();
private ICmPossibilityList ComplexFormTypes => Cache.LangProject.LexDbOA.ComplexEntryTypesOA;
private IEnumerable<ILexEntryType> ComplexFormTypesFlattened => ComplexFormTypes.PossibilitiesOS.Cast<ILexEntryType>().Flatten();

private ICmPossibilityList VariantTypes => Cache.LangProject.LexDbOA.VariantEntryTypesOA;

public void Dispose()
Expand Down Expand Up @@ -326,12 +329,9 @@ public Task DeleteSemanticDomain(Guid id)

public IAsyncEnumerable<ComplexFormType> GetComplexFormTypes()
{
return ComplexFormTypes.PossibilitiesOS
.Select(ToComplexFormType)
.ToAsyncEnumerable();
return ComplexFormTypesFlattened.Select(ToComplexFormType).ToAsyncEnumerable();
}

private ComplexFormType ToComplexFormType(ICmPossibility t)
private ComplexFormType ToComplexFormType(ILexEntryType t)
{
return new ComplexFormType() { Id = t.Guid, Name = FromLcmMultiString(t.Name) };
}
Expand All @@ -350,7 +350,7 @@ public Task<ComplexFormType> CreateComplexFormType(ComplexFormType complexFormTy
ComplexFormTypes.PossibilitiesOS.Add(lexComplexFormType);
UpdateLcmMultiString(lexComplexFormType.Name, complexFormType.Name);
});
return Task.FromResult(ToComplexFormType(ComplexFormTypes.PossibilitiesOS.Single(c => c.Guid == complexFormType.Id)));
return Task.FromResult(ToComplexFormType(ComplexFormTypesFlattened.Single(c => c.Guid == complexFormType.Id)));
}

public IAsyncEnumerable<VariantType> GetVariantTypes()
Expand Down Expand Up @@ -390,6 +390,15 @@ private Entry FromLexEntry(ILexEntry entry)
};
}

private string LexEntryHeadword(ILexEntry entry)
{
return new Entry()
{
LexemeForm = FromLcmMultiString(entry.LexemeFormOA.Form),
CitationForm = FromLcmMultiString(entry.CitationForm),
}.Headword();
}

private IList<ComplexFormType> ToComplexFormTypes(ILexEntry entry)
{
return entry.ComplexFormEntryRefs.SingleOrDefault()
Expand Down Expand Up @@ -438,9 +447,9 @@ private ComplexFormComponent ToEntryReference(ILexEntry component, ILexEntry com
return new ComplexFormComponent
{
ComponentEntryId = component.Guid,
ComponentHeadword = component.HeadWord.Text,
ComponentHeadword = LexEntryHeadword(component),
ComplexFormEntryId = complexEntry.Guid,
ComplexFormHeadword = complexEntry.HeadWord.Text
ComplexFormHeadword = LexEntryHeadword(complexEntry)
};
}

Expand All @@ -452,7 +461,7 @@ private ComplexFormComponent ToSenseReference(ILexSense componentSense, ILexEntr
ComponentSenseId = componentSense.Guid,
ComponentHeadword = componentSense.Entry.HeadWord.Text,
ComplexFormEntryId = complexEntry.Guid,
ComplexFormHeadword = complexEntry.HeadWord.Text
ComplexFormHeadword = LexEntryHeadword(complexEntry)
};
}

Expand Down Expand Up @@ -559,40 +568,48 @@ public IAsyncEnumerable<Entry> SearchEntries(string query, QueryOptions? options
public async Task<Entry> CreateEntry(Entry entry)
{
entry.Id = entry.Id == default ? Guid.NewGuid() : entry.Id;
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Entry",
"Remove entry",
Cache.ServiceLocator.ActionHandler,
() =>
{
var lexEntry = LexEntryFactory.Create(entry.Id, Cache.ServiceLocator.GetInstance<ILangProjectRepository>().Singleton.LexDbOA);
lexEntry.LexemeFormOA = Cache.ServiceLocator.GetInstance<IMoStemAllomorphFactory>().Create();
UpdateLcmMultiString(lexEntry.LexemeFormOA.Form, entry.LexemeForm);
UpdateLcmMultiString(lexEntry.CitationForm, entry.CitationForm);
UpdateLcmMultiString(lexEntry.LiteralMeaning, entry.LiteralMeaning);
UpdateLcmMultiString(lexEntry.Comment, entry.Note);

foreach (var sense in entry.Senses)
{
CreateSense(lexEntry, sense);
}

//form types should be created before components, otherwise the form type "unspecified" will be added
foreach (var complexFormType in entry.ComplexFormTypes)
{
AddComplexFormType(lexEntry, complexFormType.Id);
}

foreach (var component in entry.Components)
{
AddComplexFormComponent(lexEntry, component);
}

foreach (var complexForm in entry.ComplexForms)
try
{
UndoableUnitOfWorkHelper.DoUsingNewOrCurrentUOW("Create Entry",
"Remove entry",
Cache.ServiceLocator.ActionHandler,
() =>
{
var complexLexEntry = EntriesRepository.GetObject(complexForm.ComplexFormEntryId);
AddComplexFormComponent(complexLexEntry, complexForm);
}
});
var lexEntry = LexEntryFactory.Create(entry.Id,
Cache.ServiceLocator.GetInstance<ILangProjectRepository>().Singleton.LexDbOA);
lexEntry.LexemeFormOA = Cache.ServiceLocator.GetInstance<IMoStemAllomorphFactory>().Create();
UpdateLcmMultiString(lexEntry.LexemeFormOA.Form, entry.LexemeForm);
UpdateLcmMultiString(lexEntry.CitationForm, entry.CitationForm);
UpdateLcmMultiString(lexEntry.LiteralMeaning, entry.LiteralMeaning);
UpdateLcmMultiString(lexEntry.Comment, entry.Note);

foreach (var sense in entry.Senses)
{
CreateSense(lexEntry, sense);
}

//form types should be created before components, otherwise the form type "unspecified" will be added
foreach (var complexFormType in entry.ComplexFormTypes)
{
AddComplexFormType(lexEntry, complexFormType.Id);
}

foreach (var component in entry.Components)
{
AddComplexFormComponent(lexEntry, component);
}

foreach (var complexForm in entry.ComplexForms)
{
var complexLexEntry = EntriesRepository.GetObject(complexForm.ComplexFormEntryId);
AddComplexFormComponent(complexLexEntry, complexForm);
}
});
}
catch (Exception e)
{
throw new CreateObjectException($"Failed to create entry {entry}", e);
}

return await GetEntry(entry.Id) ?? throw new InvalidOperationException("Entry was not created");
}
Expand Down Expand Up @@ -693,15 +710,16 @@ internal void AddComplexFormType(ILexEntry lexEntry, Guid complexFormTypeId)
entryRef.HideMinorEntry = 0;
}

var lexEntryType = (ILexEntryType)ComplexFormTypes.PossibilitiesOS.Single(c => c.Guid == complexFormTypeId);
var lexEntryType = ComplexFormTypesFlattened.Single(c => c.Guid == complexFormTypeId);
entryRef.ComplexEntryTypesRS.Add(lexEntryType);
}

internal void RemoveComplexFormType(ILexEntry lexEntry, Guid complexFormTypeId)
{
ILexEntryRef? entryRef = lexEntry.ComplexFormEntryRefs.SingleOrDefault();
if (entryRef is null) return;
var lexEntryType = (ILexEntryType)ComplexFormTypes.PossibilitiesOS.Single(c => c.Guid == complexFormTypeId);
var lexEntryType = entryRef.ComplexEntryTypesRS.SingleOrDefault(c => c.Guid == complexFormTypeId);
if (lexEntryType is null) return;
entryRef.ComplexEntryTypesRS.Remove(lexEntryType);
}

Expand Down
18 changes: 18 additions & 0 deletions backend/FwLite/FwDataMiniLcmBridge/Api/PossibilityExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using SIL.LCModel;

namespace FwDataMiniLcmBridge.Api;

public static class PossibilityExtensions
{
public static IEnumerable<T> Flatten<T>(this IEnumerable<T> enumerable) where T : ICmPossibility
{
foreach (var cmPossibility in enumerable)
{
yield return cmPossibility;
foreach (var child in Flatten(cmPossibility.SubPossibilitiesOS.Cast<T>()))
{
yield return child;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace FwDataMiniLcmBridge.Api.UpdateProxy;

public class UpdateEntryProxy : Entry
public record UpdateEntryProxy : Entry
{
private readonly ILexEntry _lcmEntry;
private readonly FwDataMiniLcmApi _lexboxLcmApi;
Expand Down
2 changes: 1 addition & 1 deletion backend/FwLite/FwLiteProjectSync.Tests/EntrySyncTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ public async Task CanChangeComplexFormViaSync_ComplexForms()
[Fact]
public async Task CanChangeComplexFormTypeViaSync()
{
var complexFormType = await _fixture.CrdtApi.CreateComplexFormType(new() { Name = new() { { "en", "complexFormType" } } });
var entry = await _fixture.CrdtApi.CreateEntry(new() { LexemeForm = { { "en", "complexForm1" } } });
var complexFormType = await _fixture.CrdtApi.GetComplexFormTypes().FirstAsync();
var after = (Entry) entry.Copy();
after.ComplexFormTypes = [complexFormType];
await EntrySync.Sync(after, entry, _fixture.CrdtApi);
Expand Down
14 changes: 14 additions & 0 deletions backend/FwLite/FwLiteProjectSync.Tests/Fixtures/LexboxConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using System.ComponentModel.DataAnnotations;

namespace FwLiteProjectSync.Tests.Fixtures;

public class LexboxConfig
{
[Required, Url, RegularExpression(@"^.+/$", ErrorMessage = "Must end with '/'")]
public required string LexboxUrl { get; set; }
public string HgWebUrl => $"{LexboxUrl}hg/";
[Required]
public required string LexboxUsername { get; set; }
[Required]
public required string LexboxPassword { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using SIL.CommandLineProcessing;
using SIL.PlatformUtilities;
using SIL.Progress;

namespace FwLiteProjectSync.Tests.Fixtures;

public static class MercurialTestHelper
{
public static string HgCommand =>
Path.Combine("Mercurial", Platform.IsWindows ? "hg.exe" : "hg");

private static string RunHgCommand(string repoPath, string args)
{
var result = CommandLineRunner.Run(HgCommand, args, repoPath, 120, new NullProgress());
if (result.ExitCode == 0) return result.StandardOutput;
throw new Exception(
$"hg {args} failed.\nStdOut: {result.StandardOutput}\nStdErr: {result.StandardError}");

}

public static void HgClean(string repoPath, string exclude)
{
RunHgCommand(repoPath, $"purge --no-confirm --exclude {exclude}");
}

public static void HgUpdate(string repoPath, string rev)
{
RunHgCommand(repoPath, $"update \"{rev}\"");
}
}

Loading