513fa1aa16
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.
143 lines
6.3 KiB
C#
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;
|
|
}
|
|
}
|