From 570d13291603fe1810eba6c21b9341dcd97f9fe9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A2=81=E8=96=84=E4=BA=91?= Date: Sun, 2 Aug 2026 19:43:20 +0800 Subject: [PATCH] test: bind Local G2 diagnostic evidence --- .../Test/LocalG2DiagnosticEvidenceLoader.cs | 68 ++++++++++++++++--- ...smoothing_local_g2_diagnostic_evidence.ps1 | 39 ++++++----- 2 files changed, 81 insertions(+), 26 deletions(-) diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticEvidenceLoader.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticEvidenceLoader.cs index 87f0045..98b3683 100644 --- a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticEvidenceLoader.cs +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticEvidenceLoader.cs @@ -12,6 +12,26 @@ public sealed class LocalG2DiagnosticEvidenceLoader private const string FixtureSha256 = "3d05daee5a211b3e7aa0b77193423b5fa07d3135e241a4413be3518fc7efe563"; private const string CandidateStableKey = "single-turn/s0/r0/w5/seed2"; private const string CandidateSha256 = "7cefb76c77e48a49bf3212e7a2472e23036c3899daa94b5c6b68fa5db29a3392"; + private const double WindowStartArcLengthMeters = 1.9199999999999982d; + private const double WindowEndArcLengthMeters = 2.1199999999999983d; + private const double StartGeometricCurvaturePerMeter = 0.41666666666666663d; + private const double EndGeometricCurvaturePerMeter = 0d; + private const double StartVehicleCurvaturePerMeter = 0.41666666666666663d; + private const double EndVehicleCurvaturePerMeter = 0d; + + private static readonly LocalG2DiagnosticEvidencePointContract[] ExpectedCandidatePoints = + { + new(2.7216397035494579d, 1.7279184433567971d, 1.9199999999999982d, 0.799999999999999d, 0.799999999999999d), + new(2.7355188205174521d, 1.7423187041196302d, 1.9399999999999982d, 0.80751185024996908d, 0.80751185024996908d), + new(2.7492871654421882d, 1.7568248978050907d, 1.9599999999999982d, 0.8157618125921976d, 0.8157618125921976d), + new(2.7629234692741469d, 1.7714552535786874d, 1.9799999999999982d, 0.82542852165703573d, 0.82542852165703573d), + new(2.7764244476704873d, 1.786210614200662d, 1.9999999999999982d, 0.83333333333333237d, 0.83333333333333237d), + new(2.7925350453990334d, 1.803999635249544d, 2.0239999999999982d, 0.835253334801853d, 0.835253334801853d), + new(2.80865418003672d, 1.8217809211927092d, 2.0479999999999983d, 0.83333333355178374d, 0.83333333355178374d), + new(2.8248074262445737d, 1.8395312269498194d, 2.0719999999999983d, 0.8318933325232104d, 0.8318933325232104d), + new(2.8409691920174533d, 1.8572737784943611d, 2.0959999999999983d, 0.83237333274470626d, 0.83237333274470626d), + new(2.8571139169604551d, 1.8750318365841865d, 2.1199999999999983d, 0.83333333333333237d, 0.83333333333333237d), + }; public LocalG2DiagnosticEvidence LoadAndVerify(string evidencePath) { @@ -46,11 +66,13 @@ public sealed class LocalG2DiagnosticEvidenceLoader !string.Equals(evidence.EvaluatorResult, "InsufficientClearance", StringComparison.Ordinal) || !string.Equals(evidence.StopGate, "Clearance", StringComparison.Ordinal) || !string.Equals(evidence.PublishedStatus, "Unchanged", StringComparison.Ordinal) || - !IsFinite(evidence.WindowStartArcLengthMeters) || !IsFinite(evidence.WindowEndArcLengthMeters) || - evidence.WindowEndArcLengthMeters.Value <= evidence.WindowStartArcLengthMeters.Value || - !IsFinite(evidence.StartGeometricCurvaturePerMeter) || !IsFinite(evidence.EndGeometricCurvaturePerMeter) || - !IsFinite(evidence.StartVehicleCurvaturePerMeter) || !IsFinite(evidence.EndVehicleCurvaturePerMeter) || - evidence.CandidatePoints == null || evidence.CandidatePoints.Count != 10) + !IsExpected(evidence.WindowStartArcLengthMeters, WindowStartArcLengthMeters) || + !IsExpected(evidence.WindowEndArcLengthMeters, WindowEndArcLengthMeters) || + !IsExpected(evidence.StartGeometricCurvaturePerMeter, StartGeometricCurvaturePerMeter) || + !IsExpected(evidence.EndGeometricCurvaturePerMeter, EndGeometricCurvaturePerMeter) || + !IsExpected(evidence.StartVehicleCurvaturePerMeter, StartVehicleCurvaturePerMeter) || + !IsExpected(evidence.EndVehicleCurvaturePerMeter, EndVehicleCurvaturePerMeter) || + evidence.CandidatePoints == null || evidence.CandidatePoints.Count != ExpectedCandidatePoints.Length) { throw new InvalidDataException("Local G2 diagnostic evidence contract is invalid."); } @@ -59,8 +81,11 @@ public sealed class LocalG2DiagnosticEvidenceLoader for (int index = 0; index < evidence.CandidatePoints.Count; index++) { LocalG2DiagnosticEvidencePoint point = evidence.CandidatePoints[index]; - if (point == null || !IsFinite(point.X) || !IsFinite(point.Y) || !IsFinite(point.ReferenceArcLengthMeters) || - !IsFinite(point.HeadingRadians) || !IsFinite(point.UnwrappedHeadingRadians) || + LocalG2DiagnosticEvidencePointContract expected = ExpectedCandidatePoints[index]; + if (point == null || !IsExpected(point.X, expected.X) || !IsExpected(point.Y, expected.Y) || + !IsExpected(point.ReferenceArcLengthMeters, expected.ReferenceArcLengthMeters) || + !IsExpected(point.HeadingRadians, expected.HeadingRadians) || + !IsExpected(point.UnwrappedHeadingRadians, expected.UnwrappedHeadingRadians) || point.Source != (int)SmoothedPathPointSource.LocalG2Transition || (index > 0 && point.ReferenceArcLengthMeters.Value <= previousArc)) { @@ -83,6 +108,29 @@ public sealed class LocalG2DiagnosticEvidenceLoader } private static bool IsFinite(double value) => !double.IsNaN(value) && !double.IsInfinity(value); + + private static bool IsExpected(double? value, double expected) + { + return IsFinite(value) && value.Value == expected; + } + + private readonly struct LocalG2DiagnosticEvidencePointContract + { + public LocalG2DiagnosticEvidencePointContract(double x, double y, double referenceArcLengthMeters, double headingRadians, double unwrappedHeadingRadians) + { + X = x; + Y = y; + ReferenceArcLengthMeters = referenceArcLengthMeters; + HeadingRadians = headingRadians; + UnwrappedHeadingRadians = unwrappedHeadingRadians; + } + + public double X { get; } + public double Y { get; } + public double ReferenceArcLengthMeters { get; } + public double HeadingRadians { get; } + public double UnwrappedHeadingRadians { get; } + } } public sealed class LocalG2DiagnosticEvidence @@ -92,8 +140,8 @@ public sealed class LocalG2DiagnosticEvidence public string ScenarioId { get; set; } public string CandidateStableKey { get; set; } public string CandidateSha256 { get; set; } - public int CandidateIndex { get; set; } - public int SegmentIndex { get; set; } + public int? CandidateIndex { get; set; } + public int? SegmentIndex { get; set; } public double? WindowStartArcLengthMeters { get; set; } public double? WindowEndArcLengthMeters { get; set; } public double? StartGeometricCurvaturePerMeter { get; set; } @@ -113,5 +161,5 @@ public sealed class LocalG2DiagnosticEvidencePoint public double? ReferenceArcLengthMeters { get; set; } public double? HeadingRadians { get; set; } public double? UnwrappedHeadingRadians { get; set; } - public int Source { get; set; } + public int? Source { get; set; } } diff --git a/ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1 b/ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1 index fca18b8..d70bbb1 100644 --- a/ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1 +++ b/ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1 @@ -34,23 +34,30 @@ Assert-Equal 'InsufficientClearance' $evidence.EvaluatorResult 'Diagnostic evide Assert-Equal 'Clearance' $evidence.StopGate 'Diagnostic evidence must retain the observed stop gate.' Assert-Equal 'Unchanged' $evidence.PublishedStatus 'Diagnostic evidence must retain the strict final status.' -$temporaryEvidencePath = Join-Path ([System.IO.Path]::GetTempPath()) ("local-g2-diagnostic-evidence-{0}.json" -f [System.Guid]::NewGuid()) -try { - $tamperedContent = (Get-Content -LiteralPath $fixturePath -Raw).Replace('"StopGate": "Clearance"', '"StopGate": "Collision"') - [System.IO.File]::WriteAllText($temporaryEvidencePath, $tamperedContent, [System.Text.Encoding]::UTF8) - - $rejected = $false +function Assert-Rejected([string]$tamperedContent, [string]$message) { + $temporaryEvidencePath = Join-Path ([System.IO.Path]::GetTempPath()) ("local-g2-diagnostic-evidence-{0}.json" -f [System.Guid]::NewGuid()) try { - $null = $loadAndVerify.Invoke($loader, @($temporaryEvidencePath)) - } - catch { - $rejected = $true - } + [System.IO.File]::WriteAllText($temporaryEvidencePath, $tamperedContent, [System.Text.Encoding]::UTF8) - Assert-True $rejected 'Tampered diagnostic evidence must be rejected.' -} -finally { - if (Test-Path -LiteralPath $temporaryEvidencePath) { - Remove-Item -LiteralPath $temporaryEvidencePath -Force + $rejected = $false + try { + $null = $loadAndVerify.Invoke($loader, @($temporaryEvidencePath)) + } + catch { + $rejected = $true + } + + Assert-True $rejected $message + } + finally { + if (Test-Path -LiteralPath $temporaryEvidencePath) { + Remove-Item -LiteralPath $temporaryEvidencePath -Force + } } } + +$fixtureContent = Get-Content -LiteralPath $fixturePath -Raw +Assert-Rejected ($fixtureContent.Replace('"StopGate": "Clearance"', '"StopGate": "Collision"')) 'Tampered stop gate must be rejected.' +Assert-Rejected ($fixtureContent.Replace('"HeadingRadians": 0.835253334801853', '"HeadingRadians": 0.935253334801853')) 'Tampered middle candidate heading must be rejected.' +Assert-Rejected ($fixtureContent.Replace('"WindowStartArcLengthMeters": 1.9199999999999982', '"WindowStartArcLengthMeters": 1.8199999999999982')) 'Tampered window must be rejected even when candidate endpoints are unchanged.' +Assert-Rejected ([regex]::Replace($fixtureContent, '(?m)^\s*"SegmentIndex": 0,\r?\n', '')) 'Evidence missing SegmentIndex must be rejected.'