Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 758aca2355 | |||
| 06030dd1ef | |||
| e355a7674b |
+6
-1
@@ -260,7 +260,12 @@ public final class MxGatewayClient implements AutoCloseable {
|
|||||||
* @return an iterator-style stream of events
|
* @return an iterator-style stream of events
|
||||||
*/
|
*/
|
||||||
public MxEventStream streamEvents(StreamEventsRequest request) {
|
public MxEventStream streamEvents(StreamEventsRequest request) {
|
||||||
MxEventStream stream = new MxEventStream(16);
|
// The buffer must absorb the gateway's session-backlog replay burst,
|
||||||
|
// which arrives far faster than the iterator drains it. A small queue
|
||||||
|
// overflows on any moderately active session; 1024 covers a realistic
|
||||||
|
// backlog while still bounding memory and preserving overflow
|
||||||
|
// detection for a genuinely unbounded stream.
|
||||||
|
MxEventStream stream = new MxEventStream(1024);
|
||||||
MxGatewayChannels.withStreamDeadline(rawAsyncStub(), options).streamEvents(request, stream.observer());
|
MxGatewayChannels.withStreamDeadline(rawAsyncStub(), options).streamEvents(request, stream.observer());
|
||||||
return stream;
|
return stream;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,12 +17,12 @@ use clap::{Args, Parser, Subcommand, ValueEnum};
|
|||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
use mxgateway_client::generated::galaxy_repository::v1::DeployEvent;
|
use mxgateway_client::generated::galaxy_repository::v1::DeployEvent;
|
||||||
use mxgateway_client::generated::mxaccess_gateway::v1::{
|
use mxgateway_client::generated::mxaccess_gateway::v1::{
|
||||||
CloseSessionRequest, MxCommand, MxCommandKind, MxCommandRequest, OpenSessionRequest,
|
CloseSessionRequest, MxCommand, MxCommandKind, MxCommandRequest, MxEvent, MxEventFamily,
|
||||||
PingCommand, StreamEventsRequest,
|
MxValue as ProtoMxValue, OpenSessionRequest, PingCommand, StreamEventsRequest,
|
||||||
};
|
};
|
||||||
use mxgateway_client::{
|
use mxgateway_client::{
|
||||||
ApiKey, ClientOptions, Error, GalaxyClient, GatewayClient, MxValue, CLIENT_VERSION,
|
ApiKey, ClientOptions, Error, GalaxyClient, GatewayClient, MxValue, MxValueProjection,
|
||||||
GATEWAY_PROTOCOL_VERSION, WORKER_PROTOCOL_VERSION,
|
CLIENT_VERSION, GATEWAY_PROTOCOL_VERSION, WORKER_PROTOCOL_VERSION,
|
||||||
};
|
};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
@@ -451,7 +451,7 @@ async fn run(cli: Cli) -> Result<(), Error> {
|
|||||||
after_worker_sequence,
|
after_worker_sequence,
|
||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
let mut events = Vec::new();
|
let mut events: Vec<Value> = Vec::new();
|
||||||
let mut event_count = 0usize;
|
let mut event_count = 0usize;
|
||||||
while event_count < max_events {
|
while event_count < max_events {
|
||||||
let Some(event) = stream.next().await else {
|
let Some(event) = stream.next().await else {
|
||||||
@@ -460,21 +460,17 @@ async fn run(cli: Cli) -> Result<(), Error> {
|
|||||||
let event = event?;
|
let event = event?;
|
||||||
event_count += 1;
|
event_count += 1;
|
||||||
if jsonl {
|
if jsonl {
|
||||||
println!(
|
println!("{}", event_to_json(&event));
|
||||||
"{}",
|
|
||||||
json!({
|
|
||||||
"workerSequence": event.worker_sequence,
|
|
||||||
"family": event.family,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else if json {
|
} else if json {
|
||||||
events.push(event);
|
events.push(event_to_json(&event));
|
||||||
} else {
|
} else {
|
||||||
println!("{} {}", event.worker_sequence, event.family);
|
println!("{} {}", event.worker_sequence, event.family);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if json {
|
if json {
|
||||||
println!("{}", json!({ "eventCount": event_count }));
|
// `eventCount` is preserved for back-compat; `events` carries
|
||||||
|
// the per-event detail the cross-language e2e matrix compares.
|
||||||
|
println!("{}", json!({ "eventCount": event_count, "events": events }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Command::Write {
|
Command::Write {
|
||||||
@@ -841,6 +837,49 @@ fn print_deploy_event(event: &DeployEvent, use_json: bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Render a streamed [`MxEvent`] as a JSON object. The scalar value is
|
||||||
|
/// projected into protojson-style `*Value` keys so the cross-language e2e
|
||||||
|
/// matrix can extract and compare event values uniformly across all five
|
||||||
|
/// client CLIs.
|
||||||
|
fn event_to_json(event: &MxEvent) -> Value {
|
||||||
|
// Render the family as the proto enum name (e.g. MX_EVENT_FAMILY_ON_WRITE_COMPLETE)
|
||||||
|
// so it matches the protobuf-JSON the .NET/Go/Java/Python CLIs emit.
|
||||||
|
let family = MxEventFamily::try_from(event.family)
|
||||||
|
.map(|family| family.as_str_name().to_owned())
|
||||||
|
.unwrap_or_else(|_| event.family.to_string());
|
||||||
|
json!({
|
||||||
|
"family": family,
|
||||||
|
"sessionId": event.session_id,
|
||||||
|
"serverHandle": event.server_handle,
|
||||||
|
"itemHandle": event.item_handle,
|
||||||
|
"quality": event.quality,
|
||||||
|
"workerSequence": event.worker_sequence,
|
||||||
|
"value": event.value.as_ref().map(event_value_to_json),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Project an [`MxValue`] into a protojson-shaped JSON object whose single
|
||||||
|
/// key names the scalar kind (`int32Value`, `stringValue`, ...), matching
|
||||||
|
/// the protobuf-JSON the .NET/Go/Java CLIs emit.
|
||||||
|
fn event_value_to_json(value: &ProtoMxValue) -> Value {
|
||||||
|
match MxValue::from_proto(value.clone()).projection() {
|
||||||
|
MxValueProjection::Bool(inner) => json!({ "boolValue": inner }),
|
||||||
|
MxValueProjection::Int32(inner) => json!({ "int32Value": inner }),
|
||||||
|
// protojson renders 64-bit integers as strings; mirror that here.
|
||||||
|
MxValueProjection::Int64(inner) => json!({ "int64Value": inner.to_string() }),
|
||||||
|
MxValueProjection::Float(inner) => json!({ "floatValue": inner }),
|
||||||
|
MxValueProjection::Double(inner) => json!({ "doubleValue": inner }),
|
||||||
|
MxValueProjection::String(inner) => json!({ "stringValue": inner }),
|
||||||
|
MxValueProjection::Timestamp(ts) => {
|
||||||
|
json!({ "timestampValue": { "seconds": ts.seconds, "nanos": ts.nanos } })
|
||||||
|
}
|
||||||
|
MxValueProjection::Array(_) => json!({ "arrayValue": {} }),
|
||||||
|
MxValueProjection::Raw(bytes) => json!({ "rawValue": { "byteCount": bytes.len() } }),
|
||||||
|
MxValueProjection::Null => json!({ "isNull": true }),
|
||||||
|
MxValueProjection::Unset => Value::Null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse a small but practically-complete subset of RFC3339:
|
/// Parse a small but practically-complete subset of RFC3339:
|
||||||
/// `YYYY-MM-DDTHH:MM:SS[.fffffffff][Z|+HH:MM|-HH:MM]`. Returns the
|
/// `YYYY-MM-DDTHH:MM:SS[.fffffffff][Z|+HH:MM|-HH:MM]`. Returns the
|
||||||
/// corresponding `prost_types::Timestamp` (Unix seconds + nanoseconds).
|
/// corresponding `prost_types::Timestamp` (Unix seconds + nanoseconds).
|
||||||
|
|||||||
+49
-5
@@ -171,11 +171,39 @@ powershell -ExecutionPolicy Bypass -File scripts/discover-testmachine-tags.ps1 -
|
|||||||
```
|
```
|
||||||
|
|
||||||
`scripts/run-client-e2e-tests.ps1` drives the .NET, Go, Rust, Python, and Java
|
`scripts/run-client-e2e-tests.ps1` drives the .NET, Go, Rust, Python, and Java
|
||||||
client CLIs through a live gateway session. For each client it opens one
|
client CLIs through a live gateway session. The gateway and worker are assumed
|
||||||
session, registers, verifies `SubscribeBulk` and `UnsubscribeBulk` on a bounded
|
to be already running at `-Endpoint`; the script does not start or stop them.
|
||||||
tag subset, adds and advises every discovered test tag, reads a bounded event
|
For each client it runs these phases, then closes the session in a `finally`
|
||||||
stream, then closes the session in a `finally` path. The script writes a JSON
|
path and writes a JSON report under `artifacts/e2e/`:
|
||||||
report under `artifacts/e2e/`.
|
|
||||||
|
1. **Session + register** — opens one session and registers.
|
||||||
|
2. **Bulk** — verifies `SubscribeBulk` / `UnsubscribeBulk` on a bounded tag
|
||||||
|
subset (skip with `-SkipBulk`).
|
||||||
|
3. **Add-item / advise** — adds and advises every discovered test tag.
|
||||||
|
4. **Stream** — asserts a bounded event stream delivers at least one event
|
||||||
|
(skip with `-SkipStream`).
|
||||||
|
5. **Parity** — asserts MXAccess error paths are rejected rather than silently
|
||||||
|
succeeding: an invalid item handle and an unknown session id (skip with
|
||||||
|
`-SkipParity`).
|
||||||
|
6. **Auth rejection** — asserts `open-session` is rejected when the API key is
|
||||||
|
missing, and (when `-RejectScopeApiKeyEnv` names an insufficient-scope key)
|
||||||
|
when the key lacks the required scope. Skip with `-SkipAuth`.
|
||||||
|
7. **Write round-trip** — *opt-in (`-VerifyWrite`).* Runs right after
|
||||||
|
`register`: adds and advises a configurable writable attribute
|
||||||
|
(`-WriteAttribute`, default `TestChangingInt`), writes a per-client
|
||||||
|
sentinel value, then streams events and asserts an `OnWriteComplete` event
|
||||||
|
for that item is observed — proof the write round-tripped through the
|
||||||
|
gateway, worker, and MXAccess provider. The written value being echoed back
|
||||||
|
in an `OnDataChange` is recorded best-effort (`echoObserved`): a
|
||||||
|
provider-driven attribute such as `TestChangingInt` accepts the write but
|
||||||
|
immediately overwrites it, so no data-change carries the value back. The
|
||||||
|
Rust `stream-events` CLI emits full per-event JSON (`family`, `itemHandle`,
|
||||||
|
`value`) so all five clients apply the same checks.
|
||||||
|
|
||||||
|
It is opt-in because it mutates live tag state. The phase fails fast if the
|
||||||
|
write command is rejected — e.g. against a gateway whose worker predates
|
||||||
|
write support (`MxAccessCommandExecutor` returning `InvalidRequest` for
|
||||||
|
`Write`/`Write2`/`WriteSecured`/`WriteSecured2`).
|
||||||
|
|
||||||
Build the gateway and worker, start the gateway, and provide a valid API key
|
Build the gateway and worker, start the gateway, and provide a valid API key
|
||||||
before running the client e2e script:
|
before running the client e2e script:
|
||||||
@@ -192,9 +220,25 @@ powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -Clien
|
|||||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -BulkTagCount 10
|
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -BulkTagCount 10
|
||||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -SkipStream
|
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -SkipStream
|
||||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -SkipBulk
|
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -SkipBulk
|
||||||
|
# Write round-trip (opt-in): point at a writable scalar attribute and its
|
||||||
|
# value type.
|
||||||
|
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -VerifyWrite -WriteAttribute TestChangingInt -WriteType int32
|
||||||
|
# Auth rejection: also assert an insufficient-scope key is denied.
|
||||||
|
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -RejectScopeApiKeyEnv MXGATEWAY_READONLY_API_KEY
|
||||||
|
# Run all five clients concurrently as isolated child processes.
|
||||||
|
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -Parallel
|
||||||
|
# Validate the flow offline (prints commands, contacts no gateway).
|
||||||
|
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -DryRun
|
||||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -Endpoint localhost:5000 -ApiKeyEnv MXGATEWAY_API_KEY
|
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -Endpoint localhost:5000 -ApiKeyEnv MXGATEWAY_API_KEY
|
||||||
```
|
```
|
||||||
|
|
||||||
|
When `-VerifyWrite` is enabled, the write round-trip fails loudly if the write
|
||||||
|
command is rejected, if `-WriteAttribute` does not name a writable scalar
|
||||||
|
attribute, or if no `OnWriteComplete` event is observed for the written item
|
||||||
|
within `-WriteEchoMaxEvents` (default 200) streamed events. Raise
|
||||||
|
`-WriteEchoMaxEvents` if the gateway's per-session event backlog is large
|
||||||
|
enough to push `OnWriteComplete` past that bound.
|
||||||
|
|
||||||
## Focused Commands
|
## Focused Commands
|
||||||
|
|
||||||
Run the cross-language smoke matrix tests after changing the documented client
|
Run the cross-language smoke matrix tests after changing the documented client
|
||||||
|
|||||||
+664
-143
@@ -1,3 +1,17 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Cross-language client e2e matrix for the MXAccess Gateway.
|
||||||
|
|
||||||
|
.DESCRIPTION
|
||||||
|
Drives the .NET, Go, Rust, Python, and Java client CLIs against a running
|
||||||
|
gateway + worker. For each language the script exercises session open/close,
|
||||||
|
register, bulk subscribe/unsubscribe, per-tag add-item/advise, event
|
||||||
|
streaming, a write round-trip with value assertion, error-path (parity)
|
||||||
|
checks, and API-key auth rejection.
|
||||||
|
|
||||||
|
The gateway and worker are assumed to be already running at -Endpoint; the
|
||||||
|
script does not start or stop them.
|
||||||
|
#>
|
||||||
[CmdletBinding()]
|
[CmdletBinding()]
|
||||||
param(
|
param(
|
||||||
[string[]]$Clients = @("dotnet", "go", "rust", "python", "java"),
|
[string[]]$Clients = @("dotnet", "go", "rust", "python", "java"),
|
||||||
@@ -19,8 +33,29 @@ param(
|
|||||||
[int]$BulkTagCount = 6,
|
[int]$BulkTagCount = 6,
|
||||||
[switch]$SkipStream,
|
[switch]$SkipStream,
|
||||||
[switch]$SkipBulk,
|
[switch]$SkipBulk,
|
||||||
|
# Write round-trip. Opt-in because it mutates live tag state: it writes a
|
||||||
|
# sentinel value to -WriteAttribute and asserts an OnWriteComplete event
|
||||||
|
# confirms the write reached the MXAccess provider.
|
||||||
|
[switch]$VerifyWrite,
|
||||||
|
[string]$WriteAttribute = "TestChangingInt",
|
||||||
|
[string]$WriteType = "int32",
|
||||||
|
[int]$WriteValueBase = 424200,
|
||||||
|
[int]$WriteEchoMaxEvents = 200,
|
||||||
|
# Error-path (parity) checks.
|
||||||
|
[switch]$SkipParity,
|
||||||
|
# API-key auth rejection checks.
|
||||||
|
[switch]$SkipAuth,
|
||||||
|
# Optional env var holding an API key whose scopes are insufficient for
|
||||||
|
# open-session; when supplied the auth phase also asserts that key is
|
||||||
|
# rejected (PermissionDenied) on top of the always-on missing-key check.
|
||||||
|
[string]$RejectScopeApiKeyEnv,
|
||||||
|
# Run each language client concurrently as an isolated child process.
|
||||||
|
[switch]$Parallel,
|
||||||
[switch]$DryRun,
|
[switch]$DryRun,
|
||||||
[string]$ReportPath
|
[string]$ReportPath,
|
||||||
|
# Internal: set by -Parallel on each spawned child so it always writes its
|
||||||
|
# report (even under -DryRun) for the parent to merge. Not for direct use.
|
||||||
|
[switch]$EmitReport
|
||||||
)
|
)
|
||||||
|
|
||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
@@ -56,6 +91,10 @@ if ($BulkTagCount -lt 1) {
|
|||||||
throw "BulkTagCount must be greater than zero."
|
throw "BulkTagCount must be greater than zero."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($WriteEchoMaxEvents -lt 1) {
|
||||||
|
throw "WriteEchoMaxEvents must be greater than zero."
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($client in $Clients) {
|
foreach ($client in $Clients) {
|
||||||
if ($validClients -notcontains $client) {
|
if ($validClients -notcontains $client) {
|
||||||
throw "Unsupported client '$client'. Supported clients: $($validClients -join ', ')."
|
throw "Unsupported client '$client'. Supported clients: $($validClients -join ', ')."
|
||||||
@@ -116,7 +155,10 @@ function Invoke-NativeCommand {
|
|||||||
[string]$FilePath,
|
[string]$FilePath,
|
||||||
[string[]]$Arguments,
|
[string[]]$Arguments,
|
||||||
[string]$WorkingDirectory,
|
[string]$WorkingDirectory,
|
||||||
[hashtable]$Environment = @{}
|
[hashtable]$Environment = @{},
|
||||||
|
# When set, a non-zero exit code is returned to the caller instead of
|
||||||
|
# throwing. Used by the parity and auth phases, which expect failure.
|
||||||
|
[switch]$AllowFailure
|
||||||
)
|
)
|
||||||
|
|
||||||
$process = [System.Diagnostics.Process]::new()
|
$process = [System.Diagnostics.Process]::new()
|
||||||
@@ -155,7 +197,7 @@ function Invoke-NativeCommand {
|
|||||||
stderr = $stderr
|
stderr = $stderr
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($result.exitCode -ne 0) {
|
if ($result.exitCode -ne 0 -and -not $AllowFailure) {
|
||||||
throw "Command failed with exit code $($result.exitCode): $commandLine`n$stderr`n$stdout"
|
throw "Command failed with exit code $($result.exitCode): $commandLine`n$stderr`n$stdout"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,6 +285,25 @@ function Get-StreamEventCount {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Returns the per-event objects from a stream-events reply so the write
|
||||||
|
# round-trip can inspect their values. Mirrors Get-StreamEventCount: .NET,
|
||||||
|
# Rust, and Python aggregate under `events`; Go and Java emit one event
|
||||||
|
# object per line (Read-JsonObject collapses NDJSON into an array).
|
||||||
|
function Get-StreamEvents {
|
||||||
|
param(
|
||||||
|
[string]$Client,
|
||||||
|
[object]$Json
|
||||||
|
)
|
||||||
|
|
||||||
|
switch ($Client) {
|
||||||
|
"dotnet" { return @($Json.events) }
|
||||||
|
"go" { return @($Json) }
|
||||||
|
"rust" { return @($Json.events) }
|
||||||
|
"python" { return @($Json.events) }
|
||||||
|
"java" { return @($Json) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Get-PropertyValue {
|
function Get-PropertyValue {
|
||||||
param(
|
param(
|
||||||
[object]$Object,
|
[object]$Object,
|
||||||
@@ -263,6 +324,71 @@ function Get-PropertyValue {
|
|||||||
return $null
|
return $null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Extracts the item handle from a streamed event, tolerating camelCase
|
||||||
|
# (protobuf-JSON) and snake_case (some MessageToDict shapes).
|
||||||
|
function Get-EventItemHandle {
|
||||||
|
param([object]$Event)
|
||||||
|
|
||||||
|
$handle = Get-PropertyValue -Object $Event -Names @("itemHandle", "item_handle")
|
||||||
|
if ($null -eq $handle) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
return [int]$handle
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extracts the event family as a string. All five CLIs render it as the
|
||||||
|
# protobuf enum name (e.g. MX_EVENT_FAMILY_ON_WRITE_COMPLETE).
|
||||||
|
function Get-EventFamily {
|
||||||
|
param([object]$Event)
|
||||||
|
|
||||||
|
return [string](Get-PropertyValue -Object $Event -Names @("family"))
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extracts the scalar payload from a streamed event's MxValue as a string.
|
||||||
|
# The MxValue oneof renders to one protobuf-JSON `*Value` key; all five
|
||||||
|
# CLIs (after the Rust stream-events extension) emit the same key names.
|
||||||
|
function Get-EventScalar {
|
||||||
|
param([object]$Event)
|
||||||
|
|
||||||
|
$value = Get-PropertyValue -Object $Event -Names @("value")
|
||||||
|
if ($null -eq $value) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($key in @("boolValue", "int32Value", "int64Value", "floatValue", "doubleValue", "stringValue")) {
|
||||||
|
$property = $value.PSObject.Properties[$key]
|
||||||
|
if ($null -ne $property -and $null -ne $property.Value) {
|
||||||
|
return [string]$property.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Compares a written value against an observed event scalar. Numeric values
|
||||||
|
# are compared numerically (so 42 matches 42.0); everything else compares as
|
||||||
|
# a trimmed, case-insensitive string.
|
||||||
|
function Test-ValueEquals {
|
||||||
|
param(
|
||||||
|
[string]$Expected,
|
||||||
|
[string]$Observed
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Observed)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$expectedNumber = 0.0
|
||||||
|
$observedNumber = 0.0
|
||||||
|
if ([double]::TryParse($Expected, [ref]$expectedNumber) -and
|
||||||
|
[double]::TryParse($Observed, [ref]$observedNumber)) {
|
||||||
|
return $expectedNumber -eq $observedNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
return [string]::Equals($Expected.Trim(), $Observed.Trim(), [StringComparison]::OrdinalIgnoreCase)
|
||||||
|
}
|
||||||
|
|
||||||
function Get-BulkResults {
|
function Get-BulkResults {
|
||||||
param(
|
param(
|
||||||
[string]$Client,
|
[string]$Client,
|
||||||
@@ -315,12 +441,20 @@ function Get-ClientCommand {
|
|||||||
param(
|
param(
|
||||||
[string]$Client,
|
[string]$Client,
|
||||||
[string]$Operation,
|
[string]$Operation,
|
||||||
[hashtable]$Values
|
[hashtable]$Values,
|
||||||
|
# The environment variable the client reads the API key from. Defaults
|
||||||
|
# to the run-wide -ApiKeyEnv; the auth phase overrides it to drive a
|
||||||
|
# missing-key or insufficient-scope rejection.
|
||||||
|
[string]$ApiKeyEnvName = $ApiKeyEnv
|
||||||
)
|
)
|
||||||
|
|
||||||
$httpEndpoint = ConvertTo-HttpEndpoint -Value $Endpoint
|
$httpEndpoint = ConvertTo-HttpEndpoint -Value $Endpoint
|
||||||
$hostEndpoint = ConvertTo-HostEndpoint -Value $Endpoint
|
$hostEndpoint = ConvertTo-HostEndpoint -Value $Endpoint
|
||||||
$clientName = "mxgw-$Client-e2e"
|
$clientName = "mxgw-$Client-e2e"
|
||||||
|
$streamMaxEvents = if ($Values.ContainsKey("maxEvents")) { [int]$Values.maxEvents } else { $EventLimit }
|
||||||
|
# Python's stream-events call ends on a wall-clock timeout; give it enough
|
||||||
|
# headroom to drain a large write-echo budget.
|
||||||
|
$pythonStreamTimeout = [Math]::Max(15, [int][Math]::Ceiling($streamMaxEvents / 4.0))
|
||||||
|
|
||||||
switch ($Client) {
|
switch ($Client) {
|
||||||
"dotnet" {
|
"dotnet" {
|
||||||
@@ -328,7 +462,7 @@ function Get-ClientCommand {
|
|||||||
"run", "--project", "clients/dotnet/MxGateway.Client.Cli", "--",
|
"run", "--project", "clients/dotnet/MxGateway.Client.Cli", "--",
|
||||||
$Operation,
|
$Operation,
|
||||||
"--endpoint", $httpEndpoint,
|
"--endpoint", $httpEndpoint,
|
||||||
"--api-key-env", $ApiKeyEnv,
|
"--api-key-env", $ApiKeyEnvName,
|
||||||
"--timeout", "60s",
|
"--timeout", "60s",
|
||||||
"--json"
|
"--json"
|
||||||
)
|
)
|
||||||
@@ -344,8 +478,10 @@ function Get-ClientCommand {
|
|||||||
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--items", $Values.items)
|
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--items", $Values.items)
|
||||||
} elseif ($Operation -eq "unsubscribe-bulk") {
|
} elseif ($Operation -eq "unsubscribe-bulk") {
|
||||||
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handles", $Values.itemHandles)
|
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handles", $Values.itemHandles)
|
||||||
|
} elseif ($Operation -eq "write") {
|
||||||
|
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handle", "$($Values.itemHandle)", "--type", $Values.valueType, "--value", $Values.value)
|
||||||
} elseif ($Operation -eq "stream-events") {
|
} elseif ($Operation -eq "stream-events") {
|
||||||
$arguments += @("--session-id", $Values.sessionId, "--max-events", "$EventLimit")
|
$arguments += @("--session-id", $Values.sessionId, "--max-events", "$streamMaxEvents")
|
||||||
} elseif ($Operation -eq "close-session") {
|
} elseif ($Operation -eq "close-session") {
|
||||||
$arguments += @("--session-id", $Values.sessionId)
|
$arguments += @("--session-id", $Values.sessionId)
|
||||||
}
|
}
|
||||||
@@ -355,7 +491,7 @@ function Get-ClientCommand {
|
|||||||
$arguments = @(
|
$arguments = @(
|
||||||
"run", "./cmd/mxgw-go", $Operation,
|
"run", "./cmd/mxgw-go", $Operation,
|
||||||
"-endpoint", $hostEndpoint,
|
"-endpoint", $hostEndpoint,
|
||||||
"-api-key-env", $ApiKeyEnv,
|
"-api-key-env", $ApiKeyEnvName,
|
||||||
"-plaintext",
|
"-plaintext",
|
||||||
"-json"
|
"-json"
|
||||||
)
|
)
|
||||||
@@ -371,8 +507,10 @@ function Get-ClientCommand {
|
|||||||
$arguments += @("-session-id", $Values.sessionId, "-server-handle", "$($Values.serverHandle)", "-items", $Values.items)
|
$arguments += @("-session-id", $Values.sessionId, "-server-handle", "$($Values.serverHandle)", "-items", $Values.items)
|
||||||
} elseif ($Operation -eq "unsubscribe-bulk") {
|
} elseif ($Operation -eq "unsubscribe-bulk") {
|
||||||
$arguments += @("-session-id", $Values.sessionId, "-server-handle", "$($Values.serverHandle)", "-item-handles", $Values.itemHandles)
|
$arguments += @("-session-id", $Values.sessionId, "-server-handle", "$($Values.serverHandle)", "-item-handles", $Values.itemHandles)
|
||||||
|
} elseif ($Operation -eq "write") {
|
||||||
|
$arguments += @("-session-id", $Values.sessionId, "-server-handle", "$($Values.serverHandle)", "-item-handle", "$($Values.itemHandle)", "-type", $Values.valueType, "-value", $Values.value)
|
||||||
} elseif ($Operation -eq "stream-events") {
|
} elseif ($Operation -eq "stream-events") {
|
||||||
$arguments += @("-session-id", $Values.sessionId, "-limit", "$EventLimit")
|
$arguments += @("-session-id", $Values.sessionId, "-limit", "$streamMaxEvents")
|
||||||
} elseif ($Operation -eq "close-session") {
|
} elseif ($Operation -eq "close-session") {
|
||||||
$arguments += @("-session-id", $Values.sessionId)
|
$arguments += @("-session-id", $Values.sessionId)
|
||||||
}
|
}
|
||||||
@@ -382,7 +520,7 @@ function Get-ClientCommand {
|
|||||||
$arguments = @(
|
$arguments = @(
|
||||||
"run", "-p", "mxgw-cli", "--", $Operation,
|
"run", "-p", "mxgw-cli", "--", $Operation,
|
||||||
"--endpoint", $httpEndpoint,
|
"--endpoint", $httpEndpoint,
|
||||||
"--api-key-env", $ApiKeyEnv,
|
"--api-key-env", $ApiKeyEnvName,
|
||||||
"--json"
|
"--json"
|
||||||
)
|
)
|
||||||
if ($Operation -eq "open-session") {
|
if ($Operation -eq "open-session") {
|
||||||
@@ -397,8 +535,11 @@ function Get-ClientCommand {
|
|||||||
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--items", $Values.items)
|
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--items", $Values.items)
|
||||||
} elseif ($Operation -eq "unsubscribe-bulk") {
|
} elseif ($Operation -eq "unsubscribe-bulk") {
|
||||||
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handles", $Values.itemHandles)
|
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handles", $Values.itemHandles)
|
||||||
|
} elseif ($Operation -eq "write") {
|
||||||
|
# Rust names the type flag --value-type, unlike the other CLIs.
|
||||||
|
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handle", "$($Values.itemHandle)", "--value-type", $Values.valueType, "--value", $Values.value)
|
||||||
} elseif ($Operation -eq "stream-events") {
|
} elseif ($Operation -eq "stream-events") {
|
||||||
$arguments += @("--session-id", $Values.sessionId, "--max-events", "$EventLimit")
|
$arguments += @("--session-id", $Values.sessionId, "--max-events", "$streamMaxEvents")
|
||||||
} elseif ($Operation -eq "close-session") {
|
} elseif ($Operation -eq "close-session") {
|
||||||
$arguments += @("--session-id", $Values.sessionId)
|
$arguments += @("--session-id", $Values.sessionId)
|
||||||
}
|
}
|
||||||
@@ -408,7 +549,7 @@ function Get-ClientCommand {
|
|||||||
$arguments = @(
|
$arguments = @(
|
||||||
"-m", "mxgateway_cli", $Operation,
|
"-m", "mxgateway_cli", $Operation,
|
||||||
"--endpoint", $hostEndpoint,
|
"--endpoint", $hostEndpoint,
|
||||||
"--api-key-env", $ApiKeyEnv,
|
"--api-key-env", $ApiKeyEnvName,
|
||||||
"--plaintext",
|
"--plaintext",
|
||||||
"--json"
|
"--json"
|
||||||
)
|
)
|
||||||
@@ -424,8 +565,10 @@ function Get-ClientCommand {
|
|||||||
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--items", $Values.items)
|
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--items", $Values.items)
|
||||||
} elseif ($Operation -eq "unsubscribe-bulk") {
|
} elseif ($Operation -eq "unsubscribe-bulk") {
|
||||||
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handles", $Values.itemHandles)
|
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handles", $Values.itemHandles)
|
||||||
|
} elseif ($Operation -eq "write") {
|
||||||
|
$arguments += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handle", "$($Values.itemHandle)", "--type", $Values.valueType, "--value", $Values.value)
|
||||||
} elseif ($Operation -eq "stream-events") {
|
} elseif ($Operation -eq "stream-events") {
|
||||||
$arguments += @("--session-id", $Values.sessionId, "--max-events", "$EventLimit", "--timeout", "15")
|
$arguments += @("--session-id", $Values.sessionId, "--max-events", "$streamMaxEvents", "--timeout", "$pythonStreamTimeout")
|
||||||
} elseif ($Operation -eq "close-session") {
|
} elseif ($Operation -eq "close-session") {
|
||||||
$arguments += @("--session-id", $Values.sessionId)
|
$arguments += @("--session-id", $Values.sessionId)
|
||||||
}
|
}
|
||||||
@@ -438,7 +581,7 @@ function Get-ClientCommand {
|
|||||||
$cliArgs = @(
|
$cliArgs = @(
|
||||||
$Operation,
|
$Operation,
|
||||||
"--endpoint", $hostEndpoint,
|
"--endpoint", $hostEndpoint,
|
||||||
"--api-key-env", $ApiKeyEnv,
|
"--api-key-env", $ApiKeyEnvName,
|
||||||
"--plaintext",
|
"--plaintext",
|
||||||
"--json"
|
"--json"
|
||||||
)
|
)
|
||||||
@@ -454,54 +597,529 @@ function Get-ClientCommand {
|
|||||||
$cliArgs += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--items", $Values.items)
|
$cliArgs += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--items", $Values.items)
|
||||||
} elseif ($Operation -eq "unsubscribe-bulk") {
|
} elseif ($Operation -eq "unsubscribe-bulk") {
|
||||||
$cliArgs += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handles", $Values.itemHandles)
|
$cliArgs += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handles", $Values.itemHandles)
|
||||||
|
} elseif ($Operation -eq "write") {
|
||||||
|
$cliArgs += @("--session-id", $Values.sessionId, "--server-handle", "$($Values.serverHandle)", "--item-handle", "$($Values.itemHandle)", "--type", $Values.valueType, "--value", $Values.value)
|
||||||
} elseif ($Operation -eq "stream-events") {
|
} elseif ($Operation -eq "stream-events") {
|
||||||
$cliArgs += @("--session-id", $Values.sessionId, "--limit", "$EventLimit")
|
$cliArgs += @("--session-id", $Values.sessionId, "--limit", "$streamMaxEvents")
|
||||||
} elseif ($Operation -eq "close-session") {
|
} elseif ($Operation -eq "close-session") {
|
||||||
$cliArgs += @("--session-id", $Values.sessionId)
|
$cliArgs += @("--session-id", $Values.sessionId)
|
||||||
}
|
}
|
||||||
$arguments = @("--quiet", ":mxgateway-cli:run", "--args=$($cliArgs -join ' ')")
|
$arguments = @("--quiet", ":mxgateway-cli:run", "--args=$($cliArgs -join ' ')")
|
||||||
return [pscustomobject]@{ file = "gradle"; args = $arguments; cwd = (Join-Path $repoRoot "clients/java"); env = @{} }
|
# Gradle ships as gradle.bat on Windows; .NET's Process.Start
|
||||||
|
# (UseShellExecute=false) cannot launch a batch file directly, so
|
||||||
|
# resolve the launcher and run it through cmd.exe.
|
||||||
|
$gradleCommand = Get-Command "gradle.bat", "gradle.cmd", "gradle.exe", "gradle" `
|
||||||
|
-ErrorAction SilentlyContinue | Select-Object -First 1
|
||||||
|
if ($null -eq $gradleCommand) {
|
||||||
|
throw "The 'gradle' command was not found on PATH; the Java client e2e flow requires Gradle."
|
||||||
|
}
|
||||||
|
return [pscustomobject]@{
|
||||||
|
file = "cmd.exe"
|
||||||
|
args = @("/c", $gradleCommand.Source) + $arguments
|
||||||
|
cwd = (Join-Path $repoRoot "clients/java")
|
||||||
|
env = @{}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Synthesizes a dry-run JSON reply for an operation so the flow can be
|
||||||
|
# validated without a live gateway.
|
||||||
|
function Get-DryRunReply {
|
||||||
|
param(
|
||||||
|
[string]$Client,
|
||||||
|
[string]$Operation,
|
||||||
|
[hashtable]$Values
|
||||||
|
)
|
||||||
|
|
||||||
|
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 } } } }
|
||||||
|
"write" { return [pscustomobject]@{ ok = $true; operation = "write"; reply = [pscustomobject]@{} } }
|
||||||
|
"subscribe-bulk" {
|
||||||
|
$results = @($Values.items -split "," | ForEach-Object -Begin { $index = 1 } -Process {
|
||||||
|
[pscustomobject]@{ itemHandle = $index++; tagAddress = $_; wasSuccessful = $true }
|
||||||
|
})
|
||||||
|
return [pscustomobject]@{ subscribeBulk = [pscustomobject]@{ results = $results }; results = $results }
|
||||||
|
}
|
||||||
|
"unsubscribe-bulk" {
|
||||||
|
$results = @($Values.itemHandles -split "," | ForEach-Object {
|
||||||
|
[pscustomobject]@{ itemHandle = [int]$_; wasSuccessful = $true }
|
||||||
|
})
|
||||||
|
return [pscustomobject]@{ unsubscribeBulk = [pscustomobject]@{ results = $results }; results = $results }
|
||||||
|
}
|
||||||
|
"stream-events" {
|
||||||
|
# Synthesize an OnDataChange (carrying the written value) and an
|
||||||
|
# OnWriteComplete so the write round-trip assertion passes under
|
||||||
|
# -DryRun. The reply is shaped per client: Go and Java emit one
|
||||||
|
# event object per line (Read-JsonObject collapses NDJSON to a
|
||||||
|
# bare array), the others aggregate the events under `events`.
|
||||||
|
$itemHandle = if ($Values.ContainsKey("echoItemHandle")) { [int]$Values.echoItemHandle } else { 1 }
|
||||||
|
$echoValue = if ($Values.ContainsKey("echoValue")) { $Values.echoValue } else { 1 }
|
||||||
|
$dataEvent = [pscustomobject]@{
|
||||||
|
workerSequence = 1
|
||||||
|
family = "MX_EVENT_FAMILY_ON_DATA_CHANGE"
|
||||||
|
itemHandle = $itemHandle
|
||||||
|
value = [pscustomobject]@{ int32Value = $echoValue }
|
||||||
|
}
|
||||||
|
$writeCompleteEvent = [pscustomobject]@{
|
||||||
|
workerSequence = 2
|
||||||
|
family = "MX_EVENT_FAMILY_ON_WRITE_COMPLETE"
|
||||||
|
itemHandle = $itemHandle
|
||||||
|
}
|
||||||
|
$events = @($dataEvent, $writeCompleteEvent)
|
||||||
|
switch ($Client) {
|
||||||
|
"go" { return ,$events }
|
||||||
|
"java" { return ,$events }
|
||||||
|
"rust" { return [pscustomobject]@{ eventCount = $events.Count; events = $events } }
|
||||||
|
default { return [pscustomobject]@{ events = $events } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default { return [pscustomobject]@{ ok = $true; reply = [pscustomobject]@{} } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function Invoke-ClientOperation {
|
function Invoke-ClientOperation {
|
||||||
param(
|
param(
|
||||||
[string]$Client,
|
[string]$Client,
|
||||||
[string]$Operation,
|
[string]$Operation,
|
||||||
[hashtable]$Values = @{}
|
[hashtable]$Values = @{},
|
||||||
|
[string]$ApiKeyEnvName = $ApiKeyEnv
|
||||||
)
|
)
|
||||||
|
|
||||||
$command = Get-ClientCommand -Client $Client -Operation $Operation -Values $Values
|
$command = Get-ClientCommand -Client $Client -Operation $Operation -Values $Values -ApiKeyEnvName $ApiKeyEnvName
|
||||||
$result = Invoke-NativeCommand `
|
$result = Invoke-NativeCommand `
|
||||||
-FilePath $command.file `
|
-FilePath $command.file `
|
||||||
-Arguments $command.args `
|
-Arguments $command.args `
|
||||||
-WorkingDirectory $command.cwd `
|
-WorkingDirectory $command.cwd `
|
||||||
-Environment $command.env
|
-Environment $command.env
|
||||||
if ($DryRun) {
|
if ($DryRun) {
|
||||||
switch ($Operation) {
|
return Get-DryRunReply -Client $Client -Operation $Operation -Values $Values
|
||||||
"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 } } } }
|
|
||||||
"subscribe-bulk" {
|
|
||||||
$results = @($Values.items -split "," | ForEach-Object -Begin { $index = 1 } -Process {
|
|
||||||
[pscustomobject]@{ itemHandle = $index++; tagAddress = $_; wasSuccessful = $true }
|
|
||||||
})
|
|
||||||
return [pscustomobject]@{ subscribeBulk = [pscustomobject]@{ results = $results }; results = $results }
|
|
||||||
}
|
|
||||||
"unsubscribe-bulk" {
|
|
||||||
$results = @($Values.itemHandles -split "," | ForEach-Object {
|
|
||||||
[pscustomobject]@{ itemHandle = [int]$_; wasSuccessful = $true }
|
|
||||||
})
|
|
||||||
return [pscustomobject]@{ unsubscribeBulk = [pscustomobject]@{ results = $results }; results = $results }
|
|
||||||
}
|
|
||||||
"stream-events" { return [pscustomobject]@{ eventCount = 1; events = @([pscustomobject]@{ workerSequence = 1 }) } }
|
|
||||||
default { return [pscustomobject]@{ ok = $true; reply = [pscustomobject]@{} } }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return Read-JsonObject -Text $result.stdout
|
return Read-JsonObject -Text $result.stdout
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Runs a client operation that is expected to fail, returning the raw process
|
||||||
|
# result (exit code + stderr) without throwing. Under -DryRun a synthetic
|
||||||
|
# failure is returned so the parity and auth phases can be exercised offline.
|
||||||
|
function Invoke-ClientOperationExpectingFailure {
|
||||||
|
param(
|
||||||
|
[string]$Client,
|
||||||
|
[string]$Operation,
|
||||||
|
[hashtable]$Values = @{},
|
||||||
|
[string]$ApiKeyEnvName = $ApiKeyEnv
|
||||||
|
)
|
||||||
|
|
||||||
|
if ($DryRun) {
|
||||||
|
$command = Get-ClientCommand -Client $Client -Operation $Operation -Values $Values -ApiKeyEnvName $ApiKeyEnvName
|
||||||
|
Write-Host "[dry-run] $(Join-CommandLine -FilePath $command.file -Arguments $command.args)"
|
||||||
|
return [pscustomobject]@{ exitCode = 1; stdout = ""; stderr = "[dry-run] synthetic expected failure" }
|
||||||
|
}
|
||||||
|
|
||||||
|
$command = Get-ClientCommand -Client $Client -Operation $Operation -Values $Values -ApiKeyEnvName $ApiKeyEnvName
|
||||||
|
return Invoke-NativeCommand `
|
||||||
|
-FilePath $command.file `
|
||||||
|
-Arguments $command.args `
|
||||||
|
-WorkingDirectory $command.cwd `
|
||||||
|
-Environment $command.env `
|
||||||
|
-AllowFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
# Runs the full e2e flow for a single language client and returns the result
|
||||||
|
# record. Discovered tags are passed in so the (slow) SQL discovery runs once.
|
||||||
|
function Invoke-ClientFlow {
|
||||||
|
param(
|
||||||
|
[string]$Client,
|
||||||
|
[object[]]$Tags
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host "Running $Client client e2e flow against $($Tags.Count) discovered tags."
|
||||||
|
$sessionId = $null
|
||||||
|
$serverHandle = $null
|
||||||
|
$clientResult = [ordered]@{
|
||||||
|
language = $Client
|
||||||
|
sessionId = $null
|
||||||
|
serverHandle = $null
|
||||||
|
bulk = $null
|
||||||
|
addedItems = @()
|
||||||
|
eventCount = 0
|
||||||
|
write = $null
|
||||||
|
parity = @()
|
||||||
|
auth = @()
|
||||||
|
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
|
||||||
|
|
||||||
|
# --- Write round-trip + value assertion ---------------------------
|
||||||
|
# Runs right after register, before the bulk and add-item phases, so
|
||||||
|
# only a small backlog of events precedes the write. The gateway
|
||||||
|
# replays the per-session event buffer from the start, so the
|
||||||
|
# post-write OnWriteComplete must be reachable within the bounded
|
||||||
|
# -WriteEchoMaxEvents window.
|
||||||
|
if ($VerifyWrite) {
|
||||||
|
$writeTag = @($Tags | Where-Object {
|
||||||
|
$_.attributeName -eq $WriteAttribute
|
||||||
|
}) | Select-Object -First 1
|
||||||
|
|
||||||
|
if ($null -eq $writeTag) {
|
||||||
|
Write-Warning "$Client write phase skipped: no discovered tag has attribute '$WriteAttribute'."
|
||||||
|
} else {
|
||||||
|
$writeAddJson = Invoke-ClientOperation -Client $Client -Operation "add-item" -Values @{
|
||||||
|
sessionId = $sessionId
|
||||||
|
serverHandle = $serverHandle
|
||||||
|
item = $writeTag.fullTagReference
|
||||||
|
}
|
||||||
|
$writeItemHandle = Get-ItemHandle -Client $Client -Json $writeAddJson
|
||||||
|
Invoke-ClientOperation -Client $Client -Operation "advise" -Values @{
|
||||||
|
sessionId = $sessionId
|
||||||
|
serverHandle = $serverHandle
|
||||||
|
itemHandle = $writeItemHandle
|
||||||
|
} | Out-Null
|
||||||
|
|
||||||
|
$sentinelValue = "$($WriteValueBase + $script:clientFlowIndex)"
|
||||||
|
Invoke-ClientOperation -Client $Client -Operation "write" -Values @{
|
||||||
|
sessionId = $sessionId
|
||||||
|
serverHandle = $serverHandle
|
||||||
|
itemHandle = $writeItemHandle
|
||||||
|
valueType = $WriteType
|
||||||
|
value = $sentinelValue
|
||||||
|
} | Out-Null
|
||||||
|
|
||||||
|
$writeStreamJson = Invoke-ClientOperation -Client $Client -Operation "stream-events" -Values @{
|
||||||
|
sessionId = $sessionId
|
||||||
|
maxEvents = $WriteEchoMaxEvents
|
||||||
|
echoItemHandle = $writeItemHandle
|
||||||
|
echoValue = $sentinelValue
|
||||||
|
}
|
||||||
|
$writeEvents = @(Get-StreamEvents -Client $Client -Json $writeStreamJson)
|
||||||
|
$writeItemEvents = @($writeEvents | Where-Object {
|
||||||
|
(Get-EventItemHandle -Event $_) -eq $writeItemHandle
|
||||||
|
})
|
||||||
|
|
||||||
|
# The reliable write round-trip signal: MXAccess fires
|
||||||
|
# OnWriteComplete once the write reaches the provider. The
|
||||||
|
# value echo is best-effort — a provider-driven attribute
|
||||||
|
# (e.g. a simulated counter) accepts the write but does not
|
||||||
|
# hold the value, so no OnDataChange carries it back.
|
||||||
|
$writeCompleteEvent = $writeItemEvents | Where-Object {
|
||||||
|
(Get-EventFamily -Event $_) -match "WRITE_COMPLETE"
|
||||||
|
} | Select-Object -First 1
|
||||||
|
$echoEvent = $writeItemEvents | Where-Object {
|
||||||
|
Test-ValueEquals -Expected $sentinelValue -Observed (Get-EventScalar -Event $_)
|
||||||
|
} | Select-Object -First 1
|
||||||
|
|
||||||
|
if ($null -eq $writeCompleteEvent) {
|
||||||
|
throw ("$Client write round-trip failed: wrote $WriteType=$sentinelValue to " +
|
||||||
|
"'$($writeTag.fullTagReference)' (item handle $writeItemHandle) but no " +
|
||||||
|
"OnWriteComplete event was observed in $($writeEvents.Count) streamed event(s). " +
|
||||||
|
"Increase -WriteEchoMaxEvents, or drop -VerifyWrite.")
|
||||||
|
}
|
||||||
|
|
||||||
|
$clientResult.write = [ordered]@{
|
||||||
|
attributeName = $WriteAttribute
|
||||||
|
fullTagReference = $writeTag.fullTagReference
|
||||||
|
itemHandle = $writeItemHandle
|
||||||
|
valueType = $WriteType
|
||||||
|
value = $sentinelValue
|
||||||
|
writeCompleteObserved = $true
|
||||||
|
echoObserved = ($null -ne $echoEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not $SkipBulk) {
|
||||||
|
$bulkTags = @($Tags | Select-Object -First ([Math]::Min($BulkTagCount, $Tags.Count)))
|
||||||
|
$bulkItems = ($bulkTags | ForEach-Object { $_.fullTagReference }) -join ","
|
||||||
|
$subscribeBulkJson = Invoke-ClientOperation -Client $Client -Operation "subscribe-bulk" -Values @{
|
||||||
|
sessionId = $sessionId
|
||||||
|
serverHandle = $serverHandle
|
||||||
|
items = $bulkItems
|
||||||
|
}
|
||||||
|
$subscribeResults = @(Get-BulkResults -Client $Client -Operation "subscribe-bulk" -Json $subscribeBulkJson)
|
||||||
|
Assert-BulkResults -Client $Client -Operation "subscribe-bulk" -Results $subscribeResults -ExpectedCount $bulkTags.Count
|
||||||
|
$bulkItemHandles = @(Get-BulkItemHandles -Results $subscribeResults)
|
||||||
|
if ($bulkItemHandles.Count -ne $bulkTags.Count) {
|
||||||
|
throw "$Client subscribe-bulk returned $($bulkItemHandles.Count) usable item handle(s); expected $($bulkTags.Count)."
|
||||||
|
}
|
||||||
|
|
||||||
|
$unsubscribeBulkJson = Invoke-ClientOperation -Client $Client -Operation "unsubscribe-bulk" -Values @{
|
||||||
|
sessionId = $sessionId
|
||||||
|
serverHandle = $serverHandle
|
||||||
|
itemHandles = $bulkItemHandles -join ","
|
||||||
|
}
|
||||||
|
$unsubscribeResults = @(Get-BulkResults -Client $Client -Operation "unsubscribe-bulk" -Json $unsubscribeBulkJson)
|
||||||
|
Assert-BulkResults -Client $Client -Operation "unsubscribe-bulk" -Results $unsubscribeResults -ExpectedCount $bulkItemHandles.Count
|
||||||
|
|
||||||
|
$clientResult.bulk = [ordered]@{
|
||||||
|
tagCount = $bulkTags.Count
|
||||||
|
subscribedCount = $subscribeResults.Count
|
||||||
|
unsubscribedCount = $unsubscribeResults.Count
|
||||||
|
itemHandles = $bulkItemHandles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Event streaming ----------------------------------------------
|
||||||
|
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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Error-path (parity) checks -----------------------------------
|
||||||
|
# MXAccess parity: an invalid item handle and an unknown session must
|
||||||
|
# both be rejected rather than silently succeeding.
|
||||||
|
if (-not $SkipParity) {
|
||||||
|
$parityChecks = @(
|
||||||
|
[ordered]@{
|
||||||
|
check = "invalid-item-handle"
|
||||||
|
operation = "advise"
|
||||||
|
values = @{ sessionId = $sessionId; serverHandle = $serverHandle; itemHandle = 2147483647 }
|
||||||
|
},
|
||||||
|
[ordered]@{
|
||||||
|
check = "unknown-session"
|
||||||
|
operation = "register"
|
||||||
|
values = @{ sessionId = [guid]::NewGuid().ToString() }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach ($parityCheck in $parityChecks) {
|
||||||
|
$parityResult = Invoke-ClientOperationExpectingFailure `
|
||||||
|
-Client $Client -Operation $parityCheck.operation -Values $parityCheck.values
|
||||||
|
$passed = $parityResult.exitCode -ne 0
|
||||||
|
$clientResult.parity += [ordered]@{
|
||||||
|
check = $parityCheck.check
|
||||||
|
operation = $parityCheck.operation
|
||||||
|
exitCode = $parityResult.exitCode
|
||||||
|
passed = $passed
|
||||||
|
}
|
||||||
|
if (-not $passed) {
|
||||||
|
throw "$Client parity check '$($parityCheck.check)' expected $($parityCheck.operation) to fail but it exited 0."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- API-key auth rejection ---------------------------------------
|
||||||
|
# Runs after a working session is established, so a non-zero exit is
|
||||||
|
# an auth rejection rather than the gateway being unreachable.
|
||||||
|
if (-not $SkipAuth) {
|
||||||
|
$authChecks = @(
|
||||||
|
[ordered]@{ check = "missing-api-key"; apiKeyEnv = $script:missingKeyEnvName }
|
||||||
|
)
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($RejectScopeApiKeyEnv)) {
|
||||||
|
$authChecks += [ordered]@{ check = "insufficient-scope"; apiKeyEnv = $RejectScopeApiKeyEnv }
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($authCheck in $authChecks) {
|
||||||
|
$authResult = Invoke-ClientOperationExpectingFailure `
|
||||||
|
-Client $Client -Operation "open-session" -ApiKeyEnvName $authCheck.apiKeyEnv
|
||||||
|
$passed = $authResult.exitCode -ne 0
|
||||||
|
$clientResult.auth += [ordered]@{
|
||||||
|
check = $authCheck.check
|
||||||
|
exitCode = $authResult.exitCode
|
||||||
|
passed = $passed
|
||||||
|
}
|
||||||
|
if (-not $passed) {
|
||||||
|
throw "$Client auth check '$($authCheck.check)' expected open-session to be rejected but it exited 0."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
$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 {
|
||||||
|
$clientResult.error = "$($clientResult.error) close-session failed: $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $clientResult
|
||||||
|
}
|
||||||
|
|
||||||
|
# Forwards every run parameter to a single-client child invocation used by
|
||||||
|
# -Parallel. -Parallel itself is intentionally omitted so the child runs the
|
||||||
|
# serial path.
|
||||||
|
function Get-ChildArgumentList {
|
||||||
|
param(
|
||||||
|
[string]$Client,
|
||||||
|
[string]$ChildReportPath
|
||||||
|
)
|
||||||
|
|
||||||
|
$childArgs = @(
|
||||||
|
"-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $PSCommandPath,
|
||||||
|
"-Clients", $Client,
|
||||||
|
"-MachineStart", "$MachineStart",
|
||||||
|
"-MachineEnd", "$MachineEnd",
|
||||||
|
"-Attributes", ($Attributes -join ","),
|
||||||
|
"-Endpoint", $Endpoint,
|
||||||
|
"-ApiKeyEnv", $ApiKeyEnv,
|
||||||
|
"-SqlServer", $SqlServer,
|
||||||
|
"-Database", $Database,
|
||||||
|
"-EventLimit", "$EventLimit",
|
||||||
|
"-BulkTagCount", "$BulkTagCount",
|
||||||
|
"-WriteAttribute", $WriteAttribute,
|
||||||
|
"-WriteType", $WriteType,
|
||||||
|
"-WriteValueBase", "$WriteValueBase",
|
||||||
|
"-WriteEchoMaxEvents", "$WriteEchoMaxEvents",
|
||||||
|
"-ReportPath", $ChildReportPath,
|
||||||
|
"-EmitReport"
|
||||||
|
)
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($RejectScopeApiKeyEnv)) {
|
||||||
|
$childArgs += @("-RejectScopeApiKeyEnv", $RejectScopeApiKeyEnv)
|
||||||
|
}
|
||||||
|
if ($SkipStream) { $childArgs += "-SkipStream" }
|
||||||
|
if ($SkipBulk) { $childArgs += "-SkipBulk" }
|
||||||
|
if ($VerifyWrite) { $childArgs += "-VerifyWrite" }
|
||||||
|
if ($SkipParity) { $childArgs += "-SkipParity" }
|
||||||
|
if ($SkipAuth) { $childArgs += "-SkipAuth" }
|
||||||
|
if ($DryRun) { $childArgs += "-DryRun" }
|
||||||
|
return $childArgs
|
||||||
|
}
|
||||||
|
|
||||||
|
# An env var name that is guaranteed not to be set in this process, used to
|
||||||
|
# drive the missing-API-key auth rejection.
|
||||||
|
$script:missingKeyEnvName = "MXGW_E2E_MISSING_KEY_" + ([guid]::NewGuid().ToString("N"))
|
||||||
|
|
||||||
|
# --- Parallel mode: fan out one isolated child process per client ----------
|
||||||
|
if ($Parallel -and $Clients.Count -gt 1) {
|
||||||
|
Write-Host "Running $($Clients.Count) client e2e flows in parallel."
|
||||||
|
$childRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("mxgw-e2e-" + ([guid]::NewGuid().ToString("N")))
|
||||||
|
New-Item -ItemType Directory -Path $childRoot -Force | Out-Null
|
||||||
|
|
||||||
|
$children = @()
|
||||||
|
foreach ($client in $Clients) {
|
||||||
|
$childReport = Join-Path $childRoot "$client.json"
|
||||||
|
$childLog = Join-Path $childRoot "$client.log"
|
||||||
|
$childArgs = Get-ChildArgumentList -Client $client -ChildReportPath $childReport
|
||||||
|
$process = Start-Process -FilePath "pwsh" -ArgumentList $childArgs `
|
||||||
|
-RedirectStandardOutput $childLog -RedirectStandardError "$childLog.err" `
|
||||||
|
-NoNewWindow -PassThru
|
||||||
|
$children += [pscustomobject]@{
|
||||||
|
client = $client
|
||||||
|
process = $process
|
||||||
|
report = $childReport
|
||||||
|
log = $childLog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($child in $children) {
|
||||||
|
$child.process.WaitForExit()
|
||||||
|
}
|
||||||
|
|
||||||
|
$mergedClients = @()
|
||||||
|
$discoveredTags = @()
|
||||||
|
$hadFailure = $false
|
||||||
|
foreach ($child in $children) {
|
||||||
|
foreach ($logPath in @($child.log, "$($child.log).err")) {
|
||||||
|
if ((Test-Path $logPath) -and -not [string]::IsNullOrWhiteSpace((Get-Content -Raw -Path $logPath))) {
|
||||||
|
Write-Host "----- $($child.client) -----"
|
||||||
|
Get-Content -Path $logPath | ForEach-Object { Write-Host $_ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($child.process.ExitCode -ne 0) {
|
||||||
|
$hadFailure = $true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $child.report) {
|
||||||
|
$childRun = Get-Content -Raw -Path $child.report | ConvertFrom-Json
|
||||||
|
$mergedClients += @($childRun.clients)
|
||||||
|
if ($discoveredTags.Count -eq 0) {
|
||||||
|
$discoveredTags = @($childRun.discoveredTags)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$hadFailure = $true
|
||||||
|
$mergedClients += [pscustomobject]@{
|
||||||
|
language = $child.client
|
||||||
|
error = "Child process exited $($child.process.ExitCode) without writing a report."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$run = [ordered]@{
|
||||||
|
schemaVersion = 1
|
||||||
|
endpoint = $Endpoint
|
||||||
|
apiKeyEnv = $ApiKeyEnv
|
||||||
|
machineStart = $MachineStart
|
||||||
|
machineEnd = $MachineEnd
|
||||||
|
attributes = $Attributes
|
||||||
|
eventLimit = $EventLimit
|
||||||
|
bulkTagCount = $BulkTagCount
|
||||||
|
skipStream = [bool]$SkipStream
|
||||||
|
skipBulk = [bool]$SkipBulk
|
||||||
|
verifyWrite = [bool]$VerifyWrite
|
||||||
|
skipParity = [bool]$SkipParity
|
||||||
|
skipAuth = [bool]$SkipAuth
|
||||||
|
writeAttribute = $WriteAttribute
|
||||||
|
parallel = $true
|
||||||
|
discoveredTags = $discoveredTags
|
||||||
|
clients = $mergedClients
|
||||||
|
completedAt = (Get-Date).ToUniversalTime().ToString("O")
|
||||||
|
success = -not $hadFailure
|
||||||
|
}
|
||||||
|
|
||||||
|
$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 merged e2e report to $ReportPath"
|
||||||
|
Remove-Item -Path $childRoot -Recurse -Force -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if ($hadFailure) {
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- Serial mode -----------------------------------------------------------
|
||||||
$discoveryJson = & $discoveryScript `
|
$discoveryJson = & $discoveryScript `
|
||||||
-MachineStart $MachineStart `
|
-MachineStart $MachineStart `
|
||||||
-MachineEnd $MachineEnd `
|
-MachineEnd $MachineEnd `
|
||||||
@@ -534,129 +1152,32 @@ $run = [ordered]@{
|
|||||||
bulkTagCount = $BulkTagCount
|
bulkTagCount = $BulkTagCount
|
||||||
skipStream = [bool]$SkipStream
|
skipStream = [bool]$SkipStream
|
||||||
skipBulk = [bool]$SkipBulk
|
skipBulk = [bool]$SkipBulk
|
||||||
|
verifyWrite = [bool]$VerifyWrite
|
||||||
|
skipParity = [bool]$SkipParity
|
||||||
|
skipAuth = [bool]$SkipAuth
|
||||||
|
writeAttribute = $WriteAttribute
|
||||||
|
parallel = $false
|
||||||
startedAt = (Get-Date).ToUniversalTime().ToString("O")
|
startedAt = (Get-Date).ToUniversalTime().ToString("O")
|
||||||
discoveredTags = $tags
|
discoveredTags = $tags
|
||||||
clients = @()
|
clients = @()
|
||||||
}
|
}
|
||||||
|
|
||||||
$hadFailure = $false
|
$hadFailure = $false
|
||||||
|
$script:clientFlowIndex = 0
|
||||||
|
|
||||||
foreach ($client in $Clients) {
|
foreach ($client in $Clients) {
|
||||||
Write-Host "Running $client client e2e flow against $($tags.Count) discovered tags."
|
$clientResult = Invoke-ClientFlow -Client $client -Tags $tags
|
||||||
$sessionId = $null
|
if (-not [string]::IsNullOrWhiteSpace($clientResult.error)) {
|
||||||
$serverHandle = $null
|
|
||||||
$clientResult = [ordered]@{
|
|
||||||
language = $client
|
|
||||||
sessionId = $null
|
|
||||||
serverHandle = $null
|
|
||||||
bulk = $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
|
|
||||||
|
|
||||||
if (-not $SkipBulk) {
|
|
||||||
$bulkTags = @($tags | Select-Object -First ([Math]::Min($BulkTagCount, $tags.Count)))
|
|
||||||
$bulkItems = ($bulkTags | ForEach-Object { $_.fullTagReference }) -join ","
|
|
||||||
$subscribeBulkJson = Invoke-ClientOperation -Client $client -Operation "subscribe-bulk" -Values @{
|
|
||||||
sessionId = $sessionId
|
|
||||||
serverHandle = $serverHandle
|
|
||||||
items = $bulkItems
|
|
||||||
}
|
|
||||||
$subscribeResults = @(Get-BulkResults -Client $client -Operation "subscribe-bulk" -Json $subscribeBulkJson)
|
|
||||||
Assert-BulkResults -Client $client -Operation "subscribe-bulk" -Results $subscribeResults -ExpectedCount $bulkTags.Count
|
|
||||||
$bulkItemHandles = @(Get-BulkItemHandles -Results $subscribeResults)
|
|
||||||
if ($bulkItemHandles.Count -ne $bulkTags.Count) {
|
|
||||||
throw "$client subscribe-bulk returned $($bulkItemHandles.Count) usable item handle(s); expected $($bulkTags.Count)."
|
|
||||||
}
|
|
||||||
|
|
||||||
$unsubscribeBulkJson = Invoke-ClientOperation -Client $client -Operation "unsubscribe-bulk" -Values @{
|
|
||||||
sessionId = $sessionId
|
|
||||||
serverHandle = $serverHandle
|
|
||||||
itemHandles = $bulkItemHandles -join ","
|
|
||||||
}
|
|
||||||
$unsubscribeResults = @(Get-BulkResults -Client $client -Operation "unsubscribe-bulk" -Json $unsubscribeBulkJson)
|
|
||||||
Assert-BulkResults -Client $client -Operation "unsubscribe-bulk" -Results $unsubscribeResults -ExpectedCount $bulkItemHandles.Count
|
|
||||||
|
|
||||||
$clientResult.bulk = [ordered]@{
|
|
||||||
tagCount = $bulkTags.Count
|
|
||||||
subscribedCount = $subscribeResults.Count
|
|
||||||
unsubscribedCount = $unsubscribeResults.Count
|
|
||||||
itemHandles = $bulkItemHandles
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
$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.clients += $clientResult
|
||||||
|
$script:clientFlowIndex++
|
||||||
}
|
}
|
||||||
|
|
||||||
$run.completedAt = (Get-Date).ToUniversalTime().ToString("O")
|
$run.completedAt = (Get-Date).ToUniversalTime().ToString("O")
|
||||||
$run.success = -not $hadFailure
|
$run.success = -not $hadFailure
|
||||||
|
|
||||||
if (-not $DryRun) {
|
if (-not $DryRun -or $EmitReport) {
|
||||||
$reportDirectory = Split-Path -Parent $ReportPath
|
$reportDirectory = Split-Path -Parent $ReportPath
|
||||||
if (-not [string]::IsNullOrWhiteSpace($reportDirectory)) {
|
if (-not [string]::IsNullOrWhiteSpace($reportDirectory)) {
|
||||||
New-Item -ItemType Directory -Path $reportDirectory -Force | Out-Null
|
New-Item -ItemType Directory -Path $reportDirectory -Force | Out-Null
|
||||||
|
|||||||
@@ -46,6 +46,78 @@ public sealed class VariantConverterTests
|
|||||||
Assert.Equal("VT_DATE", converted.VariantType);
|
Assert.Equal("VT_DATE", converted.VariantType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that scalar MxValue kinds convert to the matching boxed CLR type for a COM write.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void ConvertToComValue_WithInt32_ReturnsBoxedInt()
|
||||||
|
{
|
||||||
|
object? result = _converter.ConvertToComValue(new MxValue { Int32Value = 123 });
|
||||||
|
|
||||||
|
Assert.Equal(123, Assert.IsType<int>(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that a boolean MxValue converts to a boxed bool for a COM write.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void ConvertToComValue_WithBool_ReturnsBoxedBool()
|
||||||
|
{
|
||||||
|
object? result = _converter.ConvertToComValue(new MxValue { BoolValue = true });
|
||||||
|
|
||||||
|
Assert.True(Assert.IsType<bool>(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that a string MxValue converts to a string for a COM write.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void ConvertToComValue_WithString_ReturnsString()
|
||||||
|
{
|
||||||
|
object? result = _converter.ConvertToComValue(new MxValue { StringValue = "abc" });
|
||||||
|
|
||||||
|
Assert.Equal("abc", Assert.IsType<string>(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that a timestamp MxValue converts to a UTC DateTime the COM marshaler renders as VT_DATE.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void ConvertToComValue_WithTimestamp_ReturnsUtcDateTime()
|
||||||
|
{
|
||||||
|
DateTime dateTime = new(2026, 5, 19, 12, 0, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
object? result = _converter.ConvertToComValue(
|
||||||
|
new MxValue { TimestampValue = ProtobufTimestamp.FromDateTime(dateTime) });
|
||||||
|
|
||||||
|
Assert.Equal(dateTime, Assert.IsType<DateTime>(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that an MXAccess-null MxValue converts to a CLR null.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void ConvertToComValue_WithNull_ReturnsNull()
|
||||||
|
{
|
||||||
|
object? result = _converter.ConvertToComValue(new MxValue { IsNull = true });
|
||||||
|
|
||||||
|
Assert.Null(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that an integer-array MxValue converts to an int array the COM marshaler renders as a SAFEARRAY.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void ConvertToComValue_WithInt32Array_ReturnsInt32Array()
|
||||||
|
{
|
||||||
|
MxValue value = new()
|
||||||
|
{
|
||||||
|
ArrayValue = new MxArray
|
||||||
|
{
|
||||||
|
Int32Values = new Int32Array { Values = { 1, 2, 3 } },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
object? result = _converter.ConvertToComValue(value);
|
||||||
|
|
||||||
|
Assert.Equal(new[] { 1, 2, 3 }, Assert.IsType<int[]>(result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that an MxValue with no value kind set cannot be converted for a COM write.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void ConvertToComValue_WithNoKind_Throws()
|
||||||
|
{
|
||||||
|
Assert.Throws<ArgumentException>(() => _converter.ConvertToComValue(new MxValue()));
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Verifies that file time values with expected time data type are converted to protobuf timestamps.</summary>
|
/// <summary>Verifies that file time values with expected time data type are converted to protobuf timestamps.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Convert_WithFileTimeAndExpectedTime_ProjectsTimestamp()
|
public void Convert_WithFileTimeAndExpectedTime_ProjectsTimestamp()
|
||||||
|
|||||||
@@ -379,10 +379,10 @@ public sealed class AlarmCommandExecutorTests
|
|||||||
public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds) { }
|
public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds) { }
|
||||||
public void Suspend(int serverHandle, int itemHandle) { }
|
public void Suspend(int serverHandle, int itemHandle) { }
|
||||||
public void Activate(int serverHandle, int itemHandle) { }
|
public void Activate(int serverHandle, int itemHandle) { }
|
||||||
public void Write(int serverHandle, int itemHandle, object value, int userId) { }
|
public void Write(int serverHandle, int itemHandle, object? value, int userId) { }
|
||||||
public void Write2(int serverHandle, int itemHandle, object value, object timestampValue, int userId) { }
|
public void Write2(int serverHandle, int itemHandle, object? value, object? timestampValue, int userId) { }
|
||||||
public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object value) { }
|
public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value) { }
|
||||||
public void WriteSecured2(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object value, object timestampValue) { }
|
public void WriteSecured2(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value, object? timestampValue) { }
|
||||||
public int AuthenticateUser(string userName, string password) => 0;
|
public int AuthenticateUser(string userName, string password) => 0;
|
||||||
public int ArchestrAUserToId(string userName) => 0;
|
public int ArchestrAUserToId(string userName) => 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -134,5 +134,26 @@ public sealed class MxAccessComServerTests
|
|||||||
{
|
{
|
||||||
calls.Add($"AdviseSupervisory:{serverHandle}:{itemHandle}");
|
calls.Add($"AdviseSupervisory:{serverHandle}:{itemHandle}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Write(int serverHandle, int itemHandle, object? value, int userId)
|
||||||
|
{
|
||||||
|
calls.Add($"Write:{serverHandle}:{itemHandle}:{value}:{userId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Write2(int serverHandle, int itemHandle, object? value, object? timestamp, int userId)
|
||||||
|
{
|
||||||
|
calls.Add($"Write2:{serverHandle}:{itemHandle}:{value}:{timestamp}:{userId}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteSecured(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value)
|
||||||
|
{
|
||||||
|
calls.Add($"WriteSecured:{serverHandle}:{itemHandle}:{currentUserId}:{verifierUserId}:{value}");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void WriteSecured2(
|
||||||
|
int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value, object? timestamp)
|
||||||
|
{
|
||||||
|
calls.Add($"WriteSecured2:{serverHandle}:{itemHandle}:{currentUserId}:{verifierUserId}:{value}:{timestamp}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Google.Protobuf.WellKnownTypes;
|
||||||
using MxGateway.Contracts.Proto;
|
using MxGateway.Contracts.Proto;
|
||||||
using MxGateway.Worker.MxAccess;
|
using MxGateway.Worker.MxAccess;
|
||||||
using MxGateway.Worker.Sta;
|
using MxGateway.Worker.Sta;
|
||||||
@@ -617,6 +618,143 @@ public sealed class MxAccessCommandExecutorTests
|
|||||||
Assert.Null(factory.FakeComObject.AdviseServerHandle);
|
Assert.Null(factory.FakeComObject.AdviseServerHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that Write dispatches the converted value to MXAccess on the STA thread.</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task DispatchAsync_Write_CallsMxAccessOnStaWithConvertedValue()
|
||||||
|
{
|
||||||
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 70);
|
||||||
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
||||||
|
using StaRuntime runtime = CreateRuntime();
|
||||||
|
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
|
||||||
|
await session.StartAsync(workerProcessId: 1234);
|
||||||
|
|
||||||
|
MxCommandReply reply = await session.DispatchAsync(CreateWriteCommand(
|
||||||
|
"write", serverHandle: 70, itemHandle: 700, value: 123, userId: 5));
|
||||||
|
|
||||||
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
||||||
|
Assert.Equal(MxCommandKind.Write, reply.Kind);
|
||||||
|
Assert.Equal(70, fakeComObject.WriteServerHandle);
|
||||||
|
Assert.Equal(700, fakeComObject.WriteItemHandle);
|
||||||
|
Assert.Equal(123, fakeComObject.WriteValue);
|
||||||
|
Assert.Equal(5, fakeComObject.WriteUserId);
|
||||||
|
Assert.Equal(runtime.StaThreadId, fakeComObject.WriteThreadId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that Write2 forwards the converted value and timestamp to MXAccess.</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task DispatchAsync_Write2_ForwardsValueAndTimestamp()
|
||||||
|
{
|
||||||
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 71);
|
||||||
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
||||||
|
using StaRuntime runtime = CreateRuntime();
|
||||||
|
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
|
||||||
|
await session.StartAsync(workerProcessId: 1234);
|
||||||
|
DateTime timestamp = new(2026, 5, 19, 12, 0, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
MxCommandReply reply = await session.DispatchAsync(CreateWrite2Command(
|
||||||
|
"write2", serverHandle: 71, itemHandle: 710, value: 456, timestamp: timestamp, userId: 6));
|
||||||
|
|
||||||
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
||||||
|
Assert.Equal(MxCommandKind.Write2, reply.Kind);
|
||||||
|
Assert.Equal(710, fakeComObject.WriteItemHandle);
|
||||||
|
Assert.Equal(456, fakeComObject.WriteValue);
|
||||||
|
Assert.Equal(timestamp, fakeComObject.WriteTimestamp);
|
||||||
|
Assert.Equal(6, fakeComObject.WriteUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that WriteSecured forwards the operator and verifier user ids to MXAccess.</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task DispatchAsync_WriteSecured_ForwardsUserIds()
|
||||||
|
{
|
||||||
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 72);
|
||||||
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
||||||
|
using StaRuntime runtime = CreateRuntime();
|
||||||
|
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
|
||||||
|
await session.StartAsync(workerProcessId: 1234);
|
||||||
|
|
||||||
|
MxCommandReply reply = await session.DispatchAsync(CreateWriteSecuredCommand(
|
||||||
|
"write-secured", serverHandle: 72, itemHandle: 720, value: 789, currentUserId: 11, verifierUserId: 22));
|
||||||
|
|
||||||
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
||||||
|
Assert.Equal(MxCommandKind.WriteSecured, reply.Kind);
|
||||||
|
Assert.Equal(720, fakeComObject.WriteItemHandle);
|
||||||
|
Assert.Equal(789, fakeComObject.WriteValue);
|
||||||
|
Assert.Equal(11, fakeComObject.WriteCurrentUserId);
|
||||||
|
Assert.Equal(22, fakeComObject.WriteVerifierUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that WriteSecured2 forwards user ids, value, and timestamp to MXAccess.</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task DispatchAsync_WriteSecured2_ForwardsUserIdsValueAndTimestamp()
|
||||||
|
{
|
||||||
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 73);
|
||||||
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
||||||
|
using StaRuntime runtime = CreateRuntime();
|
||||||
|
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
|
||||||
|
await session.StartAsync(workerProcessId: 1234);
|
||||||
|
DateTime timestamp = new(2026, 5, 19, 13, 30, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
MxCommandReply reply = await session.DispatchAsync(CreateWriteSecured2Command(
|
||||||
|
"write-secured2", serverHandle: 73, itemHandle: 730, value: 1011,
|
||||||
|
timestamp: timestamp, currentUserId: 33, verifierUserId: 44));
|
||||||
|
|
||||||
|
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
||||||
|
Assert.Equal(MxCommandKind.WriteSecured2, reply.Kind);
|
||||||
|
Assert.Equal(1011, fakeComObject.WriteValue);
|
||||||
|
Assert.Equal(timestamp, fakeComObject.WriteTimestamp);
|
||||||
|
Assert.Equal(33, fakeComObject.WriteCurrentUserId);
|
||||||
|
Assert.Equal(44, fakeComObject.WriteVerifierUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that Write without a payload returns an invalid request error.</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task DispatchAsync_WriteWithoutPayload_ReturnsInvalidRequest()
|
||||||
|
{
|
||||||
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 74);
|
||||||
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
||||||
|
using StaRuntime runtime = CreateRuntime();
|
||||||
|
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
|
||||||
|
await session.StartAsync(workerProcessId: 1234);
|
||||||
|
|
||||||
|
MxCommandReply reply = await session.DispatchAsync(new StaCommand(
|
||||||
|
"session-1",
|
||||||
|
"missing-write-payload",
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.Write,
|
||||||
|
}));
|
||||||
|
|
||||||
|
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
|
||||||
|
Assert.Null(fakeComObject.WriteServerHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that Write without a value returns an invalid request error.</summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task DispatchAsync_WriteWithoutValue_ReturnsInvalidRequest()
|
||||||
|
{
|
||||||
|
FakeMxAccessComObject fakeComObject = new(registerHandle: 75);
|
||||||
|
FakeMxAccessComObjectFactory factory = new(fakeComObject);
|
||||||
|
using StaRuntime runtime = CreateRuntime();
|
||||||
|
using MxAccessStaSession session = new(runtime, factory, new NoopEventSink());
|
||||||
|
await session.StartAsync(workerProcessId: 1234);
|
||||||
|
|
||||||
|
MxCommandReply reply = await session.DispatchAsync(new StaCommand(
|
||||||
|
"session-1",
|
||||||
|
"missing-write-value",
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.Write,
|
||||||
|
Write = new WriteCommand
|
||||||
|
{
|
||||||
|
ServerHandle = 75,
|
||||||
|
ItemHandle = 750,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
Assert.Equal(ProtocolStatusCode.InvalidRequest, reply.ProtocolStatus.Code);
|
||||||
|
Assert.Null(fakeComObject.WriteServerHandle);
|
||||||
|
}
|
||||||
|
|
||||||
private static StaCommand CreateRegisterCommand(
|
private static StaCommand CreateRegisterCommand(
|
||||||
string correlationId,
|
string correlationId,
|
||||||
string clientName)
|
string clientName)
|
||||||
@@ -729,6 +867,126 @@ public sealed class MxAccessCommandExecutorTests
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static MxValue CreateIntegerValue(int value)
|
||||||
|
{
|
||||||
|
return new MxValue
|
||||||
|
{
|
||||||
|
DataType = MxDataType.Integer,
|
||||||
|
VariantType = "VT_I4",
|
||||||
|
Int32Value = value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MxValue CreateTimestampValue(DateTime timestamp)
|
||||||
|
{
|
||||||
|
return new MxValue
|
||||||
|
{
|
||||||
|
DataType = MxDataType.Time,
|
||||||
|
VariantType = "VT_DATE",
|
||||||
|
TimestampValue = Timestamp.FromDateTime(timestamp),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StaCommand CreateWriteCommand(
|
||||||
|
string correlationId,
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
int value,
|
||||||
|
int userId)
|
||||||
|
{
|
||||||
|
return new StaCommand(
|
||||||
|
"session-1",
|
||||||
|
correlationId,
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.Write,
|
||||||
|
Write = new WriteCommand
|
||||||
|
{
|
||||||
|
ServerHandle = serverHandle,
|
||||||
|
ItemHandle = itemHandle,
|
||||||
|
Value = CreateIntegerValue(value),
|
||||||
|
UserId = userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StaCommand CreateWrite2Command(
|
||||||
|
string correlationId,
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
int value,
|
||||||
|
DateTime timestamp,
|
||||||
|
int userId)
|
||||||
|
{
|
||||||
|
return new StaCommand(
|
||||||
|
"session-1",
|
||||||
|
correlationId,
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.Write2,
|
||||||
|
Write2 = new Write2Command
|
||||||
|
{
|
||||||
|
ServerHandle = serverHandle,
|
||||||
|
ItemHandle = itemHandle,
|
||||||
|
Value = CreateIntegerValue(value),
|
||||||
|
TimestampValue = CreateTimestampValue(timestamp),
|
||||||
|
UserId = userId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StaCommand CreateWriteSecuredCommand(
|
||||||
|
string correlationId,
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
int value,
|
||||||
|
int currentUserId,
|
||||||
|
int verifierUserId)
|
||||||
|
{
|
||||||
|
return new StaCommand(
|
||||||
|
"session-1",
|
||||||
|
correlationId,
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.WriteSecured,
|
||||||
|
WriteSecured = new WriteSecuredCommand
|
||||||
|
{
|
||||||
|
ServerHandle = serverHandle,
|
||||||
|
ItemHandle = itemHandle,
|
||||||
|
CurrentUserId = currentUserId,
|
||||||
|
VerifierUserId = verifierUserId,
|
||||||
|
Value = CreateIntegerValue(value),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StaCommand CreateWriteSecured2Command(
|
||||||
|
string correlationId,
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
int value,
|
||||||
|
DateTime timestamp,
|
||||||
|
int currentUserId,
|
||||||
|
int verifierUserId)
|
||||||
|
{
|
||||||
|
return new StaCommand(
|
||||||
|
"session-1",
|
||||||
|
correlationId,
|
||||||
|
new MxCommand
|
||||||
|
{
|
||||||
|
Kind = MxCommandKind.WriteSecured2,
|
||||||
|
WriteSecured2 = new WriteSecured2Command
|
||||||
|
{
|
||||||
|
ServerHandle = serverHandle,
|
||||||
|
ItemHandle = itemHandle,
|
||||||
|
CurrentUserId = currentUserId,
|
||||||
|
VerifierUserId = verifierUserId,
|
||||||
|
Value = CreateIntegerValue(value),
|
||||||
|
TimestampValue = CreateTimestampValue(timestamp),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private static StaCommand CreateUnAdviseCommand(
|
private static StaCommand CreateUnAdviseCommand(
|
||||||
string correlationId,
|
string correlationId,
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
@@ -1080,6 +1338,118 @@ public sealed class MxAccessCommandExecutorTests
|
|||||||
throw adviseSupervisoryException;
|
throw adviseSupervisoryException;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Gets the server handle passed to the most recent write, if called.</summary>
|
||||||
|
public int? WriteServerHandle { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Gets the item handle passed to the most recent write, if called.</summary>
|
||||||
|
public int? WriteItemHandle { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Gets the value passed to the most recent write, if called.</summary>
|
||||||
|
public object? WriteValue { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Gets the timestamp passed to the most recent timestamped write, if called.</summary>
|
||||||
|
public object? WriteTimestamp { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Gets the user id passed to the most recent Write/Write2, if called.</summary>
|
||||||
|
public int? WriteUserId { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Gets the current user id passed to the most recent secured write, if called.</summary>
|
||||||
|
public int? WriteCurrentUserId { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Gets the verifier user id passed to the most recent secured write, if called.</summary>
|
||||||
|
public int? WriteVerifierUserId { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Gets the thread ID on which the most recent write was called.</summary>
|
||||||
|
public int? WriteThreadId { get; private set; }
|
||||||
|
|
||||||
|
/// <summary>Writes a value to an item and tracks the operation.</summary>
|
||||||
|
/// <param name="serverHandle">Server handle for the write.</param>
|
||||||
|
/// <param name="itemHandle">Item handle to write to.</param>
|
||||||
|
/// <param name="value">Value to write.</param>
|
||||||
|
/// <param name="userId">MXAccess user id for the write.</param>
|
||||||
|
public void Write(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
object? value,
|
||||||
|
int userId)
|
||||||
|
{
|
||||||
|
operationNames.Add($"Write:{serverHandle}:{itemHandle}");
|
||||||
|
WriteServerHandle = serverHandle;
|
||||||
|
WriteItemHandle = itemHandle;
|
||||||
|
WriteValue = value;
|
||||||
|
WriteUserId = userId;
|
||||||
|
WriteThreadId = Environment.CurrentManagedThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Writes a timestamped value to an item and tracks the operation.</summary>
|
||||||
|
/// <param name="serverHandle">Server handle for the write.</param>
|
||||||
|
/// <param name="itemHandle">Item handle to write to.</param>
|
||||||
|
/// <param name="value">Value to write.</param>
|
||||||
|
/// <param name="timestamp">Source timestamp for the write.</param>
|
||||||
|
/// <param name="userId">MXAccess user id for the write.</param>
|
||||||
|
public void Write2(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
object? value,
|
||||||
|
object? timestamp,
|
||||||
|
int userId)
|
||||||
|
{
|
||||||
|
operationNames.Add($"Write2:{serverHandle}:{itemHandle}");
|
||||||
|
WriteServerHandle = serverHandle;
|
||||||
|
WriteItemHandle = itemHandle;
|
||||||
|
WriteValue = value;
|
||||||
|
WriteTimestamp = timestamp;
|
||||||
|
WriteUserId = userId;
|
||||||
|
WriteThreadId = Environment.CurrentManagedThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Performs a secured write to an item and tracks the operation.</summary>
|
||||||
|
/// <param name="serverHandle">Server handle for the write.</param>
|
||||||
|
/// <param name="itemHandle">Item handle to write to.</param>
|
||||||
|
/// <param name="currentUserId">Operator user id.</param>
|
||||||
|
/// <param name="verifierUserId">Verifier user id.</param>
|
||||||
|
/// <param name="value">Value to write.</param>
|
||||||
|
public void WriteSecured(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
int currentUserId,
|
||||||
|
int verifierUserId,
|
||||||
|
object? value)
|
||||||
|
{
|
||||||
|
operationNames.Add($"WriteSecured:{serverHandle}:{itemHandle}");
|
||||||
|
WriteServerHandle = serverHandle;
|
||||||
|
WriteItemHandle = itemHandle;
|
||||||
|
WriteCurrentUserId = currentUserId;
|
||||||
|
WriteVerifierUserId = verifierUserId;
|
||||||
|
WriteValue = value;
|
||||||
|
WriteThreadId = Environment.CurrentManagedThreadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Performs a secured timestamped write to an item and tracks the operation.</summary>
|
||||||
|
/// <param name="serverHandle">Server handle for the write.</param>
|
||||||
|
/// <param name="itemHandle">Item handle to write to.</param>
|
||||||
|
/// <param name="currentUserId">Operator user id.</param>
|
||||||
|
/// <param name="verifierUserId">Verifier user id.</param>
|
||||||
|
/// <param name="value">Value to write.</param>
|
||||||
|
/// <param name="timestamp">Source timestamp for the write.</param>
|
||||||
|
public void WriteSecured2(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
int currentUserId,
|
||||||
|
int verifierUserId,
|
||||||
|
object? value,
|
||||||
|
object? timestamp)
|
||||||
|
{
|
||||||
|
operationNames.Add($"WriteSecured2:{serverHandle}:{itemHandle}");
|
||||||
|
WriteServerHandle = serverHandle;
|
||||||
|
WriteItemHandle = itemHandle;
|
||||||
|
WriteCurrentUserId = currentUserId;
|
||||||
|
WriteVerifierUserId = verifierUserId;
|
||||||
|
WriteValue = value;
|
||||||
|
WriteTimestamp = timestamp;
|
||||||
|
WriteThreadId = Environment.CurrentManagedThreadId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Factory for creating fake MXAccess COM objects in tests.</summary>
|
/// <summary>Factory for creating fake MXAccess COM objects in tests.</summary>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
using Google.Protobuf;
|
using Google.Protobuf;
|
||||||
using Google.Protobuf.WellKnownTypes;
|
using Google.Protobuf.WellKnownTypes;
|
||||||
using MxGateway.Contracts.Proto;
|
using MxGateway.Contracts.Proto;
|
||||||
@@ -118,6 +119,63 @@ public sealed class VariantConverter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Converts an <see cref="MxValue"/> into a CLR object suitable for an
|
||||||
|
/// MXAccess COM write. The COM marshaler boxes the returned value into the
|
||||||
|
/// matching VARIANT, so this is the inverse of <see cref="Convert(object?)"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="value">Protobuf value to convert.</param>
|
||||||
|
/// <returns>A COM-marshalable value, or <see langword="null"/> for an MXAccess null.</returns>
|
||||||
|
public object? ConvertToComValue(MxValue value)
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.IsNull)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.KindCase switch
|
||||||
|
{
|
||||||
|
MxValue.KindOneofCase.BoolValue => value.BoolValue,
|
||||||
|
MxValue.KindOneofCase.Int32Value => value.Int32Value,
|
||||||
|
MxValue.KindOneofCase.Int64Value => value.Int64Value,
|
||||||
|
MxValue.KindOneofCase.FloatValue => value.FloatValue,
|
||||||
|
MxValue.KindOneofCase.DoubleValue => value.DoubleValue,
|
||||||
|
MxValue.KindOneofCase.StringValue => value.StringValue,
|
||||||
|
// The COM marshaler renders a DateTime as VT_DATE; MXAccess accepts
|
||||||
|
// it as the timestamped-write time argument.
|
||||||
|
MxValue.KindOneofCase.TimestampValue => value.TimestampValue.ToDateTime(),
|
||||||
|
MxValue.KindOneofCase.ArrayValue => ConvertToComArray(value.ArrayValue),
|
||||||
|
MxValue.KindOneofCase.RawValue => throw new ArgumentException(
|
||||||
|
"MxValue raw payloads cannot be written to MXAccess.", nameof(value)),
|
||||||
|
_ => throw new ArgumentException(
|
||||||
|
"MxValue has no value kind set; nothing to write.", nameof(value)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Array ConvertToComArray(MxArray array)
|
||||||
|
{
|
||||||
|
return array.ValuesCase switch
|
||||||
|
{
|
||||||
|
MxArray.ValuesOneofCase.BoolValues => array.BoolValues.Values.ToArray(),
|
||||||
|
MxArray.ValuesOneofCase.Int32Values => array.Int32Values.Values.ToArray(),
|
||||||
|
MxArray.ValuesOneofCase.Int64Values => array.Int64Values.Values.ToArray(),
|
||||||
|
MxArray.ValuesOneofCase.FloatValues => array.FloatValues.Values.ToArray(),
|
||||||
|
MxArray.ValuesOneofCase.DoubleValues => array.DoubleValues.Values.ToArray(),
|
||||||
|
MxArray.ValuesOneofCase.StringValues => array.StringValues.Values.ToArray(),
|
||||||
|
MxArray.ValuesOneofCase.TimestampValues =>
|
||||||
|
array.TimestampValues.Values.Select(timestamp => timestamp.ToDateTime()).ToArray(),
|
||||||
|
MxArray.ValuesOneofCase.RawValues => throw new ArgumentException(
|
||||||
|
"MxArray raw payloads cannot be written to MXAccess.", nameof(array)),
|
||||||
|
_ => throw new ArgumentException(
|
||||||
|
"MxArray has no element values set; nothing to write.", nameof(array)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static MxValue ConvertScalar(
|
private static MxValue ConvertScalar(
|
||||||
object value,
|
object value,
|
||||||
MxDataType expectedDataType)
|
MxDataType expectedDataType)
|
||||||
|
|||||||
@@ -56,4 +56,56 @@ public interface IMxAccessServer
|
|||||||
void AdviseSupervisory(
|
void AdviseSupervisory(
|
||||||
int serverHandle,
|
int serverHandle,
|
||||||
int itemHandle);
|
int itemHandle);
|
||||||
|
|
||||||
|
/// <summary>Writes a value to an item.</summary>
|
||||||
|
/// <param name="serverHandle">Server handle identifying the registration.</param>
|
||||||
|
/// <param name="itemHandle">Item handle to write to.</param>
|
||||||
|
/// <param name="value">COM-marshalable value to write; <see langword="null"/> writes an MXAccess null.</param>
|
||||||
|
/// <param name="userId">MXAccess user id (security classification) for the write.</param>
|
||||||
|
void Write(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
object? value,
|
||||||
|
int userId);
|
||||||
|
|
||||||
|
/// <summary>Writes a value with an explicit source timestamp to an item.</summary>
|
||||||
|
/// <param name="serverHandle">Server handle identifying the registration.</param>
|
||||||
|
/// <param name="itemHandle">Item handle to write to.</param>
|
||||||
|
/// <param name="value">COM-marshalable value to write; <see langword="null"/> writes an MXAccess null.</param>
|
||||||
|
/// <param name="timestamp">COM-marshalable source timestamp for the write.</param>
|
||||||
|
/// <param name="userId">MXAccess user id (security classification) for the write.</param>
|
||||||
|
void Write2(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
object? value,
|
||||||
|
object? timestamp,
|
||||||
|
int userId);
|
||||||
|
|
||||||
|
/// <summary>Performs a secured/verified write to an item.</summary>
|
||||||
|
/// <param name="serverHandle">Server handle identifying the registration.</param>
|
||||||
|
/// <param name="itemHandle">Item handle to write to.</param>
|
||||||
|
/// <param name="currentUserId">MXAccess user id of the operator performing the write.</param>
|
||||||
|
/// <param name="verifierUserId">MXAccess user id of the verifier authorizing the write.</param>
|
||||||
|
/// <param name="value">COM-marshalable value to write; <see langword="null"/> writes an MXAccess null.</param>
|
||||||
|
void WriteSecured(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
int currentUserId,
|
||||||
|
int verifierUserId,
|
||||||
|
object? value);
|
||||||
|
|
||||||
|
/// <summary>Performs a secured/verified write with an explicit source timestamp.</summary>
|
||||||
|
/// <param name="serverHandle">Server handle identifying the registration.</param>
|
||||||
|
/// <param name="itemHandle">Item handle to write to.</param>
|
||||||
|
/// <param name="currentUserId">MXAccess user id of the operator performing the write.</param>
|
||||||
|
/// <param name="verifierUserId">MXAccess user id of the verifier authorizing the write.</param>
|
||||||
|
/// <param name="value">COM-marshalable value to write; <see langword="null"/> writes an MXAccess null.</param>
|
||||||
|
/// <param name="timestamp">COM-marshalable source timestamp for the write.</param>
|
||||||
|
void WriteSecured2(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
int currentUserId,
|
||||||
|
int verifierUserId,
|
||||||
|
object? value,
|
||||||
|
object? timestamp);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -140,6 +140,74 @@ public sealed class MxAccessComServer : IMxAccessServer
|
|||||||
AsProxyServer4().AdviseSupervisory(serverHandle, itemHandle);
|
AsProxyServer4().AdviseSupervisory(serverHandle, itemHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Write(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
object? value,
|
||||||
|
int userId)
|
||||||
|
{
|
||||||
|
if (mxAccessComObject is IMxAccessServer typedFake)
|
||||||
|
{
|
||||||
|
typedFake.Write(serverHandle, itemHandle, value, userId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsProxyServer().Write(serverHandle, itemHandle, value!, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Write2(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
object? value,
|
||||||
|
object? timestamp,
|
||||||
|
int userId)
|
||||||
|
{
|
||||||
|
if (mxAccessComObject is IMxAccessServer typedFake)
|
||||||
|
{
|
||||||
|
typedFake.Write2(serverHandle, itemHandle, value, timestamp, userId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsProxyServer4().Write2(serverHandle, itemHandle, value!, timestamp!, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void WriteSecured(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
int currentUserId,
|
||||||
|
int verifierUserId,
|
||||||
|
object? value)
|
||||||
|
{
|
||||||
|
if (mxAccessComObject is IMxAccessServer typedFake)
|
||||||
|
{
|
||||||
|
typedFake.WriteSecured(serverHandle, itemHandle, currentUserId, verifierUserId, value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsProxyServer().WriteSecured(serverHandle, itemHandle, currentUserId, verifierUserId, value!);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void WriteSecured2(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
int currentUserId,
|
||||||
|
int verifierUserId,
|
||||||
|
object? value,
|
||||||
|
object? timestamp)
|
||||||
|
{
|
||||||
|
if (mxAccessComObject is IMxAccessServer typedFake)
|
||||||
|
{
|
||||||
|
typedFake.WriteSecured2(serverHandle, itemHandle, currentUserId, verifierUserId, value, timestamp);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AsProxyServer4().WriteSecured2(serverHandle, itemHandle, currentUserId, verifierUserId, value!, timestamp!);
|
||||||
|
}
|
||||||
|
|
||||||
private ILMXProxyServer AsProxyServer()
|
private ILMXProxyServer AsProxyServer()
|
||||||
{
|
{
|
||||||
return mxAccessComObject as ILMXProxyServer
|
return mxAccessComObject as ILMXProxyServer
|
||||||
|
|||||||
@@ -74,6 +74,10 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
|
|||||||
MxCommandKind.Advise => ExecuteAdvise(command),
|
MxCommandKind.Advise => ExecuteAdvise(command),
|
||||||
MxCommandKind.UnAdvise => ExecuteUnAdvise(command),
|
MxCommandKind.UnAdvise => ExecuteUnAdvise(command),
|
||||||
MxCommandKind.AdviseSupervisory => ExecuteAdviseSupervisory(command),
|
MxCommandKind.AdviseSupervisory => ExecuteAdviseSupervisory(command),
|
||||||
|
MxCommandKind.Write => ExecuteWrite(command),
|
||||||
|
MxCommandKind.Write2 => ExecuteWrite2(command),
|
||||||
|
MxCommandKind.WriteSecured => ExecuteWriteSecured(command),
|
||||||
|
MxCommandKind.WriteSecured2 => ExecuteWriteSecured2(command),
|
||||||
MxCommandKind.AddItemBulk => ExecuteAddItemBulk(command),
|
MxCommandKind.AddItemBulk => ExecuteAddItemBulk(command),
|
||||||
MxCommandKind.AdviseItemBulk => ExecuteAdviseItemBulk(command),
|
MxCommandKind.AdviseItemBulk => ExecuteAdviseItemBulk(command),
|
||||||
MxCommandKind.RemoveItemBulk => ExecuteRemoveItemBulk(command),
|
MxCommandKind.RemoveItemBulk => ExecuteRemoveItemBulk(command),
|
||||||
@@ -223,6 +227,108 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
|
|||||||
return CreateOkReply(command);
|
return CreateOkReply(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private MxCommandReply ExecuteWrite(StaCommand command)
|
||||||
|
{
|
||||||
|
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.Write)
|
||||||
|
{
|
||||||
|
return CreateInvalidRequestReply(command, "Write command payload is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteCommand writeCommand = command.Command.Write;
|
||||||
|
if (writeCommand.Value is null)
|
||||||
|
{
|
||||||
|
return CreateInvalidRequestReply(command, "Write command value is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
session.Write(
|
||||||
|
writeCommand.ServerHandle,
|
||||||
|
writeCommand.ItemHandle,
|
||||||
|
variantConverter.ConvertToComValue(writeCommand.Value),
|
||||||
|
writeCommand.UserId);
|
||||||
|
|
||||||
|
return CreateOkReply(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MxCommandReply ExecuteWrite2(StaCommand command)
|
||||||
|
{
|
||||||
|
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.Write2)
|
||||||
|
{
|
||||||
|
return CreateInvalidRequestReply(command, "Write2 command payload is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Write2Command write2Command = command.Command.Write2;
|
||||||
|
if (write2Command.Value is null)
|
||||||
|
{
|
||||||
|
return CreateInvalidRequestReply(command, "Write2 command value is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (write2Command.TimestampValue is null)
|
||||||
|
{
|
||||||
|
return CreateInvalidRequestReply(command, "Write2 command timestamp value is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
session.Write2(
|
||||||
|
write2Command.ServerHandle,
|
||||||
|
write2Command.ItemHandle,
|
||||||
|
variantConverter.ConvertToComValue(write2Command.Value),
|
||||||
|
variantConverter.ConvertToComValue(write2Command.TimestampValue),
|
||||||
|
write2Command.UserId);
|
||||||
|
|
||||||
|
return CreateOkReply(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MxCommandReply ExecuteWriteSecured(StaCommand command)
|
||||||
|
{
|
||||||
|
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.WriteSecured)
|
||||||
|
{
|
||||||
|
return CreateInvalidRequestReply(command, "WriteSecured command payload is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteSecuredCommand writeSecuredCommand = command.Command.WriteSecured;
|
||||||
|
if (writeSecuredCommand.Value is null)
|
||||||
|
{
|
||||||
|
return CreateInvalidRequestReply(command, "WriteSecured command value is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
session.WriteSecured(
|
||||||
|
writeSecuredCommand.ServerHandle,
|
||||||
|
writeSecuredCommand.ItemHandle,
|
||||||
|
writeSecuredCommand.CurrentUserId,
|
||||||
|
writeSecuredCommand.VerifierUserId,
|
||||||
|
variantConverter.ConvertToComValue(writeSecuredCommand.Value));
|
||||||
|
|
||||||
|
return CreateOkReply(command);
|
||||||
|
}
|
||||||
|
|
||||||
|
private MxCommandReply ExecuteWriteSecured2(StaCommand command)
|
||||||
|
{
|
||||||
|
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.WriteSecured2)
|
||||||
|
{
|
||||||
|
return CreateInvalidRequestReply(command, "WriteSecured2 command payload is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteSecured2Command writeSecured2Command = command.Command.WriteSecured2;
|
||||||
|
if (writeSecured2Command.Value is null)
|
||||||
|
{
|
||||||
|
return CreateInvalidRequestReply(command, "WriteSecured2 command value is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (writeSecured2Command.TimestampValue is null)
|
||||||
|
{
|
||||||
|
return CreateInvalidRequestReply(command, "WriteSecured2 command timestamp value is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
session.WriteSecured2(
|
||||||
|
writeSecured2Command.ServerHandle,
|
||||||
|
writeSecured2Command.ItemHandle,
|
||||||
|
writeSecured2Command.CurrentUserId,
|
||||||
|
writeSecured2Command.VerifierUserId,
|
||||||
|
variantConverter.ConvertToComValue(writeSecured2Command.Value),
|
||||||
|
variantConverter.ConvertToComValue(writeSecured2Command.TimestampValue));
|
||||||
|
|
||||||
|
return CreateOkReply(command);
|
||||||
|
}
|
||||||
|
|
||||||
private MxCommandReply ExecuteAddItemBulk(StaCommand command)
|
private MxCommandReply ExecuteAddItemBulk(StaCommand command)
|
||||||
{
|
{
|
||||||
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.AddItemBulk)
|
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.AddItemBulk)
|
||||||
|
|||||||
@@ -227,6 +227,78 @@ public sealed class MxAccessSession : IDisposable
|
|||||||
MxAccessAdviceKind.Supervisory);
|
MxAccessAdviceKind.Supervisory);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Writes a value to an item.</summary>
|
||||||
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
||||||
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
||||||
|
/// <param name="value">COM-marshalable value to write.</param>
|
||||||
|
/// <param name="userId">MXAccess user id (security classification) for the write.</param>
|
||||||
|
public void Write(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
object? value,
|
||||||
|
int userId)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
mxAccessServer.Write(serverHandle, itemHandle, value, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Writes a value with an explicit source timestamp to an item.</summary>
|
||||||
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
||||||
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
||||||
|
/// <param name="value">COM-marshalable value to write.</param>
|
||||||
|
/// <param name="timestamp">COM-marshalable source timestamp for the write.</param>
|
||||||
|
/// <param name="userId">MXAccess user id (security classification) for the write.</param>
|
||||||
|
public void Write2(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
object? value,
|
||||||
|
object? timestamp,
|
||||||
|
int userId)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
mxAccessServer.Write2(serverHandle, itemHandle, value, timestamp, userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Performs a secured/verified write to an item.</summary>
|
||||||
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
||||||
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
||||||
|
/// <param name="currentUserId">MXAccess user id of the operator performing the write.</param>
|
||||||
|
/// <param name="verifierUserId">MXAccess user id of the verifier authorizing the write.</param>
|
||||||
|
/// <param name="value">COM-marshalable value to write.</param>
|
||||||
|
public void WriteSecured(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
int currentUserId,
|
||||||
|
int verifierUserId,
|
||||||
|
object? value)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
mxAccessServer.WriteSecured(serverHandle, itemHandle, currentUserId, verifierUserId, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Performs a secured/verified write with an explicit source timestamp.</summary>
|
||||||
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
||||||
|
/// <param name="itemHandle">Handle returned by the worker.</param>
|
||||||
|
/// <param name="currentUserId">MXAccess user id of the operator performing the write.</param>
|
||||||
|
/// <param name="verifierUserId">MXAccess user id of the verifier authorizing the write.</param>
|
||||||
|
/// <param name="value">COM-marshalable value to write.</param>
|
||||||
|
/// <param name="timestamp">COM-marshalable source timestamp for the write.</param>
|
||||||
|
public void WriteSecured2(
|
||||||
|
int serverHandle,
|
||||||
|
int itemHandle,
|
||||||
|
int currentUserId,
|
||||||
|
int verifierUserId,
|
||||||
|
object? value,
|
||||||
|
object? timestamp)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
mxAccessServer.WriteSecured2(serverHandle, itemHandle, currentUserId, verifierUserId, value, timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Adds multiple items in bulk, returning success/failure results.</summary>
|
/// <summary>Adds multiple items in bulk, returning success/failure results.</summary>
|
||||||
/// <param name="serverHandle">Handle returned by the worker.</param>
|
/// <param name="serverHandle">Handle returned by the worker.</param>
|
||||||
/// <param name="tagAddresses">Enumerable of item definitions to add.</param>
|
/// <param name="tagAddresses">Enumerable of item definitions to add.</param>
|
||||||
|
|||||||
Reference in New Issue
Block a user