116 lines
4.5 KiB
C#
116 lines
4.5 KiB
C#
using System.Reflection;
|
|
|
|
namespace ZB.MOM.WW.ScadaBridge.Host.Tests;
|
|
|
|
/// <summary>
|
|
/// WP-16: Tests for CoordinatedShutdown configuration.
|
|
/// Verifies no Environment.Exit calls exist in source and HOCON config is correct.
|
|
/// </summary>
|
|
public class CoordinatedShutdownTests
|
|
{
|
|
[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);
|
|
Assert.DoesNotContain("Environment.Exit", content,
|
|
StringComparison.Ordinal);
|
|
}
|
|
}
|
|
|
|
[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);
|
|
}
|
|
|
|
[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("));
|
|
}
|
|
|
|
[Fact]
|
|
public void SiteSingletons_RegisterThroughRegistrarWithDrain()
|
|
{
|
|
var hostProjectDir = FindHostProjectDirectory();
|
|
Assert.NotNull(hostProjectDir);
|
|
var content = File.ReadAllText(Path.Combine(hostProjectDir!, "Actors", "AkkaHostedService.cs"));
|
|
|
|
// Round-2 N5: 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;
|
|
}
|
|
}
|