dc9c0c950c
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>
298 lines
11 KiB
C#
298 lines
11 KiB
C#
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.");
|
|
}
|
|
}
|