chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the Rider Solution Explorer mirrors the module structure. Folders: Core, Server, Drivers (with a nested Driver CLIs subfolder), Client, Tooling. - Move every project folder on disk with git mv (history preserved as renames). - Recompute relative paths in 57 .csproj files: cross-category ProjectReferences, the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external mxaccessgw refs in Driver.Galaxy and its test project. - Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders. - Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL, integration, install). Build green (0 errors); unit tests pass. Docs left for a separate pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
using System.Net.Sockets;
|
||||
using Xunit;
|
||||
using Xunit.Sdk;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.TwinCAT.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Reachability probe for a TwinCAT 3 XAR runtime on a Hyper-V VM or dedicated
|
||||
/// Windows box. TCP-probes ADS port 48898 on the operator-supplied host. Tests
|
||||
/// skip via <see cref="TwinCATFactAttribute"/> / <see cref="TwinCATTheoryAttribute"/>
|
||||
/// when the runtime isn't reachable, so <c>dotnet test</c> on a fresh clone without
|
||||
/// a TwinCAT VM stays green. Matches the
|
||||
/// <see cref="Modbus.IntegrationTests.ModbusSimulatorFixture"/> /
|
||||
/// <see cref="S7.IntegrationTests.Snap7ServerFixture"/> /
|
||||
/// <c>OpcPlcFixture</c> / <c>AbServerFixture</c> patterns.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Why a VM, not a container</b>: TwinCAT XAR bypasses the Windows
|
||||
/// kernel scheduler to hit real-time PLC cycles. It can't run inside Docker, and
|
||||
/// on bare metal it conflicts with Hyper-V / WSL 2 — that's why this repo's dev
|
||||
/// environment puts XAR in a dedicated Hyper-V VM per
|
||||
/// <c>docs/v2/dev-environment.md</c> §Integration host. The fixture treats the VM
|
||||
/// as a black-box ADS endpoint reachable over TCP.</para>
|
||||
///
|
||||
/// <para><b>License rotation</b>: the free XAR trial license expires every 7 days.
|
||||
/// When it lapses the runtime goes silent + the fixture's TCP probe fails; tests
|
||||
/// skip with the reason message until the operator renews via
|
||||
/// <c>TcActivate.exe /reactivate</c> (or buys a paid runtime). Intentionally surfaces
|
||||
/// as a skip rather than a hang because "trial expired" is operator action, not a
|
||||
/// test failure.</para>
|
||||
///
|
||||
/// <para><b>Env var overrides</b>:
|
||||
/// <list type="bullet">
|
||||
/// <item><c>TWINCAT_TARGET_HOST</c> — IP or hostname of the XAR VM (default
|
||||
/// <c>localhost</c>, assumed to be unset on the average dev box + result in a
|
||||
/// clean skip).</item>
|
||||
/// <item><c>TWINCAT_TARGET_NETID</c> — AMS NetId the tests address (e.g.
|
||||
/// <c>5.23.91.23.1.1</c>). Seeded on the target VM via TwinCAT System
|
||||
/// Manager → Routes; the dev box's AmsNetId also needs a bilateral route
|
||||
/// entry on the VM side. No sensible default — tests skip if unset.</item>
|
||||
/// <item><c>TWINCAT_TARGET_PORT</c> — ADS target port (default <c>851</c> =
|
||||
/// TC3 PLC runtime 1). Set to <c>852</c> for runtime 2, etc.</item>
|
||||
/// </list></para>
|
||||
/// </remarks>
|
||||
public sealed class TwinCATXarFixture : IAsyncLifetime
|
||||
{
|
||||
private const string HostEnvVar = "TWINCAT_TARGET_HOST";
|
||||
private const string NetIdEnvVar = "TWINCAT_TARGET_NETID";
|
||||
private const string PortEnvVar = "TWINCAT_TARGET_PORT";
|
||||
|
||||
/// <summary>ADS-over-TCP port on the XAR host. Not the PLC runtime port (that's
|
||||
/// <see cref="AmsPort"/>).</summary>
|
||||
public const int AdsTcpPort = 48898;
|
||||
|
||||
/// <summary>TC3 PLC runtime 1. Override via <see cref="PortEnvVar"/>.</summary>
|
||||
public const int DefaultAmsPort = 851;
|
||||
|
||||
public string TargetHost { get; }
|
||||
public string? TargetNetId { get; }
|
||||
public int AmsPort { get; }
|
||||
public string? SkipReason { get; }
|
||||
|
||||
public TwinCATXarFixture()
|
||||
{
|
||||
TargetHost = Environment.GetEnvironmentVariable(HostEnvVar) ?? "localhost";
|
||||
TargetNetId = Environment.GetEnvironmentVariable(NetIdEnvVar);
|
||||
AmsPort = int.TryParse(Environment.GetEnvironmentVariable(PortEnvVar), out var p)
|
||||
? p : DefaultAmsPort;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(TargetNetId))
|
||||
{
|
||||
SkipReason = $"TwinCAT XAR unreachable: {NetIdEnvVar} is not set. " +
|
||||
$"Start the XAR VM + set {HostEnvVar}=<vm-ip> and {NetIdEnvVar}=<vm-ams-netid>.";
|
||||
return;
|
||||
}
|
||||
|
||||
if (!TcpProbe(TargetHost, AdsTcpPort, TimeSpan.FromSeconds(2)))
|
||||
{
|
||||
SkipReason = $"TwinCAT XAR at {TargetHost}:{AdsTcpPort} not reachable within 2 s. " +
|
||||
$"Verify the XAR VM is running, its trial license hasn't expired " +
|
||||
$"(run TcActivate.exe /reactivate on the VM), and {HostEnvVar}/{NetIdEnvVar} point at it.";
|
||||
}
|
||||
}
|
||||
|
||||
public ValueTask InitializeAsync() => ValueTask.CompletedTask;
|
||||
public ValueTask DisposeAsync() => ValueTask.CompletedTask;
|
||||
|
||||
/// <summary><c>true</c> when the XAR runtime is reachable + the AmsNetId is set.
|
||||
/// Used by the skip attributes to avoid spinning up the fixture for every test
|
||||
/// class.</summary>
|
||||
public static bool IsRuntimeAvailable()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(NetIdEnvVar))) return false;
|
||||
var host = Environment.GetEnvironmentVariable(HostEnvVar) ?? "localhost";
|
||||
return TcpProbe(host, AdsTcpPort, TimeSpan.FromMilliseconds(500));
|
||||
}
|
||||
|
||||
private static bool TcpProbe(string host, int port, TimeSpan timeout)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var client = new TcpClient();
|
||||
var task = client.ConnectAsync(host, port);
|
||||
return task.Wait(timeout) && client.Connected;
|
||||
}
|
||||
catch { return false; }
|
||||
}
|
||||
}
|
||||
|
||||
[Xunit.CollectionDefinition(Name)]
|
||||
public sealed class TwinCATXarCollection : Xunit.ICollectionFixture<TwinCATXarFixture>
|
||||
{
|
||||
public const string Name = "TwinCATXar";
|
||||
}
|
||||
|
||||
/// <summary><c>[Fact]</c>-equivalent gated on <see cref="TwinCATXarFixture.IsRuntimeAvailable"/>.</summary>
|
||||
public sealed class TwinCATFactAttribute : FactAttribute
|
||||
{
|
||||
public TwinCATFactAttribute()
|
||||
{
|
||||
if (!TwinCATXarFixture.IsRuntimeAvailable())
|
||||
Skip = "TwinCAT XAR not reachable. See docs/drivers/TwinCAT-Test-Fixture.md " +
|
||||
"for setup; typical cause is the trial license expired or TWINCAT_TARGET_NETID is unset.";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary><c>[Theory]</c>-equivalent with the same gate as <see cref="TwinCATFactAttribute"/>.</summary>
|
||||
public sealed class TwinCATTheoryAttribute : TheoryAttribute
|
||||
{
|
||||
public TwinCATTheoryAttribute()
|
||||
{
|
||||
if (!TwinCATXarFixture.IsRuntimeAvailable())
|
||||
Skip = "TwinCAT XAR not reachable. See docs/drivers/TwinCAT-Test-Fixture.md " +
|
||||
"for setup; typical cause is the trial license expired or TWINCAT_TARGET_NETID is unset.";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user