using System.Globalization; using Azaion.Missions.E2E.Fixtures; using Azaion.Missions.E2E.Helpers; using Xunit; namespace Azaion.Missions.E2E.Tests.ResourceLimits; /// /// NFT-RES-LIM-01..03 — three observations on a SINGLE 5-minute sustained /// load window. The window itself lives in /// (class-scoped, runs once); each /// test asserts one metric against its provisional gate. /// /// /// The fixture skips itself when docker primitives are unavailable; the /// tests detect that via /// and surface the same reason through Skip.IfNot. The fixture /// also flips /// if the SUT crashes mid-window — every test fails fast with a clear /// message rather than reporting a misleading metric. /// [Collection("ResLimSteadyState")] [Trait("Category", "ResLim")] public sealed class SteadyStateLoadTests : TestBase, IClassFixture { private static readonly MetricCsvRecorder Csv = new("RESLIM_RESULTS_FILE"); private const long ProvisionalRssCapMiB = 250; private const int ProvisionalConnectionCap = 100; private const int ProvisionalFdCap = 1024; private readonly SteadyStateLoadFixture _load; public SteadyStateLoadTests(SteadyStateLoadFixture load) => _load = load; [SkippableFact] [Trait("Traces", "H1|H6|O10")] [Trait("max_ms", "360000")] public void NFT_RES_LIM_01_steady_state_rss_within_provisional_gate_and_no_leak() { Skip.If(_load.SkipReason is not null, _load.SkipReason); Skip.IfNot(_load.LoadGeneratorMetTargetRps, "runner cannot sustain target load (NFR Reliability — not a SUT defect)"); Assert.False(_load.SutExitedDuringWindow, "SUT exited during measurement window"); // Arrange var samplesMiB = _load.RssBytesSamples.Select(b => b / (double)(1024 * 1024)).ToList(); Assert.True(samplesMiB.Count >= 30, $"expected ≥ 30 RSS samples over 5-min window, got {samplesMiB.Count}"); // Act var p95 = LatencyPercentiles.P95(samplesMiB); var finalMiB = samplesMiB[^1]; var leakRatio = Math.Abs(finalMiB - p95) / Math.Max(p95, 1.0); var withinCap = p95 <= ProvisionalRssCapMiB; var noLeak = leakRatio <= 0.20; var pass = withinCap && noLeak; Csv.Record( category: "ResLim", scenario: "NFT-RES-LIM-01", result: pass ? "pass" : "fail", traces: $"H1|H6|O10; " + $"P95_RSS_MiB={p95.ToString("F1", CultureInfo.InvariantCulture)}; " + $"FINAL_RSS_MiB={finalMiB.ToString("F1", CultureInfo.InvariantCulture)}; " + $"LEAK_RATIO={leakRatio.ToString("F2", CultureInfo.InvariantCulture)}"); // Assert — provisional gate; lock at measured + 50% after first green run. Assert.True(withinCap, $"P95 RSS {p95:F1} MiB exceeds provisional {ProvisionalRssCapMiB} MiB gate"); Assert.True(noLeak, $"final RSS {finalMiB:F1} MiB diverges {leakRatio:P0} from P95 {p95:F1} MiB (gate 20%)"); } [SkippableFact] [Trait("Traces", "O10")] [Trait("max_ms", "360000")] public void NFT_RES_LIM_02_npgsql_connection_pool_within_100_no_unbounded_growth() { Skip.If(_load.SkipReason is not null, _load.SkipReason); Skip.IfNot(_load.LoadGeneratorMetTargetRps, "runner cannot sustain target load (NFR Reliability — not a SUT defect)"); Assert.False(_load.SutExitedDuringWindow, "SUT exited during measurement window"); var samples = _load.NpgsqlConnectionSamples; Assert.True(samples.Count >= 30, $"expected ≥ 30 connection samples over 5-min window, got {samples.Count}"); // Act var max = samples.Max(); var firstMinuteSampleCount = 60 / SteadyStateLoadFixture.SampleIntervalSeconds; var firstMinute = samples.Take(firstMinuteSampleCount).ToList(); var firstMinuteMean = firstMinute.Average(); var finalCount = samples[^1]; var withinCap = max <= ProvisionalConnectionCap; var noUnboundedGrowth = finalCount <= 1.3 * Math.Max(firstMinuteMean, 1.0); var pass = withinCap && noUnboundedGrowth; Csv.Record( category: "ResLim", scenario: "NFT-RES-LIM-02", result: pass ? "pass" : "fail", traces: $"O10; MAX_NPGSQL_CONNS={max}; " + $"FINAL_CONNS={finalCount}; " + $"MINUTE1_MEAN={firstMinuteMean.ToString("F1", CultureInfo.InvariantCulture)}"); // Assert Assert.True(withinCap, $"max Npgsql connections {max} exceeds provisional cap {ProvisionalConnectionCap}"); Assert.True(noUnboundedGrowth, $"final connection count {finalCount} > 1.3 × first-minute mean {firstMinuteMean:F1}"); } [SkippableFact] [Trait("Traces", "H6|O10")] [Trait("max_ms", "360000")] public void NFT_RES_LIM_03_file_descriptors_within_1024_no_leak() { Skip.If(_load.SkipReason is not null, _load.SkipReason); Skip.IfNot(_load.LoadGeneratorMetTargetRps, "runner cannot sustain target load (NFR Reliability — not a SUT defect)"); Assert.False(_load.SutExitedDuringWindow, "SUT exited during measurement window"); var samples = _load.FileDescriptorSamples; Assert.True(samples.Count >= 30, $"expected ≥ 30 FD samples over 5-min window, got {samples.Count}"); // Act var max = samples.Max(); var minuteOneSampleCount = 60 / SteadyStateLoadFixture.SampleIntervalSeconds; // The spec calls out "count at t=1min" — anchor on the sample whose // timestamp is closest to (start + 60s). var minuteOneIx = Math.Min(minuteOneSampleCount - 1, samples.Count - 1); var minuteOneCount = samples[minuteOneIx]; var finalCount = samples[^1]; var withinCap = max <= ProvisionalFdCap; var noLeak = finalCount <= 1.3 * Math.Max(minuteOneCount, 1); var pass = withinCap && noLeak; Csv.Record( category: "ResLim", scenario: "NFT-RES-LIM-03", result: pass ? "pass" : "fail", traces: $"H6|O10; MAX_FD={max}; " + $"FINAL_FD={finalCount}; MINUTE1_FD={minuteOneCount}"); // Assert Assert.True(withinCap, $"max FD count {max} exceeds provisional cap {ProvisionalFdCap}"); Assert.True(noLeak, $"final FD count {finalCount} > 1.3 × minute-1 count {minuteOneCount}"); } }