140 lines
5.3 KiB
C#
140 lines
5.3 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. The fixture takes an
|
|
/// <see cref="AbServerProfile"/> (see <see cref="KnownProfiles"/>) so each AB family — ControlLogix,
|
|
/// CompactLogix, Micro800, GuardLogix — starts the simulator with the right <c>--plc</c>
|
|
/// mode + preseed tag set. Binary is expected on PATH; CI resolves that via a job step
|
|
/// that downloads the pinned Windows build from libplctag GitHub Releases before
|
|
/// <c>dotnet test</c> — see <c>docs/v2/test-data-sources.md §2.CI</c> for the exact step.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// <para><c>ab_server</c> is a C binary shipped in libplctag's repo (MIT). On developer
|
|
/// workstations it's built once from source and placed on PATH; on CI the workflow file
|
|
/// fetches a version-pinned prebuilt + stages it. Tests skip (via
|
|
/// <see cref="AbServerFactAttribute"/>) when the binary is not on PATH so a fresh clone
|
|
/// without the simulator still gets a green unit-test run.</para>
|
|
///
|
|
/// <para>Per-family profiles live in <see cref="KnownProfiles"/>. When a test wants a
|
|
/// specific family, instantiate the fixture with that profile — either via a
|
|
/// <see cref="IClassFixture{TFixture}"/> derived type or by constructing directly in a
|
|
/// parametric test (the latter is used below for the smoke suite).</para>
|
|
/// </remarks>
|
|
public sealed class AbServerFixture : IAsyncLifetime
|
|
{
|
|
private Process? _proc;
|
|
|
|
/// <summary>The profile the simulator was started with. Same instance the driver-side options should use.</summary>
|
|
public AbServerProfile Profile { get; }
|
|
public int Port { get; }
|
|
public bool IsAvailable { get; private set; }
|
|
|
|
public AbServerFixture() : this(KnownProfiles.ControlLogix, AbServerProfile.DefaultPort) { }
|
|
|
|
public AbServerFixture(AbServerProfile profile) : this(profile, AbServerProfile.DefaultPort) { }
|
|
|
|
public AbServerFixture(AbServerProfile profile, int port)
|
|
{
|
|
Profile = profile ?? throw new ArgumentNullException(nameof(profile));
|
|
Port = port;
|
|
}
|
|
|
|
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 = Profile.BuildCliArgs(Port),
|
|
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="AbServerFactAttribute"/> 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.";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// <c>[Theory]</c>-equivalent that skips when <c>ab_server</c> is not on PATH. Pair with
|
|
/// <c>[MemberData(nameof(KnownProfiles.All))]</c>-style providers to run one theory row per
|
|
/// profile so a single test covers all four families.
|
|
/// </summary>
|
|
public sealed class AbServerTheoryAttribute : TheoryAttribute
|
|
{
|
|
public AbServerTheoryAttribute()
|
|
{
|
|
if (AbServerFixture.LocateBinary() is null)
|
|
Skip = "ab_server not on PATH; install libplctag test binaries to run.";
|
|
}
|
|
}
|