using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Driver.MTConnect.Tests; /// /// #650 — DATA_SET / TABLE observations are served as a String carrying compact, deterministic JSON. /// Pins the parse (entries, cells, removed, resetTriggered) and the index's merge semantics /// (/current replaces, /sample merges, UNAVAILABLE drops the accumulated set). /// public sealed class MTConnectDataSetTests { private const uint Good = 0x00000000u; private const uint BadNoCommunication = 0x80310000u; private static string Doc(string events, string attrs = "") => $"""
{events} """; private static string DataSet(string body, string attrs = "") => $"""{body}"""; private static string Table(string body, string attrs = "") => $"""{body}"""; private static MTConnectObservationIndex NewIndex() => new([new MTConnectTagDefinition("ds", DriverDataType.String), new MTConnectTagDefinition("tb", DriverDataType.String)]); private static void Apply(MTConnectObservationIndex idx, string events, bool baseline = false) => idx.Apply(MTConnectStreamsParser.Parse(Doc(events)), baseline); [Fact] public void Parser_captures_entries_cells_removed_and_reset() { var result = MTConnectStreamsParser.Parse(Doc( DataSet("""1""", " resetTriggered=\"MANUAL\"") + Table("""1.52"""))); var ds = result.Observations[0]; ds.ResetTriggered.ShouldBeTrue(); ds.Entries!.Count.ShouldBe(2); ds.Entries[0].ShouldBe(new MTConnectDataSetEntry("a", "1", false)); ds.Entries[1].Removed.ShouldBeTrue(); var tb = result.Observations[1]; tb.Entries!.ShouldHaveSingleItem().Cells!.Select(c => (c.Key, c.Value)) .ShouldBe([("X", "1.5"), ("Y", "2")]); } [Fact] public void Data_set_is_served_as_sorted_compact_json_with_numbers_and_strings() { var idx = NewIndex(); Apply(idx, DataSet("""71.25A & "B"007"""), baseline: true); var snap = idx.Get("ds"); snap.StatusCode.ShouldBe(Good); snap.Value.ShouldBe("""{"name":"A & \"B\"","offsetX":1.25,"part":"007","toolNumber":7}"""); } [Fact] public void Table_is_served_as_nested_json() { var idx = NewIndex(); Apply(idx, Table("""321.0"""), baseline: true); idx.Get("tb").Value.ShouldBe("""{"G54":{"X":1.0,"Y":2},"G55":{"X":3}}"""); } [Fact] public void Sample_delta_merges_and_removed_entry_disappears() { var idx = NewIndex(); Apply(idx, DataSet("""12"""), baseline: true); Apply(idx, DataSet("""3x""")); idx.Get("ds").Value.ShouldBe("""{"a":1,"b":3,"c":"x"}"""); Apply(idx, DataSet("""""")); idx.Get("ds").Value.ShouldBe("""{"b":3,"c":"x"}"""); } [Fact] public void Table_row_delta_replaces_the_row_and_removed_row_disappears() { var idx = NewIndex(); Apply(idx, Table("""123"""), baseline: true); Apply(idx, Table("""9""")); idx.Get("tb").Value.ShouldBe("""{"G54":{"X":9}}"""); } [Fact] public void Current_baseline_replaces_rather_than_merges() { var idx = NewIndex(); Apply(idx, DataSet("""12"""), baseline: true); Apply(idx, DataSet("""3"""), baseline: true); idx.Get("ds").Value.ShouldBe("""{"c":3}"""); } [Fact] public void ResetTriggered_replaces_the_accumulated_set_on_a_sample() { var idx = NewIndex(); Apply(idx, DataSet("""1"""), baseline: true); Apply(idx, DataSet("""9""", " resetTriggered=\"MANUAL\"")); idx.Get("ds").Value.ShouldBe("""{"z":9}"""); } [Fact] public void ResetTriggered_with_no_entries_is_an_empty_set_not_an_absence() { var idx = NewIndex(); Apply(idx, DataSet("""1"""), baseline: true); Apply(idx, DataSet("", " resetTriggered=\"MANUAL\"")); var snap = idx.Get("ds"); snap.StatusCode.ShouldBe(Good); snap.Value.ShouldBe("{}"); } [Fact] public void Unavailable_is_BadNoCommunication_and_drops_the_accumulated_set() { var idx = NewIndex(); Apply(idx, DataSet("""1"""), baseline: true); Apply(idx, DataSet("UNAVAILABLE")); var snap = idx.Get("ds"); snap.StatusCode.ShouldBe(BadNoCommunication); snap.Value.ShouldBeNull(); // A delta after the outage must not resurrect the pre-outage entry. Apply(idx, DataSet("""2""")); idx.Get("ds").Value.ShouldBe("""{"b":2}"""); } [Fact] public void Several_observations_in_one_document_fold_in_order() { var idx = NewIndex(); Apply(idx, DataSet("""1""") + DataSet("""2"""), baseline: true); idx.Get("ds").Value.ShouldBe("""{"a":1,"b":2}"""); } [Fact] public void Clear_forgets_the_accumulated_set() { var idx = NewIndex(); Apply(idx, DataSet("""1"""), baseline: true); idx.Clear(); Apply(idx, DataSet("""2""")); idx.Get("ds").Value.ShouldBe("""{"b":2}"""); } [Fact] public void Structured_content_that_is_not_entries_stays_BadNotSupported() { var idx = NewIndex(); Apply(idx, DataSet("1"), baseline: true); idx.Get("ds").StatusCode.ShouldBe(0x803D0000u); } [Fact] public void ResetTriggered_on_an_ordinary_sample_keeps_its_numeric_value() { // Regression: resetTriggered is also a standard attribute on accumulating SAMPLEs / counters. It must // never turn one into an empty set. var idx = new MTConnectObservationIndex( [new MTConnectTagDefinition("pc", DriverDataType.Int64), new MTConnectTagDefinition("amt", DriverDataType.Float64)]); idx.Apply(MTConnectStreamsParser.Parse(Doc( """5""" + """2.5"""))); idx.Get("pc").StatusCode.ShouldBe(Good); idx.Get("pc").Value.ShouldBe(5L); idx.Get("amt").Value.ShouldBe(2.5d); } [Fact] public void An_empty_current_data_set_is_Good_empty_json_like_a_reset_one() { var idx = NewIndex(); Apply(idx, DataSet("""1"""), baseline: true); Apply(idx, DataSet(""), baseline: true); var snap = idx.Get("ds"); snap.StatusCode.ShouldBe(Good); snap.Value.ShouldBe("{}"); } [Fact] public void An_all_removed_set_is_the_same_empty_json() { var idx = NewIndex(); Apply(idx, DataSet("""1"""), baseline: true); Apply(idx, DataSet("""""")); idx.Get("ds").Value.ShouldBe("{}"); } }