Compare commits
2 Commits
e355a7674b
...
758aca2355
| Author | SHA1 | Date | |
|---|---|---|---|
| 758aca2355 | |||
| 06030dd1ef |
+6
-1
@@ -260,7 +260,12 @@ public final class MxGatewayClient implements AutoCloseable {
|
||||
* @return an iterator-style stream of events
|
||||
*/
|
||||
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());
|
||||
return stream;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ use clap::{Args, Parser, Subcommand, ValueEnum};
|
||||
use futures_util::StreamExt;
|
||||
use mxgateway_client::generated::galaxy_repository::v1::DeployEvent;
|
||||
use mxgateway_client::generated::mxaccess_gateway::v1::{
|
||||
CloseSessionRequest, MxCommand, MxCommandKind, MxCommandRequest, MxEvent,
|
||||
CloseSessionRequest, MxCommand, MxCommandKind, MxCommandRequest, MxEvent, MxEventFamily,
|
||||
MxValue as ProtoMxValue, OpenSessionRequest, PingCommand, StreamEventsRequest,
|
||||
};
|
||||
use mxgateway_client::{
|
||||
@@ -842,8 +842,13 @@ fn print_deploy_event(event: &DeployEvent, use_json: bool) {
|
||||
/// 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": event.family,
|
||||
"family": family,
|
||||
"sessionId": event.session_id,
|
||||
"serverHandle": event.server_handle,
|
||||
"itemHandle": event.item_handle,
|
||||
|
||||
+28
-16
@@ -180,19 +180,30 @@ path and writes a JSON report under `artifacts/e2e/`:
|
||||
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. **Write round-trip** — writes a per-client sentinel value to a configurable
|
||||
writable attribute (`-WriteAttribute`, default `TestChangingInt`), then
|
||||
asserts the same value is echoed back through the event stream. Skip with
|
||||
`-SkipWrite`. The Rust `stream-events` CLI emits full per-event JSON
|
||||
(`itemHandle` + `value`) so all five clients run an identical value compare.
|
||||
5. **Stream** — asserts a bounded event stream delivers at least one event
|
||||
4. **Stream** — asserts a bounded event stream delivers at least one event
|
||||
(skip with `-SkipStream`).
|
||||
6. **Parity** — asserts MXAccess error paths are rejected rather than silently
|
||||
5. **Parity** — asserts MXAccess error paths are rejected rather than silently
|
||||
succeeding: an invalid item handle and an unknown session id (skip with
|
||||
`-SkipParity`).
|
||||
7. **Auth rejection** — asserts `open-session` is rejected when the API key is
|
||||
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
|
||||
before running the client e2e script:
|
||||
@@ -209,9 +220,9 @@ 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 -SkipStream
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -SkipBulk
|
||||
# Write round-trip: point at a writable scalar attribute and its value type.
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -WriteAttribute TestChangingInt -WriteType int32
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -SkipWrite
|
||||
# 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.
|
||||
@@ -221,11 +232,12 @@ powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -DryRu
|
||||
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1 -Endpoint localhost:5000 -ApiKeyEnv MXGATEWAY_API_KEY
|
||||
```
|
||||
|
||||
The write round-trip fails loudly if `-WriteAttribute` does not name a writable
|
||||
scalar attribute, if the write is rejected, or if the sentinel value is not
|
||||
observed within `-WriteEchoMaxEvents` (default 200) streamed events. Point
|
||||
`-WriteAttribute` at a stable writable attribute, raise `-WriteEchoMaxEvents`,
|
||||
or pass `-SkipWrite` if no suitable attribute is deployed.
|
||||
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
|
||||
|
||||
|
||||
@@ -33,8 +33,10 @@ param(
|
||||
[int]$BulkTagCount = 6,
|
||||
[switch]$SkipStream,
|
||||
[switch]$SkipBulk,
|
||||
# Write round-trip + value assertion.
|
||||
[switch]$SkipWrite,
|
||||
# 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,
|
||||
@@ -335,6 +337,14 @@ function Get-EventItemHandle {
|
||||
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.
|
||||
@@ -595,7 +605,20 @@ function Get-ClientCommand {
|
||||
$cliArgs += @("--session-id", $Values.sessionId)
|
||||
}
|
||||
$arguments = @("--quiet", ":mxgateway-cli:run", "--args=$($cliArgs -join ' ')")
|
||||
return [pscustomobject]@{ file = "gradle"; args = $arguments; cwd = (Join-Path $repoRoot "clients/java"); env = @{} }
|
||||
# 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 = @{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -627,23 +650,30 @@ function Get-DryRunReply {
|
||||
return [pscustomobject]@{ unsubscribeBulk = [pscustomobject]@{ results = $results }; results = $results }
|
||||
}
|
||||
"stream-events" {
|
||||
# Echo the requested write value back 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 an `events` property.
|
||||
# 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 }
|
||||
$event = [pscustomobject]@{
|
||||
$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 ,@($event) }
|
||||
"java" { return ,@($event) }
|
||||
"rust" { return [pscustomobject]@{ eventCount = 1; events = @($event) } }
|
||||
default { return [pscustomobject]@{ events = @($event) } }
|
||||
"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]@{} } }
|
||||
@@ -735,6 +765,83 @@ function Invoke-ClientFlow {
|
||||
$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 ","
|
||||
@@ -788,71 +895,15 @@ function Invoke-ClientFlow {
|
||||
}
|
||||
}
|
||||
|
||||
# --- Write round-trip + value assertion ---------------------------
|
||||
# Write a per-client sentinel value to a configured writable
|
||||
# attribute, then assert it is echoed back through the event stream.
|
||||
$writeTarget = $null
|
||||
if (-not $SkipWrite) {
|
||||
$writeTarget = @($clientResult.addedItems | Where-Object {
|
||||
$_.attributeName -eq $WriteAttribute
|
||||
}) | Select-Object -First 1
|
||||
}
|
||||
|
||||
$doWrite = $null -ne $writeTarget
|
||||
$sentinelValue = $null
|
||||
if ($doWrite) {
|
||||
$sentinelValue = "$($WriteValueBase + $script:clientFlowIndex)"
|
||||
Invoke-ClientOperation -Client $Client -Operation "write" -Values @{
|
||||
# --- Event streaming ----------------------------------------------
|
||||
if (-not $SkipStream) {
|
||||
$streamJson = Invoke-ClientOperation -Client $Client -Operation "stream-events" -Values @{
|
||||
sessionId = $sessionId
|
||||
serverHandle = $serverHandle
|
||||
itemHandle = $writeTarget.itemHandle
|
||||
valueType = $WriteType
|
||||
value = $sentinelValue
|
||||
} | Out-Null
|
||||
} elseif (-not $SkipWrite) {
|
||||
Write-Warning "$Client write phase skipped: no discovered tag has attribute '$WriteAttribute'."
|
||||
}
|
||||
|
||||
# --- Event streaming (also serves the write echo assertion) -------
|
||||
$captureEvents = (-not $SkipStream) -or $doWrite
|
||||
if ($captureEvents) {
|
||||
$streamValues = @{ sessionId = $sessionId }
|
||||
if ($doWrite) {
|
||||
$streamValues.maxEvents = $WriteEchoMaxEvents
|
||||
$streamValues.echoItemHandle = $writeTarget.itemHandle
|
||||
$streamValues.echoValue = $sentinelValue
|
||||
}
|
||||
$streamJson = Invoke-ClientOperation -Client $Client -Operation "stream-events" -Values $streamValues
|
||||
$events = @(Get-StreamEvents -Client $Client -Json $streamJson)
|
||||
$clientResult.eventCount = Get-StreamEventCount -Client $Client -Json $streamJson
|
||||
|
||||
if (-not $SkipStream -and $clientResult.eventCount -lt 1) {
|
||||
if ($clientResult.eventCount -lt 1) {
|
||||
throw "The $Client stream-events command returned no events."
|
||||
}
|
||||
|
||||
if ($doWrite) {
|
||||
$echoEvent = $events | Where-Object {
|
||||
(Get-EventItemHandle -Event $_) -eq $writeTarget.itemHandle -and
|
||||
(Test-ValueEquals -Expected $sentinelValue -Observed (Get-EventScalar -Event $_))
|
||||
} | Select-Object -First 1
|
||||
|
||||
if ($null -eq $echoEvent) {
|
||||
throw ("$Client write round-trip failed: wrote $WriteType=$sentinelValue to " +
|
||||
"'$($writeTarget.fullTagReference)' (item handle $($writeTarget.itemHandle)) " +
|
||||
"but no matching value was observed in $($events.Count) streamed event(s). " +
|
||||
"Increase -WriteEchoMaxEvents, point -WriteAttribute at a writable attribute, or pass -SkipWrite.")
|
||||
}
|
||||
|
||||
$clientResult.write = [ordered]@{
|
||||
attributeName = $WriteAttribute
|
||||
fullTagReference = $writeTarget.fullTagReference
|
||||
itemHandle = $writeTarget.itemHandle
|
||||
valueType = $WriteType
|
||||
value = $sentinelValue
|
||||
echoObserved = $true
|
||||
echoWorkerSequence = (Get-PropertyValue -Object $echoEvent -Names @("workerSequence", "worker_sequence"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# --- Error-path (parity) checks -----------------------------------
|
||||
@@ -965,7 +1016,7 @@ function Get-ChildArgumentList {
|
||||
}
|
||||
if ($SkipStream) { $childArgs += "-SkipStream" }
|
||||
if ($SkipBulk) { $childArgs += "-SkipBulk" }
|
||||
if ($SkipWrite) { $childArgs += "-SkipWrite" }
|
||||
if ($VerifyWrite) { $childArgs += "-VerifyWrite" }
|
||||
if ($SkipParity) { $childArgs += "-SkipParity" }
|
||||
if ($SkipAuth) { $childArgs += "-SkipAuth" }
|
||||
if ($DryRun) { $childArgs += "-DryRun" }
|
||||
@@ -1043,7 +1094,7 @@ if ($Parallel -and $Clients.Count -gt 1) {
|
||||
bulkTagCount = $BulkTagCount
|
||||
skipStream = [bool]$SkipStream
|
||||
skipBulk = [bool]$SkipBulk
|
||||
skipWrite = [bool]$SkipWrite
|
||||
verifyWrite = [bool]$VerifyWrite
|
||||
skipParity = [bool]$SkipParity
|
||||
skipAuth = [bool]$SkipAuth
|
||||
writeAttribute = $WriteAttribute
|
||||
@@ -1101,7 +1152,7 @@ $run = [ordered]@{
|
||||
bulkTagCount = $BulkTagCount
|
||||
skipStream = [bool]$SkipStream
|
||||
skipBulk = [bool]$SkipBulk
|
||||
skipWrite = [bool]$SkipWrite
|
||||
verifyWrite = [bool]$VerifyWrite
|
||||
skipParity = [bool]$SkipParity
|
||||
skipAuth = [bool]$SkipAuth
|
||||
writeAttribute = $WriteAttribute
|
||||
|
||||
@@ -46,6 +46,78 @@ public sealed class VariantConverterTests
|
||||
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>
|
||||
[Fact]
|
||||
public void Convert_WithFileTimeAndExpectedTime_ProjectsTimestamp()
|
||||
|
||||
@@ -379,10 +379,10 @@ public sealed class AlarmCommandExecutorTests
|
||||
public void SetBufferedUpdateInterval(int serverHandle, int updateIntervalMilliseconds) { }
|
||||
public void Suspend(int serverHandle, int itemHandle) { }
|
||||
public void Activate(int serverHandle, int itemHandle) { }
|
||||
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 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 Write(int serverHandle, int itemHandle, object? value, 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 WriteSecured2(int serverHandle, int itemHandle, int currentUserId, int verifierUserId, object? value, object? timestampValue) { }
|
||||
public int AuthenticateUser(string userName, string password) => 0;
|
||||
public int ArchestrAUserToId(string userName) => 0;
|
||||
}
|
||||
|
||||
@@ -134,5 +134,26 @@ public sealed class MxAccessComServerTests
|
||||
{
|
||||
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.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using MxGateway.Contracts.Proto;
|
||||
using MxGateway.Worker.MxAccess;
|
||||
using MxGateway.Worker.Sta;
|
||||
@@ -617,6 +618,143 @@ public sealed class MxAccessCommandExecutorTests
|
||||
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(
|
||||
string correlationId,
|
||||
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(
|
||||
string correlationId,
|
||||
int serverHandle,
|
||||
@@ -1080,6 +1338,118 @@ public sealed class MxAccessCommandExecutorTests
|
||||
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>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
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(
|
||||
object value,
|
||||
MxDataType expectedDataType)
|
||||
|
||||
@@ -56,4 +56,56 @@ public interface IMxAccessServer
|
||||
void AdviseSupervisory(
|
||||
int serverHandle,
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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()
|
||||
{
|
||||
return mxAccessComObject as ILMXProxyServer
|
||||
|
||||
@@ -74,6 +74,10 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
|
||||
MxCommandKind.Advise => ExecuteAdvise(command),
|
||||
MxCommandKind.UnAdvise => ExecuteUnAdvise(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.AdviseItemBulk => ExecuteAdviseItemBulk(command),
|
||||
MxCommandKind.RemoveItemBulk => ExecuteRemoveItemBulk(command),
|
||||
@@ -223,6 +227,108 @@ public sealed class MxAccessCommandExecutor : IStaCommandExecutor
|
||||
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)
|
||||
{
|
||||
if (command.Command.PayloadCase != MxCommand.PayloadOneofCase.AddItemBulk)
|
||||
|
||||
@@ -227,6 +227,78 @@ public sealed class MxAccessSession : IDisposable
|
||||
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>
|
||||
/// <param name="serverHandle">Handle returned by the worker.</param>
|
||||
/// <param name="tagAddresses">Enumerable of item definitions to add.</param>
|
||||
|
||||
Reference in New Issue
Block a user