147 lines
8.2 KiB
C#
147 lines
8.2 KiB
C#
using System.Globalization;
|
|
using ZB.MOM.WW.OtOpcUa.Driver.AbCip.PlcFamilies;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential;
|
|
|
|
/// <summary>
|
|
/// The one endpoint the read-only differential harness targets, read from the environment.
|
|
/// <list type="bullet">
|
|
/// <item><c>AB_DIFF_ENDPOINT</c> — <c>host:port</c> (port defaults to 44818). Absent ⇒ every live test skips.</item>
|
|
/// <item><c>AB_DIFF_PATH</c> — CIP path, default <c>1,0</c>.</item>
|
|
/// <item><c>AB_DIFF_FAMILY</c> — an <see cref="AbCipPlcFamily"/> name, default <c>CompactLogix</c>.</item>
|
|
/// <item><c>AB_DIFF_TAGS</c> — optional comma list of tag names; absent ⇒ selected from browse.</item>
|
|
/// <item><c>AB_DIFF_MAX_TAGS</c> — cap on the selected tags, default 200, at most 500 (higher fails fast).</item>
|
|
/// <item><c>AB_DIFF_MAX_TEMPLATES</c> — cap on template reads per transport, default 50, at most 500.</item>
|
|
/// <item><c>AB_DIFF_PACE_MS</c> — delay before every harness-issued request, default 20 ms; 0 for ab_server.</item>
|
|
/// <item><c>AB_DIFF_COUNT_OPENS</c> — <c>1</c> ⇒ assert exactly one native Forward Open over the run.</item>
|
|
/// <item><c>AB_LAT_TAGS</c> — latency comparison: tags read per pass, default 100, at most 200 (higher fails fast).</item>
|
|
/// <item><c>AB_LAT_PASSES</c> — latency comparison: passes per transport, default 30, at most 50.</item>
|
|
/// <item><c>AB_LAT_PASS_GAP_MS</c> — latency comparison: pause between passes, default 200 ms, at most 10000.</item>
|
|
/// </list>
|
|
/// </summary>
|
|
internal sealed record DifferentialTarget(
|
|
string Host,
|
|
int Port,
|
|
string CipPath,
|
|
AbCipPlcFamily Family,
|
|
IReadOnlyList<string>? Tags,
|
|
int MaxTags,
|
|
bool CountOpens,
|
|
int MaxTemplates = DifferentialTarget.DefaultMaxTemplates,
|
|
int PaceMs = DifferentialTarget.DefaultPaceMs,
|
|
int LatencyTags = DifferentialTarget.DefaultLatencyTags,
|
|
int LatencyPasses = DifferentialTarget.DefaultLatencyPasses,
|
|
int LatencyPassGapMs = DifferentialTarget.DefaultLatencyPassGapMs)
|
|
{
|
|
public const string EndpointVar = "AB_DIFF_ENDPOINT";
|
|
public const string PathVar = "AB_DIFF_PATH";
|
|
public const string FamilyVar = "AB_DIFF_FAMILY";
|
|
public const string TagsVar = "AB_DIFF_TAGS";
|
|
public const string MaxTagsVar = "AB_DIFF_MAX_TAGS";
|
|
public const string CountOpensVar = "AB_DIFF_COUNT_OPENS";
|
|
public const string MaxTemplatesVar = "AB_DIFF_MAX_TEMPLATES";
|
|
public const string PaceVar = "AB_DIFF_PACE_MS";
|
|
public const string LatencyTagsVar = "AB_LAT_TAGS";
|
|
public const string LatencyPassesVar = "AB_LAT_PASSES";
|
|
public const string LatencyPassGapVar = "AB_LAT_PASS_GAP_MS";
|
|
|
|
/// <summary>Latency comparison ceilings: a production press is the target, and each pass is one burst.</summary>
|
|
public const int LatencyTagsCeiling = 200;
|
|
public const int LatencyPassesCeiling = 50;
|
|
public const int DefaultLatencyTags = 100;
|
|
public const int DefaultLatencyPasses = 30;
|
|
public const int DefaultLatencyPassGapMs = 200;
|
|
|
|
/// <summary>Hard ceiling on <c>AB_DIFF_MAX_TAGS</c> / <c>AB_DIFF_MAX_TEMPLATES</c>: a production press is the target.</summary>
|
|
public const int Ceiling = 500;
|
|
public const int DefaultMaxTemplates = 50;
|
|
public const int DefaultPaceMs = 20;
|
|
|
|
private const int DefaultPort = 44818;
|
|
private const string DefaultPath = "1,0";
|
|
private const AbCipPlcFamily DefaultFamily = AbCipPlcFamily.CompactLogix;
|
|
private const int DefaultMaxTags = 200;
|
|
|
|
/// <summary>Per-operation timeout for both transports.</summary>
|
|
public static readonly TimeSpan OperationTimeout = TimeSpan.FromSeconds(5);
|
|
|
|
/// <summary>Skip reason used by every live test when the endpoint is not configured.</summary>
|
|
public static string SkipReason =>
|
|
$"{EndpointVar} is not set — point it at host:port of the one controller to compare (e.g. 10.100.0.35:44818 for ab_server).";
|
|
|
|
/// <summary>The configured target, or <c>null</c> when <c>AB_DIFF_ENDPOINT</c> is absent.</summary>
|
|
/// <exception cref="InvalidOperationException">An env var is present but malformed.</exception>
|
|
public static DifferentialTarget? FromEnvironment()
|
|
{
|
|
var endpoint = Environment.GetEnvironmentVariable(EndpointVar)?.Trim();
|
|
if (string.IsNullOrEmpty(endpoint)) return null;
|
|
|
|
var parts = endpoint.Split(':', 2);
|
|
var host = parts[0];
|
|
var port = DefaultPort;
|
|
if (parts.Length == 2 && (!int.TryParse(parts[1], NumberStyles.None, CultureInfo.InvariantCulture, out port) || port is < 1 or > 65535))
|
|
throw new InvalidOperationException($"{EndpointVar}='{endpoint}' is not host:port.");
|
|
if (host.Length == 0) throw new InvalidOperationException($"{EndpointVar}='{endpoint}' has no host.");
|
|
|
|
// An explicitly empty AB_DIFF_PATH is honoured (direct / no-backplane targets).
|
|
var path = Environment.GetEnvironmentVariable(PathVar) ?? DefaultPath;
|
|
|
|
var family = DefaultFamily;
|
|
if (Environment.GetEnvironmentVariable(FamilyVar) is { Length: > 0 } rawFamily
|
|
&& !Enum.TryParse(rawFamily.Trim(), ignoreCase: true, out family))
|
|
throw new InvalidOperationException(
|
|
$"{FamilyVar}='{rawFamily}' is not one of {string.Join(", ", Enum.GetNames<AbCipPlcFamily>())}.");
|
|
|
|
IReadOnlyList<string>? tags = null;
|
|
if (Environment.GetEnvironmentVariable(TagsVar) is { Length: > 0 } rawTags)
|
|
tags = rawTags.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
|
|
|
var max = BoundedInt(MaxTagsVar, DefaultMaxTags, 1, Ceiling);
|
|
var maxTemplates = BoundedInt(MaxTemplatesVar, DefaultMaxTemplates, 1, Ceiling);
|
|
var pace = BoundedInt(PaceVar, DefaultPaceMs, 0, 10_000);
|
|
if (tags is { Count: > Ceiling })
|
|
throw new InvalidOperationException($"{TagsVar} names {tags.Count} tags; the ceiling is {Ceiling}.");
|
|
|
|
var latencyTags = BoundedInt(LatencyTagsVar, DefaultLatencyTags, 1, LatencyTagsCeiling);
|
|
var latencyPasses = BoundedInt(LatencyPassesVar, DefaultLatencyPasses, 1, LatencyPassesCeiling);
|
|
var latencyGap = BoundedInt(LatencyPassGapVar, DefaultLatencyPassGapMs, 0, 10_000);
|
|
|
|
var countOpens = Environment.GetEnvironmentVariable(CountOpensVar) == "1";
|
|
return new DifferentialTarget(host, port, path, family, tags, max, countOpens, maxTemplates, pace,
|
|
latencyTags, latencyPasses, latencyGap);
|
|
}
|
|
|
|
/// <summary>Reads an integer knob; out of range FAILS rather than clamping, so a typo never silently widens a press run.</summary>
|
|
private static int BoundedInt(string name, int fallback, int min, int max)
|
|
{
|
|
if (Environment.GetEnvironmentVariable(name) is not { Length: > 0 } raw) return fallback;
|
|
if (!int.TryParse(raw.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out var value) || value < min || value > max)
|
|
throw new InvalidOperationException($"{name}='{raw}' must be an integer in [{min}, {max}].");
|
|
return value;
|
|
}
|
|
|
|
/// <summary>The family profile the driver would use for this target.</summary>
|
|
public AbCipPlcFamilyProfile Profile => AbCipPlcFamilyProfile.ForFamily(Family);
|
|
|
|
/// <summary>Create params for one tag (or listing pseudo-tag), built the way the driver builds them.</summary>
|
|
public AbCipTagCreateParams Params(
|
|
string tagName, int elementCount = 1, bool isArray = false, AbCipDataType? authoredType = null) => new(
|
|
Gateway: Host,
|
|
Port: Port,
|
|
CipPath: CipPath,
|
|
LibplctagPlcAttribute: Profile.LibplctagPlcAttribute,
|
|
TagName: tagName,
|
|
Timeout: OperationTimeout,
|
|
AllowPacking: Profile.SupportsRequestPacking,
|
|
ConnectionSize: Profile.DefaultConnectionSize,
|
|
ElementCount: elementCount < 1 ? 1 : elementCount,
|
|
IsArray: isArray,
|
|
AuthoredType: authoredType,
|
|
UseConnectedMessaging: Profile.SupportsConnectedMessaging);
|
|
|
|
/// <inheritdoc />
|
|
public override string ToString() =>
|
|
$"{Host}:{Port} path '{CipPath}' family {Family} ({Profile.LibplctagPlcAttribute}, connection size {Profile.DefaultConnectionSize}), " +
|
|
$"max tags {MaxTags}, max templates {MaxTemplates}, pace {PaceMs} ms";
|
|
}
|