Files
mxaccessgw/scripts/run-client-e2e-tests.ps1
T
2026-04-27 12:10:40 -04:00

531 lines
19 KiB
PowerShell

[CmdletBinding()]
param(
[string[]]$Clients = @("dotnet", "go", "rust", "python", "java"),
[int]$MachineStart = 1,
[int]$MachineEnd = 20,
[string[]]$Attributes = @(
"ProtectedValue",
"TestChangingInt",
"TestBoolArray",
"TestIntArray",
"TestDateTimeArray",
"TestStringArray"
),
[string]$Endpoint = "localhost:5000",
[string]$ApiKeyEnv = "MXGATEWAY_API_KEY",
[string]$SqlServer = "localhost",
[string]$Database = "ZB",
[int]$EventLimit = 5,
[switch]$SkipStream,
[switch]$DryRun,
[string]$ReportPath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$repoRoot = Resolve-Path (Join-Path $PSScriptRoot "..")
$discoveryScript = Join-Path $PSScriptRoot "discover-testmachine-tags.ps1"
$validClients = @("dotnet", "go", "rust", "python", "java")
$Clients = @($Clients | ForEach-Object {
$_ -split ","
} | ForEach-Object {
$_.Trim().ToLowerInvariant()
} | Where-Object {
-not [string]::IsNullOrWhiteSpace($_)
})
$Attributes = @($Attributes | ForEach-Object {
$_ -split ","
} | ForEach-Object {
$_.Trim()
} | Where-Object {
-not [string]::IsNullOrWhiteSpace($_)
})
if ($Clients.Count -eq 0) {
throw "At least one client is required."
}
if ($Attributes.Count -eq 0) {
throw "At least one attribute is required."
}
foreach ($client in $Clients) {
if ($validClients -notcontains $client) {
throw "Unsupported client '$client'. Supported clients: $($validClients -join ', ')."
}
}
if ([string]::IsNullOrWhiteSpace($ReportPath)) {
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$ReportPath = Join-Path $repoRoot "artifacts/e2e/testmachine-client-e2e-$timestamp.json"
}
function ConvertTo-HttpEndpoint {
param([string]$Value)
if ($Value.StartsWith("http://", [StringComparison]::OrdinalIgnoreCase) -or
$Value.StartsWith("https://", [StringComparison]::OrdinalIgnoreCase)) {
return $Value
}
return "http://$Value"
}
function ConvertTo-HostEndpoint {
param([string]$Value)
$hostValue = $Value
if ($hostValue.StartsWith("http://", [StringComparison]::OrdinalIgnoreCase)) {
$hostValue = $hostValue.Substring(7)
} elseif ($hostValue.StartsWith("https://", [StringComparison]::OrdinalIgnoreCase)) {
$hostValue = $hostValue.Substring(8)
}
return $hostValue.TrimEnd("/")
}
function Join-CommandLine {
param(
[string]$FilePath,
[string[]]$Arguments
)
$parts = @($FilePath) + $Arguments
return ($parts | ForEach-Object { ConvertTo-NativeArgument -Value $_ }) -join " "
}
function ConvertTo-NativeArgument {
param([string]$Value)
if ($Value -notmatch '[\s"]') {
return $Value
}
return '"' + $Value.Replace('\', '\\').Replace('"', '\"') + '"'
}
function Invoke-NativeCommand {
param(
[string]$FilePath,
[string[]]$Arguments,
[string]$WorkingDirectory,
[hashtable]$Environment = @{}
)
$process = [System.Diagnostics.Process]::new()
$process.StartInfo.FileName = $FilePath
$process.StartInfo.Arguments = ($Arguments | ForEach-Object {
ConvertTo-NativeArgument -Value $_
}) -join " "
$process.StartInfo.WorkingDirectory = $WorkingDirectory
$process.StartInfo.RedirectStandardOutput = $true
$process.StartInfo.RedirectStandardError = $true
$process.StartInfo.UseShellExecute = $false
foreach ($entry in $Environment.GetEnumerator()) {
$process.StartInfo.Environment[$entry.Key] = [string]$entry.Value
}
$commandLine = Join-CommandLine -FilePath $FilePath -Arguments $Arguments
if ($DryRun) {
Write-Host "[dry-run] $commandLine"
return [pscustomobject]@{
commandLine = $commandLine
exitCode = 0
stdout = "{}"
stderr = ""
}
}
[void]$process.Start()
$stdout = $process.StandardOutput.ReadToEnd()
$stderr = $process.StandardError.ReadToEnd()
$process.WaitForExit()
$result = [pscustomobject]@{
commandLine = $commandLine
exitCode = $process.ExitCode
stdout = $stdout
stderr = $stderr
}
if ($result.exitCode -ne 0) {
throw "Command failed with exit code $($result.exitCode): $commandLine`n$stderr`n$stdout"
}
return $result
}
function Read-JsonObject {
param([string]$Text)
if ([string]::IsNullOrWhiteSpace($Text)) {
return $null
}
try {
return $Text | ConvertFrom-Json
} catch {
$jsonLines = @()
foreach ($line in ($Text -split "`r?`n")) {
$trimmed = $line.Trim()
if ($trimmed.StartsWith("{") -and $trimmed.EndsWith("}")) {
$jsonLines += ($trimmed | ConvertFrom-Json)
}
}
return $jsonLines
}
}
function Get-OpenSessionId {
param(
[string]$Client,
[object]$Json
)
switch ($Client) {
"dotnet" { return $Json.sessionId }
"go" { return $Json.reply.sessionId }
"rust" { return $Json.sessionId }
"python" { return $Json.sessionId }
"java" { return $Json.reply.sessionId }
}
}
function Get-ServerHandle {
param(
[string]$Client,
[object]$Json
)
switch ($Client) {
"dotnet" { return [int]$Json.register.serverHandle }
"go" { return [int]$Json.reply.register.serverHandle }
"rust" { return [int]$Json.serverHandle }
"python" { return [int]$Json.serverHandle }
"java" { return [int]$Json.reply.register.serverHandle }
}
}
function Get-ItemHandle {
param(
[string]$Client,
[object]$Json
)
switch ($Client) {
"dotnet" { return [int]$Json.addItem.itemHandle }
"go" { return [int]$Json.reply.addItem.itemHandle }
"rust" { return [int]$Json.itemHandle }
"python" { return [int]$Json.itemHandle }
"java" { return [int]$Json.reply.addItem.itemHandle }
}
}
function Get-StreamEventCount {
param(
[string]$Client,
[object]$Json
)
switch ($Client) {
"dotnet" { return @($Json.events).Count }
"go" { return @($Json).Count }
"rust" { return [int]$Json.eventCount }
"python" { return @($Json.events).Count }
"java" { return @($Json).Count }
}
}
function Get-ClientCommand {
param(
[string]$Client,
[string]$Operation,
[hashtable]$Values
)
$httpEndpoint = ConvertTo-HttpEndpoint -Value $Endpoint
$hostEndpoint = ConvertTo-HostEndpoint -Value $Endpoint
$clientName = "mxgw-$Client-e2e"
switch ($Client) {
"dotnet" {
$arguments = @(
"run", "--project", "clients/dotnet/MxGateway.Client.Cli", "--",
$Operation,
"--endpoint", $httpEndpoint,
"--api-key-env", $ApiKeyEnv,
"--timeout", "60s",
"--json"
)
if ($Operation -eq "open-session") {
$arguments += @("--client-name", $clientName)
} elseif ($Operation -eq "register") {
$arguments += @("--session-id", $Values.sessionId, "--client-name", $clientName)
} elseif ($Operation -eq "add-item") {
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item", $Values.item)
} elseif ($Operation -eq "advise") {
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handle", "$($Values.itemHandle)")
} elseif ($Operation -eq "stream-events") {
$arguments += @("--session-id", $Values.sessionId, "--max-events", "$EventLimit")
} elseif ($Operation -eq "close-session") {
$arguments += @("--session-id", $Values.sessionId)
}
return [pscustomobject]@{ file = "dotnet"; args = $arguments; cwd = $repoRoot; env = @{} }
}
"go" {
$arguments = @(
"run", "./cmd/mxgw-go", $Operation,
"-endpoint", $hostEndpoint,
"-api-key-env", $ApiKeyEnv,
"-plaintext",
"-json"
)
if ($Operation -eq "open-session") {
$arguments += @("-client-session-name", $clientName)
} elseif ($Operation -eq "register") {
$arguments += @("-session-id", $Values.sessionId, "-client-name", $clientName)
} elseif ($Operation -eq "add-item") {
$arguments += @("-session-id", $Values.sessionId, "-server-handle", "$($Values.serverHandle)", "-item", $Values.item)
} elseif ($Operation -eq "advise") {
$arguments += @("-session-id", $Values.sessionId, "-server-handle", "$($Values.serverHandle)", "-item-handle", "$($Values.itemHandle)")
} elseif ($Operation -eq "stream-events") {
$arguments += @("-session-id", $Values.sessionId, "-limit", "$EventLimit")
} elseif ($Operation -eq "close-session") {
$arguments += @("-session-id", $Values.sessionId)
}
return [pscustomobject]@{ file = "go"; args = $arguments; cwd = (Join-Path $repoRoot "clients/go"); env = @{} }
}
"rust" {
$arguments = @(
"run", "-p", "mxgw-cli", "--", $Operation,
"--endpoint", $httpEndpoint,
"--api-key-env", $ApiKeyEnv,
"--json"
)
if ($Operation -eq "open-session") {
$arguments += @("--client-name", $clientName)
} elseif ($Operation -eq "register") {
$arguments += @("--session-id", $Values.sessionId, "--client-name", $clientName)
} elseif ($Operation -eq "add-item") {
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item", $Values.item)
} elseif ($Operation -eq "advise") {
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handle", "$($Values.itemHandle)")
} elseif ($Operation -eq "stream-events") {
$arguments += @("--session-id", $Values.sessionId, "--max-events", "$EventLimit")
} elseif ($Operation -eq "close-session") {
$arguments += @("--session-id", $Values.sessionId)
}
return [pscustomobject]@{ file = "cargo"; args = $arguments; cwd = (Join-Path $repoRoot "clients/rust"); env = @{} }
}
"python" {
$arguments = @(
"-m", "mxgateway_cli", $Operation,
"--endpoint", $hostEndpoint,
"--api-key-env", $ApiKeyEnv,
"--plaintext",
"--json"
)
if ($Operation -eq "open-session") {
$arguments += @("--client-name", $clientName)
} elseif ($Operation -eq "register") {
$arguments += @("--session-id", $Values.sessionId, "--client-name", $clientName)
} elseif ($Operation -eq "add-item") {
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item", $Values.item)
} elseif ($Operation -eq "advise") {
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handle", "$($Values.itemHandle)")
} elseif ($Operation -eq "stream-events") {
$arguments += @("--session-id", $Values.sessionId, "--max-events", "$EventLimit", "--timeout", "15")
} elseif ($Operation -eq "close-session") {
$arguments += @("--session-id", $Values.sessionId)
}
$env = @{
PYTHONPATH = Join-Path $repoRoot "clients/python/src"
}
return [pscustomobject]@{ file = "py"; args = @("-3.12") + $arguments; cwd = (Join-Path $repoRoot "clients/python"); env = $env }
}
"java" {
$cliArgs = @(
$Operation,
"--endpoint", $hostEndpoint,
"--api-key-env", $ApiKeyEnv,
"--plaintext",
"--json"
)
if ($Operation -eq "open-session") {
$cliArgs += @("--client-session-name", $clientName)
} elseif ($Operation -eq "register") {
$cliArgs += @("--session-id", $Values.sessionId, "--client-name", $clientName)
} elseif ($Operation -eq "add-item") {
$cliArgs += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item", $Values.item)
} elseif ($Operation -eq "advise") {
$cliArgs += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handle", "$($Values.itemHandle)")
} elseif ($Operation -eq "stream-events") {
$cliArgs += @("--session-id", $Values.sessionId, "--limit", "$EventLimit")
} elseif ($Operation -eq "close-session") {
$cliArgs += @("--session-id", $Values.sessionId)
}
$arguments = @("--quiet", ":mxgateway-cli:run", "--args=$($cliArgs -join ' ')")
return [pscustomobject]@{ file = "gradle"; args = $arguments; cwd = (Join-Path $repoRoot "clients/java"); env = @{} }
}
}
}
function Invoke-ClientOperation {
param(
[string]$Client,
[string]$Operation,
[hashtable]$Values = @{}
)
$command = Get-ClientCommand -Client $Client -Operation $Operation -Values $Values
$result = Invoke-NativeCommand `
-FilePath $command.file `
-Arguments $command.args `
-WorkingDirectory $command.cwd `
-Environment $command.env
if ($DryRun) {
switch ($Operation) {
"open-session" { return [pscustomobject]@{ sessionId = "dry-run-session-$Client"; reply = [pscustomobject]@{ sessionId = "dry-run-session-$Client" } } }
"register" { return [pscustomobject]@{ serverHandle = 1; register = [pscustomobject]@{ serverHandle = 1 }; reply = [pscustomobject]@{ register = [pscustomobject]@{ serverHandle = 1 } } } }
"add-item" { return [pscustomobject]@{ itemHandle = 1; addItem = [pscustomobject]@{ itemHandle = 1 }; reply = [pscustomobject]@{ addItem = [pscustomobject]@{ itemHandle = 1 } } } }
"stream-events" { return [pscustomobject]@{ eventCount = 1; events = @([pscustomobject]@{ workerSequence = 1 }) } }
default { return [pscustomobject]@{ ok = $true; reply = [pscustomobject]@{} } }
}
}
return Read-JsonObject -Text $result.stdout
}
$discoveryJson = & $discoveryScript `
-MachineStart $MachineStart `
-MachineEnd $MachineEnd `
-Attributes $Attributes `
-SqlServer $SqlServer `
-Database $Database `
-Json
$convertedTags = $discoveryJson | ConvertFrom-Json
$tags = @($convertedTags)
if ($tags.Count -eq 1 -and $tags[0] -is [System.Array]) {
$tags = @($tags[0])
}
$expectedTagCount = (($MachineEnd - $MachineStart + 1) * $Attributes.Count)
if ($tags.Count -ne $expectedTagCount) {
$found = $tags | Group-Object -Property tagName | ForEach-Object {
"$($_.Name)=$($_.Count)"
}
throw "Expected $expectedTagCount discovered test tags, found $($tags.Count): $($found -join ', ')"
}
$run = [ordered]@{
schemaVersion = 1
endpoint = $Endpoint
apiKeyEnv = $ApiKeyEnv
machineStart = $MachineStart
machineEnd = $MachineEnd
attributes = $Attributes
eventLimit = $EventLimit
skipStream = [bool]$SkipStream
startedAt = (Get-Date).ToUniversalTime().ToString("O")
discoveredTags = $tags
clients = @()
}
$hadFailure = $false
foreach ($client in $Clients) {
Write-Host "Running $client client e2e flow against $($tags.Count) discovered tags."
$sessionId = $null
$serverHandle = $null
$clientResult = [ordered]@{
language = $client
sessionId = $null
serverHandle = $null
addedItems = @()
eventCount = 0
closed = $false
error = $null
}
try {
$openJson = Invoke-ClientOperation -Client $client -Operation "open-session"
$sessionId = Get-OpenSessionId -Client $client -Json $openJson
if ([string]::IsNullOrWhiteSpace($sessionId)) {
throw "The $client open-session command did not return a session id."
}
$clientResult.sessionId = $sessionId
$registerJson = Invoke-ClientOperation -Client $client -Operation "register" -Values @{
sessionId = $sessionId
}
$serverHandle = Get-ServerHandle -Client $client -Json $registerJson
$clientResult.serverHandle = $serverHandle
foreach ($tag in $tags) {
$addJson = Invoke-ClientOperation -Client $client -Operation "add-item" -Values @{
sessionId = $sessionId
serverHandle = $serverHandle
item = $tag.fullTagReference
}
$itemHandle = Get-ItemHandle -Client $client -Json $addJson
Invoke-ClientOperation -Client $client -Operation "advise" -Values @{
sessionId = $sessionId
serverHandle = $serverHandle
itemHandle = $itemHandle
} | Out-Null
$clientResult.addedItems += [ordered]@{
tagName = $tag.tagName
attributeName = $tag.attributeName
fullTagReference = $tag.fullTagReference
itemHandle = $itemHandle
protectedWriteRequired = $tag.attributeName -eq "ProtectedValue"
}
}
if (-not $SkipStream) {
$streamJson = Invoke-ClientOperation -Client $client -Operation "stream-events" -Values @{
sessionId = $sessionId
}
$clientResult.eventCount = Get-StreamEventCount -Client $client -Json $streamJson
if ($clientResult.eventCount -lt 1) {
throw "The $client stream-events command returned no events."
}
}
} catch {
$hadFailure = $true
$clientResult.error = $_.Exception.Message
Write-Warning "$client e2e flow failed: $($clientResult.error)"
} finally {
if (-not [string]::IsNullOrWhiteSpace($sessionId)) {
try {
Invoke-ClientOperation -Client $client -Operation "close-session" -Values @{
sessionId = $sessionId
} | Out-Null
$clientResult.closed = $true
} catch {
$hadFailure = $true
$clientResult.error = "$($clientResult.error) close-session failed: $($_.Exception.Message)"
}
}
}
$run.clients += $clientResult
}
$run.completedAt = (Get-Date).ToUniversalTime().ToString("O")
$run.success = -not $hadFailure
if (-not $DryRun) {
$reportDirectory = Split-Path -Parent $ReportPath
if (-not [string]::IsNullOrWhiteSpace($reportDirectory)) {
New-Item -ItemType Directory -Path $reportDirectory -Force | Out-Null
}
$run | ConvertTo-Json -Depth 8 | Set-Content -Path $ReportPath -Encoding UTF8
Write-Host "Wrote e2e report to $ReportPath"
}
if ($hadFailure) {
exit 1
}