using System.IO; using System.Text.Json; using System.Text.Json.Nodes; using Microsoft.Extensions.Logging.Abstractions; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Import; namespace ZB.MOM.WW.OtOpcUa.Driver.AbLegacy.Tests.Import; /// /// End-to-end golden-snapshot test. Loads the canonical CSV fixture from /// Fixtures/rslogix-canonical.csv, runs it through /// , then compares the resulting tag list to /// Fixtures/rslogix-canonical-expected.json. /// /// /// /// Mismatch path: the test writes the actual JSON to a temp file path and prints the /// path in the failure message, so the dev can run /// cp $TEMP/rslogix-canonical-actual.json tests/.../Fixtures/rslogix-canonical-expected.json /// to bless the new shape. Treats both sides as trees so /// insignificant whitespace + key-order differences don't false-fail. /// /// [Trait("Category", "Unit")] public sealed class RsLogixSymbolImportGoldenTests { private const string Device = "ab://192.168.1.20/1,0"; private static string FixturePath(string name) => Path.Combine(AppContext.BaseDirectory, "Fixtures", name); [Fact] public void Canonical_csv_matches_golden_json() { var importer = new RsLogixSymbolImport(NullLogger.Instance); using var stream = File.OpenRead(FixturePath("rslogix-canonical.csv")); var result = importer.Parse(stream, Device); result.ParsedCount.ShouldBe(8); result.SkippedCount.ShouldBe(0); result.ErrorCount.ShouldBe(0); var actualPayload = new { Tags = result.Tags.Select(t => new { Name = t.Name, DeviceHostAddress = t.DeviceHostAddress, Address = t.Address, DataType = t.DataType.ToString(), Writable = t.Writable, }).ToArray() }; var actualJson = JsonSerializer.Serialize(actualPayload, new JsonSerializerOptions { WriteIndented = true }); var expectedJson = File.ReadAllText(FixturePath("rslogix-canonical-expected.json")); var actualNode = JsonNode.Parse(actualJson)!; var expectedNode = JsonNode.Parse(expectedJson)!; if (!JsonTreesEqual(actualNode, expectedNode)) { // Dump the actual JSON to a discoverable temp path so the dev can `cp` it over // the fixture once they've reviewed the diff. The test message points straight // at the file. var dump = Path.Combine(Path.GetTempPath(), "rslogix-canonical-actual.json"); File.WriteAllText(dump, actualJson); throw new Xunit.Sdk.XunitException( $"RSLogix golden mismatch. Actual written to: {dump}\n--- Expected ---\n{expectedJson}\n--- Actual ---\n{actualJson}"); } } /// /// Structural JSON equality — recursively compares two trees /// by shape + value, ignoring property-order on objects. Cheaper than pulling in a /// dedicated diff library for one assertion. /// private static bool JsonTreesEqual(JsonNode? a, JsonNode? b) { if (a is null && b is null) return true; if (a is null || b is null) return false; if (a is JsonObject ao && b is JsonObject bo) { if (ao.Count != bo.Count) return false; foreach (var kvp in ao) { if (!bo.TryGetPropertyValue(kvp.Key, out var bv)) return false; if (!JsonTreesEqual(kvp.Value, bv)) return false; } return true; } if (a is JsonArray aa && b is JsonArray ba) { if (aa.Count != ba.Count) return false; for (var i = 0; i < aa.Count; i++) { if (!JsonTreesEqual(aa[i], ba[i])) return false; } return true; } // Primitives — fall back to canonical JSON form for value equality. return a.ToJsonString() == b.ToJsonString(); } }