junction/tests/Junction.Tests/Unit/MtconnectParserTests.cs
dtrentin 507753f82e init: scaffold Junction multi-protocol machine monitor (M1 walking skeleton)
Greenfield C# app reading industrial machine data over pluggable protocols.
M1 walking skeleton: MTConnect plugin polls a machine, Avalonia dashboard
shows live last datum. Build 0 warn/0 err on net48 + net8.0 (Linux),
115 unit tests + 2 docker-gated integration tests.

Projects:
- Junction.Domain (netstandard2.0): Result<T>, models, IProtocolDriver,
  IMachineRepository, plugin manifest. Zero package deps.
- Junction.Core (netstandard2.0): PollingEngine, PluginLoader
  (Assembly.LoadFrom), MachineMonitor, DI extensions.
- Junction.Persistence (netstandard2.0): SqliteMachineRepository (Dapper),
  schema, connection factory. Provider-swap seam to SQL Server.
- Junction.Protocols.MTConnect (netstandard2.0): HTTP driver + namespace-
  version-agnostic parser (MTConnect 1.7 + 2.0). Runtime plugin.
- Junction.App (net48;net8.0): Avalonia MVVM, live dashboard, NLog, DI root.
- Junction.Tests (net8.0): xUnit + Moq, 117 tests.
- mock/: docker-compose MTConnect agent (ladder99/agent).

Target net48 for Windows 7/8 fleet compatibility. Avalonia pinned 11.3.x.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-21 23:32:56 +02:00

164 lines
5.5 KiB
C#

using System;
using System.IO;
using System.Linq;
using Junction.Domain.Models;
using Junction.Protocols.MTConnect.Parsing;
using Xunit;
namespace Junction.Tests.Unit
{
public sealed class MtconnectParserTests
{
private static readonly Guid MachineId = Guid.Parse("11111111-2222-3333-4444-555555555555");
private static string LoadFixture(string name)
{
var path = Path.Combine(AppContext.BaseDirectory, "Fixtures", "mtconnect", name);
return File.ReadAllText(path);
}
// ---- PROBE ----
[Fact]
public void Probe_ParsesDataItemDescriptors()
{
var result = MtconnectProbeParser.Parse(LoadFixture("probe.xml"));
Assert.True(result.IsSuccess);
var items = result.Value;
Assert.True(items.Count > 0);
// Spot-check a known DataItem from the fixture.
var pos = items.SingleOrDefault(d => d.Id == "x1_pos");
Assert.NotNull(pos);
Assert.Equal("POSITION", pos!.Type);
Assert.Equal("SAMPLE", pos.Category);
Assert.Equal("ACTUAL", pos.SubType);
Assert.Equal("MILLIMETER", pos.Units);
Assert.Equal("dev1", pos.DeviceId);
Assert.Equal("junction-vmc-01", pos.DeviceUuid);
// Availability descriptor present.
Assert.Contains(items, d => d.Id == "dev1_avail" && d.Type == "AVAILABILITY");
}
[Fact]
public void Probe_MalformedRoot_Fails()
{
var result = MtconnectProbeParser.Parse("<NotAProbe/>");
Assert.False(result.IsSuccess);
Assert.False(result.WasCancelled);
Assert.NotEmpty(result.Errors);
}
// ---- CURRENT ----
[Fact]
public void Current_ParsesSnapshot_WithKnownObservations()
{
var result = MtconnectCurrentParser.Parse(LoadFixture("current.xml"), MachineId);
Assert.True(result.IsSuccess);
var snapshot = result.Value;
Assert.Equal(MachineId, snapshot.MachineId);
Assert.False(snapshot.IsEmpty);
// Known position value.
var xPos = snapshot.TryGetItem("x1_pos");
Assert.NotNull(xPos);
Assert.Equal("125.4300", xPos!.Value);
Assert.Equal("Position", xPos.Name);
Assert.Equal("Samples", xPos.Category);
// EXECUTION = ACTIVE.
var exec = snapshot.TryGetItem("path1_exec");
Assert.NotNull(exec);
Assert.Equal("ACTIVE", exec!.Value);
// Condition state carried as value.
var tempCond = snapshot.TryGetItem("c1_temp_cond");
Assert.NotNull(tempCond);
Assert.Equal("Condition", tempCond!.Category);
Assert.Equal("Normal", tempCond.Value);
// AVAILABILITY -> Connected.
Assert.Equal(ConnectionState.Connected, snapshot.ConnectionState);
// CapturedAt = latest observed timestamp (10240 seq @ .240Z).
Assert.Equal(
DateTimeOffset.Parse("2026-07-21T09:15:30.240Z"),
snapshot.CapturedAt);
}
[Fact]
public void Current_Unavailable_YieldsDisconnected()
{
var result = MtconnectCurrentParser.Parse(LoadFixture("current_unavailable.xml"), MachineId);
Assert.True(result.IsSuccess);
Assert.Equal(ConnectionState.Disconnected, result.Value.ConnectionState);
}
[Fact]
public void Current_Malformed_FailsWithoutThrowing()
{
// current_malformed.xml is truncated (unclosed elements) -> XmlException internally.
var result = MtconnectCurrentParser.Parse(LoadFixture("current_malformed.xml"), MachineId);
Assert.False(result.IsSuccess);
Assert.False(result.WasCancelled);
Assert.NotEmpty(result.Errors);
}
// ---- VERSION-AGNOSTIC PROOF ----
[Fact]
public void Current_IsNamespaceVersionAgnostic()
{
var v17 = LoadFixture("current.xml");
var v20 = v17.Replace(":1.7", ":2.0");
Assert.Contains(":2.0", v20);
Assert.DoesNotContain(":1.7", v20);
var r17 = MtconnectCurrentParser.Parse(v17, MachineId);
var r20 = MtconnectCurrentParser.Parse(v20, MachineId);
Assert.True(r17.IsSuccess);
Assert.True(r20.IsSuccess);
var s17 = r17.Value;
var s20 = r20.Value;
Assert.Equal(s17.Items.Count, s20.Items.Count);
Assert.Equal(s17.ConnectionState, s20.ConnectionState);
Assert.Equal(s17.CapturedAt, s20.CapturedAt);
// Item-by-item identical (id, value, category, timestamp).
foreach (var a in s17.Items)
{
var b = s20.TryGetItem(a.Id);
Assert.NotNull(b);
Assert.Equal(a.Value, b!.Value);
Assert.Equal(a.Category, b.Category);
Assert.Equal(a.Name, b.Name);
Assert.Equal(a.Timestamp, b.Timestamp);
}
}
[Fact]
public void Probe_IsNamespaceVersionAgnostic()
{
var v17 = LoadFixture("probe.xml");
var v20 = v17.Replace(":1.7", ":2.0");
var r17 = MtconnectProbeParser.Parse(v17);
var r20 = MtconnectProbeParser.Parse(v20);
Assert.True(r17.IsSuccess);
Assert.True(r20.IsSuccess);
Assert.Equal(r17.Value.Count, r20.Value.Count);
}
}
}