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>
221 lines
8.1 KiB
PowerShell
221 lines
8.1 KiB
PowerShell
#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: 100–1000. A CNC's
|
||
FWLIB handle pool is finite (~5–10), 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 }
|