rename: prefix gateway projects/namespaces with ZB.MOM.WW + sln→slnx
Apply the ZB.MOM.WW. prefix to all gateway-side projects, folders,
.csproj/.sln contents, C# namespaces, using directives, generated proto
C# (csharp_namespace + checked-in generated files), InternalsVisibleTo
attributes, project-name string literals (LoadProject, .sln lookups,
worker exe paths, staticwebassets manifest), and the install/script/doc
references that point at any of the above. Migrate the solution from
.sln to .slnx via `dotnet sln migrate` and delete the old file.
External-runtime identifiers are intentionally NOT prefixed so external
configuration keeps working:
- GatewayMetrics.cs MeterName ("MxGateway.Server")
- DashboardAuthenticationDefaults Scheme/Policy ("MxGateway.Dashboard")
- GatewayRequestLoggingMiddleware logger category ("MxGateway.Request")
- StaRuntime thread name ("MxGateway.Worker.STA")
- appsettings.json root section "MxGateway" + env-var prefix
MxGateway__... and secret-name MxGateway:ApiKeyPepper
- C:\ProgramData\MxGateway\ data dir paths
Also fixes two tests that were not rename-related but became visible
while validating the rename:
- WorkerLiveMxAccessSmokeTests.ShutDownAsync: cancellation that the
gateway service correctly maps to RpcException(Cancelled) per gRPC
convention was being misclassified as a stream fault. Added a sibling
catch on RpcException with StatusCode.Cancelled.
- IntegrationTestEnvironment.ResolveRepositoryRoot: extracted IsRepositoryRoot
and made it accept either a .git marker OR a .sln/.slnx next to src/
so the worker-exe walker works in non-git working copies.
clients/proto/proto-inputs.json's protoRoot updated to point at
src/ZB.MOM.WW.MxGateway.Contracts/Protos.
Verified by `dotnet build` and a full `dotnet test` of the .slnx with
MXGATEWAY_RUN_LIVE_{MXACCESS,LDAP,GALAXY}_TESTS=1:
Tests: 472/472 pass
Worker.Tests: 280/280 pass (4 dev-rig [Fact(Skip=...)] skipped)
IntegrationTests: 18/18 pass
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,404 @@
|
||||
using System.Text.Json;
|
||||
using Google.Protobuf;
|
||||
using ZB.MOM.WW.MxGateway.Contracts;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Contracts;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for behavior fixture manifests and contract conformance.
|
||||
/// </summary>
|
||||
public sealed class ClientBehaviorFixtureTests
|
||||
{
|
||||
private static readonly JsonParser ProtobufJsonParser = new(JsonParser.Settings.Default);
|
||||
|
||||
/// <summary>
|
||||
/// Verifies the behavior manifest declares correct protocol versions and fixture references.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BehaviorManifest_DeclaresCurrentProtocolVersionsAndExistingFixtures()
|
||||
{
|
||||
using JsonDocument manifest = LoadBehaviorManifest();
|
||||
JsonElement root = manifest.RootElement;
|
||||
|
||||
Assert.Equal(1, root.GetProperty("schemaVersion").GetInt32());
|
||||
Assert.Equal("mxaccess-gateway-client-behavior", root.GetProperty("fixtureSet").GetString());
|
||||
Assert.Equal(GatewayContractInfo.GatewayProtocolVersion, root.GetProperty("gatewayProtocolVersion").GetUInt32());
|
||||
Assert.Equal(GatewayContractInfo.WorkerProtocolVersion, root.GetProperty("workerProtocolVersion").GetUInt32());
|
||||
|
||||
HashSet<string> fixtureIds = new(StringComparer.Ordinal);
|
||||
foreach (JsonElement fixture in root.GetProperty("fixtures").EnumerateArray())
|
||||
{
|
||||
string id = fixture.GetProperty("id").GetString()!;
|
||||
string path = fixture.GetProperty("path").GetString()!;
|
||||
string category = fixture.GetProperty("category").GetString()!;
|
||||
string messageType = fixture.GetProperty("messageType").GetString()!;
|
||||
|
||||
Assert.True(fixtureIds.Add(id), $"Duplicate behavior fixture id '{id}'.");
|
||||
Assert.Contains(category, KnownCategories);
|
||||
Assert.Contains(messageType, KnownMessageTypes);
|
||||
Assert.True(
|
||||
File.Exists(Path.Combine(GetBehaviorFixtureRoot().FullName, path)),
|
||||
$"Expected behavior fixture '{path}' to exist.");
|
||||
Assert.False(Path.IsPathRooted(path), $"Fixture path '{path}' must be relative.");
|
||||
Assert.NotEmpty(fixture.GetProperty("expectation").GetString()!);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies proto inputs manifest references the expected behavior fixture root.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ProtoInputManifest_ReferencesBehaviorFixtureRoot()
|
||||
{
|
||||
DirectoryInfo repositoryRoot = FindRepositoryRoot();
|
||||
string manifestPath = Path.Combine(repositoryRoot.FullName, "clients", "proto", "proto-inputs.json");
|
||||
|
||||
using JsonDocument manifest = JsonDocument.Parse(File.ReadAllText(manifestPath));
|
||||
string fixtureRoot = manifest.RootElement.GetProperty("behaviorFixtureRoot").GetString()!;
|
||||
|
||||
Assert.Equal("clients/proto/fixtures/behavior", fixtureRoot);
|
||||
Assert.True(Directory.Exists(Path.Combine(repositoryRoot.FullName, fixtureRoot)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies command reply fixtures parse and preserve MXAccess details.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CommandReplyFixtures_ParseWithCurrentContractAndPreserveMxAccessDetails()
|
||||
{
|
||||
IReadOnlyList<JsonElement> fixtures = LoadManifestFixtures("command_replies");
|
||||
Assert.NotEmpty(fixtures);
|
||||
|
||||
foreach (JsonElement fixture in fixtures)
|
||||
{
|
||||
MxCommandReply reply = ParseFixture<MxCommandReply>(
|
||||
fixture,
|
||||
MxCommandReply.Parser);
|
||||
|
||||
Assert.NotEqual(MxCommandKind.Unspecified, reply.Kind);
|
||||
Assert.NotEqual(ProtocolStatusCode.Unspecified, reply.ProtocolStatus.Code);
|
||||
Assert.True(reply.HasHresult, $"Fixture '{GetFixtureId(fixture)}' must carry an HRESULT.");
|
||||
Assert.NotEmpty(reply.Statuses);
|
||||
Assert.NotEqual(MxDataType.Unspecified, reply.ReturnValue.DataType);
|
||||
Assert.True(
|
||||
reply.ReturnValue.KindCase != MxValue.KindOneofCase.None || reply.ReturnValue.IsNull,
|
||||
$"Fixture '{GetFixtureId(fixture)}' must carry a typed value, raw value, or explicit null.");
|
||||
}
|
||||
|
||||
MxCommandReply failedWrite = ParseFixture<MxCommandReply>(
|
||||
Assert.Single(fixtures, fixture => GetFixtureId(fixture) == "command-reply.write.mxaccess-failure"),
|
||||
MxCommandReply.Parser);
|
||||
|
||||
Assert.Equal(ProtocolStatusCode.MxaccessFailure, failedWrite.ProtocolStatus.Code);
|
||||
Assert.Equal(-2147220992, failedWrite.Hresult);
|
||||
Assert.True(failedWrite.Statuses.Count > 1);
|
||||
Assert.All(failedWrite.Statuses, status => Assert.Equal(0, status.Success));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies event stream fixtures have monotonic sequences and expected event families.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void EventStreamFixtures_ParseWithMonotonicSequencesAndExpectedFamilies()
|
||||
{
|
||||
IReadOnlyList<JsonElement> fixtures = LoadManifestFixtures("event_streams");
|
||||
Assert.NotEmpty(fixtures);
|
||||
|
||||
foreach (JsonElement fixture in fixtures)
|
||||
{
|
||||
using JsonDocument document = JsonDocument.Parse(File.ReadAllText(GetFixturePath(fixture)));
|
||||
ulong previousSequence = 0;
|
||||
List<MxEventFamily> families = [];
|
||||
|
||||
foreach (JsonElement eventElement in document.RootElement.GetProperty("events").EnumerateArray())
|
||||
{
|
||||
MxEvent gatewayEvent = ProtobufJsonParser.Parse<MxEvent>(eventElement.GetRawText());
|
||||
|
||||
Assert.True(gatewayEvent.WorkerSequence > previousSequence);
|
||||
Assert.Equal(document.RootElement.GetProperty("sessionId").GetString(), gatewayEvent.SessionId);
|
||||
Assert.NotEmpty(gatewayEvent.Statuses);
|
||||
AssertEventBodyMatchesFamily(gatewayEvent);
|
||||
|
||||
previousSequence = gatewayEvent.WorkerSequence;
|
||||
families.Add(gatewayEvent.Family);
|
||||
}
|
||||
|
||||
Assert.Contains(MxEventFamily.OnDataChange, families);
|
||||
Assert.Contains(MxEventFamily.OnWriteComplete, families);
|
||||
Assert.Contains(MxEventFamily.OperationComplete, families);
|
||||
Assert.Contains(MxEventFamily.OnBufferedDataChange, families);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies value conversion fixtures parse typed values and raw fallbacks.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void ValueConversionFixtures_ParseTypedValuesAndRawFallbacks()
|
||||
{
|
||||
JsonElement cases = LoadCaseSet("value_conversion", "cases");
|
||||
bool sawRawFallback = false;
|
||||
bool sawRawArrayFallback = false;
|
||||
bool sawTypedArray = false;
|
||||
|
||||
foreach (JsonElement valueCase in cases.EnumerateArray())
|
||||
{
|
||||
MxValue value = ProtobufJsonParser.Parse<MxValue>(
|
||||
valueCase.GetProperty("value").GetRawText());
|
||||
string expectedKind = valueCase.GetProperty("expectedKind").GetString()!;
|
||||
|
||||
Assert.NotEqual(MxDataType.Unspecified, value.DataType);
|
||||
AssertJsonKindMatchesValueKind(expectedKind, value);
|
||||
|
||||
sawRawFallback |= value.KindCase == MxValue.KindOneofCase.RawValue
|
||||
&& !string.IsNullOrWhiteSpace(value.RawDiagnostic)
|
||||
&& value.RawDataType != 0;
|
||||
sawRawArrayFallback |= value.KindCase == MxValue.KindOneofCase.ArrayValue
|
||||
&& value.ArrayValue.ValuesCase == MxArray.ValuesOneofCase.RawValues
|
||||
&& !string.IsNullOrWhiteSpace(value.ArrayValue.RawDiagnostic)
|
||||
&& value.ArrayValue.RawElementDataType != 0;
|
||||
sawTypedArray |= value.KindCase == MxValue.KindOneofCase.ArrayValue
|
||||
&& value.ArrayValue.ValuesCase != MxArray.ValuesOneofCase.RawValues;
|
||||
}
|
||||
|
||||
Assert.True(sawRawFallback, "Expected at least one raw scalar fallback case.");
|
||||
Assert.True(sawRawArrayFallback, "Expected at least one raw array fallback case.");
|
||||
Assert.True(sawTypedArray, "Expected at least one typed array case.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies status conversion fixtures parse status arrays and raw fields.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void StatusConversionFixtures_ParseStatusArraysAndRawFields()
|
||||
{
|
||||
JsonElement cases = LoadCaseSet("status_conversion", "cases");
|
||||
bool sawRawUnknown = false;
|
||||
|
||||
foreach (JsonElement statusCase in cases.EnumerateArray())
|
||||
{
|
||||
MxStatusProxy status = ProtobufJsonParser.Parse<MxStatusProxy>(
|
||||
statusCase.GetProperty("status").GetRawText());
|
||||
|
||||
Assert.NotEqual(MxStatusCategory.Unspecified, status.Category);
|
||||
Assert.NotEqual(MxStatusSource.Unspecified, status.DetectedBy);
|
||||
Assert.NotEmpty(status.DiagnosticText);
|
||||
|
||||
sawRawUnknown |= status.Category == MxStatusCategory.Unknown
|
||||
&& status.RawCategory != 0
|
||||
&& status.RawDetectedBy != 0;
|
||||
}
|
||||
|
||||
Assert.True(sawRawUnknown, "Expected a status case with unknown raw native fields.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies auth error fixtures map authentication/authorization and redact credentials.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void AuthErrorFixtures_MapAuthenticationAuthorizationAndRedactCredentials()
|
||||
{
|
||||
JsonElement cases = LoadCaseSet("auth_errors", "cases");
|
||||
HashSet<string> statusCodes = new(StringComparer.Ordinal);
|
||||
|
||||
foreach (JsonElement authCase in cases.EnumerateArray())
|
||||
{
|
||||
string grpcStatusCode = authCase.GetProperty("grpcStatusCode").GetString()!;
|
||||
string category = authCase.GetProperty("clientErrorCategory").GetString()!;
|
||||
string redactedOutput = authCase.GetProperty("expectedRedactedOutput").GetString()!;
|
||||
string serialized = authCase.GetRawText();
|
||||
|
||||
Assert.Contains(grpcStatusCode, AuthGrpcStatusCodes);
|
||||
Assert.Contains(category, AuthClientErrorCategories);
|
||||
string authorization = authCase.GetProperty("inputMetadata").GetProperty("authorization").GetString()!;
|
||||
if (!string.IsNullOrEmpty(authorization))
|
||||
{
|
||||
Assert.Contains("<redacted>", serialized);
|
||||
}
|
||||
|
||||
Assert.DoesNotContain("mxgw_", serialized, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("secret", redactedOutput, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
statusCodes.Add(grpcStatusCode);
|
||||
}
|
||||
|
||||
Assert.Contains("UNAUTHENTICATED", statusCodes);
|
||||
Assert.Contains("PERMISSION_DENIED", statusCodes);
|
||||
}
|
||||
|
||||
/// <summary>Verifies timeout and cancellation test fixtures document client and worker behavior.</summary>
|
||||
[Fact]
|
||||
public void TimeoutCancelFixtures_DocumentClientWaitAndWorkerCommandBehavior()
|
||||
{
|
||||
JsonElement cases = LoadCaseSet("timeout_cancel", "cases");
|
||||
HashSet<string> statusCodes = new(StringComparer.Ordinal);
|
||||
|
||||
foreach (JsonElement timeoutCase in cases.EnumerateArray())
|
||||
{
|
||||
string grpcStatusCode = timeoutCase.GetProperty("grpcStatusCode").GetString()!;
|
||||
|
||||
Assert.Contains(grpcStatusCode, TimeoutGrpcStatusCodes);
|
||||
Assert.NotEmpty(timeoutCase.GetProperty("clientDeadline").GetString()!);
|
||||
Assert.NotEmpty(timeoutCase.GetProperty("gatewayWaitBehavior").GetString()!);
|
||||
Assert.NotEmpty(timeoutCase.GetProperty("workerCommandBehavior").GetString()!);
|
||||
Assert.NotEmpty(timeoutCase.GetProperty("expectedClientAction").GetString()!);
|
||||
|
||||
statusCodes.Add(grpcStatusCode);
|
||||
}
|
||||
|
||||
Assert.Contains("DEADLINE_EXCEEDED", statusCodes);
|
||||
Assert.Contains("CANCELLED", statusCodes);
|
||||
}
|
||||
|
||||
private static readonly string[] KnownCategories =
|
||||
[
|
||||
"command_replies",
|
||||
"event_streams",
|
||||
"value_conversion",
|
||||
"status_conversion",
|
||||
"auth_errors",
|
||||
"timeout_cancel",
|
||||
];
|
||||
|
||||
private static readonly string[] KnownMessageTypes =
|
||||
[
|
||||
"mxaccess_gateway.v1.MxCommandReply",
|
||||
"mxaccess_gateway.v1.MxEvent",
|
||||
"mxaccess_gateway.v1.MxValue",
|
||||
"mxaccess_gateway.v1.MxStatusProxy",
|
||||
"client_behavior.v1.AuthErrorCase",
|
||||
"client_behavior.v1.TimeoutCancelCase",
|
||||
];
|
||||
|
||||
private static readonly string[] AuthGrpcStatusCodes =
|
||||
[
|
||||
"UNAUTHENTICATED",
|
||||
"PERMISSION_DENIED",
|
||||
];
|
||||
|
||||
private static readonly string[] AuthClientErrorCategories =
|
||||
[
|
||||
"AuthenticationError",
|
||||
"AuthorizationError",
|
||||
];
|
||||
|
||||
private static readonly string[] TimeoutGrpcStatusCodes =
|
||||
[
|
||||
"DEADLINE_EXCEEDED",
|
||||
"CANCELLED",
|
||||
];
|
||||
|
||||
private static T ParseFixture<T>(
|
||||
JsonElement fixture,
|
||||
MessageParser<T> parser)
|
||||
where T : IMessage<T>
|
||||
{
|
||||
return parser.ParseJson(File.ReadAllText(GetFixturePath(fixture)));
|
||||
}
|
||||
|
||||
private static JsonElement LoadCaseSet(
|
||||
string category,
|
||||
string propertyName)
|
||||
{
|
||||
JsonElement fixture = Assert.Single(LoadManifestFixtures(category));
|
||||
using JsonDocument document = JsonDocument.Parse(File.ReadAllText(GetFixturePath(fixture)));
|
||||
|
||||
return document.RootElement.GetProperty(propertyName).Clone();
|
||||
}
|
||||
|
||||
private static IReadOnlyList<JsonElement> LoadManifestFixtures(string category)
|
||||
{
|
||||
using JsonDocument manifest = LoadBehaviorManifest();
|
||||
|
||||
return manifest.RootElement
|
||||
.GetProperty("fixtures")
|
||||
.EnumerateArray()
|
||||
.Where(fixture => fixture.GetProperty("category").GetString() == category)
|
||||
.Select(fixture => fixture.Clone())
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static JsonDocument LoadBehaviorManifest()
|
||||
{
|
||||
return JsonDocument.Parse(File.ReadAllText(Path.Combine(GetBehaviorFixtureRoot().FullName, "manifest.json")));
|
||||
}
|
||||
|
||||
private static string GetFixturePath(JsonElement fixture)
|
||||
{
|
||||
return Path.Combine(GetBehaviorFixtureRoot().FullName, fixture.GetProperty("path").GetString()!);
|
||||
}
|
||||
|
||||
private static string GetFixtureId(JsonElement fixture)
|
||||
{
|
||||
return fixture.GetProperty("id").GetString()!;
|
||||
}
|
||||
|
||||
private static DirectoryInfo GetBehaviorFixtureRoot()
|
||||
{
|
||||
DirectoryInfo repositoryRoot = FindRepositoryRoot();
|
||||
|
||||
return new DirectoryInfo(Path.Combine(repositoryRoot.FullName, "clients", "proto", "fixtures", "behavior"));
|
||||
}
|
||||
|
||||
private static DirectoryInfo FindRepositoryRoot()
|
||||
{
|
||||
DirectoryInfo? current = new(AppContext.BaseDirectory);
|
||||
|
||||
while (current is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current.FullName, "CLAUDE.md"))
|
||||
&& Directory.Exists(Path.Combine(current.FullName, "src"))
|
||||
&& Directory.Exists(Path.Combine(current.FullName, "clients")))
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Could not locate the repository root from the test output directory.");
|
||||
}
|
||||
|
||||
private static void AssertEventBodyMatchesFamily(MxEvent gatewayEvent)
|
||||
{
|
||||
switch (gatewayEvent.Family)
|
||||
{
|
||||
case MxEventFamily.OnDataChange:
|
||||
Assert.Equal(MxEvent.BodyOneofCase.OnDataChange, gatewayEvent.BodyCase);
|
||||
break;
|
||||
case MxEventFamily.OnWriteComplete:
|
||||
Assert.Equal(MxEvent.BodyOneofCase.OnWriteComplete, gatewayEvent.BodyCase);
|
||||
break;
|
||||
case MxEventFamily.OperationComplete:
|
||||
Assert.Equal(MxEvent.BodyOneofCase.OperationComplete, gatewayEvent.BodyCase);
|
||||
break;
|
||||
case MxEventFamily.OnBufferedDataChange:
|
||||
Assert.Equal(MxEvent.BodyOneofCase.OnBufferedDataChange, gatewayEvent.BodyCase);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unexpected event family '{gatewayEvent.Family}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertJsonKindMatchesValueKind(
|
||||
string expectedKind,
|
||||
MxValue value)
|
||||
{
|
||||
MxValue.KindOneofCase expected = expectedKind switch
|
||||
{
|
||||
"boolValue" => MxValue.KindOneofCase.BoolValue,
|
||||
"int32Value" => MxValue.KindOneofCase.Int32Value,
|
||||
"int64Value" => MxValue.KindOneofCase.Int64Value,
|
||||
"floatValue" => MxValue.KindOneofCase.FloatValue,
|
||||
"doubleValue" => MxValue.KindOneofCase.DoubleValue,
|
||||
"stringValue" => MxValue.KindOneofCase.StringValue,
|
||||
"timestampValue" => MxValue.KindOneofCase.TimestampValue,
|
||||
"arrayValue" => MxValue.KindOneofCase.ArrayValue,
|
||||
"rawValue" => MxValue.KindOneofCase.RawValue,
|
||||
_ => throw new InvalidOperationException($"Unexpected expected value kind '{expectedKind}'."),
|
||||
};
|
||||
|
||||
Assert.Equal(expected, value.KindCase);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using System.Text.Json;
|
||||
using Google.Protobuf;
|
||||
using ZB.MOM.WW.MxGateway.Contracts;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Contracts;
|
||||
|
||||
public sealed class ClientProtoInputTests
|
||||
{
|
||||
/// <summary>Verifies that the proto inputs manifest declares current protocol versions and existing source files.</summary>
|
||||
[Fact]
|
||||
public void Manifest_DeclaresCurrentProtocolVersionsAndExistingInputs()
|
||||
{
|
||||
DirectoryInfo repositoryRoot = FindRepositoryRoot();
|
||||
string manifestPath = Path.Combine(repositoryRoot.FullName, "clients", "proto", "proto-inputs.json");
|
||||
|
||||
using JsonDocument manifest = JsonDocument.Parse(File.ReadAllText(manifestPath));
|
||||
JsonElement root = manifest.RootElement;
|
||||
|
||||
Assert.Equal(1, root.GetProperty("schemaVersion").GetInt32());
|
||||
Assert.Equal(GatewayContractInfo.GatewayProtocolVersion, root.GetProperty("gatewayProtocolVersion").GetUInt32());
|
||||
Assert.Equal(GatewayContractInfo.WorkerProtocolVersion, root.GetProperty("workerProtocolVersion").GetUInt32());
|
||||
|
||||
string protoRoot = Path.Combine(repositoryRoot.FullName, root.GetProperty("protoRoot").GetString()!);
|
||||
foreach (JsonElement sourceFile in root.GetProperty("sourceFiles").EnumerateArray())
|
||||
{
|
||||
string sourcePath = Path.Combine(protoRoot, sourceFile.GetProperty("path").GetString()!);
|
||||
Assert.True(File.Exists(sourcePath), $"Expected proto source file '{sourcePath}' to exist.");
|
||||
}
|
||||
|
||||
foreach (JsonProperty output in root.GetProperty("generatedOutputs").EnumerateObject())
|
||||
{
|
||||
string outputPath = Path.Combine(repositoryRoot.FullName, output.Value.GetString()!);
|
||||
Assert.True(Directory.Exists(outputPath), $"Expected generated output directory '{outputPath}' to exist.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the OpenSessionReply fixture parses with the current contract version.</summary>
|
||||
[Fact]
|
||||
public void OpenSessionReplyFixture_ParsesWithCurrentContract()
|
||||
{
|
||||
OpenSessionReply reply = ParseFixture(
|
||||
"open-session-reply.ok.json",
|
||||
OpenSessionReply.Parser);
|
||||
|
||||
Assert.Equal(GatewayContractInfo.GatewayProtocolVersion, reply.GatewayProtocolVersion);
|
||||
Assert.Equal(GatewayContractInfo.WorkerProtocolVersion, reply.WorkerProtocolVersion);
|
||||
Assert.Equal(ProtocolStatusCode.Ok, reply.ProtocolStatus.Code);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the RegisterCommand fixture parses with the current contract version.</summary>
|
||||
[Fact]
|
||||
public void RegisterCommandRequestFixture_ParsesWithCurrentContract()
|
||||
{
|
||||
MxCommandRequest request = ParseFixture(
|
||||
"register-command-request.json",
|
||||
MxCommandRequest.Parser);
|
||||
|
||||
Assert.Equal(MxCommandKind.Register, request.Command.Kind);
|
||||
Assert.Equal("fixture-client", request.Command.Register.ClientName);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the OnDataChange event fixture parses with the current contract version.</summary>
|
||||
[Fact]
|
||||
public void OnDataChangeEventFixture_ParsesWithCurrentContract()
|
||||
{
|
||||
MxEvent gatewayEvent = ParseFixture(
|
||||
"on-data-change-event.json",
|
||||
MxEvent.Parser);
|
||||
|
||||
Assert.Equal(MxEventFamily.OnDataChange, gatewayEvent.Family);
|
||||
Assert.Equal(1ul, gatewayEvent.WorkerSequence);
|
||||
Assert.Equal(MxDataType.Integer, gatewayEvent.Value.DataType);
|
||||
Assert.Equal(MxEvent.BodyOneofCase.OnDataChange, gatewayEvent.BodyCase);
|
||||
}
|
||||
|
||||
private static T ParseFixture<T>(
|
||||
string fixtureName,
|
||||
MessageParser<T> parser)
|
||||
where T : IMessage<T>
|
||||
{
|
||||
DirectoryInfo repositoryRoot = FindRepositoryRoot();
|
||||
string fixturePath = Path.Combine(repositoryRoot.FullName, "clients", "proto", "fixtures", "golden", fixtureName);
|
||||
|
||||
return parser.ParseJson(File.ReadAllText(fixturePath));
|
||||
}
|
||||
|
||||
private static DirectoryInfo FindRepositoryRoot()
|
||||
{
|
||||
DirectoryInfo? current = new(AppContext.BaseDirectory);
|
||||
|
||||
while (current is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current.FullName, "CLAUDE.md"))
|
||||
&& Directory.Exists(Path.Combine(current.FullName, "src"))
|
||||
&& Directory.Exists(Path.Combine(current.FullName, "clients")))
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Could not locate the repository root from the test output directory.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,279 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Contracts;
|
||||
|
||||
public sealed class CrossLanguageSmokeMatrixTests
|
||||
{
|
||||
/// <summary>Verifies that the smoke matrix declares the integration gate and JSON comparison shape.</summary>
|
||||
[Fact]
|
||||
public void Matrix_DeclaresIntegrationGateAndComparisonShape()
|
||||
{
|
||||
using JsonDocument matrix = LoadSmokeMatrix();
|
||||
JsonElement root = matrix.RootElement;
|
||||
|
||||
Assert.Equal(1, root.GetProperty("schemaVersion").GetInt32());
|
||||
Assert.Equal("mxaccess-gateway-cross-language-smoke-matrix", root.GetProperty("fixtureSet").GetString());
|
||||
|
||||
JsonElement integrationGate = root.GetProperty("integrationGate");
|
||||
Assert.Equal("MXGATEWAY_INTEGRATION", integrationGate.GetProperty("variable").GetString());
|
||||
Assert.Equal("1", integrationGate.GetProperty("requiredValue").GetString());
|
||||
|
||||
JsonElement defaultInputs = root.GetProperty("defaultInputs");
|
||||
Assert.Equal("MXGATEWAY_ENDPOINT", defaultInputs.GetProperty("endpointVariable").GetString());
|
||||
Assert.Equal("localhost:5000", defaultInputs.GetProperty("endpointFallback").GetString());
|
||||
Assert.Equal("MXGATEWAY_API_KEY", defaultInputs.GetProperty("apiKeyVariable").GetString());
|
||||
Assert.Equal("MXGATEWAY_TEST_ITEM", defaultInputs.GetProperty("itemVariable").GetString());
|
||||
|
||||
AssertRequiredFields(
|
||||
root.GetProperty("jsonComparison").GetProperty("commonFields"),
|
||||
"language",
|
||||
"operation",
|
||||
"sessionId",
|
||||
"serverHandle",
|
||||
"itemHandle",
|
||||
"events",
|
||||
"closeStatus");
|
||||
AssertRequiredFields(
|
||||
root.GetProperty("failureOutput").GetProperty("requiredContextFields"),
|
||||
"language",
|
||||
"endpoint",
|
||||
"authContext");
|
||||
|
||||
JsonElement authContext = root.GetProperty("failureOutput").GetProperty("authContext");
|
||||
Assert.Equal("MXGATEWAY_API_KEY", authContext.GetProperty("sourceVariable").GetString());
|
||||
Assert.Equal("<redacted>", authContext.GetProperty("redactedValue").GetString());
|
||||
AssertForbiddenLiterals(authContext.GetProperty("forbiddenLiterals"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the smoke matrix covers every supported client with equivalent smoke steps.</summary>
|
||||
[Fact]
|
||||
public void Matrix_CoversEverySupportedClientWithEquivalentSmokeSteps()
|
||||
{
|
||||
using JsonDocument matrix = LoadSmokeMatrix();
|
||||
JsonElement root = matrix.RootElement;
|
||||
string[] requiredOperations = GetStrings(root.GetProperty("requiredOperations"));
|
||||
Dictionary<string, JsonElement> clientsByLanguage = [];
|
||||
|
||||
foreach (JsonElement client in root.GetProperty("clients").EnumerateArray())
|
||||
{
|
||||
string language = client.GetProperty("language").GetString()!;
|
||||
|
||||
Assert.True(clientsByLanguage.TryAdd(language, client), $"Duplicate smoke client '{language}'.");
|
||||
Assert.Contains(language, ExpectedLanguages);
|
||||
AssertClientWorkDirectoryExists(client);
|
||||
AssertIntegrationSkip(client);
|
||||
AssertRequiredFields(client.GetProperty("failureContextFields"), "language", "endpoint", "authContext");
|
||||
AssertSmokeCommands(client, requiredOperations);
|
||||
AssertOptionalWriteCommand(client.GetProperty("optionalWriteCommand").GetString()!);
|
||||
AssertCommandUsesJsonAndAuthEnv(client.GetProperty("bundledSmokeCommand").GetString()!);
|
||||
Assert.Contains("TestChildObject.TestInt", client.GetProperty("bundledSmokeCommand").GetString()!, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
Assert.Equal(ExpectedLanguages.OrderBy(language => language, StringComparer.Ordinal), clientsByLanguage.Keys.OrderBy(language => language, StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the smoke matrix keeps live smoke opt-in and secrets out of commands.</summary>
|
||||
[Fact]
|
||||
public void Matrix_KeepsLiveSmokeOptInAndSecretsOutOfCommands()
|
||||
{
|
||||
using JsonDocument matrix = LoadSmokeMatrix();
|
||||
JsonElement root = matrix.RootElement;
|
||||
string[] forbiddenLiterals = GetStrings(root.GetProperty("failureOutput").GetProperty("authContext").GetProperty("forbiddenLiterals"));
|
||||
|
||||
foreach (JsonElement client in root.GetProperty("clients").EnumerateArray())
|
||||
{
|
||||
string language = client.GetProperty("language").GetString()!;
|
||||
|
||||
AssertIntegrationSkip(client);
|
||||
|
||||
foreach (JsonElement commandStep in client.GetProperty("commands").EnumerateArray())
|
||||
{
|
||||
AssertNoForbiddenLiterals(language, commandStep.GetProperty("command").GetString()!, forbiddenLiterals);
|
||||
}
|
||||
|
||||
AssertNoForbiddenLiterals(language, client.GetProperty("optionalWriteCommand").GetString()!, forbiddenLiterals);
|
||||
AssertNoForbiddenLiterals(language, client.GetProperty("bundledSmokeCommand").GetString()!, forbiddenLiterals);
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly string[] ExpectedLanguages =
|
||||
[
|
||||
"dotnet",
|
||||
"go",
|
||||
"rust",
|
||||
"python",
|
||||
"java",
|
||||
];
|
||||
|
||||
private static void AssertSmokeCommands(
|
||||
JsonElement client,
|
||||
string[] requiredOperations)
|
||||
{
|
||||
Dictionary<string, JsonElement> commandsByOperation = [];
|
||||
|
||||
foreach (JsonElement commandStep in client.GetProperty("commands").EnumerateArray())
|
||||
{
|
||||
string operation = commandStep.GetProperty("operation").GetString()!;
|
||||
string command = commandStep.GetProperty("command").GetString()!;
|
||||
|
||||
Assert.True(commandsByOperation.TryAdd(operation, commandStep), $"Duplicate smoke operation '{operation}'.");
|
||||
AssertCommandUsesJsonAndAuthEnv(command);
|
||||
Assert.Contains("localhost:5000", command, StringComparison.Ordinal);
|
||||
AssertOperationPlaceholders(operation, command);
|
||||
}
|
||||
|
||||
Assert.Equal(requiredOperations.OrderBy(operation => operation, StringComparer.Ordinal), commandsByOperation.Keys.OrderBy(operation => operation, StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
private static void AssertOperationPlaceholders(
|
||||
string operation,
|
||||
string command)
|
||||
{
|
||||
switch (operation)
|
||||
{
|
||||
case "open-session":
|
||||
Assert.Contains("smoke", command, StringComparison.Ordinal);
|
||||
break;
|
||||
case "register":
|
||||
Assert.Contains("<session-id>", command, StringComparison.Ordinal);
|
||||
Assert.Contains("smoke", command, StringComparison.Ordinal);
|
||||
break;
|
||||
case "add-item":
|
||||
Assert.Contains("<session-id>", command, StringComparison.Ordinal);
|
||||
Assert.Contains("<server-handle>", command, StringComparison.Ordinal);
|
||||
Assert.Contains("TestChildObject.TestInt", command, StringComparison.Ordinal);
|
||||
break;
|
||||
case "advise":
|
||||
Assert.Contains("<session-id>", command, StringComparison.Ordinal);
|
||||
Assert.Contains("<server-handle>", command, StringComparison.Ordinal);
|
||||
Assert.Contains("<item-handle>", command, StringComparison.Ordinal);
|
||||
break;
|
||||
case "stream-events":
|
||||
Assert.Contains("<session-id>", command, StringComparison.Ordinal);
|
||||
Assert.True(
|
||||
command.Contains("--max-events 1", StringComparison.Ordinal)
|
||||
|| command.Contains("-limit 1", StringComparison.Ordinal)
|
||||
|| command.Contains("--limit 1", StringComparison.Ordinal),
|
||||
$"Stream command '{command}' must bound event reads.");
|
||||
break;
|
||||
case "close-session":
|
||||
Assert.Contains("<session-id>", command, StringComparison.Ordinal);
|
||||
break;
|
||||
default:
|
||||
throw new InvalidOperationException($"Unexpected smoke operation '{operation}'.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertOptionalWriteCommand(string command)
|
||||
{
|
||||
AssertCommandUsesJsonAndAuthEnv(command);
|
||||
Assert.Contains("<session-id>", command, StringComparison.Ordinal);
|
||||
Assert.Contains("<server-handle>", command, StringComparison.Ordinal);
|
||||
Assert.Contains("<item-handle>", command, StringComparison.Ordinal);
|
||||
Assert.Contains("<write-value>", command, StringComparison.Ordinal);
|
||||
Assert.Contains("int32", command, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static void AssertCommandUsesJsonAndAuthEnv(string command)
|
||||
{
|
||||
Assert.True(
|
||||
command.Contains("--json", StringComparison.Ordinal) || command.Contains("-json", StringComparison.Ordinal),
|
||||
$"Command '{command}' must request JSON output.");
|
||||
Assert.Contains("MXGATEWAY_API_KEY", command, StringComparison.Ordinal);
|
||||
Assert.Contains("api-key-env", command, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static void AssertIntegrationSkip(JsonElement client)
|
||||
{
|
||||
JsonElement integrationSkip = client.GetProperty("integrationSkip");
|
||||
|
||||
Assert.Equal("MXGATEWAY_INTEGRATION", integrationSkip.GetProperty("variable").GetString());
|
||||
Assert.Equal("1", integrationSkip.GetProperty("requiredValue").GetString());
|
||||
}
|
||||
|
||||
private static void AssertClientWorkDirectoryExists(JsonElement client)
|
||||
{
|
||||
string workingDirectory = client.GetProperty("workingDirectory").GetString()!;
|
||||
DirectoryInfo repositoryRoot = FindRepositoryRoot();
|
||||
string fullPath = Path.GetFullPath(Path.Combine(repositoryRoot.FullName, workingDirectory));
|
||||
|
||||
Assert.True(Directory.Exists(fullPath), $"Smoke client working directory '{workingDirectory}' must exist.");
|
||||
Assert.StartsWith(repositoryRoot.FullName, fullPath, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private static void AssertRequiredFields(
|
||||
JsonElement fields,
|
||||
params string[] expectedFields)
|
||||
{
|
||||
HashSet<string> declared = GetStrings(fields).ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
foreach (string expectedField in expectedFields)
|
||||
{
|
||||
Assert.Contains(expectedField, declared);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertForbiddenLiterals(JsonElement forbiddenLiterals)
|
||||
{
|
||||
string[] values = GetStrings(forbiddenLiterals);
|
||||
|
||||
Assert.Contains("mxgw_visible_secret", values);
|
||||
Assert.Contains("Bearer mxgw_visible_secret", values);
|
||||
}
|
||||
|
||||
private static void AssertNoForbiddenLiterals(
|
||||
string language,
|
||||
string command,
|
||||
string[] forbiddenLiterals)
|
||||
{
|
||||
foreach (string forbiddenLiteral in forbiddenLiterals)
|
||||
{
|
||||
Assert.DoesNotContain(forbiddenLiteral, command, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
Assert.DoesNotContain(" --api-key ", command, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain(" -api-key ", command, StringComparison.Ordinal);
|
||||
Assert.Contains("api-key-env", command, StringComparison.Ordinal);
|
||||
Assert.Contains("MXGATEWAY_API_KEY", command, StringComparison.Ordinal);
|
||||
Assert.False(command.Contains("Bearer ", StringComparison.Ordinal), $"Smoke command for '{language}' must not include bearer tokens.");
|
||||
}
|
||||
|
||||
private static string[] GetStrings(JsonElement array)
|
||||
{
|
||||
return array
|
||||
.EnumerateArray()
|
||||
.Select(element => element.GetString()!)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static JsonDocument LoadSmokeMatrix()
|
||||
{
|
||||
return JsonDocument.Parse(File.ReadAllText(Path.Combine(GetSmokeFixtureRoot().FullName, "cross-language-smoke-matrix.json")));
|
||||
}
|
||||
|
||||
private static DirectoryInfo GetSmokeFixtureRoot()
|
||||
{
|
||||
DirectoryInfo repositoryRoot = FindRepositoryRoot();
|
||||
|
||||
return new DirectoryInfo(Path.Combine(repositoryRoot.FullName, "clients", "proto", "fixtures", "smoke"));
|
||||
}
|
||||
|
||||
private static DirectoryInfo FindRepositoryRoot()
|
||||
{
|
||||
DirectoryInfo? current = new(AppContext.BaseDirectory);
|
||||
|
||||
while (current is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current.FullName, "CLAUDE.md"))
|
||||
&& Directory.Exists(Path.Combine(current.FullName, "src"))
|
||||
&& Directory.Exists(Path.Combine(current.FullName, "clients")))
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Could not locate the repository root from the test output directory.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using ZB.MOM.WW.MxGateway.Contracts;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Contracts;
|
||||
|
||||
public sealed class GatewayContractInfoTests
|
||||
{
|
||||
/// <summary>Verifies that the default backend name is "mxaccess-worker".</summary>
|
||||
[Fact]
|
||||
public void DefaultBackendName_IsMxAccessWorker()
|
||||
{
|
||||
Assert.Equal("mxaccess-worker", GatewayContractInfo.DefaultBackendName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pins the current <see cref="GatewayContractInfo.GatewayProtocolVersion"/>
|
||||
/// constant at 3. Both the alarm proto extension (`AcknowledgeAlarm` /
|
||||
/// `QueryActiveAlarms` RPCs, the `OnAlarmTransitionEvent` body, and the
|
||||
/// alarm command/reply payload cases) and the bulk write/read command
|
||||
/// family extension (`WriteBulk` / `Write2Bulk` / `WriteSecuredBulk` /
|
||||
/// `WriteSecured2Bulk` / `ReadBulk` plus their `BulkWriteReply` and
|
||||
/// `BulkReadReply` payloads) shipped under version 3 — both were strictly
|
||||
/// additive contract changes, so neither required a further bump. A
|
||||
/// future breaking contract change should bump this constant and update
|
||||
/// this test in lock-step.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GatewayProtocolVersion_IsVersionThree()
|
||||
{
|
||||
Assert.Equal(3u, GatewayContractInfo.GatewayProtocolVersion);
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the worker protocol version starts at version one.</summary>
|
||||
[Fact]
|
||||
public void WorkerProtocolVersion_StartsAtVersionOne()
|
||||
{
|
||||
Assert.Equal(1u, GatewayContractInfo.WorkerProtocolVersion);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
using System.Text.Json;
|
||||
using ZB.MOM.WW.MxGateway.Contracts;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Contracts;
|
||||
|
||||
public sealed class ParityFixtureMatrixTests
|
||||
{
|
||||
/// <summary>Verifies that the parity matrix declares current protocol versions and comparison fields.</summary>
|
||||
[Fact]
|
||||
public void Matrix_DeclaresCurrentProtocolVersionsAndComparisonFields()
|
||||
{
|
||||
using JsonDocument matrix = LoadParityMatrix();
|
||||
JsonElement root = matrix.RootElement;
|
||||
|
||||
Assert.Equal(1, root.GetProperty("schemaVersion").GetInt32());
|
||||
Assert.Equal("mxaccess-gateway-parity-fixture-matrix", root.GetProperty("fixtureSet").GetString());
|
||||
Assert.Equal(GatewayContractInfo.GatewayProtocolVersion, root.GetProperty("gatewayProtocolVersion").GetUInt32());
|
||||
Assert.Equal(GatewayContractInfo.WorkerProtocolVersion, root.GetProperty("workerProtocolVersion").GetUInt32());
|
||||
|
||||
JsonElement comparisonFormat = root.GetProperty("comparisonFormat");
|
||||
AssertRequiredFields(
|
||||
comparisonFormat.GetProperty("directMxAccess").GetProperty("requiredFields"),
|
||||
"method",
|
||||
"arguments",
|
||||
"returnedValue",
|
||||
"hresult",
|
||||
"statuses",
|
||||
"events");
|
||||
AssertRequiredFields(
|
||||
comparisonFormat.GetProperty("gatewayResult").GetProperty("requiredFields"),
|
||||
"kind",
|
||||
"protocolStatus",
|
||||
"returnValue",
|
||||
"hresult",
|
||||
"statuses",
|
||||
"events");
|
||||
AssertRequiredFields(
|
||||
comparisonFormat.GetProperty("eventFields"),
|
||||
"family",
|
||||
"value",
|
||||
"quality",
|
||||
"sourceTimestamp",
|
||||
"statuses",
|
||||
"workerSequence");
|
||||
AssertRequiredFields(
|
||||
comparisonFormat.GetProperty("comparisonKeys"),
|
||||
"hresult",
|
||||
"statusArrayShape",
|
||||
"statusRawFields",
|
||||
"eventFamilyOrder",
|
||||
"eventPayloadShape",
|
||||
"valueProjection",
|
||||
"rawFallbackMetadata");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the parity matrix covers every public MXAccess method.</summary>
|
||||
[Fact]
|
||||
public void Matrix_CoversEveryPublicMxAccessMethod()
|
||||
{
|
||||
using JsonDocument matrix = LoadParityMatrix();
|
||||
JsonElement methodFixtures = matrix.RootElement.GetProperty("methodFixtures");
|
||||
|
||||
Dictionary<string, JsonElement> fixturesByMethod = [];
|
||||
HashSet<string> ids = new(StringComparer.Ordinal);
|
||||
|
||||
foreach (JsonElement fixture in methodFixtures.EnumerateArray())
|
||||
{
|
||||
string id = fixture.GetProperty("id").GetString()!;
|
||||
string method = fixture.GetProperty("method").GetString()!;
|
||||
string commandKind = fixture.GetProperty("commandKind").GetString()!;
|
||||
string status = fixture.GetProperty("status").GetString()!;
|
||||
|
||||
Assert.True(ids.Add(id), $"Duplicate parity fixture id '{id}'.");
|
||||
Assert.True(fixturesByMethod.TryAdd(method, fixture), $"Duplicate parity method '{method}'.");
|
||||
Assert.StartsWith("MX_COMMAND_KIND_", commandKind, StringComparison.Ordinal);
|
||||
Assert.Contains(status, KnownFixtureStatuses);
|
||||
Assert.NotEmpty(fixture.GetProperty("assertions").EnumerateArray());
|
||||
AssertCaptureReferencesAreRelative(fixture.GetProperty("captureReferences"));
|
||||
}
|
||||
|
||||
Assert.Equal(ExpectedPublicMethods.Order(StringComparer.Ordinal), fixturesByMethod.Keys.Order(StringComparer.Ordinal));
|
||||
|
||||
foreach (string method in ExpectedPublicMethods)
|
||||
{
|
||||
JsonElement fixture = fixturesByMethod[method];
|
||||
string status = fixture.GetProperty("status").GetString()!;
|
||||
|
||||
Assert.True(
|
||||
status == "planned_fixture" || status == "documented_gap",
|
||||
$"Method '{method}' must have a planned parity fixture or documented gap.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the parity matrix covers required scenario groups.</summary>
|
||||
[Fact]
|
||||
public void Matrix_CoversRequiredParityScenarioGroups()
|
||||
{
|
||||
using JsonDocument matrix = LoadParityMatrix();
|
||||
HashSet<string> knownFixtureIds = GetFixtureIds(matrix.RootElement);
|
||||
Dictionary<string, JsonElement> groupsById = [];
|
||||
|
||||
foreach (JsonElement group in matrix.RootElement.GetProperty("scenarioGroups").EnumerateArray())
|
||||
{
|
||||
string id = group.GetProperty("id").GetString()!;
|
||||
|
||||
Assert.True(groupsById.TryAdd(id, group), $"Duplicate parity scenario group '{id}'.");
|
||||
Assert.NotEmpty(group.GetProperty("description").GetString()!);
|
||||
Assert.NotEmpty(group.GetProperty("fixtureIds").EnumerateArray());
|
||||
AssertCaptureReferencesAreRelative(group.GetProperty("captureReferences"));
|
||||
|
||||
foreach (JsonElement fixtureIdElement in group.GetProperty("fixtureIds").EnumerateArray())
|
||||
{
|
||||
string fixtureId = fixtureIdElement.GetString()!;
|
||||
Assert.Contains(fixtureId, knownFixtureIds);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (string requiredGroup in RequiredScenarioGroups)
|
||||
{
|
||||
Assert.True(groupsById.ContainsKey(requiredGroup), $"Missing required parity scenario group '{requiredGroup}'.");
|
||||
}
|
||||
|
||||
AssertScenarioCovers(groupsById["invalid_handles"], "method.remove-item.basic", "method.write.value-status-matrix");
|
||||
AssertScenarioCovers(groupsById["write_statuses"], "method.write.value-status-matrix", "event.on-write-complete.status");
|
||||
AssertScenarioCovers(groupsById["secured_writes"], "method.write-secured.rejection-gap", "method.write-secured2.authenticated");
|
||||
AssertScenarioCovers(groupsById["add_item_context"], "method.add-item2.context", "method.add-buffered-item.context");
|
||||
AssertScenarioCovers(groupsById["buffered_registration"], "method.add-buffered-item.context", "event.on-buffered-data-change.batch-gap");
|
||||
}
|
||||
|
||||
/// <summary>Verifies that the parity matrix covers every public MXAccess event family.</summary>
|
||||
[Fact]
|
||||
public void Matrix_CoversEveryPublicMxAccessEventFamily()
|
||||
{
|
||||
using JsonDocument matrix = LoadParityMatrix();
|
||||
Dictionary<string, JsonElement> fixturesByFamily = [];
|
||||
|
||||
foreach (JsonElement fixture in matrix.RootElement.GetProperty("eventFixtures").EnumerateArray())
|
||||
{
|
||||
string family = fixture.GetProperty("family").GetString()!;
|
||||
string status = fixture.GetProperty("status").GetString()!;
|
||||
|
||||
Assert.True(fixturesByFamily.TryAdd(family, fixture), $"Duplicate parity event family '{family}'.");
|
||||
Assert.Contains(status, KnownFixtureStatuses);
|
||||
Assert.NotEmpty(fixture.GetProperty("assertions").EnumerateArray());
|
||||
AssertCaptureReferencesAreRelative(fixture.GetProperty("captureReferences"));
|
||||
}
|
||||
|
||||
foreach (string eventFamily in ExpectedEventFamilies)
|
||||
{
|
||||
Assert.True(fixturesByFamily.ContainsKey(eventFamily), $"Missing parity fixture for event family '{eventFamily}'.");
|
||||
}
|
||||
|
||||
Assert.Equal("documented_gap", fixturesByFamily["MX_EVENT_FAMILY_OPERATION_COMPLETE"].GetProperty("status").GetString());
|
||||
Assert.Equal("documented_gap", fixturesByFamily["MX_EVENT_FAMILY_ON_BUFFERED_DATA_CHANGE"].GetProperty("status").GetString());
|
||||
}
|
||||
|
||||
private static readonly string[] ExpectedPublicMethods =
|
||||
[
|
||||
"Register",
|
||||
"Unregister",
|
||||
"AddItem",
|
||||
"AddItem2",
|
||||
"RemoveItem",
|
||||
"Advise",
|
||||
"UnAdvise",
|
||||
"AdviseSupervisory",
|
||||
"AddBufferedItem",
|
||||
"SetBufferedUpdateInterval",
|
||||
"Suspend",
|
||||
"Activate",
|
||||
"Write",
|
||||
"Write2",
|
||||
"WriteSecured",
|
||||
"WriteSecured2",
|
||||
"AuthenticateUser",
|
||||
"ArchestrAUserToId",
|
||||
];
|
||||
|
||||
private static readonly string[] ExpectedEventFamilies =
|
||||
[
|
||||
"MX_EVENT_FAMILY_ON_DATA_CHANGE",
|
||||
"MX_EVENT_FAMILY_ON_WRITE_COMPLETE",
|
||||
"MX_EVENT_FAMILY_OPERATION_COMPLETE",
|
||||
"MX_EVENT_FAMILY_ON_BUFFERED_DATA_CHANGE",
|
||||
];
|
||||
|
||||
private static readonly string[] RequiredScenarioGroups =
|
||||
[
|
||||
"invalid_handles",
|
||||
"write_statuses",
|
||||
"secured_writes",
|
||||
"add_item_context",
|
||||
"buffered_registration",
|
||||
];
|
||||
|
||||
private static readonly string[] KnownFixtureStatuses =
|
||||
[
|
||||
"planned_fixture",
|
||||
"documented_gap",
|
||||
];
|
||||
|
||||
private static void AssertRequiredFields(
|
||||
JsonElement fields,
|
||||
params string[] expectedFields)
|
||||
{
|
||||
HashSet<string> declared = fields
|
||||
.EnumerateArray()
|
||||
.Select(field => field.GetString()!)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
foreach (string expectedField in expectedFields)
|
||||
{
|
||||
Assert.Contains(expectedField, declared);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertCaptureReferencesAreRelative(JsonElement captureReferences)
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
foreach (JsonElement captureReference in captureReferences.EnumerateArray())
|
||||
{
|
||||
string path = captureReference.GetString()!;
|
||||
|
||||
Assert.StartsWith("captures/", path, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("\\", path, StringComparison.Ordinal);
|
||||
Assert.False(Path.IsPathRooted(path), $"Capture reference '{path}' must be relative.");
|
||||
count++;
|
||||
}
|
||||
|
||||
Assert.True(count > 0, "Each parity fixture must reference at least one MXAccess capture.");
|
||||
}
|
||||
|
||||
private static void AssertScenarioCovers(
|
||||
JsonElement group,
|
||||
params string[] fixtureIds)
|
||||
{
|
||||
HashSet<string> declared = group
|
||||
.GetProperty("fixtureIds")
|
||||
.EnumerateArray()
|
||||
.Select(fixtureId => fixtureId.GetString()!)
|
||||
.ToHashSet(StringComparer.Ordinal);
|
||||
|
||||
foreach (string fixtureId in fixtureIds)
|
||||
{
|
||||
Assert.Contains(fixtureId, declared);
|
||||
}
|
||||
}
|
||||
|
||||
private static HashSet<string> GetFixtureIds(JsonElement root)
|
||||
{
|
||||
HashSet<string> ids = new(StringComparer.Ordinal);
|
||||
|
||||
foreach (JsonElement fixture in root.GetProperty("methodFixtures").EnumerateArray())
|
||||
{
|
||||
ids.Add(fixture.GetProperty("id").GetString()!);
|
||||
}
|
||||
|
||||
foreach (JsonElement fixture in root.GetProperty("eventFixtures").EnumerateArray())
|
||||
{
|
||||
ids.Add(fixture.GetProperty("id").GetString()!);
|
||||
}
|
||||
|
||||
return ids;
|
||||
}
|
||||
|
||||
private static JsonDocument LoadParityMatrix()
|
||||
{
|
||||
return JsonDocument.Parse(File.ReadAllText(Path.Combine(GetParityFixtureRoot().FullName, "parity-fixture-matrix.json")));
|
||||
}
|
||||
|
||||
private static DirectoryInfo GetParityFixtureRoot()
|
||||
{
|
||||
DirectoryInfo repositoryRoot = FindRepositoryRoot();
|
||||
|
||||
return new DirectoryInfo(Path.Combine(repositoryRoot.FullName, "clients", "proto", "fixtures", "parity"));
|
||||
}
|
||||
|
||||
private static DirectoryInfo FindRepositoryRoot()
|
||||
{
|
||||
DirectoryInfo? current = new(AppContext.BaseDirectory);
|
||||
|
||||
while (current is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current.FullName, "CLAUDE.md"))
|
||||
&& Directory.Exists(Path.Combine(current.FullName, "src"))
|
||||
&& Directory.Exists(Path.Combine(current.FullName, "clients")))
|
||||
{
|
||||
return current;
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
throw new DirectoryNotFoundException("Could not locate the repository root from the test output directory.");
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user