Browse Source

report: bench completion is self-describing — no pool-size hardcode

The ◐ 'bench started, not finished' badge fired for every v2.7.0 lane:
bench_complete() matched max_points == 176, the old dev-era pool, while
current runs report 184 (92×2). Completion now reads the JSON's own
markers — status ('interrupted' fails), total_scenarios vs
scenario_results coverage, final_score for legacy status-less files —
so any future harness pool sizes just work.
Tests cover legacy-176, v2.7.0-184, partial-coverage and interrupted.
Alexander Korolev 3 tuần trước cách đây
mục cha
commit
7eba2f933f
3 tập tin đã thay đổi với 58 bổ sung và 17 xóa
  1. 3 2
      src/report/mod.rs
  2. 31 12
      src/report/parse.rs
  3. 24 3
      src/report/tests.rs

+ 3 - 2
src/report/mod.rs

@@ -51,8 +51,9 @@ pub(crate) struct ReportEntry {
     points: Option<String>,
     /// points/max as a ratio, for sorting the Points column (null when absent).
     ratio: Option<f64>,
-    /// The bench covered the full scenario pool (max_points == 176, or a
-    /// text report that printed its final summary — those only print at
+    /// The bench ran to completion — decided from the JSON's own
+    /// `status`/coverage metadata (pool size never assumed), or for a
+    /// text report from its printed final summary (those only print at
     /// the end of a completed run).
     #[serde(default)]
     done: bool,

+ 31 - 12
src/report/parse.rs

@@ -33,19 +33,38 @@ pub(crate) fn parse_bench_json(content: &str) -> Option<(f64, Option<String>, Op
     Some((pct, points, ratio))
 }
 
-/// Did this bench cover the full scenario pool? The campaign's hardmode
-/// pool is 88 scenarios × 2 pt = 176 (`max_points` in the JSON). A run
-/// interrupted early writes fewer. Non-bench JSONs → false.
+/// Did this bench run to the end? Decided from the JSON's own metadata,
+/// never from a fixed pool size: the tool stamps `status` (absent on
+/// legacy files) and `total_scenarios` beside `scenario_results`, so
+/// coverage is self-describing for every harness generation (88×2=176 in
+/// the dev-era, 92×2=184 since v2.7.0, whatever comes next). A run
+/// interrupted early either says so in `status` or leaves the results
+/// map shorter than the scenario count.
 pub(crate) fn bench_complete(content: &str) -> bool {
-    serde_json::from_str::<serde_json::Value>(content)
-        .ok()
-        .and_then(|v| {
-            v.get("scores")?
-                .get("max_points")?
-                .as_u64()
-                .map(|mp| mp == 176)
-        })
-        .unwrap_or(false)
+    let Ok(v) = serde_json::from_str::<serde_json::Value>(content) else {
+        return false;
+    };
+    let scores = match v.get("scores") {
+        Some(s) if s.is_object() => s,
+        _ => return false,
+    };
+    let covered = match (
+        v.get("total_scenarios").and_then(|x| x.as_u64()),
+        scores
+            .get("scenario_results")
+            .and_then(|x| x.as_object())
+            .map(|m| m.len() as u64),
+    ) {
+        (Some(total), Some(ran)) => ran >= total,
+        _ => true, // no coverage metadata to contradict
+    };
+    match v.get("status").and_then(|x| x.as_str()) {
+        Some("completed") => covered,
+        Some(_) => false,
+        // Legacy files carry no status — a printed final score was
+        // written only by a finished run.
+        None => scores.get("final_score").and_then(|x| x.as_f64()).is_some() && covered,
+    }
 }
 
 /// First non-empty perf box among the given contents (md first, then log —

+ 24 - 3
src/report/tests.rs

@@ -170,12 +170,33 @@ fn qrow(name: &str, parent: &str, model: &str, tp: u32, pct: Option<f64>) -> Rep
 
 #[test]
 fn bench_complete_requires_full_pool() {
+    // Legacy dev-era file (176-point pool, no status marker).
     assert!(bench_complete(
-        r#"{"scores": {"final_score": 92, "total_points": 162, "max_points": 176}}"#
+        r#"{"scores": {"final_score": 92, "total_points": 162, "max_points": 176, "scenario_results": {}}}"#
     ));
-    // interrupted run: pool not fully covered
+    // v2.7.0 pool (184 pts) — completed by coverage, no hardcode.
+    let mut results = serde_json::Map::new();
+    for i in 0..92 {
+        results.insert(format!("TC-{i}"), serde_json::json!({"pass": true}));
+    }
+    assert!(bench_complete(
+        &serde_json::json!({"status": "completed", "total_scenarios": 92,
+            "scores": {"final_score": 91, "total_points": 168, "max_points": 184,
+                       "scenario_results": results}})
+        .to_string()
+    ));
+    // Interrupted early: fewer scenarios ran than the tool declares.
+    let partial =
+        serde_json::Map::from_iter((0..44).map(|i| (format!("TC-{i}"), serde_json::json!({}))));
+    assert!(!bench_complete(
+        &serde_json::json!({"status": "completed", "total_scenarios": 92,
+            "scores": {"final_score": 45, "total_points": 88, "max_points": 184,
+                       "scenario_results": partial}})
+        .to_string()
+    ));
+    // Explicit non-completed status always fails, coverage or not.
     assert!(!bench_complete(
-        r#"{"scores": {"final_score": 40, "total_points": 70, "max_points": 88}}"#
+        r#"{"status": "interrupted", "total_scenarios": 92, "scores": {"final_score": 90, "scenario_results": {}}}"#
     ));
     assert!(!bench_complete("not json"));
 }