Files
ScadaBridge/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CoordinatedShutdownTests.cs
T
Joseph Doherty 513fa1aa16 docs(tests): fix CommentChecker findings across tests/ — 13,392 → 0 (repo-wide scan clean)
Comment/XML-doc changes only:
- add summaries to ~10k test methods, fixtures, fakes and helpers
  (+ <param> on theories, <returns> on async Task methods,
  <inheritdoc/> on interface implementations the checker can resolve)
- strip ~2.6k project-tracking references (WP*, P*-*, row-NN, Task N,
  #NN, Component-0NN) from comments, keeping the explanatory text;
  reword false positives in comments only
- real docs on fakes implementing BCL/library interfaces
  (IDisposable, ILogger, IOptionsMonitor, IAsyncLifetime)
- reword the last 5 src/ false positives (CWE-1236, failures-1,
  year-0001, M365)

Only non-comment edits: four one-line nested classes split across lines
so members can carry docs. Build 0 errors / 0 warnings.
2026-10-07 15:05:49 -04:00

143 lines
6.3 KiB
C#

using System.Reflection;
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
/// <summary>
/// Tests for CoordinatedShutdown configuration.
/// Verifies no Environment.Exit calls exist in source and HOCON config is correct.
/// </summary>
public class CoordinatedShutdownTests
{
/// <summary>Verifies that host source does not contain environment exit.</summary>
[Fact]
public void HostSource_DoesNotContainEnvironmentExit()
{
var hostProjectDir = FindHostProjectDirectory();
Assert.NotNull(hostProjectDir);
var sourceFiles = Directory.GetFiles(hostProjectDir, "*.cs", SearchOption.AllDirectories);
Assert.NotEmpty(sourceFiles);
foreach (var file in sourceFiles)
{
var content = File.ReadAllText(file);
// Sole permitted call site: Program.cs's AppDomain.UnhandledException handler
// (ScadaBridge#34). That is the crash path — the alternative there was never a
// CoordinatedShutdown but the runtime's abort(), which cannot terminate PID 1
// and wedged the container; Environment.Exit still fires the CLR shutdown hook
// Akka binds via run-by-clr-shutdown-hook = on, so it skips nothing abort kept.
// Everywhere else the original rule stands: no code path may bypass
// CoordinatedShutdown by exiting directly.
if (Path.GetFileName(file) == "Program.cs")
{
var occurrences = CountOccurrences(content, "Environment.Exit(");
Assert.Equal(1, occurrences);
var handlerAt = content.IndexOf(
"AppDomain.CurrentDomain.UnhandledException +=", StringComparison.Ordinal);
Assert.True(handlerAt >= 0, "Program.cs must register the UnhandledException handler");
var handlerEnd = content.IndexOf("};", handlerAt, StringComparison.Ordinal);
var exitAt = content.IndexOf("Environment.Exit(", StringComparison.Ordinal);
Assert.True(exitAt > handlerAt && exitAt < handlerEnd,
"Environment.Exit in Program.cs is only permitted inside the UnhandledException handler");
continue;
}
Assert.DoesNotContain("Environment.Exit", content,
StringComparison.Ordinal);
}
}
/// <summary>Verifies that akka hosted service hocon config includes coordinated shutdown settings.</summary>
[Fact]
public void AkkaHostedService_HoconConfig_IncludesCoordinatedShutdownSettings()
{
// Read the AkkaHostedService source to verify HOCON configuration
var hostProjectDir = FindHostProjectDirectory();
Assert.NotNull(hostProjectDir);
var akkaServiceFile = Path.Combine(hostProjectDir, "Actors", "AkkaHostedService.cs");
Assert.True(File.Exists(akkaServiceFile), $"AkkaHostedService.cs not found at {akkaServiceFile}");
var content = File.ReadAllText(akkaServiceFile);
// Verify critical HOCON settings are present
Assert.Contains("run-by-clr-shutdown-hook = on", content);
Assert.Contains("run-coordinated-shutdown-when-down = on", content);
}
/// <summary>Verifies that all central singletons register through registrar with drain.</summary>
[Fact]
public void AllCentralSingletons_RegisterThroughRegistrarWithDrain()
{
var hostProjectDir = FindHostProjectDirectory();
Assert.NotNull(hostProjectDir);
var content = File.ReadAllText(Path.Combine(hostProjectDir!, "Actors", "AkkaHostedService.cs"));
// Review 01 [Medium]: notification-outbox and audit-log-ingest were the
// only central singletons WITHOUT a cluster-leave drain task. All seven
// now go through SingletonRegistrar.Start, which always adds the
// PhaseClusterLeave GracefulStop drain.
Assert.Contains("SingletonRegistrar.Start(", content);
foreach (var name in new[] { "notification-outbox", "audit-log-ingest", "site-call-audit",
"audit-log-purge", "site-audit-reconciliation",
"kpi-history-recorder", "pending-deployment-purge" })
{
Assert.Contains($"\"{name}\"", content);
}
// No hand-rolled ClusterSingletonManager registrations remain anywhere:
// the two role-scoped SITE singletons now also go through the registrar
// (SingletonRegistrar.Start(..., role: siteRole)), so every singleton in
// the file is created through the registrar with a drain task.
Assert.Equal(0, CountOccurrences(content, "ClusterSingletonManager.Props("));
}
/// <summary>Verifies that site singletons register through registrar with drain.</summary>
[Fact]
public void SiteSingletons_RegisterThroughRegistrarWithDrain()
{
var hostProjectDir = FindHostProjectDirectory();
Assert.NotNull(hostProjectDir);
var content = File.ReadAllText(Path.Combine(hostProjectDir!, "Actors", "AkkaHostedService.cs"));
// The two role-scoped SITE singletons previously stayed
// hand-rolled (bare PoisonPill, no PhaseClusterLeave drain). They now
// go through SingletonRegistrar.Start(..., role: siteRole), which
// always adds the GracefulStop drain — in-flight SQLite writes
// (static overrides, native_alarm_state) complete before handover.
Assert.Contains("\"deployment-manager\"", content);
Assert.Contains("\"event-log-handler\"", content);
Assert.Contains("role: siteRole", content);
Assert.Equal(0, CountOccurrences(content, "ClusterSingletonManager.Props("));
}
private static int CountOccurrences(string haystack, string needle)
{
int count = 0, i = 0;
while ((i = haystack.IndexOf(needle, i, StringComparison.Ordinal)) >= 0)
{
count++;
i += needle.Length;
}
return count;
}
private static string? FindHostProjectDirectory()
{
var assemblyDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
var dir = new DirectoryInfo(assemblyDir);
while (dir != null)
{
var hostPath = Path.Combine(dir.FullName, "src", "ZB.MOM.WW.ScadaBridge.Host");
if (Directory.Exists(hostPath))
return hostPath;
dir = dir.Parent;
}
return null;
}
}