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,187 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Google.Protobuf.WellKnownTypes;
|
||||
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||
using ZB.MOM.WW.MxGateway.Worker.MxAccess;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Worker.Tests.MxAccess;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for <see cref="MxAccessValueCache"/>. The cache is consumed by
|
||||
/// <see cref="MxAccessSession.ReadBulk"/> to satisfy "current value"
|
||||
/// requests for already-advised tags without touching the existing
|
||||
/// subscription, so its contract is exercised in isolation here before any
|
||||
/// STA / COM plumbing gets layered on top.
|
||||
/// </summary>
|
||||
public sealed class MxAccessValueCacheTests
|
||||
{
|
||||
[Fact]
|
||||
public void Set_ThenTryGet_ReturnsLastValueWithIncrementingVersion()
|
||||
{
|
||||
MxAccessValueCache cache = new();
|
||||
Timestamp sourceTimestamp = Timestamp.FromDateTime(new(2026, 5, 19, 9, 0, 0, DateTimeKind.Utc));
|
||||
|
||||
cache.Set(serverHandle: 7, itemHandle: 21, BuildEvent(serverHandle: 7, itemHandle: 21, intValue: 100, quality: 192, sourceTimestamp));
|
||||
|
||||
Assert.True(cache.TryGet(7, 21, out MxAccessValueCache.CachedValue first));
|
||||
Assert.Equal(1UL, first.Version);
|
||||
Assert.Equal(100, first.Value.Int32Value);
|
||||
Assert.Equal(192, first.Quality);
|
||||
Assert.Equal(sourceTimestamp, first.SourceTimestamp);
|
||||
|
||||
// A second Set on the same key bumps the version and overwrites the
|
||||
// payload. Different keys remain isolated.
|
||||
cache.Set(7, 21, BuildEvent(7, 21, intValue: 200, quality: 192, sourceTimestamp));
|
||||
cache.Set(7, 22, BuildEvent(7, 22, intValue: 999, quality: 192, sourceTimestamp));
|
||||
|
||||
Assert.True(cache.TryGet(7, 21, out MxAccessValueCache.CachedValue second));
|
||||
Assert.Equal(2UL, second.Version);
|
||||
Assert.Equal(200, second.Value.Int32Value);
|
||||
|
||||
Assert.True(cache.TryGet(7, 22, out MxAccessValueCache.CachedValue other));
|
||||
Assert.Equal(1UL, other.Version);
|
||||
Assert.Equal(999, other.Value.Int32Value);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TryGet_WithUnknownHandle_ReturnsFalse()
|
||||
{
|
||||
MxAccessValueCache cache = new();
|
||||
|
||||
Assert.False(cache.TryGet(serverHandle: 7, itemHandle: 21, out _));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Remove_DropsEntryAndResetsVersion()
|
||||
{
|
||||
MxAccessValueCache cache = new();
|
||||
cache.Set(7, 21, BuildEvent(7, 21, intValue: 1, quality: 192, Timestamp.FromDateTime(DateTime.UtcNow)));
|
||||
cache.Set(7, 21, BuildEvent(7, 21, intValue: 2, quality: 192, Timestamp.FromDateTime(DateTime.UtcNow)));
|
||||
|
||||
cache.Remove(7, 21);
|
||||
Assert.False(cache.TryGet(7, 21, out _));
|
||||
|
||||
// After Remove, a subsequent Set restarts the per-handle version from 1
|
||||
// — the cache must not serve a stale "version 3" entry that would race
|
||||
// against a reused MXAccess item handle.
|
||||
cache.Set(7, 21, BuildEvent(7, 21, intValue: 3, quality: 192, Timestamp.FromDateTime(DateTime.UtcNow)));
|
||||
Assert.True(cache.TryGet(7, 21, out MxAccessValueCache.CachedValue reset));
|
||||
Assert.Equal(1UL, reset.Version);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CurrentVersion_ReturnsZeroForUnknown_AndLatestForKnown()
|
||||
{
|
||||
MxAccessValueCache cache = new();
|
||||
Assert.Equal(0UL, cache.CurrentVersion(7, 21));
|
||||
|
||||
cache.Set(7, 21, BuildEvent(7, 21, intValue: 1, quality: 192, Timestamp.FromDateTime(DateTime.UtcNow)));
|
||||
cache.Set(7, 21, BuildEvent(7, 21, intValue: 2, quality: 192, Timestamp.FromDateTime(DateTime.UtcNow)));
|
||||
|
||||
Assert.Equal(2UL, cache.CurrentVersion(7, 21));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Worker.Tests-020: pins the contract that <c>TryWaitForUpdate</c>
|
||||
/// returns <c>false</c> when the deadline has elapsed with no
|
||||
/// <c>Set</c>, yields a default <c>CachedValue</c>, and invokes
|
||||
/// <c>pumpStep</c> at least once so MXAccess Windows messages can
|
||||
/// be dispatched. Earlier revisions of this test asserted both an
|
||||
/// elapsed-time floor (<c>stopwatch.ElapsedMilliseconds >= 60</c>)
|
||||
/// and <c>pumpCalls > 1</c> — the same wall-clock-floor race
|
||||
/// pattern Worker.Tests-003/004/013 corrected. To eliminate the
|
||||
/// timing dependency entirely (the equivalent of a manual time
|
||||
/// source for a <c>DateTime.UtcNow</c>-based deadline), the test
|
||||
/// now supplies a deadline already in the past: the loop pumps
|
||||
/// once, observes the passed deadline, and returns false
|
||||
/// deterministically without any <c>Thread.Sleep</c>. The
|
||||
/// deadline-honouring contract is what this test exists to pin;
|
||||
/// elapsed time and pump-iteration count are incidental.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void TryWaitForUpdate_ReturnsFalseAfterDeadline_WhenNoSetOccurs()
|
||||
{
|
||||
MxAccessValueCache cache = new();
|
||||
int pumpCalls = 0;
|
||||
|
||||
// Deadline already in the past — eliminates the wall-clock-floor
|
||||
// race. The loop must pump once (so MXAccess messages can dispatch
|
||||
// on the calling thread even when the deadline has just expired)
|
||||
// and then immediately observe the passed deadline.
|
||||
DateTime expiredDeadlineUtc = DateTime.UtcNow.AddMilliseconds(-1);
|
||||
|
||||
bool result = cache.TryWaitForUpdate(
|
||||
serverHandle: 7,
|
||||
itemHandle: 21,
|
||||
sinceVersion: 0,
|
||||
deadlineUtc: expiredDeadlineUtc,
|
||||
pumpStep: () => Interlocked.Increment(ref pumpCalls),
|
||||
out MxAccessValueCache.CachedValue value,
|
||||
pollIntervalMs: 5);
|
||||
|
||||
Assert.False(result);
|
||||
Assert.Equal(default, value.Value);
|
||||
Assert.Equal(1, pumpCalls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task TryWaitForUpdate_ReturnsTrue_WhenSetFiresAfterBaselineVersion()
|
||||
{
|
||||
MxAccessValueCache cache = new();
|
||||
Timestamp sourceTimestamp = Timestamp.FromDateTime(DateTime.UtcNow);
|
||||
// Baseline is "no entry yet" → wait for the first Set to land.
|
||||
Task<(bool ok, MxAccessValueCache.CachedValue value)> waitTask = Task.Run(() =>
|
||||
{
|
||||
bool ok = cache.TryWaitForUpdate(
|
||||
serverHandle: 7,
|
||||
itemHandle: 21,
|
||||
sinceVersion: 0,
|
||||
deadlineUtc: DateTime.UtcNow.AddSeconds(2),
|
||||
pumpStep: () => { },
|
||||
out MxAccessValueCache.CachedValue v,
|
||||
pollIntervalMs: 5);
|
||||
return (ok, v);
|
||||
});
|
||||
|
||||
// Race a Set against the wait loop. The cache's lock guarantees the
|
||||
// wait observes the new version before TryGet returns it.
|
||||
await Task.Delay(20);
|
||||
cache.Set(7, 21, BuildEvent(7, 21, intValue: 4242, quality: 192, sourceTimestamp));
|
||||
|
||||
(bool ok, MxAccessValueCache.CachedValue value) = await waitTask;
|
||||
Assert.True(ok);
|
||||
Assert.Equal(4242, value.Value.Int32Value);
|
||||
Assert.Equal(1UL, value.Version);
|
||||
}
|
||||
|
||||
private static MxEvent BuildEvent(
|
||||
int serverHandle,
|
||||
int itemHandle,
|
||||
int intValue,
|
||||
int quality,
|
||||
Timestamp sourceTimestamp)
|
||||
{
|
||||
MxEvent mxEvent = new()
|
||||
{
|
||||
Family = MxEventFamily.OnDataChange,
|
||||
ServerHandle = serverHandle,
|
||||
ItemHandle = itemHandle,
|
||||
Quality = quality,
|
||||
SourceTimestamp = sourceTimestamp,
|
||||
Value = new MxValue
|
||||
{
|
||||
DataType = MxDataType.Integer,
|
||||
VariantType = "VT_I4",
|
||||
Int32Value = intValue,
|
||||
},
|
||||
OnDataChange = new OnDataChangeEvent(),
|
||||
};
|
||||
mxEvent.Statuses.Add(new MxStatusProxy
|
||||
{
|
||||
Category = MxStatusCategory.Ok,
|
||||
});
|
||||
return mxEvent;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user