using System; using System.Threading; using System.Threading.Tasks; using Shouldly; using Xunit; using ZB.MOM.WW.LmxOpcUa.Host.GalaxyRepository; using ZB.MOM.WW.LmxOpcUa.Tests.Helpers; namespace ZB.MOM.WW.LmxOpcUa.Tests.GalaxyRepository { /// /// Verifies the polling service that detects Galaxy deploy changes and triggers address-space rebuilds. /// public class ChangeDetectionServiceTests { /// /// Confirms that the first poll always triggers an initial rebuild notification. /// [Fact] public async Task FirstPoll_AlwaysTriggers() { var repo = new FakeGalaxyRepository { LastDeployTime = new DateTime(2024, 1, 1) }; var service = new ChangeDetectionService(repo, 1); var triggered = false; service.OnGalaxyChanged += () => triggered = true; service.Start(); await Task.Delay(500); service.Stop(); triggered.ShouldBe(true); service.Dispose(); } /// /// Confirms that repeated polls with the same deploy timestamp do not retrigger rebuilds. /// [Fact] public async Task SameTimestamp_DoesNotTriggerAgain() { var repo = new FakeGalaxyRepository { LastDeployTime = new DateTime(2024, 1, 1) }; var service = new ChangeDetectionService(repo, 1); var triggerCount = 0; service.OnGalaxyChanged += () => Interlocked.Increment(ref triggerCount); service.Start(); await Task.Delay(2500); // Should have polled at least twice service.Stop(); triggerCount.ShouldBe(1); // Only the first poll service.Dispose(); } /// /// Confirms that a changed deploy timestamp triggers another rebuild notification. /// [Fact] public async Task ChangedTimestamp_TriggersAgain() { var repo = new FakeGalaxyRepository { LastDeployTime = new DateTime(2024, 1, 1) }; var service = new ChangeDetectionService(repo, 1); var triggerCount = 0; service.OnGalaxyChanged += () => Interlocked.Increment(ref triggerCount); service.Start(); await Task.Delay(500); // Change the deploy time repo.LastDeployTime = new DateTime(2024, 2, 1); await Task.Delay(1500); service.Stop(); triggerCount.ShouldBeGreaterThanOrEqualTo(2); service.Dispose(); } /// /// Confirms that transient polling failures do not crash the service and allow later recovery. /// [Fact] public async Task FailedPoll_DoesNotCrash_RetriesNext() { var repo = new FakeGalaxyRepository { LastDeployTime = new DateTime(2024, 1, 1) }; var service = new ChangeDetectionService(repo, 1); var triggerCount = 0; service.OnGalaxyChanged += () => Interlocked.Increment(ref triggerCount); service.Start(); await Task.Delay(500); // Make it fail repo.ShouldThrow = true; await Task.Delay(1500); // Restore and it should recover repo.ShouldThrow = false; repo.LastDeployTime = new DateTime(2024, 3, 1); await Task.Delay(1500); service.Stop(); // Should have triggered at least on first poll and on the changed timestamp triggerCount.ShouldBeGreaterThanOrEqualTo(1); service.Dispose(); } /// /// Confirms that stopping the service before it starts is a harmless no-op. /// [Fact] public void Stop_BeforeStart_DoesNotThrow() { var repo = new FakeGalaxyRepository(); var service = new ChangeDetectionService(repo, 30); service.Stop(); // Should not throw service.Dispose(); } } }