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>
139 lines
5 KiB
C#
139 lines
5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Junction.Domain;
|
|
using Junction.Domain.Models;
|
|
using Junction.Domain.Protocols;
|
|
using Moq;
|
|
using Xunit;
|
|
|
|
namespace Junction.Tests.Unit
|
|
{
|
|
/// <summary>
|
|
/// Contract/shape tests for the plugin protocol contract. Pure compile + behavior
|
|
/// against Moq doubles; no real IO.
|
|
/// </summary>
|
|
public class ProtocolContractTests
|
|
{
|
|
private static Machine SampleMachine(IReadOnlyDictionary<string, string>? config = null) =>
|
|
new Machine(
|
|
Guid.NewGuid(),
|
|
"M1",
|
|
"mtconnect",
|
|
config,
|
|
TimeSpan.FromSeconds(1));
|
|
|
|
private static MachineSnapshot SampleSnapshot(Guid machineId) =>
|
|
new MachineSnapshot(
|
|
machineId,
|
|
DateTimeOffset.UtcNow,
|
|
ConnectionState.Connected,
|
|
Array.Empty<DataItem>());
|
|
|
|
[Fact]
|
|
public void Driver_ProtocolId_ReturnsConfiguredValue()
|
|
{
|
|
var mock = new Mock<IProtocolDriver>();
|
|
mock.SetupGet(d => d.ProtocolId).Returns("mtconnect");
|
|
|
|
Assert.Equal("mtconnect", mock.Object.ProtocolId);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Driver_ReadCurrentAsync_Ok_ReturnsSnapshotResult()
|
|
{
|
|
var machineId = Guid.NewGuid();
|
|
var snapshot = SampleSnapshot(machineId);
|
|
var mock = new Mock<IProtocolDriver>();
|
|
mock.Setup(d => d.ReadCurrentAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(Result<MachineSnapshot>.Ok(snapshot));
|
|
|
|
Result<MachineSnapshot> result = await mock.Object.ReadCurrentAsync(CancellationToken.None);
|
|
|
|
Assert.True(result.IsSuccess);
|
|
Assert.Same(snapshot, result.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Driver_ReadCurrentAsync_Fail_ReturnsFailedResult()
|
|
{
|
|
var error = OperationError.Of("driver", "read failed");
|
|
var mock = new Mock<IProtocolDriver>();
|
|
mock.Setup(d => d.ReadCurrentAsync(It.IsAny<CancellationToken>()))
|
|
.ReturnsAsync(Result<MachineSnapshot>.Fail(error));
|
|
|
|
Result<MachineSnapshot> result = await mock.Object.ReadCurrentAsync(CancellationToken.None);
|
|
|
|
Assert.False(result.IsSuccess);
|
|
Assert.NotEmpty(result.Errors);
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Driver_ReadCurrentAsync_ForwardsCancellationToken()
|
|
{
|
|
using var cts = new CancellationTokenSource();
|
|
var mock = new Mock<IProtocolDriver>();
|
|
mock.Setup(d => d.ReadCurrentAsync(cts.Token))
|
|
.ReturnsAsync(Result<MachineSnapshot>.Cancelled());
|
|
|
|
Result<MachineSnapshot> result = await mock.Object.ReadCurrentAsync(cts.Token);
|
|
|
|
Assert.True(result.WasCancelled);
|
|
mock.Verify(d => d.ReadCurrentAsync(cts.Token), Times.Once);
|
|
}
|
|
|
|
[Fact]
|
|
public void Factory_ProtocolId_ReturnsConfiguredValue()
|
|
{
|
|
var mock = new Mock<IProtocolDriverFactory>();
|
|
mock.SetupGet(f => f.ProtocolId).Returns("mtconnect");
|
|
|
|
Assert.Equal("mtconnect", mock.Object.ProtocolId);
|
|
}
|
|
|
|
[Fact]
|
|
public void Factory_Create_Ok_WrapsDriver()
|
|
{
|
|
var driver = new Mock<IProtocolDriver>().Object;
|
|
var factory = new Mock<IProtocolDriverFactory>();
|
|
factory.Setup(f => f.Create(It.IsAny<Machine>()))
|
|
.Returns(Result<IProtocolDriver>.Ok(driver));
|
|
|
|
Result<IProtocolDriver> result = factory.Object.Create(SampleMachine());
|
|
|
|
Assert.True(result.IsSuccess);
|
|
Assert.Same(driver, result.Value);
|
|
}
|
|
|
|
[Fact]
|
|
public void Factory_Create_InvalidConfig_ReturnsFail()
|
|
{
|
|
// Simulate protocol-specific validation: missing required key => Fail.
|
|
var factory = new Mock<IProtocolDriverFactory>();
|
|
factory.Setup(f => f.Create(It.Is<Machine>(m => !m.ConnectionConfig.ContainsKey("endpoint"))))
|
|
.Returns(Result<IProtocolDriver>.Fail(
|
|
OperationError.Of("factory", "missing 'endpoint'")));
|
|
|
|
Result<IProtocolDriver> result = factory.Object.Create(SampleMachine());
|
|
|
|
Assert.False(result.IsSuccess);
|
|
Assert.NotEmpty(result.Errors);
|
|
}
|
|
|
|
[Fact]
|
|
public void Factory_Create_ValidConfig_ReturnsOk()
|
|
{
|
|
var driver = new Mock<IProtocolDriver>().Object;
|
|
var factory = new Mock<IProtocolDriverFactory>();
|
|
factory.Setup(f => f.Create(It.Is<Machine>(m => m.ConnectionConfig.ContainsKey("endpoint"))))
|
|
.Returns(Result<IProtocolDriver>.Ok(driver));
|
|
|
|
var machine = SampleMachine(new Dictionary<string, string> { ["endpoint"] = "http://x" });
|
|
Result<IProtocolDriver> result = factory.Object.Create(machine);
|
|
|
|
Assert.True(result.IsSuccess);
|
|
Assert.Same(driver, result.Value);
|
|
}
|
|
}
|
|
}
|