11using System . Text ;
2+ using FwDataMiniLcmBridge . Api ;
23using FwLiteProjectSync . Tests . Fixtures ;
4+ using LcmCrdt ;
35using MiniLcm ;
46using MiniLcm . Models ;
57using MiniLcm . SyncHelpers ;
810
911namespace FwLiteProjectSync . Tests ;
1012
11- public class CrdtEntrySyncTests ( SyncFixture fixture ) : EntrySyncTestsBase ( fixture )
13+ public class CrdtEntrySyncTests ( ExtraWritingSystemsSyncFixture fixture ) : EntrySyncTestsBase ( fixture )
1214{
13- private static readonly AutoFaker AutoFaker = new ( AutoFakerDefault . Config ) ;
14-
1515 protected override IMiniLcmApi GetApi ( SyncFixture fixture )
1616 {
1717 return fixture . CrdtApi ;
1818 }
19-
20- [ Fact ]
21- public async Task CanSyncRandomEntries ( )
22- {
23- var createdEntry = await Api . CreateEntry ( await AutoFaker . EntryReadyForCreation ( Api ) ) ;
24- var after = await AutoFaker . EntryReadyForCreation ( Api , entryId : createdEntry . Id ) ;
25-
26- after . Senses = [ .. AutoFaker . Faker . Random . Shuffle ( [
27- // copy some senses over, so moves happen
28- ..AutoFaker . Faker . Random . ListItems ( createdEntry . Senses ) ,
29- ..after . Senses
30- ] ) ] ;
31-
32- await EntrySync . SyncFull ( createdEntry , after , Api ) ;
33- var actual = await Api . GetEntry ( after . Id ) ;
34- actual . Should ( ) . NotBeNull ( ) ;
35- actual . Should ( ) . BeEquivalentTo ( after , options => options
36- . For ( e => e . Senses ) . Exclude ( s => s . Order )
37- . For ( e => e . Components ) . Exclude ( c => c . Order )
38- . For ( e => e . ComplexForms ) . Exclude ( c => c . Order )
39- . For ( e => e . Senses ) . For ( s => s . ExampleSentences ) . Exclude ( e => e . Order )
40- ) ;
41- }
4219}
4320
44- public class FwDataEntrySyncTests ( SyncFixture fixture ) : EntrySyncTestsBase ( fixture )
21+ public class FwDataEntrySyncTests ( ExtraWritingSystemsSyncFixture fixture ) : EntrySyncTestsBase ( fixture )
4522{
4623 protected override IMiniLcmApi GetApi ( SyncFixture fixture )
4724 {
4825 return fixture . FwDataApi ;
4926 }
27+
28+ // this will notify us when we start syncing MorphType (if that ever happens)
29+ [ Fact ]
30+ public async Task FwDataApiDoesNotUpdateMorphType ( )
31+ {
32+ // arrange
33+ var entry = await Api . CreateEntry ( new ( )
34+ {
35+ LexemeForm = { { "en" , "morph-type-test" } } ,
36+ MorphType = MorphType . BoundStem
37+ } ) ;
38+
39+ // act
40+ var updatedEntry = entry . Copy ( ) ;
41+ updatedEntry . MorphType = MorphType . Suffix ;
42+ await EntrySync . SyncFull ( entry , updatedEntry , Api ) ;
43+
44+ // assert
45+ var actual = await Api . GetEntry ( entry . Id ) ;
46+ actual . Should ( ) . NotBeNull ( ) ;
47+ actual . MorphType . Should ( ) . Be ( MorphType . BoundStem ) ;
48+ }
5049}
5150
52- public abstract class EntrySyncTestsBase ( SyncFixture fixture ) : IClassFixture < SyncFixture > , IAsyncLifetime
51+ public abstract class EntrySyncTestsBase ( ExtraWritingSystemsSyncFixture fixture ) : IClassFixture < ExtraWritingSystemsSyncFixture > , IAsyncLifetime
5352{
54- public async Task InitializeAsync ( )
53+ public Task InitializeAsync ( )
5554 {
56- await _fixture . EnsureDefaultVernacularWritingSystemExistsInCrdt ( ) ;
5755 Api = GetApi ( _fixture ) ;
56+ return Task . CompletedTask ;
5857 }
5958
6059 public Task DisposeAsync ( )
@@ -67,6 +66,172 @@ public Task DisposeAsync()
6766 private readonly SyncFixture _fixture = fixture ;
6867 protected IMiniLcmApi Api = null ! ;
6968
69+ private static readonly AutoFaker AutoFaker = new ( AutoFakerDefault . MakeConfig (
70+ ExtraWritingSystemsSyncFixture . VernacularWritingSystems ) ) ;
71+
72+ public enum ApiType
73+ {
74+ Crdt ,
75+ FwData
76+ }
77+
78+ // The round-tripping api is not what is under test here. It's purely for preprocessing.
79+ // It's so that that the data under test is being read from a real API (i.e. fwdata or crdt)
80+ // and thus reflects whatever nuances that API may have.
81+ //
82+ // Not all of the test cases are realistic, but they should all work and they reflect the idea
83+ // that "any MiniLcmApi implementation should be compatible with any other implementation".
84+ // Even the unrealistic test cases could potentially expose unexpected, undesirable nuances in API behaviour.
85+ // They also reflect the diversity of pipelines real entries might go through.
86+ // For example, a currently real scenario is that "after" is read from fwdata and "before" is read from crdt
87+ // and then round-tripped through a json file.
88+ // That case is not explicitly covered here.
89+ //
90+ // The most critical test cases are:
91+ // Api == CrdtApi and RoundTripApi == FwDataApi
92+ // Api == FwDataApi and RoundTripApi == CrdtApi
93+ // (though, as noted above, this case doesn't perfectly reflect real usage)
94+ [ Theory ]
95+ [ InlineData ( ApiType . Crdt ) ]
96+ [ InlineData ( ApiType . FwData ) ]
97+ [ InlineData ( null ) ]
98+ public async Task CanSyncRandomEntries ( ApiType ? roundTripApiType )
99+ {
100+ // arrange
101+ var currentApiType = Api switch
102+ {
103+ FwDataMiniLcmApi => ApiType . FwData ,
104+ CrdtMiniLcmApi => ApiType . Crdt ,
105+ // This works now, because we're not currently wrapping Api,
106+ // but if we ever do, then we want this to throw, so we know we need to detect the api differently.
107+ _ => throw new InvalidOperationException ( "Unknown API type" )
108+ } ;
109+
110+ IMiniLcmApi ? roundTripApi = roundTripApiType switch
111+ {
112+ ApiType . Crdt => _fixture . CrdtApi ,
113+ ApiType . FwData => _fixture . FwDataApi ,
114+ _ => null
115+ } ;
116+
117+ var before = AutoFaker . Generate < Entry > ( ) ;
118+ var after = AutoFaker . Generate < Entry > ( ) ;
119+ after . Id = before . Id ;
120+
121+ // We have to "prepare" while before and after have no overlap (i.e. before we start mixing parts of before into after),
122+ // otherwise "PrepareToCreateEntry" would fail due to trying to create duplicate related entities.
123+ // After this we can't ADD anything to after that has dependencies
124+ // e.g. ExampleSentences are fine, because they're owned/part of an entry.
125+ // Parts of speech, on the other hand, are not owned by an entry.
126+ await Api . PrepareToCreateEntry ( before ) ;
127+ await Api . PrepareToCreateEntry ( after ) ;
128+
129+ if ( roundTripApi is not null && currentApiType != roundTripApiType )
130+ {
131+ await roundTripApi . PrepareToCreateEntry ( before ) ;
132+ await roundTripApi . PrepareToCreateEntry ( after ) ;
133+ }
134+
135+ // keep some old senses, remove others
136+ var someRandomBeforeSenses = AutoFaker . Faker . Random . ListItems ( before . Senses ) . Select ( createdSense =>
137+ {
138+ var copy = createdSense . Copy ( ) ;
139+ copy . ExampleSentences = [
140+ // shuffle to cause moves
141+ ..AutoFaker . Faker . Random . Shuffle ( [
142+ // keep some, remove others
143+ ..AutoFaker . Faker . Random . ListItems ( copy . ExampleSentences ) ,
144+ // add new
145+ AutoFaker . ExampleSentence ( copy ) ,
146+ AutoFaker . ExampleSentence ( copy ) ,
147+ ] ) ,
148+ ] ;
149+ return copy ;
150+ } ) ;
151+ // keep new, and shuffle to cause moves
152+ after . Senses = [ .. AutoFaker . Faker . Random . Shuffle ( [ .. someRandomBeforeSenses , .. after . Senses ] ) ] ;
153+
154+ after . ComplexForms = [
155+ // shuffle to cause moves
156+ ..AutoFaker . Faker . Random . Shuffle ( [
157+ // keep some, remove others
158+ ..AutoFaker . Faker . Random . ListItems ( before . ComplexForms )
159+ . Select ( createdCfc =>
160+ {
161+ var copy = createdCfc . Copy ( ) ;
162+ copy . ComponentHeadword = after . Headword ( ) ;
163+ return copy ;
164+ } ) ,
165+ // keep new
166+ ..after . ComplexForms
167+ ] ) ,
168+ ] ;
169+
170+ after . Components = [
171+ // shuffle to cause moves
172+ ..AutoFaker . Faker . Random . Shuffle ( [
173+ // keep some, remove others
174+ ..AutoFaker . Faker . Random . ListItems ( before . Components )
175+ . Select ( createdCfc =>
176+ {
177+ var copy = createdCfc . Copy ( ) ;
178+ copy . ComplexFormHeadword = after . Headword ( ) ;
179+ return copy ;
180+ } ) ,
181+ // keep new
182+ ..after . Components
183+ ] ) ,
184+ ] ;
185+
186+ // expected should not be round-tripped, because an api might manipulate it somehow.
187+ // We expect the final result to be equivalent to this "raw"/untouched, requested state.
188+ var expected = after . Copy ( ) ;
189+
190+ if ( roundTripApi is not null )
191+ {
192+ // round-tripping ensures we're dealing with realistic data
193+ // (e.g. in fwdata ComplexFormComponents do not have an Id)
194+ before = await roundTripApi . CreateEntry ( before ) ;
195+ await roundTripApi . DeleteEntry ( before . Id ) ;
196+ after = await roundTripApi . CreateEntry ( after ) ;
197+ await roundTripApi . DeleteEntry ( after . Id ) ;
198+ }
199+
200+ // before should not be round-tripped here. That's handled above.
201+ await Api . CreateEntry ( before ) ;
202+
203+ // act
204+ await EntrySync . SyncFull ( before , after , Api ) ;
205+ var actual = await Api . GetEntry ( after . Id ) ;
206+
207+ // assert
208+ actual . Should ( ) . NotBeNull ( ) ;
209+ actual . Should ( ) . BeEquivalentTo ( after , options =>
210+ {
211+ options = options
212+ . WithStrictOrdering ( )
213+ . WithoutStrictOrderingFor ( e => e . ComplexForms ) // sorted alphabetically
214+ . WithoutStrictOrderingFor ( e => e . Path . EndsWith ( $ ".{ nameof ( Sense . SemanticDomains ) } ") ) // not sorted
215+ . For ( e => e . Senses ) . Exclude ( s => s . Order )
216+ . For ( e => e . Components ) . Exclude ( c => c . Order )
217+ . For ( e => e . ComplexForms ) . Exclude ( c => c . Order )
218+ . For ( e => e . Senses ) . For ( s => s . ExampleSentences ) . Exclude ( e => e . Order ) ;
219+ if ( currentApiType == ApiType . Crdt )
220+ {
221+ // does not yet update Headwords 😕
222+ options = options
223+ . For ( e => e . Components ) . Exclude ( c => c . ComplexFormHeadword )
224+ . For ( e => e . ComplexForms ) . Exclude ( c => c . ComponentHeadword ) ;
225+ }
226+ if ( currentApiType == ApiType . FwData )
227+ {
228+ // does not support changing MorphType yet (see UpdateEntryProxy.MorphType)
229+ options = options . Excluding ( e => e . MorphType ) ;
230+ }
231+ return options ;
232+ } ) ;
233+ }
234+
70235 [ Fact ]
71236 public async Task NormalizesStringsToNFD ( )
72237 {
0 commit comments