@@ -0,0 +1,107 @@
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
/// End-to-end golden-snapshot test. Loads the canonical CSV fixture from
|
||||
/// <c>Fixtures/rslogix-canonical.csv</c>, runs it through
|
||||
/// <see cref="RsLogixSymbolImport"/>, then compares the resulting tag list to
|
||||
/// <c>Fixtures/rslogix-canonical-expected.json</c>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// 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
|
||||
/// <c>cp $TEMP/rslogix-canonical-actual.json tests/.../Fixtures/rslogix-canonical-expected.json</c>
|
||||
/// to bless the new shape. Treats both sides as <see cref="JsonNode"/> trees so
|
||||
/// insignificant whitespace + key-order differences don't false-fail.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
[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<RsLogixSymbolImport>.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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Structural JSON equality — recursively compares two <see cref="JsonNode"/> trees
|
||||
/// by shape + value, ignoring property-order on objects. Cheaper than pulling in a
|
||||
/// dedicated diff library for one assertion.
|
||||
/// </summary>
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user