From fdfd853df01314d10b251af10fc0ca614c0253f8 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:34:26 +0800 Subject: [PATCH] test: freeze Local G2 diagnostic evidence --- .../local-g2-diagnostic-single-turn.json | 30 +++++ .../Test/LocalG2DiagnosticEvidenceLoader.cs | 117 ++++++++++++++++++ ...smoothing_local_g2_diagnostic_evidence.ps1 | 56 +++++++++ 3 files changed, 203 insertions(+) create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/local-g2-diagnostic-single-turn.json create mode 100644 ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticEvidenceLoader.cs create mode 100644 ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1 diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/local-g2-diagnostic-single-turn.json b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/local-g2-diagnostic-single-turn.json new file mode 100644 index 0000000..bbbfc79 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/Fixtures/local-g2-diagnostic-single-turn.json @@ -0,0 +1,30 @@ +{ + "SourceMeasurementBatchSha256": "ac8166828813d85bf6f8b58f985839e5b2a049e75cb94186ed04f59d540e4eed", + "FixtureSha256": "3d05daee5a211b3e7aa0b77193423b5fa07d3135e241a4413be3518fc7efe563", + "ScenarioId": "single-turn", + "CandidateStableKey": "single-turn/s0/r0/w5/seed2", + "CandidateSha256": "7cefb76c77e48a49bf3212e7a2472e23036c3899daa94b5c6b68fa5db29a3392", + "CandidateIndex": 5, + "SegmentIndex": 0, + "WindowStartArcLengthMeters": 1.9199999999999982, + "WindowEndArcLengthMeters": 2.1199999999999983, + "StartGeometricCurvaturePerMeter": 0.41666666666666663, + "EndGeometricCurvaturePerMeter": 0.0, + "StartVehicleCurvaturePerMeter": 0.41666666666666663, + "EndVehicleCurvaturePerMeter": 0.0, + "EvaluatorResult": "InsufficientClearance", + "StopGate": "Clearance", + "PublishedStatus": "Unchanged", + "CandidatePoints": [ + { "X": 2.7216397035494579, "Y": 1.7279184433567971, "ReferenceArcLengthMeters": 1.9199999999999982, "HeadingRadians": 0.799999999999999, "UnwrappedHeadingRadians": 0.799999999999999, "Source": 4 }, + { "X": 2.7355188205174521, "Y": 1.7423187041196302, "ReferenceArcLengthMeters": 1.9399999999999982, "HeadingRadians": 0.80751185024996908, "UnwrappedHeadingRadians": 0.80751185024996908, "Source": 4 }, + { "X": 2.7492871654421882, "Y": 1.7568248978050907, "ReferenceArcLengthMeters": 1.9599999999999982, "HeadingRadians": 0.8157618125921976, "UnwrappedHeadingRadians": 0.8157618125921976, "Source": 4 }, + { "X": 2.7629234692741469, "Y": 1.7714552535786874, "ReferenceArcLengthMeters": 1.9799999999999982, "HeadingRadians": 0.82542852165703573, "UnwrappedHeadingRadians": 0.82542852165703573, "Source": 4 }, + { "X": 2.7764244476704873, "Y": 1.786210614200662, "ReferenceArcLengthMeters": 1.9999999999999982, "HeadingRadians": 0.83333333333333237, "UnwrappedHeadingRadians": 0.83333333333333237, "Source": 4 }, + { "X": 2.7925350453990334, "Y": 1.803999635249544, "ReferenceArcLengthMeters": 2.0239999999999982, "HeadingRadians": 0.835253334801853, "UnwrappedHeadingRadians": 0.835253334801853, "Source": 4 }, + { "X": 2.80865418003672, "Y": 1.8217809211927092, "ReferenceArcLengthMeters": 2.0479999999999983, "HeadingRadians": 0.83333333355178374, "UnwrappedHeadingRadians": 0.83333333355178374, "Source": 4 }, + { "X": 2.8248074262445737, "Y": 1.8395312269498194, "ReferenceArcLengthMeters": 2.0719999999999983, "HeadingRadians": 0.8318933325232104, "UnwrappedHeadingRadians": 0.8318933325232104, "Source": 4 }, + { "X": 2.8409691920174533, "Y": 1.8572737784943611, "ReferenceArcLengthMeters": 2.0959999999999983, "HeadingRadians": 0.83237333274470626, "UnwrappedHeadingRadians": 0.83237333274470626, "Source": 4 }, + { "X": 2.8571139169604551, "Y": 1.8750318365841865, "ReferenceArcLengthMeters": 2.1199999999999983, "HeadingRadians": 0.83333333333333237, "UnwrappedHeadingRadians": 0.83333333333333237, "Source": 4 } + ] +} diff --git a/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticEvidenceLoader.cs b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticEvidenceLoader.cs new file mode 100644 index 0000000..87f0045 --- /dev/null +++ b/ClumsyPilot/ParkrobTrajplanner/PathSmoothing/Test/LocalG2DiagnosticEvidenceLoader.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using System.IO; +using MultiWheelC.TrajectoryPlanning.PathSmoothing; +using Newtonsoft.Json; + +namespace MultiWheelC.TrajectoryPlanning.PathSmoothing.Test; + +public sealed class LocalG2DiagnosticEvidenceLoader +{ + private const string SourceMeasurementBatchSha256 = "ac8166828813d85bf6f8b58f985839e5b2a049e75cb94186ed04f59d540e4eed"; + private const string FixtureSha256 = "3d05daee5a211b3e7aa0b77193423b5fa07d3135e241a4413be3518fc7efe563"; + private const string CandidateStableKey = "single-turn/s0/r0/w5/seed2"; + private const string CandidateSha256 = "7cefb76c77e48a49bf3212e7a2472e23036c3899daa94b5c6b68fa5db29a3392"; + + public LocalG2DiagnosticEvidence LoadAndVerify(string evidencePath) + { + try + { + if (string.IsNullOrWhiteSpace(evidencePath)) + throw new InvalidDataException("Diagnostic evidence path is required."); + + LocalG2DiagnosticEvidence evidence = JsonConvert.DeserializeObject(File.ReadAllText(evidencePath)); + Validate(evidence); + return evidence; + } + catch (InvalidDataException) + { + throw; + } + catch (Exception exception) + { + throw new InvalidDataException("Local G2 diagnostic evidence is invalid.", exception); + } + } + + private static void Validate(LocalG2DiagnosticEvidence evidence) + { + if (evidence == null || + !string.Equals(evidence.SourceMeasurementBatchSha256, SourceMeasurementBatchSha256, StringComparison.Ordinal) || + !string.Equals(evidence.FixtureSha256, FixtureSha256, StringComparison.Ordinal) || + !string.Equals(evidence.ScenarioId, "single-turn", StringComparison.Ordinal) || + !string.Equals(evidence.CandidateStableKey, CandidateStableKey, StringComparison.Ordinal) || + !string.Equals(evidence.CandidateSha256, CandidateSha256, StringComparison.Ordinal) || + evidence.CandidateIndex != 5 || evidence.SegmentIndex != 0 || + !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) + { + throw new InvalidDataException("Local G2 diagnostic evidence contract is invalid."); + } + + double previousArc = 0d; + 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) || + point.Source != (int)SmoothedPathPointSource.LocalG2Transition || + (index > 0 && point.ReferenceArcLengthMeters.Value <= previousArc)) + { + throw new InvalidDataException("Local G2 diagnostic evidence point contract is invalid."); + } + + previousArc = point.ReferenceArcLengthMeters.Value; + } + + if (Math.Abs(evidence.CandidatePoints[0].ReferenceArcLengthMeters.Value - evidence.WindowStartArcLengthMeters.Value) > 1e-8d || + Math.Abs(evidence.CandidatePoints[evidence.CandidatePoints.Count - 1].ReferenceArcLengthMeters.Value - evidence.WindowEndArcLengthMeters.Value) > 1e-8d) + { + throw new InvalidDataException("Local G2 diagnostic evidence window contract is invalid."); + } + } + + private static bool IsFinite(double? value) + { + return value.HasValue && IsFinite(value.Value); + } + + private static bool IsFinite(double value) => !double.IsNaN(value) && !double.IsInfinity(value); +} + +public sealed class LocalG2DiagnosticEvidence +{ + public string SourceMeasurementBatchSha256 { get; set; } + public string FixtureSha256 { get; set; } + 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 double? WindowStartArcLengthMeters { get; set; } + public double? WindowEndArcLengthMeters { get; set; } + public double? StartGeometricCurvaturePerMeter { get; set; } + public double? EndGeometricCurvaturePerMeter { get; set; } + public double? StartVehicleCurvaturePerMeter { get; set; } + public double? EndVehicleCurvaturePerMeter { get; set; } + public List CandidatePoints { get; set; } + public string EvaluatorResult { get; set; } + public string StopGate { get; set; } + public string PublishedStatus { get; set; } +} + +public sealed class LocalG2DiagnosticEvidencePoint +{ + public double? X { get; set; } + public double? Y { get; set; } + public double? ReferenceArcLengthMeters { get; set; } + public double? HeadingRadians { get; set; } + public double? UnwrappedHeadingRadians { 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 new file mode 100644 index 0000000..fca18b8 --- /dev/null +++ b/ClumsyPilot/tests/verify_path_smoothing_local_g2_diagnostic_evidence.ps1 @@ -0,0 +1,56 @@ +$ErrorActionPreference = 'Stop' + +function Assert-Equal($expected, $actual, [string]$message) { + if ($expected -ne $actual) { + throw "$message Expected: '$expected'. Actual: '$actual'." + } +} + +function Assert-True([bool]$condition, [string]$message) { + if (-not $condition) { + throw $message + } +} + +$repositoryRoot = Split-Path -Parent $PSScriptRoot +$assemblyPath = Join-Path $repositoryRoot 'bin\Debug\netstandard2.0\ClumsyPilot.dll' +$fixturePath = Join-Path $repositoryRoot 'ParkrobTrajplanner\PathSmoothing\Test\Fixtures\local-g2-diagnostic-single-turn.json' + +if (-not (Test-Path -LiteralPath $assemblyPath)) { + throw "ClumsyPilot assembly was not found: $assemblyPath" +} + +$assembly = [System.Reflection.Assembly]::LoadFrom($assemblyPath) +$loaderType = $assembly.GetType('MultiWheelC.TrajectoryPlanning.PathSmoothing.Test.LocalG2DiagnosticEvidenceLoader', $true) +$loader = [System.Activator]::CreateInstance($loaderType) +$loadAndVerify = $loaderType.GetMethod('LoadAndVerify') +$evidence = $loadAndVerify.Invoke($loader, @($fixturePath)) + +Assert-Equal 'single-turn' $evidence.ScenarioId 'Diagnostic evidence scenario must be stable.' +Assert-Equal 'single-turn/s0/r0/w5/seed2' $evidence.CandidateStableKey 'Diagnostic evidence key must be stable.' +Assert-Equal 5 $evidence.CandidateIndex 'Diagnostic evidence candidate index must be stable.' +Assert-Equal 10 $evidence.CandidatePoints.Count 'Diagnostic evidence must retain all ten recorded samples.' +Assert-Equal 'InsufficientClearance' $evidence.EvaluatorResult 'Diagnostic evidence must retain the observed evaluator result.' +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 + try { + $null = $loadAndVerify.Invoke($loader, @($temporaryEvidencePath)) + } + catch { + $rejected = $true + } + + Assert-True $rejected 'Tampered diagnostic evidence must be rejected.' +} +finally { + if (Test-Path -LiteralPath $temporaryEvidencePath) { + Remove-Item -LiteralPath $temporaryEvidencePath -Force + } +}