110 lines
3.9 KiB
C#
110 lines
3.9 KiB
C#
using System.Diagnostics;
|
|
using Xunit;
|
|
using Xunit.Sdk;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// Shared fixture that starts libplctag's <c>ab_server</c> simulator in the background for
|
|
/// the duration of an integration test collection. Binary is expected on PATH; the per-test
|
|
/// JSON profile is passed via <c>--config</c>.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para><c>ab_server</c> is a C binary shipped in the same repo as libplctag (see
|
|
/// <c>test-data-sources.md</c> §2 and plan decision #99). On a developer workstation it's
|
|
/// built once from source and placed on PATH; in CI we intend to publish a prebuilt Windows
|
|
/// x64 binary as a GitHub release asset in a follow-up PR so the fixture can download +
|
|
/// extract it at setup time. Until then every test in this project is skipped when
|
|
/// <c>ab_server</c> is not locatable.</para>
|
|
///
|
|
/// <para>Per-family JSON profiles (ControlLogix / CompactLogix / Micro800 / GuardLogix)
|
|
/// ship under <c>Profiles/</c> and drive the simulator's tag shape — this is where the
|
|
/// UDT + Program-scope coverage gap will be filled by the hand-rolled stub in PR 6.</para>
|
|
/// </remarks>
|
|
public sealed class AbServerFixture : IAsyncLifetime
|
|
{
|
|
private Process? _proc;
|
|
public int Port { get; } = 44818;
|
|
|
|
public bool IsAvailable { get; private set; }
|
|
|
|
public ValueTask InitializeAsync() => InitializeAsync(default);
|
|
public ValueTask DisposeAsync() => DisposeAsync(default);
|
|
|
|
public async ValueTask InitializeAsync(CancellationToken cancellationToken)
|
|
{
|
|
if (LocateBinary() is not string binary)
|
|
{
|
|
IsAvailable = false;
|
|
return;
|
|
}
|
|
IsAvailable = true;
|
|
|
|
_proc = new Process
|
|
{
|
|
StartInfo = new ProcessStartInfo
|
|
{
|
|
FileName = binary,
|
|
Arguments = $"--port {Port} --plc controllogix",
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true,
|
|
},
|
|
};
|
|
_proc.Start();
|
|
|
|
// Give the server a moment to accept its listen socket before tests try to connect.
|
|
await Task.Delay(500, cancellationToken).ConfigureAwait(false);
|
|
}
|
|
|
|
public ValueTask DisposeAsync(CancellationToken cancellationToken)
|
|
{
|
|
try
|
|
{
|
|
if (_proc is { HasExited: false })
|
|
{
|
|
_proc.Kill(entireProcessTree: true);
|
|
_proc.WaitForExit(5_000);
|
|
}
|
|
}
|
|
catch { /* best-effort cleanup */ }
|
|
_proc?.Dispose();
|
|
return ValueTask.CompletedTask;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Locate <c>ab_server</c> on PATH. Returns <c>null</c> when missing — tests that
|
|
/// depend on it should use <see cref="AbServerFact"/> so CI runs without the binary
|
|
/// simply skip rather than fail.
|
|
/// </summary>
|
|
public static string? LocateBinary()
|
|
{
|
|
var names = new[] { "ab_server.exe", "ab_server" };
|
|
var path = Environment.GetEnvironmentVariable("PATH") ?? "";
|
|
foreach (var dir in path.Split(Path.PathSeparator))
|
|
{
|
|
foreach (var name in names)
|
|
{
|
|
var candidate = Path.Combine(dir, name);
|
|
if (File.Exists(candidate)) return candidate;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// <c>[Fact]</c>-equivalent that skips when <c>ab_server</c> is not available on PATH.
|
|
/// Integration tests use this instead of <c>[Fact]</c> so a developer box without
|
|
/// <c>ab_server</c> installed still gets a green run.
|
|
/// </summary>
|
|
public sealed class AbServerFactAttribute : FactAttribute
|
|
{
|
|
public AbServerFactAttribute()
|
|
{
|
|
if (AbServerFixture.LocateBinary() is null)
|
|
Skip = "ab_server not on PATH; install libplctag test binaries to run.";
|
|
}
|
|
}
|