Files
lmxopcua/scripts/e2e/test-focas.ps1
Joseph Doherty a25593a9c6 chore: organize solution into module folders (Core/Server/Drivers/Client/Tooling)
Group all 69 projects into category subfolders under src/ and tests/ so the
Rider Solution Explorer mirrors the module structure. Folders: Core, Server,
Drivers (with a nested Driver CLIs subfolder), Client, Tooling.

- Move every project folder on disk with git mv (history preserved as renames).
- Recompute relative paths in 57 .csproj files: cross-category ProjectReferences,
  the lib/ HintPath+None refs in Driver.Historian.Wonderware, and the external
  mxaccessgw refs in Driver.Galaxy and its test project.
- Rebuild ZB.MOM.WW.OtOpcUa.slnx with nested solution folders.
- Re-prefix project paths in functional scripts (e2e, compliance, smoke SQL,
  integration, install).

Build green (0 errors); unit tests pass. Docs left for a separate pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 01:55:28 -04:00

221 lines
8.1 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#Requires -Version 7.0
<#
.SYNOPSIS
End-to-end CLI test for the FOCAS (Fanuc CNC) driver.
.DESCRIPTION
Runs the CLI against either the managed wire client (default — Driver.FOCAS.Cli
dials the CNC on TCP:8193 directly, no native dependencies) or the focas-mock
Docker fixture. Hardware-gated by default because the default CncHost is
127.0.0.1; set FOCAS_TRUST_WIRE=1 once -CncHost points at a real CNC, or pass
-ProfileName to run against the Docker sim.
The script also supports three nice-to-have modes shipped 2026-04-24:
-Series — per-series matrix mode. Accepts a comma-separated list; the
core stages are run once per series, swapping the -Address to
the supplied per-series probe. Fails fast if any series's
configured address is outside the documented range (the driver
itself enforces that at InitializeAsync).
-ProfileName — for use with the Python Docker simulator (see
docs/v2/implementation/focas-simulator-plan.md). Selects a
docker-compose profile + matching -Series. When set, the
FOCAS_TRUST_WIRE gate is considered satisfied because the sim
is a legitimate non-hardware target.
-HandleLeakCycles <int> — stress stage that opens + closes <N> sessions
via the CLI's `probe` command with a short sleep between
cycles. Exercises the Tier-C supervisor's handle-recycle path
without touching user data. Typical values: 1001000. A CNC's
FWLIB handle pool is finite (~510), so this shakes out
handle-leak bugs if either side forgets to free.
.PARAMETER CncHost
IP or hostname of the CNC. Default 127.0.0.1 — override for real runs.
.PARAMETER CncPort
FOCAS TCP port. Default 8193.
.PARAMETER Address
FOCAS address to exercise. Default R100 (PMC R-file register). Ignored
when -Series is set and the series profile supplies its own probe.
.PARAMETER Series
Comma-separated list of CNC series to run the matrix against. Known:
ZeroI_D, ZeroI_F, ZeroI_MF, ZeroI_TF, Sixteen_i, Thirty_i, ThirtyOne_i,
ThirtyTwo_i, PowerMotion_i. When empty the script runs a single pass
without a series constraint.
.PARAMETER ProfileName
docker-compose profile name from tests/.../Docker/profiles/. When set,
the script assumes the Python simulator is the target + un-gates
FOCAS_TRUST_WIRE.
.PARAMETER HandleLeakCycles
Run a handle-leak stress stage with <N> open/close cycles. 0 = skip.
.PARAMETER OpcUaUrl
OtOpcUa server endpoint.
.PARAMETER BridgeNodeId
NodeId at which the server publishes the Address.
#>
param(
[string]$CncHost = "127.0.0.1",
[int]$CncPort = 8193,
[string]$Address = "R100",
[string]$Series = "",
[string]$ProfileName = "",
[int]$HandleLeakCycles = 0,
[string]$OpcUaUrl = "opc.tcp://localhost:4840",
[Parameter(Mandatory)] [string]$BridgeNodeId
)
$ErrorActionPreference = "Stop"
. "$PSScriptRoot/_common.ps1"
$simGated = -not [string]::IsNullOrWhiteSpace($ProfileName)
if (-not $simGated -and -not ($env:FOCAS_TRUST_WIRE -eq "1" -or $env:FOCAS_TRUST_WIRE -eq "true")) {
Write-Skip "FOCAS_TRUST_WIRE not set. Pass -ProfileName <profile> to run against the Docker mock in tests/.../Driver.FOCAS.IntegrationTests/Docker/, or set FOCAS_TRUST_WIRE=1 when -CncHost points at a real CNC."
exit 0
}
if ($simGated) {
Write-Info "Sim mode — profile '$ProfileName'. FOCAS_TRUST_WIRE gate bypassed."
}
# Per-series probe addresses — each one is inside the authoritative range for
# that series (docs/v2/focas-version-matrix.md). Picking one representative per
# kind (PMC / parameter / macro) is enough to exercise the driver's validator.
$seriesProbes = @{
"ZeroI_D" = "R100"
"ZeroI_F" = "R100"
"ZeroI_MF" = "R100"
"ZeroI_TF" = "R100"
"Sixteen_i" = "R100"
"Thirty_i" = "R100"
"ThirtyOne_i" = "R100"
"ThirtyTwo_i" = "R100"
"PowerMotion_i"= "R100"
}
$seriesList = @()
if (-not [string]::IsNullOrWhiteSpace($Series)) {
$seriesList = @($Series.Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ })
$unknown = @($seriesList | Where-Object { -not $seriesProbes.ContainsKey($_) })
if ($unknown.Count -gt 0) {
Write-Fail "Unknown -Series entries: $($unknown -join ', '). Known: $($seriesProbes.Keys -join ', ')."
exit 2
}
}
$focasCli = Get-CliInvocation `
-ProjectFolder "src/Drivers/Cli/ZB.MOM.WW.OtOpcUa.Driver.FOCAS.Cli" `
-ExeName "otopcua-focas-cli"
$opcUaCli = Get-CliInvocation `
-ProjectFolder "src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI" `
-ExeName "otopcua-cli"
$allResults = @()
function Invoke-FocasCore {
param(
[string]$Label,
[string]$ProbeAddress
)
Write-Header "FOCAS stages — $Label"
$commonFocas = @("-h", $CncHost, "-p", $CncPort)
$results = @()
$results += Test-Probe `
-Cli $focasCli `
-ProbeArgs (@("probe") + $commonFocas + @("-a", $ProbeAddress, "--type", "Int16"))
$writeValue = Get-Random -Minimum 1 -Maximum 9999
$results += Test-DriverLoopback `
-Cli $focasCli `
-WriteArgs (@("write") + $commonFocas + @("-a", $ProbeAddress, "-t", "Int16", "-v", $writeValue)) `
-ReadArgs (@("read") + $commonFocas + @("-a", $ProbeAddress, "-t", "Int16")) `
-ExpectedValue "$writeValue"
$bridgeValue = Get-Random -Minimum 10000 -Maximum 19999
$results += Test-ServerBridge `
-DriverCli $focasCli `
-DriverWriteArgs (@("write") + $commonFocas + @("-a", $ProbeAddress, "-t", "Int16", "-v", $bridgeValue)) `
-OpcUaCli $opcUaCli `
-OpcUaUrl $OpcUaUrl `
-OpcUaNodeId $BridgeNodeId `
-ExpectedValue "$bridgeValue"
$reverseValue = Get-Random -Minimum 20000 -Maximum 29999
$results += Test-OpcUaWriteBridge `
-OpcUaCli $opcUaCli `
-OpcUaUrl $OpcUaUrl `
-OpcUaNodeId $BridgeNodeId `
-DriverCli $focasCli `
-DriverReadArgs (@("read") + $commonFocas + @("-a", $ProbeAddress, "-t", "Int16")) `
-ExpectedValue "$reverseValue"
$subValue = Get-Random -Minimum 30000 -Maximum 32766
$results += Test-SubscribeSeesChange `
-OpcUaCli $opcUaCli `
-OpcUaUrl $OpcUaUrl `
-OpcUaNodeId $BridgeNodeId `
-DriverCli $focasCli `
-DriverWriteArgs (@("write") + $commonFocas + @("-a", $ProbeAddress, "-t", "Int16", "-v", $subValue)) `
-ExpectedValue "$subValue"
return $results
}
function Invoke-HandleLeakStage {
param([int]$Cycles)
Write-Header "FOCAS handle-leak stress — $Cycles cycles"
$commonFocas = @("-h", $CncHost, "-p", $CncPort)
$failed = 0
for ($i = 1; $i -le $Cycles; $i++) {
$probe = Test-Probe `
-Cli $focasCli `
-ProbeArgs (@("probe") + $commonFocas + @("-a", $Address, "--type", "Int16"))
if (-not $probe.Passed) {
$failed++
# First 3 failures are informative; the rest just tally.
if ($failed -le 3) {
Write-Fail "cycle $i failed: $($probe.Reason)"
}
}
# Tiny delay so a broken loop can't DDoS the CNC; FWLIB handles take a
# few tens of ms to recycle in practice.
Start-Sleep -Milliseconds 50
}
$passed = $Cycles - $failed
if ($failed -eq 0) {
Write-Pass "handle-leak stress: $passed/$Cycles cycles succeeded"
return @{ Passed = $true; Reason = "$passed/$Cycles" }
} else {
Write-Fail "handle-leak stress: $failed/$Cycles cycles failed"
return @{ Passed = $false; Reason = "$failed/$Cycles failed" }
}
}
if ($seriesList.Count -eq 0) {
$allResults += Invoke-FocasCore -Label "single" -ProbeAddress $Address
} else {
foreach ($series in $seriesList) {
$probeAddr = $seriesProbes[$series]
Write-Info "Running matrix pass for series '$series' with address $probeAddr"
$allResults += Invoke-FocasCore -Label $series -ProbeAddress $probeAddr
}
}
if ($HandleLeakCycles -gt 0) {
$allResults += Invoke-HandleLeakStage -Cycles $HandleLeakCycles
}
Write-Summary -Title "FOCAS e2e" -Results $allResults
if ($allResults | Where-Object { -not $_.Passed }) { exit 1 }