Skip to content

Commit 41567a2

Browse files
committed
test(xml-tests): close coverage FLOOR gaps on VendorExtensions
Lands the coverage-FLOOR tests missing from the initial PR TrakHound#223 surface: - XmlConfigurationBranchCoverageTests: pins the WriteXml null-configuration guard (both outputComments values), the outputComments=true comment path, and the ToConfiguration zero-length + all-null + read-side mixed branches on the XmlElement[] projection. - ConfigurationVendorExtensionsEdgeCaseTests: default-namespace (unprefixed) extensions, attribute-only extensions, text-only extensions, unicode payloads (combining marks + RTL + astral surrogate pairs), predefined entity round-tripping, dense Configuration with Motion + SensorConfiguration + vendor extension coexistence, three-vendor namespaces preserved, and three-level nesting depth. - ConfigurationVendorExtensionsXsdStrictValidationTests: crown-jewel XSD-1.0 strict validation — emits a probe envelope via the production XmlDevicesResponseDocument pipeline, loads the v2.7 Devices XSD (via XsdPreprocessor for XSD 1.1 stripping) alongside a synthetic vendor XSD declaring the element as substitutionGroup='mtc:AbstractConfiguration', and asserts zero validation errors. Negative twin rejects a foreign element that lacks a substitution declaration. - ConfigurationVendorExtensionsJsonKnownLimitTests + ConfigurationVendorExtensionsCppAgentJsonKnownLimitTests: pin the current known limitation that both JSON dialects silently drop VendorExtensions (the XML formatter is the sole surface honoring the vendor-substitution contract today), so any future first-class JSON representation must be a deliberate edit to these tests plus the interface docblock. Verified green on bluefin: - MTConnect.NET-XML-Tests: 131 passed / 0 failed - MTConnect.NET-JSON-Tests: 68 passed / 0 failed - MTConnect.NET-JSON-cppagent-Tests: 367 passed / 0 failed
1 parent 6c52619 commit 41567a2

5 files changed

Lines changed: 1165 additions & 0 deletions

File tree

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
2+
// TrakHound Inc. licenses this file to you under the MIT license.
3+
4+
using System.Linq;
5+
using System.Xml.Linq;
6+
using MTConnect.Devices.Configurations;
7+
using MTConnect.Devices.Json;
8+
using MTConnect.NET_JSON_Tests.TestHelpers;
9+
using NUnit.Framework;
10+
11+
namespace MTConnect.NET_JSON_Tests.Devices.Configurations
12+
{
13+
/// <summary>
14+
/// Pins the CURRENT known limitation of the JSON formatter with respect
15+
/// to <see cref="IConfiguration.VendorExtensions"/>: the JSON surrogate
16+
/// <see cref="JsonConfiguration"/> does NOT carry a vendor-extension
17+
/// slot — it exists only on the XML wire format. A model instance
18+
/// serialized through the JSON formatter silently drops any
19+
/// <c>VendorExtensions</c> payload, and a round trip through the JSON
20+
/// formatter returns null in that slot. This is a deliberate limitation
21+
/// (JSON has no XSD substitution-group semantics; the MTConnect JSON
22+
/// shape does not spec a "raw XML" slot), and it is pinned here so any
23+
/// future change to that behavior must be a deliberate edit to these
24+
/// tests — never a silent regression.
25+
/// </summary>
26+
/// <remarks>
27+
/// Sources:
28+
/// <list type="bullet">
29+
/// <item>XSD — <c>MTConnectDevices_2.7.xsd</c>
30+
/// <c>ComponentConfigurationType</c> defines vendor extension via
31+
/// XSD substitution group <c>AbstractConfiguration</c>; that
32+
/// mechanism has no direct JSON analogue on the wire.</item>
33+
/// <item>Interface docblock —
34+
/// <see cref="IConfiguration.VendorExtensions"/> explicitly names the
35+
/// MTConnect.NET XML formatter as the surface that honors vendor
36+
/// extensions verbatim.</item>
37+
/// </list>
38+
/// If a future change adds a first-class JSON representation for
39+
/// vendor extensions (e.g. a <c>vendorExtensions</c> array carrying
40+
/// stringified XML), replace the negative assertions in this fixture
41+
/// with positive round-trip assertions and update the interface
42+
/// docblock in the same commit.
43+
/// </remarks>
44+
[TestFixture]
45+
public class ConfigurationVendorExtensionsJsonKnownLimitTests
46+
{
47+
/// <summary>The JSON surrogate CTOR silently drops
48+
/// <see cref="IConfiguration.VendorExtensions"/> — the serialized
49+
/// JSON must not carry a <c>vendorExtensions</c> key.</summary>
50+
[Test]
51+
public void JsonConfiguration_ctor_currently_drops_VendorExtensions()
52+
{
53+
var model = new Configuration
54+
{
55+
VendorExtensions = new[]
56+
{
57+
XElement.Parse("<v:V xmlns:v=\"urn:v\">payload</v:V>")
58+
}
59+
};
60+
61+
var wire = new JsonConfiguration(model);
62+
var json = JsonRoundTripHelper.Serialize(wire);
63+
64+
Assert.That(json, Does.Not.Contain("vendorExtensions"),
65+
"The JSON surrogate has no vendor-extension slot today — regression alert if that changes.");
66+
Assert.That(json, Does.Not.Contain("payload"),
67+
"The extension payload must not leak into an unrelated JSON key.");
68+
}
69+
70+
/// <summary>A round trip through the JSON formatter returns a model
71+
/// with <see cref="IConfiguration.VendorExtensions"/> null, because
72+
/// the wire form carries no vendor-extension slot.</summary>
73+
[Test]
74+
public void JsonConfiguration_round_trip_returns_null_VendorExtensions()
75+
{
76+
var model = new Configuration
77+
{
78+
VendorExtensions = new[]
79+
{
80+
XElement.Parse("<v:V xmlns:v=\"urn:v\">payload</v:V>")
81+
}
82+
};
83+
84+
var wire = new JsonConfiguration(model);
85+
var json = JsonRoundTripHelper.Serialize(wire);
86+
var back = JsonRoundTripHelper.Deserialize<JsonConfiguration>(json)!;
87+
var round = back.ToConfiguration();
88+
89+
Assert.That(round.VendorExtensions, Is.Null,
90+
"The JSON round trip drops the vendor-extension payload — pinned known limitation.");
91+
}
92+
93+
/// <summary>The <c>JsonConfiguration</c> surrogate type surface does
94+
/// not declare a public <c>VendorExtensions</c> property today —
95+
/// pins the type surface so a hypothetical addition surfaces as a
96+
/// test-file edit rather than a silent JSON-wire shape change.</summary>
97+
[Test]
98+
public void JsonConfiguration_surface_does_not_declare_VendorExtensions_property_today()
99+
{
100+
var property = typeof(JsonConfiguration).GetProperty("VendorExtensions");
101+
Assert.That(property, Is.Null,
102+
"JsonConfiguration must NOT declare a VendorExtensions slot until a first-class JSON representation is designed. "
103+
+ "If you are intentionally adding one, update this test AND the IConfiguration.VendorExtensions docblock in the same commit.");
104+
}
105+
106+
/// <summary>Standard children on the JSON surrogate continue to
107+
/// round-trip normally when VendorExtensions is set on the model —
108+
/// pins that the current drop does not disturb sibling slots.</summary>
109+
[Test]
110+
public void JsonConfiguration_preserves_standard_children_when_VendorExtensions_dropped()
111+
{
112+
var model = new Configuration
113+
{
114+
Motion = new Motion
115+
{
116+
Id = "m1",
117+
Type = MotionType.PRISMATIC,
118+
Actuation = MotionActuationType.DIRECT,
119+
Axis = new Axis { Value = "1 2 3" }
120+
},
121+
VendorExtensions = new[]
122+
{
123+
XElement.Parse("<v:V xmlns:v=\"urn:v\">payload</v:V>")
124+
}
125+
};
126+
127+
var wire = new JsonConfiguration(model);
128+
var json = JsonRoundTripHelper.Serialize(wire);
129+
130+
Assert.That(json, Does.Contain("\"motion\":"),
131+
"Motion must serialize even when a dropped VendorExtensions payload is present on the model.");
132+
133+
var back = JsonRoundTripHelper.Deserialize<JsonConfiguration>(json)!;
134+
var round = back.ToConfiguration();
135+
136+
Assert.That(round.Motion, Is.Not.Null);
137+
Assert.That(round.Motion!.Id, Is.EqualTo("m1"));
138+
Assert.That(round.VendorExtensions, Is.Null);
139+
}
140+
141+
/// <summary>Symmetry — a model whose only Configuration content is
142+
/// vendor extensions produces a bare JSON object (all standard
143+
/// slots omitted per <c>WhenWritingNull</c>).</summary>
144+
[Test]
145+
public void JsonConfiguration_from_vendor_extensions_only_model_produces_empty_object()
146+
{
147+
var model = new Configuration
148+
{
149+
VendorExtensions = new[]
150+
{
151+
XElement.Parse("<v:V xmlns:v=\"urn:v\">payload</v:V>")
152+
}
153+
};
154+
155+
var wire = new JsonConfiguration(model);
156+
var json = JsonRoundTripHelper.Serialize(wire);
157+
158+
Assert.That(json, Is.EqualTo("{}"),
159+
"With only vendor extensions and no standard children, the JSON surrogate serializes to a bare object.");
160+
}
161+
}
162+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
2+
// TrakHound Inc. licenses this file to you under the MIT license.
3+
4+
using System.Linq;
5+
using System.Xml.Linq;
6+
using MTConnect.Devices.Configurations;
7+
using MTConnect.Devices.Json;
8+
using MTConnect.NET_JSON_cppagent_Tests.TestHelpers;
9+
using NUnit.Framework;
10+
11+
namespace MTConnect.NET_JSON_cppagent_Tests.Devices.Configurations
12+
{
13+
/// <summary>
14+
/// Pins the CURRENT known limitation of the cppagent-dialect JSON
15+
/// formatter with respect to <see cref="IConfiguration.VendorExtensions"/>:
16+
/// the cppagent-dialect <see cref="JsonConfiguration"/> does NOT carry a
17+
/// vendor-extension slot — it exists only on the XML wire format. A
18+
/// model instance serialized through the cppagent JSON formatter
19+
/// silently drops any <c>VendorExtensions</c> payload, and a round trip
20+
/// through the JSON formatter returns null in that slot. Pinned here so
21+
/// any future change to that behavior must be a deliberate edit to
22+
/// these tests — never a silent regression.
23+
/// </summary>
24+
/// <remarks>
25+
/// Sources:
26+
/// <list type="bullet">
27+
/// <item>XSD — <c>MTConnectDevices_2.7.xsd</c>
28+
/// <c>ComponentConfigurationType</c> defines vendor extension via
29+
/// XSD substitution group <c>AbstractConfiguration</c>; the cppagent
30+
/// JSON dialect (see cppagent MTConnectResponse.json schema) does
31+
/// not define a vendor-extension slot on <c>Configuration</c>.</item>
32+
/// <item>Interface docblock —
33+
/// <see cref="IConfiguration.VendorExtensions"/> explicitly names
34+
/// the MTConnect.NET XML formatter as the surface that honors
35+
/// vendor extensions verbatim.</item>
36+
/// </list>
37+
/// If a future change adds a first-class JSON representation for
38+
/// vendor extensions (e.g. a <c>VendorExtensions</c> array carrying
39+
/// stringified XML), replace the negative assertions in this fixture
40+
/// with positive round-trip assertions and update the interface
41+
/// docblock in the same commit.
42+
/// </remarks>
43+
[TestFixture]
44+
public class ConfigurationVendorExtensionsCppAgentJsonKnownLimitTests
45+
{
46+
/// <summary>The cppagent JSON surrogate CTOR silently drops
47+
/// <see cref="IConfiguration.VendorExtensions"/>.</summary>
48+
[Test]
49+
public void CppAgent_JsonConfiguration_ctor_currently_drops_VendorExtensions()
50+
{
51+
var model = new Configuration
52+
{
53+
VendorExtensions = new[]
54+
{
55+
XElement.Parse("<v:V xmlns:v=\"urn:v\">payload</v:V>")
56+
}
57+
};
58+
59+
var wire = new JsonConfiguration(model);
60+
var json = JsonRoundTripHelper.Serialize(wire);
61+
62+
Assert.That(json, Does.Not.Contain("VendorExtensions"),
63+
"The cppagent JSON surrogate has no vendor-extension slot today — regression alert if that changes.");
64+
Assert.That(json, Does.Not.Contain("payload"),
65+
"The extension payload must not leak into an unrelated JSON key.");
66+
}
67+
68+
/// <summary>A round trip through the cppagent JSON formatter returns
69+
/// a model with <see cref="IConfiguration.VendorExtensions"/> null.</summary>
70+
[Test]
71+
public void CppAgent_JsonConfiguration_round_trip_returns_null_VendorExtensions()
72+
{
73+
var model = new Configuration
74+
{
75+
VendorExtensions = new[]
76+
{
77+
XElement.Parse("<v:V xmlns:v=\"urn:v\">payload</v:V>")
78+
}
79+
};
80+
81+
var wire = new JsonConfiguration(model);
82+
var json = JsonRoundTripHelper.Serialize(wire);
83+
var back = JsonRoundTripHelper.Deserialize<JsonConfiguration>(json)!;
84+
var round = back.ToConfiguration();
85+
86+
Assert.That(round.VendorExtensions, Is.Null,
87+
"The cppagent JSON round trip drops the vendor-extension payload — pinned known limitation.");
88+
}
89+
90+
/// <summary>The cppagent <c>JsonConfiguration</c> surrogate type
91+
/// surface does not declare a public <c>VendorExtensions</c>
92+
/// property today.</summary>
93+
[Test]
94+
public void CppAgent_JsonConfiguration_surface_does_not_declare_VendorExtensions_property_today()
95+
{
96+
var property = typeof(JsonConfiguration).GetProperty("VendorExtensions");
97+
Assert.That(property, Is.Null,
98+
"cppagent-dialect JsonConfiguration must NOT declare a VendorExtensions slot until a first-class JSON representation is designed. "
99+
+ "If you are intentionally adding one, update this test AND the IConfiguration.VendorExtensions docblock in the same commit.");
100+
}
101+
102+
/// <summary>Standard children on the cppagent JSON surrogate
103+
/// continue to round-trip normally when VendorExtensions is set
104+
/// on the model.</summary>
105+
[Test]
106+
public void CppAgent_JsonConfiguration_preserves_standard_children_when_VendorExtensions_dropped()
107+
{
108+
var model = new Configuration
109+
{
110+
Motion = new Motion
111+
{
112+
Id = "m1",
113+
Type = MotionType.PRISMATIC,
114+
Actuation = MotionActuationType.DIRECT,
115+
Axis = new Axis { Value = "1 2 3" }
116+
},
117+
VendorExtensions = new[]
118+
{
119+
XElement.Parse("<v:V xmlns:v=\"urn:v\">payload</v:V>")
120+
}
121+
};
122+
123+
var wire = new JsonConfiguration(model);
124+
var json = JsonRoundTripHelper.Serialize(wire);
125+
126+
Assert.That(json, Does.Contain("\"Motion\":"),
127+
"Motion must serialize even when a dropped VendorExtensions payload is present on the model.");
128+
129+
var back = JsonRoundTripHelper.Deserialize<JsonConfiguration>(json)!;
130+
var round = back.ToConfiguration();
131+
132+
Assert.That(round.Motion, Is.Not.Null);
133+
Assert.That(round.Motion!.Id, Is.EqualTo("m1"));
134+
Assert.That(round.VendorExtensions, Is.Null);
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)