Resolve Contracts-001/004/005/006/007/008 code-review findings
Contracts-001: docs/Grpc.md still described "four MxAccessGateway RPCs" —
updated to the actual six (adding AcknowledgeAlarm and QueryActiveAlarms to
the handler and validation-rule sections).
Contracts-003 (Won't Fix): the finding is factually wrong — the <Protobuf>
item for mxaccess_worker.proto already sets ProtoRoot="Protos"; all three
items are consistent (confirmed back to the reviewed commit).
Contracts-004: corrected the stale GatewayContractInfo XML summary
("before generated protobuf contracts are introduced").
Contracts-005: no proto field/enum value was ever removed, so no reserved
ranges were invented. Added a wire-compatibility policy comment to all three
.proto files instructing future editors to reserve removed numbers.
Contracts-006: documented MxStatusProxy.success — it mirrors the COM
MXSTATUS_PROXY numeric success member, is not a boolean, and clients should
branch on category.
Contracts-007: added 13 round-trip tests covering galaxy_repository.proto
messages, bulk-subscribe payloads, and raw-value/IPC worker bodies.
Contracts-008: WorkerAlarmRpcDispatcher never assigns AcknowledgeAlarmReply.
status, so the old "native status" proto comment was misleading. Corrected
the hresult/status proto comments and documented the worker native_status →
public reply mapping in AlarmClientDiscovery.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
namespace MxGateway.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Exposes version metadata shared by gateway components before generated
|
||||
/// protobuf contracts are introduced.
|
||||
/// Holds the protocol version constants shared by gateway components.
|
||||
/// <see cref="GatewayProtocolVersion"/> is advertised to clients in
|
||||
/// <c>OpenSessionReply</c>; <see cref="WorkerProtocolVersion"/> is used to
|
||||
/// validate <c>WorkerEnvelope</c> protocol framing on the gateway↔worker pipe.
|
||||
/// </summary>
|
||||
public static class GatewayContractInfo
|
||||
{
|
||||
|
||||
@@ -21418,7 +21418,12 @@ namespace MxGateway.Contracts.Proto {
|
||||
|
||||
private int hresult_;
|
||||
/// <summary>
|
||||
/// HRESULT captured from MXAccess if the ack failed at the COM layer.
|
||||
/// Native ack return code echoed from the worker. The worker carries the
|
||||
/// ack outcome as a single int32 (AcknowledgeAlarmReplyPayload.native_status,
|
||||
/// = AlarmAckByName / AlarmAckByGUID return code; 0 = success); the gateway's
|
||||
/// WorkerAlarmRpcDispatcher copies that value here. This is the authoritative
|
||||
/// ack-outcome field for the public RPC. Absent only when the worker reply
|
||||
/// omitted the value entirely (a protocol violation).
|
||||
/// </summary>
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
@@ -21446,7 +21451,11 @@ namespace MxGateway.Contracts.Proto {
|
||||
public const int StatusFieldNumber = 5;
|
||||
private global::MxGateway.Contracts.Proto.MxStatusProxy status_;
|
||||
/// <summary>
|
||||
/// Native MxAccess status describing the outcome of the ack.
|
||||
/// Reserved for a structured MxStatusProxy view of the ack outcome. The
|
||||
/// worker by-name/by-GUID ack path produces only the int32 return code
|
||||
/// (see `hresult`), so the current gateway leaves this field UNSET on every
|
||||
/// reply. Clients must read `hresult` (and `protocol_status`) for the ack
|
||||
/// result and must not depend on `status` being populated.
|
||||
/// </summary>
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
@@ -22078,6 +22087,17 @@ namespace MxGateway.Contracts.Proto {
|
||||
/// <summary>Field number for the "success" field.</summary>
|
||||
public const int SuccessFieldNumber = 1;
|
||||
private int success_;
|
||||
/// <summary>
|
||||
/// Mirrors the `success` member of the MXAccess MXSTATUS_PROXY struct
|
||||
/// (a 16-bit signed value in the COM struct, widened to int32 on the
|
||||
/// wire). Despite the name it is NOT a boolean — it is the raw numeric
|
||||
/// indicator the worker reads off the COM struct without reinterpretation.
|
||||
/// It is carried verbatim for diagnostics; the authoritative success/
|
||||
/// failure of the operation is `category` (MX_STATUS_CATEGORY_OK marks
|
||||
/// success), with `detail`, `diagnostic_text`, `raw_category`, and
|
||||
/// `raw_detected_by` describing any non-OK outcome. Clients should branch
|
||||
/// on `category`, not on a specific `success` value.
|
||||
/// </summary>
|
||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("protoc", null)]
|
||||
public int Success {
|
||||
|
||||
@@ -7,6 +7,13 @@ option csharp_namespace = "MxGateway.Contracts.Proto.Galaxy";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "google/protobuf/wrappers.proto";
|
||||
|
||||
// Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
||||
// additively only. Never renumber or repurpose an existing field number or
|
||||
// enum value. When a field or enum value is removed, add a `reserved` range
|
||||
// (and `reserved` name) covering it in the same change so a future editor
|
||||
// cannot accidentally reuse the retired tag. There are no `reserved`
|
||||
// declarations today because no field or enum value has ever been removed.
|
||||
|
||||
// Read-only browse over the AVEVA System Platform Galaxy Repository (ZB SQL
|
||||
// database). Lets clients enumerate the deployed object hierarchy and each
|
||||
// object's dynamic attributes so they know what tag references to subscribe
|
||||
|
||||
@@ -7,6 +7,13 @@ option csharp_namespace = "MxGateway.Contracts.Proto";
|
||||
import "google/protobuf/duration.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
// Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
||||
// additively only. Never renumber or repurpose an existing field number or
|
||||
// enum value. When a field or enum value is removed, add a `reserved` range
|
||||
// (and `reserved` name) covering it in the same change so a future editor
|
||||
// cannot accidentally reuse the retired tag. There are no `reserved`
|
||||
// declarations today because no field or enum value has ever been removed.
|
||||
|
||||
// Public client API for MXAccess sessions hosted by the gateway.
|
||||
service MxAccessGateway {
|
||||
rpc OpenSession(OpenSessionRequest) returns (OpenSessionReply);
|
||||
@@ -641,9 +648,18 @@ message AcknowledgeAlarmReply {
|
||||
string session_id = 1;
|
||||
string correlation_id = 2;
|
||||
ProtocolStatus protocol_status = 3;
|
||||
// HRESULT captured from MXAccess if the ack failed at the COM layer.
|
||||
// Native ack return code echoed from the worker. The worker carries the
|
||||
// ack outcome as a single int32 (AcknowledgeAlarmReplyPayload.native_status,
|
||||
// = AlarmAckByName / AlarmAckByGUID return code; 0 = success); the gateway's
|
||||
// WorkerAlarmRpcDispatcher copies that value here. This is the authoritative
|
||||
// ack-outcome field for the public RPC. Absent only when the worker reply
|
||||
// omitted the value entirely (a protocol violation).
|
||||
optional int32 hresult = 4;
|
||||
// Native MxAccess status describing the outcome of the ack.
|
||||
// Reserved for a structured MxStatusProxy view of the ack outcome. The
|
||||
// worker by-name/by-GUID ack path produces only the int32 return code
|
||||
// (see `hresult`), so the current gateway leaves this field UNSET on every
|
||||
// reply. Clients must read `hresult` (and `protocol_status`) for the ack
|
||||
// result and must not depend on `status` being populated.
|
||||
MxStatusProxy status = 5;
|
||||
string diagnostic_message = 6;
|
||||
}
|
||||
@@ -657,6 +673,15 @@ message QueryActiveAlarmsRequest {
|
||||
}
|
||||
|
||||
message MxStatusProxy {
|
||||
// Mirrors the `success` member of the MXAccess MXSTATUS_PROXY struct
|
||||
// (a 16-bit signed value in the COM struct, widened to int32 on the
|
||||
// wire). Despite the name it is NOT a boolean — it is the raw numeric
|
||||
// indicator the worker reads off the COM struct without reinterpretation.
|
||||
// It is carried verbatim for diagnostics; the authoritative success/
|
||||
// failure of the operation is `category` (MX_STATUS_CATEGORY_OK marks
|
||||
// success), with `detail`, `diagnostic_text`, `raw_category`, and
|
||||
// `raw_detected_by` describing any non-OK outcome. Clients should branch
|
||||
// on `category`, not on a specific `success` value.
|
||||
int32 success = 1;
|
||||
MxStatusCategory category = 2;
|
||||
MxStatusSource detected_by = 3;
|
||||
|
||||
@@ -8,6 +8,13 @@ import "google/protobuf/duration.proto";
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "mxaccess_gateway.proto";
|
||||
|
||||
// Wire-compatibility policy (ProtobufStyleGuide): this contract evolves
|
||||
// additively only. Never renumber or repurpose an existing field number or
|
||||
// enum value. When a field or enum value is removed, add a `reserved` range
|
||||
// (and `reserved` name) covering it in the same change so a future editor
|
||||
// cannot accidentally reuse the retired tag. There are no `reserved`
|
||||
// declarations today because no field or enum value has ever been removed.
|
||||
|
||||
// Gateway-to-worker IPC envelope. Named-pipe framing prepends a little-endian
|
||||
// uint32 payload length to this protobuf payload.
|
||||
message WorkerEnvelope {
|
||||
|
||||
@@ -2,6 +2,7 @@ using Google.Protobuf;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using MxGateway.Contracts;
|
||||
using MxGateway.Contracts.Proto;
|
||||
using MxGateway.Contracts.Proto.Galaxy;
|
||||
|
||||
namespace MxGateway.Tests.Contracts;
|
||||
|
||||
@@ -439,4 +440,334 @@ public sealed class ProtobufContractRoundTripTests
|
||||
Assert.Equal(withoutFilter, QueryActiveAlarmsRequest.Parser.ParseFrom(withoutFilter.ToByteArray()));
|
||||
Assert.Equal(withFilter, QueryActiveAlarmsRequest.Parser.ParseFrom(withFilter.ToByteArray()));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an MxValue carrying a raw_value bytes payload round-trips.</summary>
|
||||
[Fact]
|
||||
public void MxValue_RoundTripsRawValueBytesPayload()
|
||||
{
|
||||
var original = new MxValue
|
||||
{
|
||||
DataType = MxDataType.Unknown,
|
||||
VariantType = "VT_UNKNOWN",
|
||||
RawDataType = 99,
|
||||
RawDiagnostic = "uninterpreted COM variant",
|
||||
RawValue = ByteString.CopyFrom(0x01, 0x02, 0xFE, 0xFF),
|
||||
};
|
||||
|
||||
var parsed = MxValue.Parser.ParseFrom(original.ToByteArray());
|
||||
|
||||
Assert.Equal(original, parsed);
|
||||
Assert.Equal(MxValue.KindOneofCase.RawValue, parsed.KindCase);
|
||||
Assert.Equal(new byte[] { 0x01, 0x02, 0xFE, 0xFF }, parsed.RawValue.ToByteArray());
|
||||
}
|
||||
|
||||
/// <summary>Verifies that an MxArray carrying a RawArray of byte blobs round-trips.</summary>
|
||||
[Fact]
|
||||
public void MxArray_RoundTripsRawArrayPayload()
|
||||
{
|
||||
var original = new MxArray
|
||||
{
|
||||
ElementDataType = MxDataType.Unknown,
|
||||
VariantType = "VT_ARRAY|VT_UNKNOWN",
|
||||
RawElementDataType = 99,
|
||||
RawDiagnostic = "uninterpreted SAFEARRAY",
|
||||
Dimensions = { 2 },
|
||||
RawValues = new RawArray
|
||||
{
|
||||
Values =
|
||||
{
|
||||
ByteString.CopyFrom(0xAA, 0xBB),
|
||||
ByteString.CopyFrom(0xCC, 0xDD),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var parsed = MxArray.Parser.ParseFrom(original.ToByteArray());
|
||||
|
||||
Assert.Equal(original, parsed);
|
||||
Assert.Equal(MxArray.ValuesOneofCase.RawValues, parsed.ValuesCase);
|
||||
Assert.Equal(2, parsed.RawValues.Values.Count);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a BulkSubscribeReply with per-item SubscribeResults round-trips.</summary>
|
||||
[Fact]
|
||||
public void BulkSubscribeReply_RoundTripsSubscribeResults()
|
||||
{
|
||||
var original = new BulkSubscribeReply
|
||||
{
|
||||
Results =
|
||||
{
|
||||
new SubscribeResult
|
||||
{
|
||||
ServerHandle = 10,
|
||||
TagAddress = "Provider!Tank01.Level",
|
||||
ItemHandle = 21,
|
||||
WasSuccessful = true,
|
||||
},
|
||||
new SubscribeResult
|
||||
{
|
||||
ServerHandle = 10,
|
||||
TagAddress = "Provider!Bad.Tag",
|
||||
WasSuccessful = false,
|
||||
ErrorMessage = "item not found",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var parsed = BulkSubscribeReply.Parser.ParseFrom(original.ToByteArray());
|
||||
|
||||
Assert.Equal(original, parsed);
|
||||
Assert.Equal(2, parsed.Results.Count);
|
||||
Assert.True(parsed.Results[0].WasSuccessful);
|
||||
Assert.False(parsed.Results[1].WasSuccessful);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a bulk-subscribe command and its BulkSubscribeReply payload round-trip.</summary>
|
||||
[Fact]
|
||||
public void MxCommandReply_RoundTripsBulkSubscribePayload()
|
||||
{
|
||||
var original = new MxCommandReply
|
||||
{
|
||||
SessionId = "session-1",
|
||||
CorrelationId = "gateway-correlation-bulk",
|
||||
Kind = MxCommandKind.SubscribeBulk,
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.Ok },
|
||||
Hresult = 0,
|
||||
SubscribeBulk = new BulkSubscribeReply
|
||||
{
|
||||
Results =
|
||||
{
|
||||
new SubscribeResult
|
||||
{
|
||||
ServerHandle = 5,
|
||||
TagAddress = "Provider!Tank01.Level",
|
||||
ItemHandle = 7,
|
||||
WasSuccessful = true,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var parsed = MxCommandReply.Parser.ParseFrom(original.ToByteArray());
|
||||
|
||||
Assert.Equal(original, parsed);
|
||||
Assert.Equal(MxCommandReply.PayloadOneofCase.SubscribeBulk, parsed.PayloadCase);
|
||||
Assert.Single(parsed.SubscribeBulk.Results);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a WorkerEnvelope carrying a WorkerFault body round-trips.</summary>
|
||||
[Fact]
|
||||
public void WorkerEnvelope_RoundTripsWorkerFaultBody()
|
||||
{
|
||||
var original = new WorkerEnvelope
|
||||
{
|
||||
ProtocolVersion = GatewayContractInfo.WorkerProtocolVersion,
|
||||
SessionId = "session-1",
|
||||
Sequence = 11,
|
||||
CorrelationId = "gateway-correlation-fault",
|
||||
WorkerFault = new WorkerFault
|
||||
{
|
||||
Category = WorkerFaultCategory.MxaccessCommandFailed,
|
||||
CommandMethod = "Register",
|
||||
Hresult = unchecked((int)0x80004005),
|
||||
ExceptionType = "System.Runtime.InteropServices.COMException",
|
||||
DiagnosticMessage = "MXAccess COM call failed.",
|
||||
ProtocolStatus = new ProtocolStatus { Code = ProtocolStatusCode.ProtocolViolation },
|
||||
},
|
||||
};
|
||||
|
||||
var parsed = WorkerEnvelope.Parser.ParseFrom(original.ToByteArray());
|
||||
|
||||
Assert.Equal(original, parsed);
|
||||
Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerFault, parsed.BodyCase);
|
||||
Assert.True(parsed.WorkerFault.HasHresult);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a WorkerEnvelope carrying a WorkerHeartbeat body round-trips.</summary>
|
||||
[Fact]
|
||||
public void WorkerEnvelope_RoundTripsWorkerHeartbeatBody()
|
||||
{
|
||||
var activity = Timestamp.FromDateTime(new DateTime(2026, 5, 18, 9, 0, 0, DateTimeKind.Utc));
|
||||
var original = new WorkerEnvelope
|
||||
{
|
||||
ProtocolVersion = GatewayContractInfo.WorkerProtocolVersion,
|
||||
SessionId = "session-1",
|
||||
Sequence = 12,
|
||||
CorrelationId = "gateway-correlation-heartbeat",
|
||||
WorkerHeartbeat = new WorkerHeartbeat
|
||||
{
|
||||
WorkerProcessId = 4242,
|
||||
State = WorkerState.Ready,
|
||||
LastStaActivityTimestamp = activity,
|
||||
PendingCommandCount = 3,
|
||||
OutboundEventQueueDepth = 7,
|
||||
LastEventSequence = 1234,
|
||||
CurrentCommandCorrelationId = "in-flight-1",
|
||||
},
|
||||
};
|
||||
|
||||
var parsed = WorkerEnvelope.Parser.ParseFrom(original.ToByteArray());
|
||||
|
||||
Assert.Equal(original, parsed);
|
||||
Assert.Equal(WorkerEnvelope.BodyOneofCase.WorkerHeartbeat, parsed.BodyCase);
|
||||
Assert.Equal(WorkerState.Ready, parsed.WorkerHeartbeat.State);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the Galaxy Repository service descriptor exposes its browse RPCs.</summary>
|
||||
[Fact]
|
||||
public void GalaxyRepositoryDescriptor_ContainsBrowseServiceMethods()
|
||||
{
|
||||
var service = Assert.Single(
|
||||
GalaxyRepositoryReflection.Descriptor.Services,
|
||||
descriptor => descriptor.Name == "GalaxyRepository");
|
||||
|
||||
Assert.Contains(service.Methods, method => method.Name == "TestConnection");
|
||||
Assert.Contains(service.Methods, method => method.Name == "GetLastDeployTime");
|
||||
Assert.Contains(service.Methods, method => method.Name == "DiscoverHierarchy");
|
||||
Assert.Contains(service.Methods, method => method.Name == "WatchDeployEvents");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a DiscoverHierarchyRequest round-trips through every
|
||||
/// <c>root</c> oneof arm and its proto wrapper-typed <c>max_depth</c> field.
|
||||
/// </summary>
|
||||
[Theory]
|
||||
[InlineData(0)]
|
||||
[InlineData(1)]
|
||||
[InlineData(2)]
|
||||
public void DiscoverHierarchyRequest_RoundTripsRootOneofAndWrapperFields(int rootArm)
|
||||
{
|
||||
var original = new DiscoverHierarchyRequest
|
||||
{
|
||||
PageSize = 100,
|
||||
PageToken = "page-2",
|
||||
MaxDepth = 5,
|
||||
CategoryIds = { 3, 9 },
|
||||
TemplateChainContains = { "Analog", "Pump" },
|
||||
TagNameGlob = "Tank*",
|
||||
IncludeAttributes = true,
|
||||
AlarmBearingOnly = true,
|
||||
HistorizedOnly = false,
|
||||
};
|
||||
switch (rootArm)
|
||||
{
|
||||
case 0:
|
||||
original.RootGobjectId = 4711;
|
||||
break;
|
||||
case 1:
|
||||
original.RootTagName = "Tank01";
|
||||
break;
|
||||
default:
|
||||
original.RootContainedPath = "Area1.Tank01";
|
||||
break;
|
||||
}
|
||||
|
||||
var parsed = DiscoverHierarchyRequest.Parser.ParseFrom(original.ToByteArray());
|
||||
|
||||
Assert.Equal(original, parsed);
|
||||
Assert.Equal(original.RootCase, parsed.RootCase);
|
||||
Assert.NotEqual(DiscoverHierarchyRequest.RootOneofCase.None, parsed.RootCase);
|
||||
Assert.NotNull(parsed.MaxDepth);
|
||||
Assert.Equal(5, parsed.MaxDepth!.Value);
|
||||
Assert.True(parsed.HasIncludeAttributes);
|
||||
Assert.True(parsed.IncludeAttributes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a DiscoverHierarchyReply round-trips with nested
|
||||
/// GalaxyObject and GalaxyAttribute graphs.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void DiscoverHierarchyReply_RoundTripsObjectAndAttributeGraph()
|
||||
{
|
||||
var original = new DiscoverHierarchyReply
|
||||
{
|
||||
NextPageToken = "page-3",
|
||||
TotalObjectCount = 2,
|
||||
Objects =
|
||||
{
|
||||
new GalaxyObject
|
||||
{
|
||||
GobjectId = 4711,
|
||||
TagName = "Tank01",
|
||||
ContainedName = "Tank01",
|
||||
BrowseName = "Tank 01",
|
||||
ParentGobjectId = 12,
|
||||
IsArea = false,
|
||||
CategoryId = 3,
|
||||
HostedByGobjectId = 8,
|
||||
TemplateChain = { "$AnalogDevice", "$Tank" },
|
||||
Attributes =
|
||||
{
|
||||
new GalaxyAttribute
|
||||
{
|
||||
AttributeName = "Level",
|
||||
FullTagReference = "Galaxy!Tank01.Level",
|
||||
MxDataType = 3,
|
||||
DataTypeName = "Float",
|
||||
IsArray = false,
|
||||
ArrayDimension = 0,
|
||||
ArrayDimensionPresent = false,
|
||||
MxAttributeCategory = 1,
|
||||
SecurityClassification = 0,
|
||||
IsHistorized = true,
|
||||
IsAlarm = true,
|
||||
},
|
||||
},
|
||||
},
|
||||
new GalaxyObject
|
||||
{
|
||||
GobjectId = 12,
|
||||
TagName = "Area1",
|
||||
IsArea = true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
var parsed = DiscoverHierarchyReply.Parser.ParseFrom(original.ToByteArray());
|
||||
|
||||
Assert.Equal(original, parsed);
|
||||
Assert.Equal(2, parsed.Objects.Count);
|
||||
Assert.Single(parsed.Objects[0].Attributes);
|
||||
Assert.True(parsed.Objects[0].Attributes[0].IsAlarm);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that a DeployEvent round-trips with its timestamp and counters.</summary>
|
||||
[Fact]
|
||||
public void DeployEvent_RoundTripsTimestampAndCounters()
|
||||
{
|
||||
var observed = Timestamp.FromDateTime(new DateTime(2026, 5, 18, 8, 30, 0, DateTimeKind.Utc));
|
||||
var deploy = Timestamp.FromDateTime(new DateTime(2026, 5, 18, 8, 0, 0, DateTimeKind.Utc));
|
||||
var original = new DeployEvent
|
||||
{
|
||||
Sequence = 17,
|
||||
ObservedAt = observed,
|
||||
TimeOfLastDeploy = deploy,
|
||||
TimeOfLastDeployPresent = true,
|
||||
ObjectCount = 240,
|
||||
AttributeCount = 3600,
|
||||
};
|
||||
|
||||
var parsed = DeployEvent.Parser.ParseFrom(original.ToByteArray());
|
||||
|
||||
Assert.Equal(original, parsed);
|
||||
Assert.True(parsed.TimeOfLastDeployPresent);
|
||||
Assert.Equal(deploy, parsed.TimeOfLastDeploy);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that GetLastDeployTimeReply and TestConnectionReply round-trip.</summary>
|
||||
[Fact]
|
||||
public void GalaxyConnectionReplies_RoundTrip()
|
||||
{
|
||||
var deploy = Timestamp.FromDateTime(new DateTime(2026, 5, 18, 8, 0, 0, DateTimeKind.Utc));
|
||||
var lastDeploy = new GetLastDeployTimeReply
|
||||
{
|
||||
Present = true,
|
||||
TimeOfLastDeploy = deploy,
|
||||
};
|
||||
var testConnection = new TestConnectionReply { Ok = true };
|
||||
|
||||
Assert.Equal(lastDeploy, GetLastDeployTimeReply.Parser.ParseFrom(lastDeploy.ToByteArray()));
|
||||
Assert.Equal(testConnection, TestConnectionReply.Parser.ParseFrom(testConnection.ToByteArray()));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user