using System.Globalization; using ZB.MOM.WW.OtOpcUa.Driver.AbCip.PlcFamilies; namespace ZB.MOM.WW.OtOpcUa.Driver.AbCip.IntegrationTests.Differential; /// /// The one endpoint the read-only differential harness targets, read from the environment. /// /// AB_DIFF_ENDPOINT — host:port (port defaults to 44818). Absent ⇒ every live test skips. /// AB_DIFF_PATH — CIP path, default 1,0. /// AB_DIFF_FAMILY — an name, default CompactLogix. /// AB_DIFF_TAGS — optional comma list of tag names; absent ⇒ selected from browse. /// AB_DIFF_MAX_TAGS — cap on the selected tags, default 200, at most 500 (higher fails fast). /// AB_DIFF_MAX_TEMPLATES — cap on template reads per transport, default 50, at most 500. /// AB_DIFF_PACE_MS — delay before every harness-issued request, default 20 ms; 0 for ab_server. /// AB_DIFF_COUNT_OPENS — 1 ⇒ assert exactly one native Forward Open over the run. /// AB_LAT_TAGS — latency comparison: tags read per pass, default 100, at most 200 (higher fails fast). /// AB_LAT_PASSES — latency comparison: passes per transport, default 30, at most 50. /// AB_LAT_PASS_GAP_MS — latency comparison: pause between passes, default 200 ms, at most 10000. /// /// internal sealed record DifferentialTarget( string Host, int Port, string CipPath, AbCipPlcFamily Family, IReadOnlyList? 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"; /// Latency comparison ceilings: a production press is the target, and each pass is one burst. 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; /// Hard ceiling on AB_DIFF_MAX_TAGS / AB_DIFF_MAX_TEMPLATES: a production press is the target. 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; /// Per-operation timeout for both transports. public static readonly TimeSpan OperationTimeout = TimeSpan.FromSeconds(5); /// Skip reason used by every live test when the endpoint is not configured. 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)."; /// The configured target, or null when AB_DIFF_ENDPOINT is absent. /// An env var is present but malformed. 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())}."); IReadOnlyList? 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); } /// Reads an integer knob; out of range FAILS rather than clamping, so a typo never silently widens a press run. 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; } /// The family profile the driver would use for this target. public AbCipPlcFamilyProfile Profile => AbCipPlcFamilyProfile.ForFamily(Family); /// Create params for one tag (or listing pseudo-tag), built the way the driver builds them. 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); /// 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"; }