108 lines
4.2 KiB
C#
108 lines
4.2 KiB
C#
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();
|
|
}
|
|
}
|