using NATS.Server.JetStream.Consumers; namespace NATS.Server.Tests.JetStream.Consumers; /// /// Tests for SampleTracker: sample frequency parsing and stochastic latency sampling. /// Go reference: consumer.go sampleFrequency, shouldSample, parseSampleFrequency. /// public class SampleModeTests { // --- ParseSampleFrequency --- [Fact] public void ParseSampleFrequency_one_percent() { var rate = SampleTracker.ParseSampleFrequency("1%"); rate.ShouldBe(0.01, 1e-9); } [Fact] public void ParseSampleFrequency_fifty_percent() { var rate = SampleTracker.ParseSampleFrequency("50%"); rate.ShouldBe(0.5, 1e-9); } [Fact] public void ParseSampleFrequency_hundred_percent() { var rate = SampleTracker.ParseSampleFrequency("100%"); rate.ShouldBe(1.0, 1e-9); } [Fact] public void ParseSampleFrequency_zero() { var rate = SampleTracker.ParseSampleFrequency("0%"); rate.ShouldBe(0.0, 1e-9); } [Fact] public void ParseSampleFrequency_no_percent_sign() { var rate = SampleTracker.ParseSampleFrequency("25"); rate.ShouldBe(0.25, 1e-9); } [Fact] public void ParseSampleFrequency_empty_string() { var rate = SampleTracker.ParseSampleFrequency(""); rate.ShouldBe(0.0, 1e-9); } [Fact] public void ParseSampleFrequency_null() { var rate = SampleTracker.ParseSampleFrequency(null); rate.ShouldBe(0.0, 1e-9); } [Fact] public void ParseSampleFrequency_invalid() { var rate = SampleTracker.ParseSampleFrequency("abc"); rate.ShouldBe(0.0, 1e-9); } [Fact] public void ParseSampleFrequency_over_100_clamped() { var rate = SampleTracker.ParseSampleFrequency("200%"); rate.ShouldBe(1.0, 1e-9); } // --- ShouldSample --- [Fact] public void ShouldSample_rate_100_always_samples() { var tracker = new SampleTracker(1.0); for (var i = 0; i < 20; i++) { tracker.ShouldSample().ShouldBeTrue(); } } [Fact] public void ShouldSample_rate_0_never_samples() { var tracker = new SampleTracker(0.0); for (var i = 0; i < 20; i++) { tracker.ShouldSample().ShouldBeFalse(); } } [Fact] public void ShouldSample_increments_total_deliveries() { var tracker = new SampleTracker(0.5); tracker.TotalDeliveries.ShouldBe(0L); tracker.ShouldSample(); tracker.TotalDeliveries.ShouldBe(1L); tracker.ShouldSample(); tracker.TotalDeliveries.ShouldBe(2L); tracker.ShouldSample(); tracker.TotalDeliveries.ShouldBe(3L); } [Fact] public void ShouldSample_stochastic_with_seeded_random() { // Use a seeded Random for deterministic results. // With seed 42 and rate 0.5, we can predict exact outcomes. var rng = new Random(42); var tracker = new SampleTracker(0.5, rng); // Pre-compute expected outcomes using the same seed. var expectedRng = new Random(42); var expected = new bool[10]; for (var i = 0; i < 10; i++) { expected[i] = expectedRng.NextDouble() < 0.5; } var actual = new bool[10]; for (var i = 0; i < 10; i++) { actual[i] = tracker.ShouldSample(); } actual.ShouldBe(expected); tracker.TotalDeliveries.ShouldBe(10L); } [Fact] public void RecordLatency_captures_all_fields() { var tracker = new SampleTracker(1.0); var latency = TimeSpan.FromMilliseconds(42); const ulong seq = 7UL; const string subject = "orders.new"; var before = DateTime.UtcNow; var sample = tracker.RecordLatency(latency, seq, subject); var after = DateTime.UtcNow; sample.Sequence.ShouldBe(seq); sample.Subject.ShouldBe(subject); sample.DeliveryLatency.ShouldBe(latency); sample.SampledAtUtc.ShouldBeGreaterThanOrEqualTo(before); sample.SampledAtUtc.ShouldBeLessThanOrEqualTo(after); } [Fact] public void SampleCount_tracks_sampled_only() { // Rate 1.0: every delivery is sampled. var allSampled = new SampleTracker(1.0); for (var i = 0; i < 5; i++) allSampled.ShouldSample(); allSampled.SampleCount.ShouldBe(5L); allSampled.TotalDeliveries.ShouldBe(5L); // Rate 0.0: no delivery is sampled. var noneSampled = new SampleTracker(0.0); for (var i = 0; i < 5; i++) noneSampled.ShouldSample(); noneSampled.SampleCount.ShouldBe(0L); noneSampled.TotalDeliveries.ShouldBe(5L); } }