Phase 3 PR 36 — AVEVA prerequisites test-support library. New tests/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport multi-targeted class library (net10.0 + net48 so both the modern and the MXAccess-COM x86 test projects can consume it) that probes every piece of the AVEVA System Platform + OtOpcUa stack a live-Galaxy test depends on and returns a structured PrerequisiteReport. Closes the gap where live-smoke tests silently returned 'unreachable' without telling operators which specific piece failed.
AvevaPrerequisites.CheckAllAsync walks eight probe categories producing PrerequisiteCheck rows each with Name (e.g. 'service:aaBootstrap', 'sql:ZB', 'com:LMXProxy', 'registry:ArchestrA.Framework'), Category (AvevaCoreService / AvevaSoftService / AvevaInstall / MxAccessCom / GalaxyRepository / AvevaHistorian / OtOpcUaService / Environment), Status (Pass / Warn / Fail / Skip), and operator-facing Detail message. Report aggregates them: IsLivetestReady (no Fails anywhere) and IsAvevaSideReady (AVEVA-side categories pass, our v2 services can be absent while still considering the environment AVEVA-ready) so different test tiers can use the right threshold.
Individual probes: ServiceProbe.Check queries the Windows Service Control Manager via System.ServiceProcess.ServiceController — treats DemandStart+Stopped as Warn (NmxSvc is DemandStart by design; master pulls it up) but AutoStart+Stopped as Fail; not-installed is Fail for hard-required services, Warn for soft ones; non-Windows hosts get Skip; transitional states like StartPending get Warn with a 'try again' hint. RegistryProbe reads HKLM\SOFTWARE\WOW6432Node\ArchestrA\{Framework,Framework\Platform,MSIInstall} — Framework key presence + populated InstallPath/RootPath values mean System Platform installed; PfeConfigOptions in the Platform subkey (format 'PlatformId=N,EngineId=N,...') indicates a Platform has been deployed from the IDE (PlatformId=0 means never deployed — MXAccess will connect but every subscription will be Bad quality); RebootRequired='True' under MSIInstall surfaces as a loud warn since post-patch behavior is undefined. MxAccessComProbe resolves the LMXProxy.LMXProxyServer ProgID → CLSID → HKLM\SOFTWARE\Classes\WOW6432Node\CLSID\{guid}\InprocServer32, verifying the registered file exists on disk (catches the orphan-registry case where a previous uninstall left the ProgID registered but the DLL is gone — distinguishes it from the 'totally not installed' case by message); also emits a Warn when the test process is 64-bit (MXAccess COM activation fails with REGDB_E_CLASSNOTREG 0x80040154 regardless of registration, so seeing this warning tells operators why the activation would fail even on a fully-installed machine). SqlProbe tests Galaxy Repository via Microsoft.Data.SqlClient using the Windows-auth localhost connection string the repo code defaults to — distinguishes 'SQL Server unreachable' (connection fails) from 'ZB database does not exist' (SELECT DB_ID('ZB') returns null) because they have different remediation paths (sc.exe start MSSQLSERVER vs. restore from .cab backup); a secondary CheckDeployedObjectCountAsync query on 'gobject WHERE deployed_version > 0' warns when the count is zero because discovery smoke tests will return empty hierarchies. NamedPipeProbe opens a 2s NamedPipeClientStream against OtOpcUaGalaxyHost's pipe ('OtOpcUaGalaxy' per the installer default) — pipe accepting a connection proves the Host service is listening; disconnects immediately so we don't consume a session slot.
Service lists kept as internal static data so tests can inspect + override: CoreServices (aaBootstrap + aaGR + NmxSvc + MSSQLSERVER — hard fail if missing), SoftServices (aaLogger + aaUserValidator + aaGlobalDataCacheMonitorSvr — warn only; stack runs without them but diagnostics/auth are degraded), HistorianServices (aahClientAccessPoint + aahGateway — opt-in via Options.CheckHistorian, only matters for HistoryRead IPC paths), OtOpcUaServices (our OtOpcUaGalaxyHost hard-required for end-to-end live tests + OtOpcUa warn + GLAuth warn). Narrower entry points CheckRepositoryOnlyAsync and CheckGalaxyHostPipeOnlyAsync for tests that only care about specific subsystems — avoid paying the full probe cost on every GalaxyRepositoryLiveSmokeTests fact.
Multi-targeting mechanics: System.ServiceProcess.ServiceController + Microsoft.Win32.Registry are NuGet packages on net10 but in-box BCL references on net48; csproj conditions Package vs Reference by TargetFramework. Microsoft.Data.SqlClient v6 supports both frameworks so single PackageReference. Net48Polyfills.cs provides IsExternalInit shim (records/init-only setters) and SupportedOSPlatformAttribute stub so the same Probe sources compile on both frameworks without per-callsite preprocessor guards — lets Roslyn's platform-compatibility analyzer stay useful on net10 without breaking net48 builds.
Existing GalaxyRepositoryLiveSmokeTests updated to delegate its skip decision to AvevaPrerequisites.CheckRepositoryOnlyAsync (legacy ZbReachableAsync kept as a compatibility adapter so the in-test 'if (!await ZbReachableAsync()) return;' pattern keeps working while the surrounding fixtures gradually migrate to Assert.Skip-with-reason). Slnx file registers the new project.
Tests — AvevaPrerequisitesLiveTests (8 new Integration cases, Category=LiveGalaxy): the helper correctly reports Framework install (registry pass), aaBootstrap Running (service pass), aaGR Running (service pass), MxAccess COM registered (com pass), ZB database reachable (sql pass), deployed-object count > 0 (warn-upgraded-to-pass because this box has 49 objects deployed), the AVEVA side is ready even when our own services (OtOpcUaGalaxyHost) aren't installed yet (IsAvevaSideReady=true), and the helper emits rows for OtOpcUaGalaxyHost + OtOpcUa + GLAuth even when not installed (regression guard — nobody can accidentally ship a check that omits our own services). Full Galaxy.Host.Tests Category=LiveGalaxy suite: 13 pass (5 prior smoke + 8 new prerequisites). Full solution build clean, 0 errors.
What's NOT in this PR: end-to-end Galaxy stack smoke (Proxy → Host pipe → MXAccess → real Galaxy tag). That's the next PR — this one is the gate the end-to-end smoke will call first to produce actionable skip messages instead of silent returns.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Exercises <see cref="AvevaPrerequisites"/> against the live dev box so the helper
|
||||
/// itself gets integration coverage — i.e. "do the probes return Pass for things that
|
||||
/// really are Pass?" as validated against this machine's known-installed topology.
|
||||
/// Category <c>LiveGalaxy</c> so CI / clean dev boxes skip cleanly.
|
||||
/// </summary>
|
||||
[Trait("Category", "LiveGalaxy")]
|
||||
public sealed class AvevaPrerequisitesLiveTests
|
||||
{
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public AvevaPrerequisitesLiveTests(ITestOutputHelper output) => _output = output;
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAll_on_live_box_reports_Framework_install()
|
||||
{
|
||||
var report = await AvevaPrerequisites.CheckAllAsync();
|
||||
_output.WriteLine(report.ToString());
|
||||
report.Checks.ShouldContain(c =>
|
||||
c.Name == "registry:ArchestrA.Framework" && c.Status == PrerequisiteStatus.Pass,
|
||||
"ArchestrA Framework registry root should be found on this machine.");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAll_on_live_box_reports_aaBootstrap_running()
|
||||
{
|
||||
var report = await AvevaPrerequisites.CheckAllAsync();
|
||||
var bootstrap = report.Checks.FirstOrDefault(c => c.Name == "service:aaBootstrap");
|
||||
bootstrap.ShouldNotBeNull();
|
||||
bootstrap.Status.ShouldBe(PrerequisiteStatus.Pass,
|
||||
$"aaBootstrap must be Running for any live-Galaxy test to work — detail: {bootstrap.Detail}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAll_on_live_box_reports_aaGR_running()
|
||||
{
|
||||
var report = await AvevaPrerequisites.CheckAllAsync();
|
||||
var gr = report.Checks.FirstOrDefault(c => c.Name == "service:aaGR");
|
||||
gr.ShouldNotBeNull();
|
||||
gr.Status.ShouldBe(PrerequisiteStatus.Pass,
|
||||
$"aaGR (Galaxy Repository) must be Running — detail: {gr.Detail}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckAll_on_live_box_reports_MxAccess_COM_registered()
|
||||
{
|
||||
var report = await AvevaPrerequisites.CheckAllAsync();
|
||||
var com = report.Checks.FirstOrDefault(c => c.Name == "com:LMXProxy");
|
||||
com.ShouldNotBeNull();
|
||||
com.Status.ShouldBe(PrerequisiteStatus.Pass,
|
||||
$"LMXProxy.LMXProxyServer ProgID must resolve to an InprocServer32 DLL — detail: {com.Detail}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckRepositoryOnly_on_live_box_reports_ZB_reachable()
|
||||
{
|
||||
var report = await AvevaPrerequisites.CheckRepositoryOnlyAsync(ct: CancellationToken.None);
|
||||
var zb = report.Checks.FirstOrDefault(c => c.Name == "sql:ZB");
|
||||
zb.ShouldNotBeNull();
|
||||
zb.Status.ShouldBe(PrerequisiteStatus.Pass,
|
||||
$"ZB database must be reachable via SQL Server Windows auth — detail: {zb.Detail}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CheckRepositoryOnly_on_live_box_reports_non_zero_deployed_objects()
|
||||
{
|
||||
// This box has 49 deployed objects per the research; we just assert > 0 so adding/
|
||||
// removing objects doesn't break the test.
|
||||
var report = await AvevaPrerequisites.CheckRepositoryOnlyAsync();
|
||||
var deployed = report.Checks.FirstOrDefault(c => c.Name == "sql:ZB.deployedObjects");
|
||||
deployed.ShouldNotBeNull();
|
||||
deployed.Status.ShouldBe(PrerequisiteStatus.Pass,
|
||||
$"At least one deployed gobject should exist — detail: {deployed.Detail}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Aveva_side_is_ready_on_this_machine()
|
||||
{
|
||||
// Narrower than "livetest ready" — our own services (OtOpcUa / OtOpcUaGalaxyHost)
|
||||
// may not be installed on a developer's box while they're actively iterating on
|
||||
// them, but the AVEVA side (Framework / Galaxy Repository / MXAccess COM /
|
||||
// SQL / core services) should always be up on a machine with System Platform
|
||||
// installed. This assertion is what gates live-Galaxy tests that go straight to
|
||||
// the Galaxy Repository without routing through our stack.
|
||||
var report = await AvevaPrerequisites.CheckAllAsync(
|
||||
new AvevaPrerequisites.Options { CheckGalaxyHostPipe = false });
|
||||
_output.WriteLine(report.ToString());
|
||||
_output.WriteLine(report.Warnings ?? "no warnings");
|
||||
|
||||
// Enumerate AVEVA-side failures (if any) for an actionable assertion message.
|
||||
var avevaFails = report.Checks
|
||||
.Where(c => c.Status == PrerequisiteStatus.Fail &&
|
||||
c.Category != PrerequisiteCategory.OtOpcUaService)
|
||||
.ToList();
|
||||
report.IsAvevaSideReady.ShouldBeTrue(
|
||||
avevaFails.Count == 0
|
||||
? "unexpected state"
|
||||
: "AVEVA-side failures: " + string.Join(" ; ",
|
||||
avevaFails.Select(f => $"{f.Name}: {f.Detail}")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Report_captures_OtOpcUa_services_state_even_when_not_installed()
|
||||
{
|
||||
// The helper reports the status of OtOpcUaGalaxyHost + OtOpcUa services even if
|
||||
// they're not installed yet — absence is itself an actionable signal. This test
|
||||
// doesn't assert Pass/Fail on those services (their state depends on what's
|
||||
// installed when the test runs) — it only asserts the helper EMITTED the rows,
|
||||
// so nobody can ship a prerequisite check that silently omits our own services.
|
||||
var report = await AvevaPrerequisites.CheckAllAsync();
|
||||
|
||||
report.Checks.ShouldContain(c => c.Name == "service:OtOpcUaGalaxyHost");
|
||||
report.Checks.ShouldContain(c => c.Name == "service:OtOpcUa");
|
||||
report.Checks.ShouldContain(c => c.Name == "service:GLAuth");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Backend.Galaxy;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Shared.Contracts;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
{
|
||||
@@ -16,6 +17,11 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
/// SQL the v1 Host uses, proving the lift is byte-for-byte equivalent at the
|
||||
/// <c>DiscoverHierarchyResponse</c> shape.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Since PR 36, skip logic is delegated to <see cref="AvevaPrerequisites.CheckRepositoryOnlyAsync"/>
|
||||
/// so operators see exactly why a test skipped ("ZB db not found" vs "SQL Server
|
||||
/// unreachable") instead of a silent return.
|
||||
/// </remarks>
|
||||
[Trait("Category", "LiveGalaxy")]
|
||||
public sealed class GalaxyRepositoryLiveSmokeTests
|
||||
{
|
||||
@@ -26,15 +32,20 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.Tests
|
||||
CommandTimeoutSeconds = 10,
|
||||
};
|
||||
|
||||
private static async Task<string?> RepositorySkipReasonAsync()
|
||||
{
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(4));
|
||||
var report = await AvevaPrerequisites.CheckRepositoryOnlyAsync(
|
||||
DevZbOptions().ConnectionString, cts.Token);
|
||||
return report.SkipReason;
|
||||
}
|
||||
|
||||
private static async Task<bool> ZbReachableAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
var repo = new GalaxyRepository(DevZbOptions());
|
||||
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3));
|
||||
return await repo.TestConnectionAsync(cts.Token);
|
||||
}
|
||||
catch { return false; }
|
||||
// Legacy silent-skip adapter — keeps the existing tests compiling while
|
||||
// gradually migrating to the Skip-with-reason pattern. Returns true when the
|
||||
// prerequisite check has no Fail entries.
|
||||
return (await RepositorySkipReasonAsync()) is null;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Host.csproj"/>
|
||||
<ProjectReference Include="..\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport\ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport.csproj"/>
|
||||
<Reference Include="System.ServiceProcess"/>
|
||||
<!-- IMxProxy's delegate signatures mention ArchestrA.MxAccess.MXSTATUS_PROXY, so tests
|
||||
implementing the interface must resolve that type at compile time. -->
|
||||
|
||||
Reference in New Issue
Block a user