Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1c8b353
docs(testing): seed issue-138 writeup skeleton
ottobolyos Apr 25, 2026
36ef33f
docs(testing): scope dataitem empty-name defect for issue-138
ottobolyos Apr 25, 2026
c46bfe3
test(json-cppagent-tests): add red tests for empty Name omission
ottobolyos Apr 25, 2026
62a025a
test(json-tests): add red tests for empty Name omission
ottobolyos Apr 25, 2026
3bef627
docs(testing): document issue-138 red-test matrix
ottobolyos Apr 25, 2026
0277690
fix(json-cppagent): omit empty name attribute from probe dataitems
ottobolyos Apr 25, 2026
dec27cb
fix(json): omit empty name attribute from probe dataitems
ottobolyos Apr 25, 2026
9128eec
docs(testing): document issue-138 library fix
ottobolyos Apr 25, 2026
1fb789d
test(json-cppagent): guard dataitem ctors vs unconditional name copy
ottobolyos Apr 25, 2026
108c467
test(xml): pin xml formatter empty-name omission as parity reference
ottobolyos Apr 25, 2026
b1e1dfe
docs(testing): document issue-138 regression pins
ottobolyos Apr 25, 2026
57830ff
test(json-cppagent): probe document e2e for empty Name omission
ottobolyos Apr 25, 2026
ee4ea57
docs(testing): document issue-138 e2e validation
ottobolyos Apr 25, 2026
2c99441
test(json-cppagent): bypass organizer auto-wrap in name-omission fixture
ottobolyos Apr 27, 2026
a2337e1
chore(docs): remove internal planning leak from committed tree
ottobolyos Apr 27, 2026
3b50801
test(json-cppagent): use shared RepoRootLocator for source-grep guard
ottobolyos Apr 27, 2026
e6030f3
test(json-cppagent): pin JsonDataItem relationship classifier contract
ottobolyos Apr 27, 2026
734cf99
fix(json-data-item): classify relationships via pattern matching switch
ottobolyos Apr 27, 2026
9876e55
test(integration-tests): pin HTTP server loopback binding
ottobolyos Apr 27, 2026
42b76fd
fix(integration-tests): bind embedded HTTP server to loopback
ottobolyos Apr 27, 2026
fe70c15
fix(json-data-item): use C# 7.3 null check for legacy TFM compile
ottobolyos Apr 28, 2026
2417831
style(prose): rewrite test comments inline, drop F-code references
ottobolyos Apr 30, 2026
cc7a3e7
fix(json-cppagent): omit empty name on Component and Composition
ottobolyos Apr 30, 2026
6956b3d
fix(json-cppagent): pin JsonIgnore(WhenWritingNull) on optional Name
ottobolyos May 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ public class JsonComponent
public string Id { get; set; }

[JsonPropertyName("name")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Name { get; set; }

[JsonPropertyName("nativeName")]
Expand Down Expand Up @@ -55,7 +56,7 @@ public JsonComponent(IComponent component)
{
Id = component.Id;
Uuid = component.Uuid;
Name = component.Name;
if (!string.IsNullOrEmpty(component.Name)) Name = component.Name;
NativeName = component.NativeName;
//Type = component.Type;
if (component.Description != null) Description = new JsonDescription(component.Description);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class JsonComposition
public string Type { get; set; }

[JsonPropertyName("name")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Name { get; set; }

[JsonPropertyName("nativeName")]
Expand Down Expand Up @@ -54,7 +55,7 @@ public JsonComposition(IComposition composition)
{
Id = composition.Id;
Uuid = composition.Uuid;
Name = composition.Name;
if (!string.IsNullOrEmpty(composition.Name)) Name = composition.Name;
NativeName = composition.NativeName;
Type = composition.Type;
if (composition.Description != null) Description = new JsonDescription(composition.Description);
Expand Down
53 changes: 27 additions & 26 deletions libraries/MTConnect.NET-JSON-cppagent/Devices/JsonDataItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class JsonDataItem
public string CoordinateSystemIdRef { get; set; }

[JsonPropertyName("name")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string Name { get; set; }

[JsonPropertyName("nativeScale")]
Expand Down Expand Up @@ -84,7 +85,7 @@ public JsonDataItem(IDataItem dataItem)
{
DataItemCategory = dataItem.Category.ToString();
Id = dataItem.Id;
Name = dataItem.Name;
if (!string.IsNullOrEmpty(dataItem.Name)) Name = dataItem.Name;
Type = dataItem.Type;
SubType = dataItem.SubType;
NativeUnits = dataItem.NativeUnits;
Expand All @@ -98,32 +99,32 @@ public JsonDataItem(IDataItem dataItem)
var relationships = new JsonRelationshipContainer();
foreach (var relationship in dataItem.Relationships)
{
// ComponentRelationship
if (typeof(IComponentRelationship).IsAssignableFrom(relationship.GetType()))
// Use a switch with pattern matching so each relationship is
// classified by a single is-check + cast, instead of four
// typeof().IsAssignableFrom(GetType()) reflection probes per
// element. Behavior is preserved per
// JsonDataItemRelationshipCategorizationTests.
switch (relationship)
{
if (relationships.ComponentRelationships == null) relationships.ComponentRelationships = new List<JsonRelationship>();
relationships.ComponentRelationships.Add(new JsonRelationship((IComponentRelationship)relationship));
}

// DataItemRelationship
if (typeof(IDataItemRelationship).IsAssignableFrom(relationship.GetType()))
{
if (relationships.DataItemRelationships == null) relationships.DataItemRelationships = new List<JsonRelationship>();
relationships.DataItemRelationships.Add(new JsonRelationship((IDataItemRelationship)relationship));
}

// DeviceRelationship
if (typeof(IDeviceRelationship).IsAssignableFrom(relationship.GetType()))
{
if (relationships.DeviceRelationships == null) relationships.DeviceRelationships = new List<JsonRelationship>();
relationships.DeviceRelationships.Add(new JsonRelationship((IDeviceRelationship)relationship));
}

// SpecificationRelationship
if (typeof(ISpecificationRelationship).IsAssignableFrom(relationship.GetType()))
{
if (relationships.SpecificationRelationships == null) relationships.SpecificationRelationships = new List<JsonRelationship>();
relationships.SpecificationRelationships.Add(new JsonRelationship((ISpecificationRelationship)relationship));
case IComponentRelationship componentRelationship:
if (relationships.ComponentRelationships == null) relationships.ComponentRelationships = new List<JsonRelationship>();
relationships.ComponentRelationships.Add(new JsonRelationship(componentRelationship));
break;

case IDataItemRelationship dataItemRelationship:
if (relationships.DataItemRelationships == null) relationships.DataItemRelationships = new List<JsonRelationship>();
relationships.DataItemRelationships.Add(new JsonRelationship(dataItemRelationship));
break;

case IDeviceRelationship deviceRelationship:
if (relationships.DeviceRelationships == null) relationships.DeviceRelationships = new List<JsonRelationship>();
relationships.DeviceRelationships.Add(new JsonRelationship(deviceRelationship));
break;

case ISpecificationRelationship specificationRelationship:
if (relationships.SpecificationRelationships == null) relationships.SpecificationRelationships = new List<JsonRelationship>();
relationships.SpecificationRelationships.Add(new JsonRelationship(specificationRelationship));
break;
}
}
Relationships = relationships;
Expand Down
52 changes: 26 additions & 26 deletions libraries/MTConnect.NET-JSON/Devices/JsonDataItem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public JsonDataItem(IDataItem dataItem)
{
DataItemCategory = dataItem.Category.ToString();
Id = dataItem.Id;
Name = dataItem.Name;
if (!string.IsNullOrEmpty(dataItem.Name)) Name = dataItem.Name;
Type = dataItem.Type;
SubType = dataItem.SubType;
NativeUnits = dataItem.NativeUnits;
Expand All @@ -98,32 +98,32 @@ public JsonDataItem(IDataItem dataItem)
var relationships = new JsonRelationshipContainer();
foreach (var relationship in dataItem.Relationships)
{
// ComponentRelationship
if (typeof(IComponentRelationship).IsAssignableFrom(relationship.GetType()))
// Use a switch with pattern matching so each relationship is
// classified by a single is-check + cast, instead of four
// typeof().IsAssignableFrom(GetType()) reflection probes per
// element. Behavior is preserved per
// JsonDataItemRelationshipCategorizationTests.
switch (relationship)
{
if (relationships.ComponentRelationships == null) relationships.ComponentRelationships = new List<JsonRelationship>();
relationships.ComponentRelationships.Add(new JsonRelationship((IComponentRelationship)relationship));
}

// DataItemRelationship
if (typeof(IDataItemRelationship).IsAssignableFrom(relationship.GetType()))
{
if (relationships.DataItemRelationships == null) relationships.DataItemRelationships = new List<JsonRelationship>();
relationships.DataItemRelationships.Add(new JsonRelationship((IDataItemRelationship)relationship));
}

// DeviceRelationship
if (typeof(IDeviceRelationship).IsAssignableFrom(relationship.GetType()))
{
if (relationships.DeviceRelationships == null) relationships.DeviceRelationships = new List<JsonRelationship>();
relationships.DeviceRelationships.Add(new JsonRelationship((IDeviceRelationship)relationship));
}

// SpecificationRelationship
if (typeof(ISpecificationRelationship).IsAssignableFrom(relationship.GetType()))
{
if (relationships.SpecificationRelationships == null) relationships.SpecificationRelationships = new List<JsonRelationship>();
relationships.SpecificationRelationships.Add(new JsonRelationship((ISpecificationRelationship)relationship));
case IComponentRelationship componentRelationship:
if (relationships.ComponentRelationships == null) relationships.ComponentRelationships = new List<JsonRelationship>();
relationships.ComponentRelationships.Add(new JsonRelationship(componentRelationship));
break;

case IDataItemRelationship dataItemRelationship:
if (relationships.DataItemRelationships == null) relationships.DataItemRelationships = new List<JsonRelationship>();
relationships.DataItemRelationships.Add(new JsonRelationship(dataItemRelationship));
break;

case IDeviceRelationship deviceRelationship:
if (relationships.DeviceRelationships == null) relationships.DeviceRelationships = new List<JsonRelationship>();
relationships.DeviceRelationships.Add(new JsonRelationship(deviceRelationship));
break;

case ISpecificationRelationship specificationRelationship:
if (relationships.SpecificationRelationships == null) relationships.SpecificationRelationships = new List<JsonRelationship>();
relationships.SpecificationRelationships.Add(new JsonRelationship(specificationRelationship));
break;
}
}
Relationships = relationships;
Expand Down
6 changes: 5 additions & 1 deletion tests/IntegrationTests/ClientAgentCommunicationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,11 @@ public ClientAgentCommunicationTests(

var configuration = new HttpServerConfiguration
{
Port = _fixture.CurrentAgentPort
Port = _fixture.CurrentAgentPort,
// Bind to loopback only so an in-process integration run
// cannot accidentally expose the test agent on a
// non-loopback interface of the dev machine.
Server = "127.0.0.1"
};
_server = new MTConnectHttpServer(configuration, _agent);
_server.Start();
Expand Down
65 changes: 65 additions & 0 deletions tests/IntegrationTests/HttpServerLoopbackBindingTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
// TrakHound Inc. licenses this file to you under the MIT license.

using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using Xunit;

namespace IntegrationTests
{
/// <summary>
/// Source-grep regression guard for the loopback-only binding contract.
/// Pins that <see cref="MTConnect.Configurations.HttpServerConfiguration"/>
/// in <c>ClientAgentCommunicationTests</c> binds the embedded HTTP
/// server to loopback (<c>127.0.0.1</c>) so an in-process
/// integration run cannot accidentally expose the test agent on a
/// non-loopback interface of the dev machine.
///
/// Pinned via file content rather than a runtime assertion because
/// the fixture brings the server up in its constructor and we want
/// the guard to fail at unit-test time (not when the server is
/// already bound to the wrong interface).
/// </summary>
public class HttpServerLoopbackBindingTests
{
private static string LocateRepoRoot()
{
var dir = new DirectoryInfo(
Path.GetDirectoryName(typeof(HttpServerLoopbackBindingTests).Assembly.Location)!);
while (dir != null)
{
if (File.Exists(Path.Combine(dir.FullName, "MTConnect.NET.sln")))
return dir.FullName;

dir = dir.Parent!;
}

throw new DirectoryNotFoundException(
"Could not locate MTConnect.NET.sln walking up from the test assembly.");
}

[Fact]
public void ClientAgentCommunicationTests_HttpServerConfiguration_binds_to_loopback()
{
var path = Path.Combine(LocateRepoRoot(),
"tests", "IntegrationTests", "ClientAgentCommunicationTests.cs");

Assert.True(File.Exists(path), $"Expected source file at {path}");

var src = File.ReadAllText(path);

// The fixture must initialize HttpServerConfiguration with the
// Server property pinned to "127.0.0.1" inside the same object
// initializer that sets Port. A {...} block setting Port + Server
// satisfies the pin regardless of declaration order.
var loopbackInitializer = new Regex(
@"new\s+HttpServerConfiguration\s*\{[^}]*Server\s*=\s*""127\.0\.0\.1""[^}]*\}",
RegexOptions.Singleline);

Assert.True(loopbackInitializer.IsMatch(src),
"ClientAgentCommunicationTests.cs must construct HttpServerConfiguration with Server = \"127.0.0.1\" " +
"so the embedded HTTP server binds to loopback only.");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright (c) 2026 TrakHound Inc., All Rights Reserved.
// TrakHound Inc. licenses this file to you under the MIT license.

using System.Text.Json;
using MTConnect.Devices;
using MTConnect.Devices.DataItems;
using MTConnect.Devices.Json;
using NUnit.Framework;

namespace MTConnect.Tests.Json.Devices
{
/// <summary>
/// Pins the base JSON Probe DataItem behavior: the `name` JSON property must be
/// omitted when the source IDataItem.Name is null or empty, and emitted with the
/// original value otherwise.
///
/// Source authority:
/// - XSD: https://schemas.mtconnect.org/schemas/MTConnectDevices_2.5.xsd —
/// `DataItem/@name` is declared `use="optional"`. An optional attribute with no
/// value must be omitted from the wire, not emitted as an empty placeholder.
/// - Reference shape: libraries/MTConnect.NET-XML/Devices/XmlDataItem.cs already
/// guards the XML attribute write with `string.IsNullOrEmpty(dataItem.Name)`.
/// - Public defect tracker: https://github.com/TrakHound/MTConnect.NET/issues/138.
/// </summary>
[TestFixture]
[Category("DataItemNameOmissionWhenUnsetOrEmpty")]
public class JsonDataItemEmptyNameOmissionTests
{
[Test]
public void Constructor_with_null_Name_source_does_not_serialize_name_key()
{
var source = new DataItem
{
Id = "item-1",
Type = "TEMPERATURE",
Category = DataItemCategory.SAMPLE,
Name = null
};

var json = new JsonDataItem(source).ToString();
using var doc = JsonDocument.Parse(json);

Assert.That(doc.RootElement.TryGetProperty("name", out _), Is.False,
"Base JSON Probe DataItem must omit 'name' when source Name is null");
}

[Test]
public void Constructor_with_empty_Name_source_does_not_serialize_name_key()
{
var source = new DataItem
{
Id = "item-2",
Type = "TEMPERATURE",
Category = DataItemCategory.SAMPLE,
Name = string.Empty
};

var json = new JsonDataItem(source).ToString();
using var doc = JsonDocument.Parse(json);

Assert.That(doc.RootElement.TryGetProperty("name", out _), Is.False,
"Base JSON Probe DataItem must omit 'name' when source Name is empty");
}

[Test]
public void Constructor_with_explicit_Name_source_serializes_name_key()
{
var source = new DataItem
{
Id = "item-3",
Type = "TEMPERATURE",
Category = DataItemCategory.SAMPLE,
Name = "temp"
};

var json = new JsonDataItem(source).ToString();
using var doc = JsonDocument.Parse(json);

Assert.That(doc.RootElement.TryGetProperty("name", out var nameElement), Is.True,
"Base JSON Probe DataItem must emit 'name' when source Name has a value");
Assert.That(nameElement.GetString(), Is.EqualTo("temp"));
}

[Test]
public void Constructor_with_typed_DataItem_unset_Name_does_not_serialize_name_key()
{
var source = new TemperatureDataItem
{
Id = "item-4",
Name = string.Empty
};

var json = new JsonDataItem(source).ToString();
using var doc = JsonDocument.Parse(json);

Assert.That(doc.RootElement.TryGetProperty("name", out _), Is.False,
"Cleared Name on a typed DataItem must produce no 'name' key");
}
}
}
Loading