Files
missions/tests/Azaion.Missions.E2E.Tests/Tests/ResourceLimits/SteadyStateLoadTests.cs
T
Oleksandr Bezdieniezhnykh 001e80fe96 [AZ-585] [AZ-586] ResLim+Perf NFT tests; close test cycle 1
Batch 4 of test implementation cycle 1 (existing-code Step 6, final batch).

- AZ-585 SteadyStateLoadTests + ColdStartRssTests: NFT-RES-LIM-01..04.
  SteadyStateLoadFixture runs one 5-min sustained-load window and samples
  RSS (docker stats), Npgsql conns (pg_stat_activity), and FDs
  (/proc/1/fd) every 5s; three test methods assert independently. All
  SkippableFact-gated on docker primitives.
- AZ-586 PerformanceTests: NFT-PERF-01..04. Sequential single-client,
  5 warm-ups + N measured calls, P50+P95 via LatencyPercentiles, recorded
  to PERF_RESULTS_FILE. Tagged Category=Perf so default gate excludes them.

Infrastructure:
- entrypoint.sh now applies --filter "${TEST_FILTER:-Category!=Perf}"
  per AZ-586 (default CI gate excludes performance).
- MetricCsvRecorder: idempotent CSV appender keyed on env var, used by
  both Perf and ResLim categories.

Step 6 (Implement Tests) is complete. Final report at
_docs/03_implementation/implementation_report_tests.md handoffs the
full-suite gate to test-run/SKILL.md (Step 7).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-15 09:11:53 +03:00

157 lines
6.6 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Globalization;
using Azaion.Missions.E2E.Fixtures;
using Azaion.Missions.E2E.Helpers;
using Xunit;
namespace Azaion.Missions.E2E.Tests.ResourceLimits;
/// <summary>
/// NFT-RES-LIM-01..03 — three observations on a SINGLE 5-minute sustained
/// load window. The window itself lives in
/// <see cref="SteadyStateLoadFixture"/> (class-scoped, runs once); each
/// test asserts one metric against its provisional gate.
/// </summary>
/// <remarks>
/// The fixture skips itself when docker primitives are unavailable; the
/// tests detect that via <see cref="SteadyStateLoadFixture.SkipReason"/>
/// and surface the same reason through <c>Skip.IfNot</c>. The fixture
/// also flips <see cref="SteadyStateLoadFixture.SutExitedDuringWindow"/>
/// if the SUT crashes mid-window — every test fails fast with a clear
/// message rather than reporting a misleading metric.
/// </remarks>
[Collection("ResLimSteadyState")]
[Trait("Category", "ResLim")]
public sealed class SteadyStateLoadTests : TestBase, IClassFixture<SteadyStateLoadFixture>
{
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}");
}
}