test: record actual ScadaBridge consumer and baseline authorization gates
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# Independent scripted-alarm scope baseline
|
||||
|
||||
This SDK 1.5.378.182-only source probe records the existing opaque-ID behavior
|
||||
tracked by [#797](https://gitea.dohertylan.com/dohertj2/lmxopcua/issues/797).
|
||||
It is deliberately outside the candidate solution and is not a runtime adapter.
|
||||
|
||||
Use a clean, separate checkout at original application commit
|
||||
`83ed880e4471344deaf008da9bd028c7e170bdc7`. Confirm its package pins before
|
||||
running. Copy **only** `ScriptedScopeProbe.cs` temporarily into that checkout's
|
||||
`tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests/`, refusing to
|
||||
overwrite an existing file. Run:
|
||||
|
||||
```sh
|
||||
dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests \
|
||||
-c Release --filter FullyQualifiedName~BaselineScriptedScopeProbe \
|
||||
--logger trx --results-directory /absolute/fresh-baseline-evidence
|
||||
```
|
||||
|
||||
Preserve the test exit and run the baseline checkout's
|
||||
`scripts/ci/assert-not-all-skipped.sh` with `MIN_EXECUTED=1` against the fresh TRX.
|
||||
Remove only the temporary probe you copied after the run, keeping the source
|
||||
and TRX evidence here/outside the checkout. Verify baseline tracked files remain
|
||||
unchanged. Never load this assembly or old SDK into the candidate process.
|
||||
|
||||
The real session has no application-wide roles. The production-valid opaque
|
||||
scripted ID is denied by an Equipment grant, routes nothing, and is then allowed
|
||||
by an explicit Cluster grant. A passing probe pins **both** outcomes; it does not
|
||||
claim narrow equipment authorization works. The independent candidate counterpart
|
||||
is `Sdk2OpaqueScriptedScopeBaselineTests`.
|
||||
|
||||
Do not make the ID slash-qualified to get a positive result: the current UI
|
||||
rejects `/`, and that would hide the production scope-resolution defect. A future
|
||||
fix requires reviewed ownership/scope binding without changing opaque IDs or
|
||||
widening permissions. This upgrade preserves the baseline denial and links the
|
||||
separate defect.
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project>
|
||||
<!-- A separate consumer process: never inherit this repository's SDK2 package policy. -->
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable><ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1 @@
|
||||
<Project><PropertyGroup><ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally></PropertyGroup></Project>
|
||||
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup><OutputType>Exe</OutputType></PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="$(ScadaBridgeSourceRoot)/src/ZB.MOM.WW.ScadaBridge.DataConnectionLayer/ZB.MOM.WW.ScadaBridge.DataConnectionLayer.csproj" />
|
||||
</ItemGroup>
|
||||
<Target Name="RequireConsumerSource" BeforeTargets="CollectPackageReferences">
|
||||
<Error Condition="'$(ScadaBridgeSourceRoot)' == ''" Text="Pass -p:ScadaBridgeSourceRoot=/absolute/ScadaBridge; this probe must use the actual consumer and its own pinned SDK." />
|
||||
</Target>
|
||||
</Project>
|
||||
@@ -0,0 +1,129 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Text.Json;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Protocol;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Serialization;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.DataConnections;
|
||||
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
|
||||
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer;
|
||||
using ZB.MOM.WW.ScadaBridge.DataConnectionLayer.Adapters;
|
||||
|
||||
if (args.Length != 3) throw new ArgumentException("Usage: Probe <endpoint> <None|SignAndEncrypt> <new-result-directory>");
|
||||
var endpoint = args[0];
|
||||
var mode = Enum.Parse<OpcUaSecurityMode>(args[1]);
|
||||
var root = Path.GetFullPath(args[2]);
|
||||
if (Directory.Exists(root)) throw new IOException("Refusing to reuse an evidence/PKI directory.");
|
||||
Directory.CreateDirectory(root);
|
||||
using var deadline = new CancellationTokenSource(TimeSpan.FromMinutes(2));
|
||||
var ct = deadline.Token;
|
||||
var cases = new List<object>();
|
||||
var failures = 0;
|
||||
var options = new OpcUaGlobalOptions
|
||||
{
|
||||
ApplicationName = "ScadaBridge-SDK2-compatibility",
|
||||
TrustedIssuerStorePath = Path.Combine(root, "pki", "issuers"),
|
||||
TrustedPeerStorePath = Path.Combine(root, "pki", "trusted"),
|
||||
RejectedCertificateStorePath = Path.Combine(root, "pki", "rejected"),
|
||||
};
|
||||
// Populate only this probe's trust list; never change the consumer's existing stores.
|
||||
var trustSource = Environment.GetEnvironmentVariable("SCADABRIDGE_PROBE_SERVER_CERTS");
|
||||
if (!string.IsNullOrEmpty(trustSource))
|
||||
{
|
||||
var destination = Path.Combine(options.TrustedPeerStorePath, "certs");
|
||||
Directory.CreateDirectory(destination);
|
||||
foreach (var cert in Directory.GetFiles(trustSource, "*.der")) File.Copy(cert, Path.Combine(destination, Path.GetFileName(cert)));
|
||||
}
|
||||
var factory = new RealOpcUaClientFactory(options, NullLoggerFactory.Instance);
|
||||
await using var adapter = new OpcUaDataConnection(factory, NullLogger<OpcUaDataConnection>.Instance);
|
||||
var config = new OpcUaEndpointConfig
|
||||
{
|
||||
EndpointUrl = endpoint, SecurityMode = mode, AutoAcceptUntrustedCerts = false,
|
||||
PublishingIntervalMs = 100, SamplingIntervalMs = 100,
|
||||
UserIdentity = new OpcUaUserIdentityConfig
|
||||
{
|
||||
TokenType = OpcUaUserTokenType.UsernamePassword, Username = "apikey",
|
||||
Password = Environment.GetEnvironmentVariable("OPCUA_MATRIX_PASSWORD") ?? throw new InvalidOperationException("Missing task credential"),
|
||||
},
|
||||
};
|
||||
static void Require(bool value, string reason) { if (!value) throw new InvalidOperationException(reason); }
|
||||
async Task Check(string name, Func<Task> action)
|
||||
{
|
||||
try { await action(); cases.Add(new { name, status = "passed" }); }
|
||||
catch (Exception e) { failures++; cases.Add(new { name, status = "failed", error = e.ToString() }); }
|
||||
}
|
||||
async Task Wait(Func<bool> predicate)
|
||||
{
|
||||
for (var i = 0; i < 200; i++) { if (predicate()) return; await Task.Delay(50, ct); }
|
||||
Require(false, "No expected consumer notification within 10 seconds.");
|
||||
}
|
||||
const string raw = "nsu=https://zb.com/otopcua/raw;s=Matrix/dev/";
|
||||
const string uns = "nsu=https://zb.com/otopcua/uns;s=Eq/";
|
||||
await Check("actual-adapter-connect-api-key", () => adapter.ConnectAsync(OpcUaEndpointConfigSerializer.ToFlatDict(config), ct));
|
||||
if (failures == 0)
|
||||
{
|
||||
foreach (var prefix in new[] { raw, uns })
|
||||
{
|
||||
await Check($"scalar-round-trip-{prefix}", async () =>
|
||||
{
|
||||
var write = await adapter.WriteAsync(prefix + "Scalar", 412, ct);
|
||||
Require(write.Success, write.ErrorMessage ?? "Scalar write denied");
|
||||
var read = await adapter.ReadAsync(prefix + "Scalar", ct);
|
||||
Require(read.Success && read.Value?.Value is int i && i == 412, "Scalar value/type changed");
|
||||
});
|
||||
await Check($"array-round-trip-{prefix}", async () =>
|
||||
{
|
||||
var write = await adapter.WriteAsync(prefix + "Vector", new[] { 31, 32, 33 }, ct);
|
||||
Require(write.Success, write.ErrorMessage ?? "Array write denied");
|
||||
var read = await adapter.ReadAsync(prefix + "Vector", ct);
|
||||
Require(read.Success && read.Value?.Value is int[] a && a.SequenceEqual(new[] { 31, 32, 33 }), "Array value/type changed");
|
||||
});
|
||||
}
|
||||
await Check("actual-adapter-durable-browse", async () =>
|
||||
{
|
||||
var page = await adapter.BrowseChildrenAsync("nsu=https://zb.com/otopcua/raw;s=Matrix/dev", cancellationToken: ct);
|
||||
Require(page.Children.Count(x => x.DisplayName is "Scalar" or "Vector") == 2, "Missing durable raw variable children");
|
||||
Require(page.Children.Any(x => x.DisplayName == "Alarm"), "Missing native alarm under its raw parent");
|
||||
var projected = await adapter.BrowseChildrenAsync("nsu=https://zb.com/otopcua/uns;s=Eq", cancellationToken: ct);
|
||||
Require(projected.Children.Any(x => x.DisplayName == "Vector"), "Missing UNS variable child");
|
||||
});
|
||||
await Check("actual-adapter-monitor-quality-time", async () =>
|
||||
{
|
||||
var values = new ConcurrentQueue<TagValue>();
|
||||
var handle = await adapter.SubscribeAsync(raw + "Scalar", (_, value) => values.Enqueue(value), ct);
|
||||
Require((await adapter.WriteAsync(raw + "Scalar", 713, ct)).Success, "Monitored write denied");
|
||||
await Wait(() => values.Any(x => x.Value is int i && i == 713));
|
||||
var actual = values.Last(x => x.Value is int i && i == 713);
|
||||
Require(actual.Timestamp > new DateTimeOffset(2026, 10, 1, 0, 0, 0, TimeSpan.Zero), "Missing source timestamp");
|
||||
Require(actual.Quality.ToString() == "Good", "Quality was not Good");
|
||||
await adapter.UnsubscribeAsync(handle, ct);
|
||||
});
|
||||
await Check("actual-adapter-alarm-filter-refresh", async () =>
|
||||
{
|
||||
var alarms = new ConcurrentQueue<NativeAlarmTransition>();
|
||||
var handle = await adapter.SubscribeAlarmsAsync("i=2253", null, alarms.Enqueue, ct);
|
||||
await Wait(() => alarms.Any(x => x.Kind == AlarmTransitionKind.SnapshotComplete)
|
||||
&& alarms.Any(x => x.Message == "matrix-alarm" && x.SourceReference.Contains("Alarm", StringComparison.Ordinal)));
|
||||
await adapter.UnsubscribeAlarmsAsync(handle, ct);
|
||||
});
|
||||
await Check("actual-adapter-disconnect-reconnect", async () =>
|
||||
{
|
||||
await adapter.DisconnectAsync(ct);
|
||||
await adapter.ConnectAsync(OpcUaEndpointConfigSerializer.ToFlatDict(config), ct);
|
||||
Require((await adapter.ReadAsync(raw + "Scalar", ct)).Success, "Consumer failed after reconnect");
|
||||
await adapter.DisconnectAsync(ct);
|
||||
});
|
||||
}
|
||||
var sdkAssembly = typeof(Opc.Ua.NodeId).Assembly;
|
||||
var result = new
|
||||
{
|
||||
consumer = typeof(OpcUaDataConnection).Assembly.GetName().Version?.ToString(),
|
||||
sdk = FileVersionInfo.GetVersionInfo(sdkAssembly.Location).ProductVersion,
|
||||
process = Environment.ProcessId, mode = mode.ToString(), autoAccept = false,
|
||||
endpoint, cases, failures,
|
||||
notImplementedByConsumer = new[] { "HistoryRead/CP", "Part9 acknowledgement methods" },
|
||||
};
|
||||
File.WriteAllText(Path.Combine(root, "result.json"), JsonSerializer.Serialize(result, new JsonSerializerOptions { WriteIndented = true }));
|
||||
Console.WriteLine($"Consumer probe: {cases.Count - failures} passed, {failures} failed; {Path.Combine(root, "result.json")}");
|
||||
return failures == 0 ? 0 : 1;
|
||||
@@ -0,0 +1,46 @@
|
||||
# Actual ScadaBridge consumer compatibility
|
||||
|
||||
This isolated process references the real ScadaBridge `OpcUaDataConnection` and
|
||||
`RealOpcUaClient`, with their own dependency graph. It never loads the candidate
|
||||
SDK into that process. The verified consumer source is
|
||||
`0e89966da670e289bd875166295bb0e85d8cf8b2`, pinned to SDK 1.5.378.106.
|
||||
|
||||
Build with an explicit clean consumer checkout:
|
||||
|
||||
```sh
|
||||
dotnet build scripts/validation/scadabridge-sdk-compatibility/Probe.csproj \
|
||||
-c Release -p:ScadaBridgeSourceRoot=/absolute/ScadaBridge
|
||||
```
|
||||
|
||||
First build the separate application hosts with the parent
|
||||
`scripts/e2e/test-opcua-sdk-compatibility.ps1` harness. Then choose a new output
|
||||
directory and pass its original and candidate `ProcessHost.dll` paths:
|
||||
|
||||
```sh
|
||||
python3 scripts/validation/scadabridge-sdk-compatibility/run.py \
|
||||
--baseline-host /absolute/matrix/build-baseline/bin/Release/net10.0/ProcessHost.dll \
|
||||
--candidate-host /absolute/matrix/build-candidate/bin/Release/net10.0/ProcessHost.dll \
|
||||
--probe scripts/validation/scadabridge-sdk-compatibility/bin/Release/net10.0/Probe.dll \
|
||||
--results /absolute/fresh-consumer-evidence
|
||||
```
|
||||
|
||||
The helper runs four separate consumer/server pairs: each application version
|
||||
with None and SignAndEncrypt. None is enabled only in its own test fixture;
|
||||
the ordinary compatibility host remains strict SignAndEncrypt. The task key is
|
||||
generated per process pair, issuer/trusted/rejected stores are task-local, and
|
||||
server public certificates are explicitly copied. Autoaccept remains false.
|
||||
Never commit an entire output directory: it contains the fixture's private PKI.
|
||||
|
||||
The consumed subset covers API-key activation, raw/UNS scalar and Int32[]
|
||||
roundtrips, durable hierarchical browse including the native alarm, data quality
|
||||
and timestamps, alarm filter/ConditionRefresh and disconnect/reconnect. History
|
||||
and Part 9 acknowledgment are not exposed by the inspected consumer interfaces;
|
||||
the parent Shared/CLI matrix covers those application services instead.
|
||||
|
||||
ScadaBridge#121 is a reproduced preexisting limitation: SignAndEncrypt fails
|
||||
against **both** original and candidate servers because the consumer does not
|
||||
configure an application certificate. A matching baseline failure is evidence,
|
||||
not a secure-channel pass. The runner retains the nonzero result and each actual
|
||||
failure; it does not loosen trust or change expected results to return success.
|
||||
The current None subset and the missing secure identity must be reported
|
||||
separately. No ScadaBridge production source or user PKI is modified.
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"consumer": "1.0.0.0",
|
||||
"sdk": "1.5.378.106+99044fdbb638087d62f1e16580e9f73bfda364e2",
|
||||
"process": 93934,
|
||||
"mode": "None",
|
||||
"autoAccept": false,
|
||||
"endpoint": "opc.tcp://127.0.0.1:51314/OtOpcUa",
|
||||
"cases": [
|
||||
{
|
||||
"name": "actual-adapter-connect-api-key",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"name": "scalar-round-trip-nsu=https://zb.com/otopcua/raw;s=Matrix/dev/",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"name": "array-round-trip-nsu=https://zb.com/otopcua/raw;s=Matrix/dev/",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"name": "scalar-round-trip-nsu=https://zb.com/otopcua/uns;s=Eq/",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"name": "array-round-trip-nsu=https://zb.com/otopcua/uns;s=Eq/",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"name": "actual-adapter-durable-browse",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"name": "actual-adapter-monitor-quality-time",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"name": "actual-adapter-alarm-filter-refresh",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"name": "actual-adapter-disconnect-reconnect",
|
||||
"status": "passed"
|
||||
}
|
||||
],
|
||||
"failures": 0,
|
||||
"notImplementedByConsumer": [
|
||||
"HistoryRead/CP",
|
||||
"Part9 acknowledgement methods"
|
||||
]
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"consumer": "1.0.0.0",
|
||||
"sdk": "1.5.378.106+99044fdbb638087d62f1e16580e9f73bfda364e2",
|
||||
"process": 93939,
|
||||
"mode": "SignAndEncrypt",
|
||||
"autoAccept": false,
|
||||
"endpoint": "opc.tcp://127.0.0.1:51358/OtOpcUa",
|
||||
"cases": [
|
||||
{
|
||||
"name": "actual-adapter-connect-api-key",
|
||||
"status": "failed",
|
||||
"error": "ApplicationCertificate for the security profile http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256 cannot be found.\n[80890000] 'ApplicationCertificate for the security profile http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256 cannot be found.'"
|
||||
}
|
||||
],
|
||||
"failures": 1,
|
||||
"notImplementedByConsumer": [
|
||||
"HistoryRead/CP",
|
||||
"Part9 acknowledgement methods"
|
||||
]
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"consumer": "1.0.0.0",
|
||||
"sdk": "1.5.378.106+99044fdbb638087d62f1e16580e9f73bfda364e2",
|
||||
"process": 93941,
|
||||
"mode": "None",
|
||||
"autoAccept": false,
|
||||
"endpoint": "opc.tcp://127.0.0.1:51387/OtOpcUa",
|
||||
"cases": [
|
||||
{
|
||||
"name": "actual-adapter-connect-api-key",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"name": "scalar-round-trip-nsu=https://zb.com/otopcua/raw;s=Matrix/dev/",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"name": "array-round-trip-nsu=https://zb.com/otopcua/raw;s=Matrix/dev/",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"name": "scalar-round-trip-nsu=https://zb.com/otopcua/uns;s=Eq/",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"name": "array-round-trip-nsu=https://zb.com/otopcua/uns;s=Eq/",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"name": "actual-adapter-durable-browse",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"name": "actual-adapter-monitor-quality-time",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"name": "actual-adapter-alarm-filter-refresh",
|
||||
"status": "passed"
|
||||
},
|
||||
{
|
||||
"name": "actual-adapter-disconnect-reconnect",
|
||||
"status": "passed"
|
||||
}
|
||||
],
|
||||
"failures": 0,
|
||||
"notImplementedByConsumer": [
|
||||
"HistoryRead/CP",
|
||||
"Part9 acknowledgement methods"
|
||||
]
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"consumer": "1.0.0.0",
|
||||
"sdk": "1.5.378.106+99044fdbb638087d62f1e16580e9f73bfda364e2",
|
||||
"process": 93943,
|
||||
"mode": "SignAndEncrypt",
|
||||
"autoAccept": false,
|
||||
"endpoint": "opc.tcp://127.0.0.1:51455/OtOpcUa",
|
||||
"cases": [
|
||||
{
|
||||
"name": "actual-adapter-connect-api-key",
|
||||
"status": "failed",
|
||||
"error": "ApplicationCertificate for the security profile http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256 cannot be found.\n[80890000] 'ApplicationCertificate for the security profile http://opcfoundation.org/UA/SecurityPolicy#Basic256Sha256 cannot be found.'"
|
||||
}
|
||||
],
|
||||
"failures": 1,
|
||||
"notImplementedByConsumer": [
|
||||
"HistoryRead/CP",
|
||||
"Part9 acknowledgement methods"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"artifacts": {
|
||||
"baseline_host": {
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-baseline/bin/Release/net10.0/ProcessHost.dll",
|
||||
"sha256": "841f83aeb3a32d49c06518a98963743d720015bba41ae3138649126ce3a6af1c"
|
||||
},
|
||||
"candidate_host": {
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ProcessHost.dll",
|
||||
"sha256": "e025fc21ac3559c3796ec639f0ef7b9c3026ccbbdaa4783e14b8930eae3a2043"
|
||||
},
|
||||
"probe": {
|
||||
"path": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/scripts/validation/scadabridge-sdk-compatibility/bin/Release/net10.0/Probe.dll",
|
||||
"sha256": "913ef3e35ffac8a54567e5d4df6f934572043249eaa6a4e0f829efd07b9012d7"
|
||||
}
|
||||
},
|
||||
"legs": [
|
||||
{
|
||||
"server": "baseline",
|
||||
"mode": "None",
|
||||
"serverCommand": [
|
||||
"dotnet",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-baseline/bin/Release/net10.0/ProcessHost.dll",
|
||||
"server",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/88ee30e0/scadabridge-final-matrix/baseline-None/server"
|
||||
],
|
||||
"command": [
|
||||
"dotnet",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/scripts/validation/scadabridge-sdk-compatibility/bin/Release/net10.0/Probe.dll",
|
||||
"opc.tcp://127.0.0.1:51314/OtOpcUa",
|
||||
"None",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/88ee30e0/scadabridge-final-matrix/baseline-None/consumer"
|
||||
],
|
||||
"exit": 0,
|
||||
"result": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/88ee30e0/scadabridge-final-matrix/baseline-None/consumer/result.json",
|
||||
"serverExit": 0
|
||||
},
|
||||
{
|
||||
"server": "baseline",
|
||||
"mode": "SignAndEncrypt",
|
||||
"serverCommand": [
|
||||
"dotnet",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-baseline/bin/Release/net10.0/ProcessHost.dll",
|
||||
"server",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/88ee30e0/scadabridge-final-matrix/baseline-SignAndEncrypt/server"
|
||||
],
|
||||
"command": [
|
||||
"dotnet",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/scripts/validation/scadabridge-sdk-compatibility/bin/Release/net10.0/Probe.dll",
|
||||
"opc.tcp://127.0.0.1:51358/OtOpcUa",
|
||||
"SignAndEncrypt",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/88ee30e0/scadabridge-final-matrix/baseline-SignAndEncrypt/consumer"
|
||||
],
|
||||
"exit": 1,
|
||||
"result": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/88ee30e0/scadabridge-final-matrix/baseline-SignAndEncrypt/consumer/result.json",
|
||||
"serverExit": 0
|
||||
},
|
||||
{
|
||||
"server": "candidate",
|
||||
"mode": "None",
|
||||
"serverCommand": [
|
||||
"dotnet",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ProcessHost.dll",
|
||||
"server",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/88ee30e0/scadabridge-final-matrix/candidate-None/server"
|
||||
],
|
||||
"command": [
|
||||
"dotnet",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/scripts/validation/scadabridge-sdk-compatibility/bin/Release/net10.0/Probe.dll",
|
||||
"opc.tcp://127.0.0.1:51387/OtOpcUa",
|
||||
"None",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/88ee30e0/scadabridge-final-matrix/candidate-None/consumer"
|
||||
],
|
||||
"exit": 0,
|
||||
"result": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/88ee30e0/scadabridge-final-matrix/candidate-None/consumer/result.json",
|
||||
"serverExit": 0
|
||||
},
|
||||
{
|
||||
"server": "candidate",
|
||||
"mode": "SignAndEncrypt",
|
||||
"serverCommand": [
|
||||
"dotnet",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua2-cross-version/OtOpcUa/artifacts/cross-version/run-10/build-candidate/bin/Release/net10.0/ProcessHost.dll",
|
||||
"server",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/88ee30e0/scadabridge-final-matrix/candidate-SignAndEncrypt/server"
|
||||
],
|
||||
"command": [
|
||||
"dotnet",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/scripts/validation/scadabridge-sdk-compatibility/bin/Release/net10.0/Probe.dll",
|
||||
"opc.tcp://127.0.0.1:51455/OtOpcUa",
|
||||
"SignAndEncrypt",
|
||||
"/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/88ee30e0/scadabridge-final-matrix/candidate-SignAndEncrypt/consumer"
|
||||
],
|
||||
"exit": 1,
|
||||
"result": "/Users/dohertj2/.codex/worktrees/opcua-sdk-2-upgrade/OtOpcUa/artifacts/opcua2/88ee30e0/scadabridge-final-matrix/candidate-SignAndEncrypt/consumer/result.json",
|
||||
"serverExit": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"consumerSource": "0e89966da670e289bd875166295bb0e85d8cf8b2",
|
||||
"consumerSdk": "1.5.378.106",
|
||||
"baselineServerSdk": "1.5.378.182",
|
||||
"candidateServerSdk": "2.0.1",
|
||||
"noneCasesPassed": 18,
|
||||
"secureConnectionFailures": 2,
|
||||
"samePreexistingFailure": "BadConfigurationError: ApplicationCertificate not configured",
|
||||
"issue": "https://gitea.dohertylan.com/dohertj2/ScadaBridge/issues/121",
|
||||
"releaseApproval": false
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run the real ScadaBridge consumer in separate processes against both application versions."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--baseline-host", type=Path, required=True)
|
||||
parser.add_argument("--candidate-host", type=Path, required=True)
|
||||
parser.add_argument("--probe", type=Path, required=True)
|
||||
parser.add_argument("--results", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
root = args.results.resolve()
|
||||
root.mkdir(parents=True, exist_ok=False)
|
||||
artifacts = {name: path.resolve() for name, path in vars(args).items() if name != "results"}
|
||||
for path in artifacts.values():
|
||||
if not path.is_file():
|
||||
raise FileNotFoundError(path)
|
||||
receipt = {
|
||||
"artifacts": {name: {"path": str(path), "sha256": hashlib.sha256(path.read_bytes()).hexdigest()}
|
||||
for name, path in artifacts.items()},
|
||||
"legs": [],
|
||||
}
|
||||
for version in ("baseline", "candidate"):
|
||||
for mode in ("None", "SignAndEncrypt"):
|
||||
leg = root / f"{version}-{mode}"
|
||||
leg.mkdir()
|
||||
server_root = leg / "server"
|
||||
env = os.environ.copy()
|
||||
env["OPCUA_MATRIX_PASSWORD"] = "otua_matrix_" + secrets.token_urlsafe(32)
|
||||
if mode == "None":
|
||||
env["OPCUA_MATRIX_SERVER_SECURITY"] = "none"
|
||||
else:
|
||||
env.pop("OPCUA_MATRIX_SERVER_SECURITY", None)
|
||||
server_command = ["dotnet", str(artifacts[f"{version}_host"]), "server", str(server_root)]
|
||||
result = {"server": version, "mode": mode, "serverCommand": server_command}
|
||||
with (leg / "server.log").open("w") as server_log:
|
||||
server = subprocess.Popen(server_command, env=env, stdout=server_log, stderr=subprocess.STDOUT)
|
||||
try:
|
||||
until = time.monotonic() + 40
|
||||
while not (server_root / "endpoint.txt").is_file():
|
||||
if server.poll() is not None or time.monotonic() >= until:
|
||||
raise RuntimeError(f"Server failed to become ready; inspect {leg / 'server.log'}")
|
||||
time.sleep(0.1)
|
||||
endpoint = (server_root / "endpoint.txt").read_text().strip()
|
||||
env["SCADABRIDGE_PROBE_SERVER_CERTS"] = str(server_root / "server-pki" / "own" / "certs")
|
||||
command = ["dotnet", str(artifacts["probe"]), endpoint, mode, str(leg / "consumer")]
|
||||
with (leg / "consumer.log").open("w") as log:
|
||||
probe = subprocess.run(command, env=env, stdout=log, stderr=subprocess.STDOUT, timeout=150)
|
||||
result.update(command=command, exit=probe.returncode,
|
||||
result=str(leg / "consumer" / "result.json"))
|
||||
finally:
|
||||
server_root.mkdir(exist_ok=True)
|
||||
(server_root / "stop").touch()
|
||||
try:
|
||||
server.wait(timeout=15)
|
||||
except subprocess.TimeoutExpired:
|
||||
server.terminate()
|
||||
server.wait(timeout=10)
|
||||
result["serverExit"] = server.returncode
|
||||
receipt["legs"].append(result)
|
||||
(root / "receipt.json").write_text(json.dumps(receipt, indent=2))
|
||||
# A pre-existing consumer limitation is evidence to investigate, never converted into a passing leg.
|
||||
return int(any(leg["exit"] != 0 or leg["serverExit"] != 0 for leg in receipt["legs"]))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user