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,163 @@
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport.Probes;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.TestSupport;
|
||||
|
||||
/// <summary>
|
||||
/// Entry point for live-AVEVA test fixtures. Runs every relevant probe and returns a
|
||||
/// <see cref="PrerequisiteReport"/> whose <c>SkipReason</c> feeds <c>Assert.Skip</c> when
|
||||
/// the environment isn't set up. Non-Windows hosts get a single aggregated Skip row per
|
||||
/// category instead of a flood of individual skips.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para><b>Call shape</b>:</para>
|
||||
/// <code>
|
||||
/// var report = await AvevaPrerequisites.CheckAllAsync();
|
||||
/// if (report.SkipReason is not null) Assert.Skip(report.SkipReason);
|
||||
/// </code>
|
||||
/// <para><b>Categories in rough order of 'would I want to know first?'</b>:</para>
|
||||
/// <list type="number">
|
||||
/// <item>Environment — process bitness, OS platform, RPCSS up.</item>
|
||||
/// <item>AvevaInstall — Framework registry, install paths, no pending reboot.</item>
|
||||
/// <item>AvevaCoreService — aaBootstrap / aaGR / NmxSvc running.</item>
|
||||
/// <item>MxAccessCom — LMXProxy.LMXProxyServer ProgID → CLSID → file-on-disk.</item>
|
||||
/// <item>GalaxyRepository — SQL reachable, ZB exists, deployed-object count.</item>
|
||||
/// <item>OtOpcUaService — our two Windows services + GLAuth.</item>
|
||||
/// <item>AvevaSoftService — aaLogger etc., warn only.</item>
|
||||
/// <item>AvevaHistorian — aahClientAccessPoint etc., optional.</item>
|
||||
/// </list>
|
||||
/// <para><b>What's NOT checked here</b>: end-to-end subscribe / read / write against a real
|
||||
/// Galaxy tag. That's the job of the live-smoke tests this helper gates — the helper just
|
||||
/// tells them whether running is worthwhile.</para>
|
||||
/// </remarks>
|
||||
public static class AvevaPrerequisites
|
||||
{
|
||||
// -------- Individual service lists (kept as data so tests can inspect / override) --------
|
||||
|
||||
/// <summary>Services whose absence means live-Galaxy tests can't run at all.</summary>
|
||||
internal static readonly (string Name, string Purpose)[] CoreServices =
|
||||
[
|
||||
("aaBootstrap", "master service that starts the Platform process + brokers aa* communication"),
|
||||
("aaGR", "Galaxy Repository host — mediates IDE / runtime access to ZB"),
|
||||
("NmxSvc", "Network Message Exchange — MXAccess + Bootstrap transport"),
|
||||
("MSSQLSERVER", "SQL Server instance that hosts the ZB database"),
|
||||
];
|
||||
|
||||
/// <summary>Warn-but-don't-fail AVEVA services.</summary>
|
||||
internal static readonly (string Name, string Purpose)[] SoftServices =
|
||||
[
|
||||
("aaLogger", "ArchestrA Logger — diagnostic log receiver; stack runs without it but error visibility suffers"),
|
||||
("aaUserValidator", "OS user/group auth for ArchestrA security; only required when Galaxy security mode isn't 'Open'"),
|
||||
("aaGlobalDataCacheMonitorSvr", "cross-platform global data cache; single-node dev boxes run fine without it"),
|
||||
];
|
||||
|
||||
/// <summary>Optional AVEVA Historian services — only required for HistoryRead IPC paths.</summary>
|
||||
internal static readonly (string Name, string Purpose)[] HistorianServices =
|
||||
[
|
||||
("aahClientAccessPoint", "AVEVA Historian Client Access Point — HistoryRead IPC endpoint"),
|
||||
("aahGateway", "AVEVA Historian Gateway"),
|
||||
];
|
||||
|
||||
/// <summary>OtOpcUa-stack Windows services + third-party deps we manage.</summary>
|
||||
internal static readonly (string Name, string Purpose, bool HardRequired)[] OtOpcUaServices =
|
||||
[
|
||||
("OtOpcUaGalaxyHost", "Galaxy.Host out-of-process service (net48 x86, STA + MXAccess)", true),
|
||||
("OtOpcUa", "Main OPC UA server service (hosts Proxy + DriverHost + Admin-facing DB publisher)", false),
|
||||
("GLAuth", "LDAP server (dev only) — glauth.exe on localhost:3893", false),
|
||||
];
|
||||
|
||||
// -------- Orchestrator --------
|
||||
|
||||
public static async Task<PrerequisiteReport> CheckAllAsync(
|
||||
Options? options = null, CancellationToken ct = default)
|
||||
{
|
||||
options ??= new Options();
|
||||
var checks = new List<PrerequisiteCheck>();
|
||||
|
||||
// Environment
|
||||
checks.Add(MxAccessComProbe.CheckProcessBitness());
|
||||
|
||||
// AvevaInstall — registry + files
|
||||
checks.Add(RegistryProbe.CheckFrameworkInstalled());
|
||||
checks.Add(RegistryProbe.CheckPlatformDeployed());
|
||||
checks.Add(RegistryProbe.CheckRebootPending());
|
||||
|
||||
// AvevaCoreService
|
||||
foreach (var (name, purpose) in CoreServices)
|
||||
checks.Add(ServiceProbe.Check(name, PrerequisiteCategory.AvevaCoreService, hardRequired: true, whatItDoes: purpose));
|
||||
|
||||
// MxAccessCom
|
||||
checks.Add(MxAccessComProbe.Check());
|
||||
|
||||
// GalaxyRepository
|
||||
checks.Add(await SqlProbe.CheckZbDatabaseAsync(options.SqlConnectionString, ct));
|
||||
// Deployed-object count only makes sense if the DB check passed.
|
||||
if (checks[checks.Count - 1].Status == PrerequisiteStatus.Pass)
|
||||
checks.Add(await SqlProbe.CheckDeployedObjectCountAsync(options.SqlConnectionString, ct));
|
||||
|
||||
// OtOpcUaService
|
||||
foreach (var (name, purpose, hard) in OtOpcUaServices)
|
||||
checks.Add(ServiceProbe.Check(name, PrerequisiteCategory.OtOpcUaService, hardRequired: hard, whatItDoes: purpose));
|
||||
if (options.CheckGalaxyHostPipe)
|
||||
checks.Add(await NamedPipeProbe.CheckGalaxyHostPipeAsync(options.GalaxyHostPipeName, ct));
|
||||
|
||||
// AvevaSoftService
|
||||
foreach (var (name, purpose) in SoftServices)
|
||||
checks.Add(ServiceProbe.Check(name, PrerequisiteCategory.AvevaSoftService, hardRequired: false, whatItDoes: purpose));
|
||||
|
||||
// AvevaHistorian
|
||||
if (options.CheckHistorian)
|
||||
{
|
||||
foreach (var (name, purpose) in HistorianServices)
|
||||
checks.Add(ServiceProbe.Check(name, PrerequisiteCategory.AvevaHistorian, hardRequired: false, whatItDoes: purpose));
|
||||
}
|
||||
|
||||
return new PrerequisiteReport(checks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Narrower check for tests that only need the Galaxy Repository (SQL) path — don't
|
||||
/// pay the cost of probing every aa* service when the test only reads gobject rows.
|
||||
/// </summary>
|
||||
public static async Task<PrerequisiteReport> CheckRepositoryOnlyAsync(
|
||||
string? sqlConnectionString = null, CancellationToken ct = default)
|
||||
{
|
||||
var checks = new List<PrerequisiteCheck>
|
||||
{
|
||||
await SqlProbe.CheckZbDatabaseAsync(sqlConnectionString, ct),
|
||||
};
|
||||
if (checks[0].Status == PrerequisiteStatus.Pass)
|
||||
checks.Add(await SqlProbe.CheckDeployedObjectCountAsync(sqlConnectionString, ct));
|
||||
return new PrerequisiteReport(checks);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Narrower check for the named-pipe endpoint — tests that drive the full Proxy
|
||||
/// against a live Galaxy.Host service don't need the SQL or AVEVA-internal probes
|
||||
/// (the Host does that work internally; we just need the pipe to accept).
|
||||
/// </summary>
|
||||
public static async Task<PrerequisiteReport> CheckGalaxyHostPipeOnlyAsync(
|
||||
string? pipeName = null, CancellationToken ct = default)
|
||||
{
|
||||
var checks = new List<PrerequisiteCheck>
|
||||
{
|
||||
await NamedPipeProbe.CheckGalaxyHostPipeAsync(pipeName, ct),
|
||||
};
|
||||
return new PrerequisiteReport(checks);
|
||||
}
|
||||
|
||||
/// <summary>Knobs for <see cref="CheckAllAsync"/>.</summary>
|
||||
public sealed class Options
|
||||
{
|
||||
/// <summary>SQL Server connection string — defaults to Windows-auth <c>localhost\ZB</c>.</summary>
|
||||
public string? SqlConnectionString { get; init; }
|
||||
|
||||
/// <summary>Named-pipe endpoint for OtOpcUaGalaxyHost — defaults to <c>OtOpcUaGalaxy</c>.</summary>
|
||||
public string? GalaxyHostPipeName { get; init; }
|
||||
|
||||
/// <summary>Include the named-pipe probe. Off by default — it's a seconds-long TCP-like probe and some tests don't need it.</summary>
|
||||
public bool CheckGalaxyHostPipe { get; init; } = true;
|
||||
|
||||
/// <summary>Include Historian service probes. Off by default — Historian is optional.</summary>
|
||||
public bool CheckHistorian { get; init; } = false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user