diff --git a/backend/FwLite/LcmCrdt.Tests/Changes/ChangeSerializationTests.cs b/backend/FwLite/LcmCrdt.Tests/Changes/ChangeSerializationTests.cs index 287c32950..6553d6cb6 100644 --- a/backend/FwLite/LcmCrdt.Tests/Changes/ChangeSerializationTests.cs +++ b/backend/FwLite/LcmCrdt.Tests/Changes/ChangeSerializationTests.cs @@ -1,10 +1,12 @@ using System.Reflection; +using System.Runtime.CompilerServices; using System.Text.Json; using FluentAssertions.Execution; using LcmCrdt.Changes; using LcmCrdt.Changes.Entries; using MiniLcm.Tests.AutoFakerHelpers; using SIL.Harmony.Changes; +using SIL.WritingSystems; using Soenneker.Utils.AutoBogus; using SystemTextJsonPatch; @@ -12,15 +14,22 @@ namespace LcmCrdt.Tests.Changes; public class ChangeSerializationTests { + private static readonly Lazy LazyOptions = new(() => + { + var config = new CrdtConfig(); + LcmCrdtKernel.ConfigureCrdt(config); + return config.JsonSerializerOptions; + }); + private static readonly JsonSerializerOptions Options = LazyOptions.Value; private static readonly AutoFaker Faker = new() { Config = { - Overrides = [new WritingSystemIdOverride()] + Overrides = [new WritingSystemIdOverride(), new MultiStringOverride()] } }; - public static IEnumerable Changes() + private static IEnumerable GeneratedChanges() { foreach (var type in LcmCrdtKernel.AllChangeTypes()) { @@ -34,14 +43,31 @@ public static IEnumerable Changes() } else { - change = Faker.Generate(type); + try + { + change = Faker.Generate(type); + } + catch (Exception e) + { + throw new Exception($"Failed to generate change of type {type.Name}", e); + } } - change.Should().NotBeNull($"change type {type.Name} should have been generated"); + + change.Should().NotBeNull($"change type {type.Name} should have been generated").And.BeAssignableTo(); + yield return (IChange) change; + } + + yield return SetComplexFormComponentChange.NewComplexForm(Guid.NewGuid(), Guid.NewGuid()); + yield return SetComplexFormComponentChange.NewComponent(Guid.NewGuid(), Guid.NewGuid()); + yield return SetComplexFormComponentChange.NewComponentSense(Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid()); + } + + public static IEnumerable Changes() + { + foreach (var change in GeneratedChanges()) + { yield return [change]; } - yield return [SetComplexFormComponentChange.NewComplexForm(Guid.NewGuid(), Guid.NewGuid())]; - yield return [SetComplexFormComponentChange.NewComponent(Guid.NewGuid(), Guid.NewGuid())]; - yield return [SetComplexFormComponentChange.NewComponentSense(Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid())]; } private static readonly MethodInfo PatchMethod = new Func(Patch).Method.GetGenericMethodDefinition(); @@ -55,13 +81,11 @@ private static IChange Patch() where T : class [MemberData(nameof(Changes))] public void CanRoundTripChanges(IChange change) { - var config = new CrdtConfig(); - LcmCrdtKernel.ConfigureCrdt(config); //commit id is not serialized change.CommitId = Guid.Empty; var type = change.GetType(); - var json = JsonSerializer.Serialize(change, config.JsonSerializerOptions); - var newChange = JsonSerializer.Deserialize(json, type, config.JsonSerializerOptions); + var json = JsonSerializer.Serialize(change, Options); + var newChange = JsonSerializer.Deserialize(json, type, Options); newChange.Should().BeEquivalentTo(change); } @@ -79,4 +103,34 @@ public void ChangesIncludesAllValidChangeTypes() } } } + + [Fact] + public void CanDeserializeJson() + { + //this file represents projects which already have changes applied, we want to ensure that we don't break anything. + //nothing should ever be removed from this file + //if a new property is added then a new json object should be added with that property + using var jsonFile = File.OpenRead(GetJsonFilePath("ExistingJson.json")); + var changes = JsonSerializer.Deserialize>(jsonFile, Options); + changes.Should().NotBeNullOrEmpty().And.NotContainNulls(); + + //ensure that all change types are represented and none should be removed from AllChangeTypes + changes.Select(c => c.GetType()).Distinct() + .Should().BeEquivalentTo(LcmCrdtKernel.AllChangeTypes()); + } + + //helper method, can be called manually to regenerate the json file + [Fact(Skip = "Only run manually")] + public static void GenerateNewJsonFile() + { + using var jsonFile = File.Open(GetJsonFilePath("NewJson.json"), FileMode.Create); + JsonSerializer.Serialize(jsonFile, GeneratedChanges(), Options); + } + + private static string GetJsonFilePath(string name, [CallerFilePath] string sourceFile = "") + { + return Path.Combine( + Path.GetDirectoryName(sourceFile) ?? throw new InvalidOperationException("Could not get directory of source file"), + name); + } } diff --git a/backend/FwLite/LcmCrdt.Tests/Changes/ExistingJson.json b/backend/FwLite/LcmCrdt.Tests/Changes/ExistingJson.json new file mode 100644 index 000000000..d31652188 --- /dev/null +++ b/backend/FwLite/LcmCrdt.Tests/Changes/ExistingJson.json @@ -0,0 +1,283 @@ +[ + { + "$type": "jsonPatch:Entry", + "PatchDocument": [], + "EntityId": "818d2229-959c-487b-a51e-ccc0e9fa3a8c" + }, + { + "$type": "jsonPatch:Sense", + "PatchDocument": [], + "EntityId": "337a280c-29ae-4058-bd5f-d9e82d0009e3" + }, + { + "$type": "jsonPatch:ExampleSentence", + "PatchDocument": [], + "EntityId": "66bd3b4d-10a8-4486-9385-4267c226073a" + }, + { + "$type": "jsonPatch:WritingSystem", + "PatchDocument": [], + "EntityId": "e2a389f6-c0b6-4661-b3df-2c9558ac51ad" + }, + { + "$type": "jsonPatch:PartOfSpeech", + "PatchDocument": [], + "EntityId": "3528d3cc-6150-4c0a-8b7b-9bf08c19157b" + }, + { + "$type": "jsonPatch:SemanticDomain", + "PatchDocument": [], + "EntityId": "ef6fc636-7a50-43a3-b781-1c3c4f1f3e35" + }, + { + "$type": "jsonPatch:ComplexFormType", + "PatchDocument": [], + "EntityId": "21ff6635-5638-4420-aeff-c9fdd5600a9e" + }, + { + "$type": "delete:Entry", + "EntityId": "03ff68d2-9a1b-26f7-b433-cb89b9ff3289" + }, + { + "$type": "delete:Sense", + "EntityId": "711d0efc-09ca-2e92-26d0-5f7c5b077e3e" + }, + { + "$type": "delete:ExampleSentence", + "EntityId": "5da803a8-55c1-849e-6a0b-cfe20f7942b7" + }, + { + "$type": "delete:WritingSystem", + "EntityId": "b806c726-3c6d-401e-0d2c-b8518fa9ec25" + }, + { + "$type": "delete:PartOfSpeech", + "EntityId": "31c96ad3-de02-9e51-f65f-cd66e201311e" + }, + { + "$type": "delete:SemanticDomain", + "EntityId": "e9b60857-e72c-3db5-2cb2-e4f1ac100b2a" + }, + { + "$type": "delete:ComplexFormType", + "EntityId": "d04588ae-5412-2395-7286-ce6e7d7deddf" + }, + { + "$type": "delete:ComplexFormComponent", + "EntityId": "289b4991-ae48-d808-2004-34d618c7d647" + }, + { + "$type": "SetPartOfSpeechChange", + "PartOfSpeechId": "43d86183-e45f-416a-8fc2-23e8dd210df7", + "EntityId": "855384fa-a2d7-248a-d7d8-e31e8ca72d16" + }, + { + "$type": "AddSemanticDomainChange", + "SemanticDomain": { + "Id": "0b49530d-c00c-c850-fbcb-c310dd81aa25", + "Name": { + "nbp": "driver", + "ogc": "Credit Card Account", + "txr": "Zambia", + "sxm": "Buckinghamshire" + }, + "Code": "Clothing, Industrial \u0026 Health", + "DeletedAt": "2025-01-19T21:09:19.0427134+07:00", + "Predefined": true + }, + "EntityId": "f60faeb5-4d22-a8b0-a806-89347f2b7843" + }, + { + "$type": "RemoveSemanticDomainChange", + "SemanticDomainId": "8eb5eeba-d107-1664-a659-d4c6bc90e074", + "EntityId": "5bb0d082-9de1-9091-284d-54f627b944e5" + }, + { + "$type": "ReplaceSemanticDomainChange", + "OldSemanticDomainId": "730d80ed-5e26-070c-a172-8e714b6a9cbd", + "SemanticDomain": { + "Id": "4daa2f20-4140-1bd6-ac95-5801eab210aa", + "Name": { + "ngs": "turquoise", + "tvs": "azure", + "cco": "Implementation", + "rrt": "card" + }, + "Code": "transition", + "DeletedAt": "2025-01-19T21:18:49.3256836+07:00", + "Predefined": false + }, + "EntityId": "8291cde2-0fd2-3ebc-765b-c00c6c489c3a" + }, + { + "$type": "CreateEntryChange", + "LexemeForm": { + "kof": "Kip", + "bbv": "Belarus" + }, + "CitationForm": { + "ary": "Stand-alone", + "yat": "back up" + }, + "LiteralMeaning": { + "ysm": "withdrawal" + }, + "Note": { + "zat": "transmit", + "mye": "hacking", + "hto": "Jewelery", + "juh": "indigo" + }, + "EntityId": "46d2a7aa-186e-0820-6ab7-9d9f2c4359fb" + }, + { + "$type": "CreateSenseChange", + "EntryId": "9403f290-4497-4ecc-5744-1cf7f402498a", + "Order": 0.25164027577075543, + "Definition": { + "mmt": "Customer", + "cdm": "intranet" + }, + "Gloss": { + "pix": "Small Steel Pizza", + "hmb": "auxiliary" + }, + "PartOfSpeechId": "6f664799-1385-2bd8-077f-76d1206ac233", + "SemanticDomains": [ + { + "Id": "de982bb2-22c1-a67d-af30-53278434b09d", + "Name": { + "ajs": "disintermediate" + }, + "Code": "Open-architected", + "DeletedAt": "2025-01-20T05:49:30.5251959+07:00", + "Predefined": false + } + ], + "EntityId": "fb918ba0-ba5e-7977-312e-3028d1295dd8" + }, + { + "$type": "CreateExampleSentenceChange", + "SenseId": "f3e4f3fe-1a2c-7096-fdf6-64df12809e1a", + "Order": 0.18532264410517607, + "Sentence": { + "liq": "Secured", + "kax": "ADP" + }, + "Translation": { + "xtq": "New Jersey" + }, + "Reference": "Automotive", + "EntityId": "5a96652c-32d0-72ad-a29c-8ee1b07b187a" + }, + { + "$type": "CreatePartOfSpeechChange", + "Name": { + "txh": "logistical", + "xcc": "International", + "guh": "Hryvnia", + "dbq": "parse" + }, + "Predefined": true, + "EntityId": "6258cf11-4451-1362-a5a8-8cca65548880" + }, + { + "$type": "CreateSemanticDomainChange", + "Name": { + "sgx": "Gateway", + "cmt": "Brand", + "ylo": "Intranet" + }, + "Predefined": false, + "Code": "port", + "EntityId": "61d1a336-fe39-18b5-452a-4b192946cc51" + }, + { + "$type": "CreateWritingSystemChange", + "WsId": "zms", + "Name": "quantify", + "Abbreviation": "Ergonomic", + "Font": "software", + "Exemplars": [ + "Berkshire" + ], + "Type": 1, + "Order": 0.7988004089201804, + "EntityId": "0fccfaaa-1e55-f6fb-966f-5c1e6caedf45" + }, + { + "$type": "AddComplexFormTypeChange", + "ComplexFormType": { + "Id": "d5f54e9f-65cf-360b-3955-071cd3910f4e", + "Name": { + "dag": "Handmade Concrete Table", + "dlm": "withdrawal" + }, + "DeletedAt": "2025-01-20T08:42:21.2690032+07:00" + }, + "EntityId": "5290a257-6efe-769b-1818-49529781e1d4" + }, + { + "$type": "AddEntryComponentChange", + "ComplexFormEntryId": "5b1d27c0-8990-0417-5e31-071c5f48ac9a", + "ComponentEntryId": "e476003d-69b4-1e84-46f2-f2dee858388f", + "ComponentSenseId": "434e9e2b-dad3-5fcb-f1e3-cd00aae5df10", + "EntityId": "74c8a80a-8be4-c950-c9d0-ebbf5da1ec21" + }, + { + "$type": "AddEntryComponentChange", + "Order": 0.5345900716748275, + "ComplexFormEntryId": "5b1d27c0-8990-0417-5e31-071c5f48ac9a", + "ComponentEntryId": "e476003d-69b4-1e84-46f2-f2dee858388f", + "ComponentSenseId": "434e9e2b-dad3-5fcb-f1e3-cd00aae5df10", + "EntityId": "74c8a80a-8be4-c950-c9d0-ebbf5da1ec21" + }, + { + "$type": "RemoveComplexFormTypeChange", + "ComplexFormTypeId": "d4b5503e-9381-a73d-6b18-eb3dae571f25", + "EntityId": "428b9279-8cbb-bcda-49e2-1ff80d8cba2f" + }, + { + "$type": "CreateComplexFormType", + "Name": { + "slu": "yellow" + }, + "EntityId": "e03826fb-e6d2-6e3c-1f43-9fc6a0f6c4c6" + }, + { + "$type": "SetOrderChange:Sense", + "Order": 0.4870418422144669, + "EntityId": "9d3e9006-c4b4-4316-6628-faeede75af40" + }, + { + "$type": "SetOrderChange:ExampleSentence", + "Order": 0.9610462315814435, + "EntityId": "56fb336c-5bcf-afc8-04b5-1df4176975f1" + }, + { + "$type": "SetOrderChange:ComplexFormComponent", + "Order": 0.1655437666547228, + "EntityId": "e92d4673-888c-3204-0767-17adc2a2405e" + }, + { + "$type": "SetComplexFormComponentChange", + "ComplexFormEntryId": "bf296463-7b81-4a33-b9e7-6f4f8179715e", + "ComponentEntryId": null, + "ComponentSenseId": null, + "EntityId": "d5c89801-cedc-403e-9ed6-f734ad0ddf23" + }, + { + "$type": "SetComplexFormComponentChange", + "ComplexFormEntryId": null, + "ComponentEntryId": "470eb4a9-50d7-4c28-93b2-019b1e5cf9d6", + "ComponentSenseId": null, + "EntityId": "beaa8382-2b49-45a5-81eb-e5825004fb98" + }, + { + "$type": "SetComplexFormComponentChange", + "ComplexFormEntryId": null, + "ComponentEntryId": "e0d469cd-9d2f-44f5-8147-102f683c18ed", + "ComponentSenseId": "7a1c054f-472a-473f-afed-16f75fe9a90f", + "EntityId": "01b98e0e-c95f-430d-9512-9db44fdf7b2a" + } +]