The driver-layer integration tests confirm the driver sees the PLC, and
the Client.CLI tests confirm the client sees the server. Nothing glued
them end-to-end until this PR.
- scripts/e2e/_common.ps1: shared helpers — CLI invocation (published-
binary OR `dotnet run` fallback), Test-Probe / Test-DriverLoopback /
Test-ServerBridge (all return @{Passed;Reason} hashtables).
- scripts/e2e/test-<modbus|abcip|ablegacy|s7|focas|twincat>.ps1: per-
driver three-stage script (probe → driver-loopback → server-bridge).
AB Legacy / FOCAS / TwinCAT are gated behind *_TRUST_WIRE env vars
since they need real hardware (#222) or a licensed runtime (#221).
- scripts/e2e/test-phase7-virtualtags.ps1: writes a Modbus HR, reads
the server-side VirtualTag (VT = input * 2) back via OPC UA, triggers
+ clears a scripted alarm. Exercises the Phase 7 CachedTagUpstreamSource
+ ScriptedAlarmEngine path.
- scripts/e2e/test-all.ps1: reads e2e-config.json sidecar, runs each
present driver, prints a FINAL MATRIX (PASS/FAIL/SKIP). Missing
sections SKIP rather than fail hard.
- scripts/e2e/e2e-config.sample.json: commented sample — each dev's
NodeIds are local-seed-specific so e2e-config.json is .gitignore-d.
- scripts/e2e/README.md: full walkthrough — prereqs, three-stage design,
env-var gates, expected matrix, why this is separate from `dotnet test`.
Tasks #249-#251 shipped Modbus/AbCip/AbLegacy/S7/TwinCAT CLIs but left
FOCAS out. Since test-focas.ps1 needs it, the 6th CLI ships here:
- src/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli: probe/read/write/subscribe
commands, AssemblyName `otopcua-focas-cli`. WriteCommand.ParseValue
handles the full FocasDataType enum (Bit/Byte/Int16/Int32/Float32/
Float64/String — no UInt variants; the FOCAS protocol exposes signed
PMC + Fanuc-Float only). Default DataType is Int16 to match the PMC
register convention.
Full-solution build clean (0 errors). FOCAS CLI wired into
ZB.MOM.WW.OtOpcUa.slnx. No .Tests project for the FOCAS CLI yet —
symmetric with how ProbeCommand has no unit-testable pure logic in the
other 5 CLIs either; WriteCommand.ParseValue parity will land in a
follow-up to keep this PR scoped.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
220 lines
7.5 KiB
PowerShell
220 lines
7.5 KiB
PowerShell
# Shared PowerShell helpers for the OtOpcUa end-to-end CLI test scripts.
|
|
#
|
|
# Every per-protocol script dot-sources this file and calls the Test-* functions
|
|
# below. Keeps the per-script code down to ~50 lines of parameterisation +
|
|
# bridging-tag identifiers.
|
|
#
|
|
# Conventions:
|
|
# - All test helpers return a hashtable: @{ Passed=<bool>; Reason=<string> }
|
|
# - Helpers never throw unless the test setup is itself broken (a crashed
|
|
# CLI is a test failure, not an exception).
|
|
# - Output is plain text with [PASS] / [FAIL] / [SKIP] / [INFO] prefixes so
|
|
# grep/log-scraping works.
|
|
|
|
Set-StrictMode -Version 3.0
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Colouring + prefixes.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
function Write-Header {
|
|
param([string]$Title)
|
|
Write-Host ""
|
|
Write-Host "=== $Title ===" -ForegroundColor Cyan
|
|
}
|
|
|
|
function Write-Pass {
|
|
param([string]$Message)
|
|
Write-Host "[PASS] $Message" -ForegroundColor Green
|
|
}
|
|
|
|
function Write-Fail {
|
|
param([string]$Message)
|
|
Write-Host "[FAIL] $Message" -ForegroundColor Red
|
|
}
|
|
|
|
function Write-Skip {
|
|
param([string]$Message)
|
|
Write-Host "[SKIP] $Message" -ForegroundColor Yellow
|
|
}
|
|
|
|
function Write-Info {
|
|
param([string]$Message)
|
|
Write-Host "[INFO] $Message" -ForegroundColor Gray
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CLI invocation helpers.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Resolve a CLI path from either a published binary OR a `dotnet run` fallback.
|
|
# Preferred order:
|
|
# 1. $env:OTOPCUA_CLI_BIN points at a publish/ folder → use <exe> there
|
|
# 2. Fall back to `dotnet run --project src/<ProjectFolder> --`
|
|
#
|
|
# $ProjectFolder = relative path from repo root
|
|
# $ExeName = expected AssemblyName (no .exe)
|
|
function Get-CliInvocation {
|
|
param(
|
|
[Parameter(Mandatory)] [string]$ProjectFolder,
|
|
[Parameter(Mandatory)] [string]$ExeName
|
|
)
|
|
|
|
if ($env:OTOPCUA_CLI_BIN) {
|
|
$binPath = Join-Path $env:OTOPCUA_CLI_BIN "$ExeName.exe"
|
|
if (Test-Path $binPath) {
|
|
return @{ File = $binPath; PrefixArgs = @() }
|
|
}
|
|
}
|
|
|
|
# Dotnet-run fallback. --no-build would be faster but not every CI step
|
|
# has rebuilt; default to a full run so the script is forgiving.
|
|
return @{
|
|
File = "dotnet"
|
|
PrefixArgs = @("run", "--project", $ProjectFolder, "--")
|
|
}
|
|
}
|
|
|
|
# Run a CLI and capture stdout+stderr+exitcode. Never throws.
|
|
function Invoke-Cli {
|
|
param(
|
|
[Parameter(Mandatory)] $Cli, # output of Get-CliInvocation
|
|
[Parameter(Mandatory)] [string[]]$Args, # CLI arguments (after `-- `)
|
|
[int]$TimeoutSec = 30
|
|
)
|
|
|
|
$allArgs = @($Cli.PrefixArgs) + $Args
|
|
$output = $null
|
|
$exitCode = -1
|
|
|
|
try {
|
|
$output = & $Cli.File @allArgs 2>&1 | Out-String
|
|
$exitCode = $LASTEXITCODE
|
|
}
|
|
catch {
|
|
return @{
|
|
Output = $_.Exception.Message
|
|
ExitCode = -1
|
|
}
|
|
}
|
|
|
|
return @{
|
|
Output = $output
|
|
ExitCode = $exitCode
|
|
}
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Test helpers — reusable building blocks every per-protocol script calls.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Test 1 — the driver CLI's probe command exits 0. Confirms the PLC / simulator
|
|
# is reachable and speaks the protocol. Prerequisite for everything else.
|
|
function Test-Probe {
|
|
param(
|
|
[Parameter(Mandatory)] $Cli,
|
|
[Parameter(Mandatory)] [string[]]$ProbeArgs
|
|
)
|
|
Write-Header "Probe"
|
|
$r = Invoke-Cli -Cli $Cli -Args $ProbeArgs
|
|
if ($r.ExitCode -eq 0) {
|
|
Write-Pass "driver CLI probe succeeded"
|
|
return @{ Passed = $true }
|
|
}
|
|
Write-Fail "driver CLI probe exit=$($r.ExitCode)"
|
|
Write-Host $r.Output
|
|
return @{ Passed = $false; Reason = "probe exit $($r.ExitCode)" }
|
|
}
|
|
|
|
# Test 2 — driver-loopback. Write a value via the driver CLI, read it back via
|
|
# the same CLI, assert round-trip equality. Confirms the driver itself is
|
|
# functional without pulling the OtOpcUa server into the loop.
|
|
function Test-DriverLoopback {
|
|
param(
|
|
[Parameter(Mandatory)] $Cli,
|
|
[Parameter(Mandatory)] [string[]]$WriteArgs,
|
|
[Parameter(Mandatory)] [string[]]$ReadArgs,
|
|
[Parameter(Mandatory)] [string]$ExpectedValue
|
|
)
|
|
Write-Header "Driver loopback"
|
|
|
|
$w = Invoke-Cli -Cli $Cli -Args $WriteArgs
|
|
if ($w.ExitCode -ne 0) {
|
|
Write-Fail "write failed (exit=$($w.ExitCode))"
|
|
Write-Host $w.Output
|
|
return @{ Passed = $false; Reason = "write failed" }
|
|
}
|
|
Write-Info "write ok"
|
|
|
|
$r = Invoke-Cli -Cli $Cli -Args $ReadArgs
|
|
if ($r.ExitCode -ne 0) {
|
|
Write-Fail "read failed (exit=$($r.ExitCode))"
|
|
Write-Host $r.Output
|
|
return @{ Passed = $false; Reason = "read failed" }
|
|
}
|
|
|
|
if ($r.Output -match "Value:\s+$([Regex]::Escape($ExpectedValue))\b") {
|
|
Write-Pass "round-trip equals $ExpectedValue"
|
|
return @{ Passed = $true }
|
|
}
|
|
Write-Fail "round-trip value mismatch — expected $ExpectedValue"
|
|
Write-Host $r.Output
|
|
return @{ Passed = $false; Reason = "value mismatch" }
|
|
}
|
|
|
|
# Test 3 — server bridge. Write via the driver CLI, read the corresponding
|
|
# OPC UA NodeId via the OPC UA client CLI. Confirms the full path:
|
|
# driver CLI → PLC → OtOpcUa server (polling/subscription) → OPC UA client.
|
|
function Test-ServerBridge {
|
|
param(
|
|
[Parameter(Mandatory)] $DriverCli,
|
|
[Parameter(Mandatory)] [string[]]$DriverWriteArgs,
|
|
[Parameter(Mandatory)] $OpcUaCli,
|
|
[Parameter(Mandatory)] [string]$OpcUaUrl,
|
|
[Parameter(Mandatory)] [string]$OpcUaNodeId,
|
|
[Parameter(Mandatory)] [string]$ExpectedValue,
|
|
[int]$ServerPollDelaySec = 3
|
|
)
|
|
Write-Header "Server bridge"
|
|
|
|
$w = Invoke-Cli -Cli $DriverCli -Args $DriverWriteArgs
|
|
if ($w.ExitCode -ne 0) {
|
|
Write-Fail "driver-side write failed (exit=$($w.ExitCode))"
|
|
Write-Host $w.Output
|
|
return @{ Passed = $false; Reason = "driver write failed" }
|
|
}
|
|
Write-Info "driver write ok, waiting ${ServerPollDelaySec}s for server-side poll"
|
|
Start-Sleep -Seconds $ServerPollDelaySec
|
|
|
|
$r = Invoke-Cli -Cli $OpcUaCli -Args @("read", "-u", $OpcUaUrl, "-n", $OpcUaNodeId)
|
|
if ($r.ExitCode -ne 0) {
|
|
Write-Fail "OPC UA client read failed (exit=$($r.ExitCode))"
|
|
Write-Host $r.Output
|
|
return @{ Passed = $false; Reason = "opc-ua read failed" }
|
|
}
|
|
|
|
if ($r.Output -match "Value:\s+$([Regex]::Escape($ExpectedValue))\b") {
|
|
Write-Pass "server-side read equals $ExpectedValue"
|
|
return @{ Passed = $true }
|
|
}
|
|
Write-Fail "server-side value mismatch — expected $ExpectedValue"
|
|
Write-Host $r.Output
|
|
return @{ Passed = $false; Reason = "bridge value mismatch" }
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Summary helper — caller passes an array of test results.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
function Write-Summary {
|
|
param(
|
|
[Parameter(Mandatory)] [string]$Title,
|
|
[Parameter(Mandatory)] [array]$Results
|
|
)
|
|
$passed = ($Results | Where-Object { $_.Passed }).Count
|
|
$failed = ($Results | Where-Object { -not $_.Passed }).Count
|
|
Write-Host ""
|
|
Write-Host "=== $Title summary: $passed/$($Results.Count) passed ===" `
|
|
-ForegroundColor $(if ($failed -eq 0) { "Green" } else { "Red" })
|
|
}
|