Skip to main content

sim_lib_standard_core/
harness.rs

1//! Conformance harness running profile test cases and reporting fidelity.
2
3use std::{
4    collections::{BTreeMap, BTreeSet},
5    sync::Arc,
6};
7
8use sim_kernel::{
9    Claim, ClaimKind, ClaimPattern, Cx, Datum, DatumStore, OpKey, Ref, Result, Symbol,
10    card::{card_kind_predicate, card_tests_predicate},
11    standard::standard_evidence_predicate,
12};
13
14use crate::{
15    CharacterizationScenario, FidelityBadge, LanguageProfile, ScenarioObservationLane,
16    standard_test_capability,
17};
18
19/// A conformance check: runs a profile against the runtime and reports an outcome.
20pub type ConformanceCheck =
21    Arc<dyn Fn(&mut Cx, &LanguageProfile) -> Result<ConformanceOutcome> + Send + Sync + 'static>;
22
23/// Status of a conformance outcome.
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum ConformanceStatus {
26    /// The check passed.
27    Pass,
28    /// The check failed.
29    Fail,
30    /// The case is a declared gap and is excluded from fidelity ratios.
31    Gap,
32}
33
34/// Result of running one [`ConformanceTestCase`]: pass, fail, or declared gap
35/// with optional detail.
36#[derive(Clone, Debug, PartialEq, Eq)]
37pub struct ConformanceOutcome {
38    /// Whether the check passed.
39    pub passed: bool,
40    /// Optional failure detail.
41    pub detail: Option<String>,
42    /// Exact status used by matrix runners and claim publication.
43    pub status: ConformanceStatus,
44}
45
46impl ConformanceOutcome {
47    /// A passing outcome with no detail.
48    pub fn pass() -> Self {
49        Self {
50            passed: true,
51            detail: None,
52            status: ConformanceStatus::Pass,
53        }
54    }
55
56    /// A failing outcome carrying `detail`.
57    pub fn fail(detail: impl Into<String>) -> Self {
58        Self {
59            passed: false,
60            detail: Some(detail.into()),
61            status: ConformanceStatus::Fail,
62        }
63    }
64
65    /// A failing outcome carrying `detail`.
66    pub fn fail_with(detail: impl Into<String>) -> Self {
67        Self::fail(detail)
68    }
69
70    /// A declared gap outcome carrying a detail string.
71    pub fn gap(detail: impl Into<String>) -> Self {
72        Self {
73            passed: false,
74            detail: Some(detail.into()),
75            status: ConformanceStatus::Gap,
76        }
77    }
78
79    /// Returns whether this outcome is a pass.
80    pub fn is_pass(&self) -> bool {
81        self.status == ConformanceStatus::Pass
82    }
83
84    /// Returns whether this outcome is a fail.
85    pub fn is_fail(&self) -> bool {
86        self.status == ConformanceStatus::Fail
87    }
88
89    /// Returns whether this outcome is a declared gap.
90    pub fn is_gap(&self) -> bool {
91        self.status == ConformanceStatus::Gap
92    }
93
94    /// Returns the standard status symbol for this outcome.
95    pub fn status_symbol(&self) -> Symbol {
96        match self.status {
97            ConformanceStatus::Pass => Symbol::qualified("standard/test", "pass"),
98            ConformanceStatus::Fail => Symbol::qualified("standard/test", "fail"),
99            ConformanceStatus::Gap => Symbol::qualified("standard/test", "gap"),
100        }
101    }
102}
103
104/// One conformance test: its symbol, the organ it covers, an optional badge it
105/// affects, and the check closure.
106#[derive(Clone)]
107pub struct ConformanceTestCase {
108    /// Symbol identifying the test.
109    pub symbol: Symbol,
110    /// Organ the test exercises.
111    pub organ: Symbol,
112    /// Fidelity badge whose level drops if this test fails, if any.
113    pub affected_badge: Option<Symbol>,
114    check: ConformanceCheck,
115}
116
117impl ConformanceTestCase {
118    /// Build a test for `organ` identified by `symbol`, running `check`.
119    pub fn new(symbol: Symbol, organ: Symbol, check: ConformanceCheck) -> Self {
120        Self {
121            symbol,
122            organ,
123            affected_badge: None,
124            check,
125        }
126    }
127
128    /// Mark this test as affecting `badge`, lowering its level on failure.
129    pub fn affecting_badge(mut self, badge: Symbol) -> Self {
130        self.affected_badge = Some(badge);
131        self
132    }
133
134    fn run(&self, cx: &mut Cx, profile: &LanguageProfile) -> Result<ConformanceOutcome> {
135        (self.check)(cx, profile)
136    }
137}
138
139/// Registry of conformance tests grouped by the organ they cover.
140#[derive(Default)]
141pub struct ConformanceHarness {
142    tests: BTreeMap<Symbol, Vec<ConformanceTestCase>>,
143    scenarios: BTreeMap<Symbol, CharacterizationScenario>,
144    supported_scenario_lanes: BTreeSet<ScenarioObservationLane>,
145}
146
147impl ConformanceHarness {
148    /// Create an empty harness.
149    pub fn new() -> Self {
150        Self {
151            tests: BTreeMap::new(),
152            scenarios: BTreeMap::new(),
153            supported_scenario_lanes: BTreeSet::from([
154                ScenarioObservationLane::ValueOrFailure,
155                ScenarioObservationLane::Events,
156                ScenarioObservationLane::Receipts,
157                ScenarioObservationLane::Browse,
158            ]),
159        }
160    }
161
162    /// Register `test` under its organ.
163    pub fn register_test(&mut self, test: ConformanceTestCase) {
164        self.tests.entry(test.organ.clone()).or_default().push(test);
165    }
166
167    /// Tests registered for `organ`, or an empty slice if none.
168    pub fn tests_for_organ(&self, organ: &Symbol) -> &[ConformanceTestCase] {
169        self.tests.get(organ).map(Vec::as_slice).unwrap_or_default()
170    }
171
172    /// Total number of registered tests across all organs.
173    pub fn test_count(&self) -> usize {
174        self.tests.values().map(Vec::len).sum()
175    }
176
177    /// Restrict the scenario observation lanes supported by this harness.
178    pub fn with_supported_scenario_lanes(
179        mut self,
180        lanes: impl IntoIterator<Item = ScenarioObservationLane>,
181    ) -> Self {
182        self.supported_scenario_lanes = lanes.into_iter().collect();
183        self
184    }
185
186    /// Register one uniquely identified characterization scenario.
187    pub fn register_scenario(&mut self, scenario: CharacterizationScenario) -> Result<()> {
188        if self.scenarios.contains_key(&scenario.spec.id) {
189            return Err(sim_kernel::Error::Eval(format!(
190                "duplicate scenario id {}",
191                scenario.spec.id
192            )));
193        }
194        self.scenarios.insert(scenario.spec.id.clone(), scenario);
195        Ok(())
196    }
197
198    /// Preflight every registered scenario, then run them in stable id order.
199    ///
200    /// No driver runs unless the complete registry is valid.
201    pub fn run_scenarios(&self, cx: &mut Cx) -> Result<Vec<Symbol>> {
202        for scenario in self.scenarios.values() {
203            scenario.spec.validate(&self.supported_scenario_lanes)?;
204        }
205        let mut completed = Vec::with_capacity(self.scenarios.len());
206        for scenario in self.scenarios.values() {
207            (scenario.driver)(cx, &scenario.spec)?;
208            completed.push(scenario.spec.id.clone());
209        }
210        Ok(completed)
211    }
212}
213
214/// Report of running the harness against a profile: per-organ results and the
215/// fidelity badges as lowered by any failures.
216#[derive(Clone, Debug, PartialEq, Eq)]
217pub struct StandardTestReport {
218    /// Symbol of the tested profile.
219    pub profile: Symbol,
220    /// Per-organ test reports.
221    pub organs: Vec<OrganTestReport>,
222    /// Fidelity badges after applying test failures.
223    pub reported_badges: Vec<FidelityBadge>,
224}
225
226impl StandardTestReport {
227    /// Whether every organ's tests passed.
228    pub fn passed(&self) -> bool {
229        self.organs.iter().all(OrganTestReport::passed)
230    }
231
232    /// Total number of test results across all organs.
233    pub fn result_count(&self) -> usize {
234        self.organs.iter().map(|organ| organ.tests.len()).sum()
235    }
236}
237
238/// Per-organ slice of a [`StandardTestReport`].
239#[derive(Clone, Debug, PartialEq, Eq)]
240pub struct OrganTestReport {
241    /// The organ these results cover.
242    pub organ: Symbol,
243    /// Per-test reports for this organ.
244    pub tests: Vec<ConformanceTestReport>,
245}
246
247impl OrganTestReport {
248    /// Whether every test for this organ passed.
249    pub fn passed(&self) -> bool {
250        self.tests.iter().all(|test| test.passed)
251    }
252}
253
254/// Result of one conformance test, with a reference to its published evidence.
255#[derive(Clone, Debug, PartialEq, Eq)]
256pub struct ConformanceTestReport {
257    /// Symbol of the test.
258    pub test: Symbol,
259    /// Whether the test passed.
260    pub passed: bool,
261    /// Optional failure detail.
262    pub detail: Option<String>,
263    /// Reference to the published test-run evidence.
264    pub evidence: Ref,
265}
266
267/// Operation key for the standard test operation.
268pub fn standard_test_op_key() -> OpKey {
269    OpKey::new(Symbol::new("standard"), Symbol::new("test"), 1)
270}
271
272/// Datum tag identifying a published test-run record.
273pub fn standard_test_run_kind() -> Symbol {
274    Symbol::qualified("standard", "test-run")
275}
276
277/// Claim predicate relating a subject to a test-run evidence ref.
278pub fn standard_test_result_predicate() -> Symbol {
279    standard_symbol("test-result")
280}
281
282/// Claim predicate relating a test run to its profile.
283pub fn standard_test_profile_predicate() -> Symbol {
284    standard_symbol("test-profile")
285}
286
287/// Claim predicate relating a test run to its organ.
288pub fn standard_test_organ_predicate() -> Symbol {
289    standard_symbol("test-organ")
290}
291
292/// Claim predicate relating a test run to its test case.
293pub fn standard_test_case_predicate() -> Symbol {
294    standard_symbol("test-case")
295}
296
297/// Claim predicate relating a test run to its pass/fail status.
298pub fn standard_test_status_predicate() -> Symbol {
299    standard_symbol("test-status")
300}
301
302/// Claim predicate relating a subject to its reported fidelity badge.
303pub fn standard_reported_fidelity_predicate() -> Symbol {
304    standard_symbol("reported-fidelity")
305}
306
307/// Claim predicate relating a subject to its reported fidelity level.
308pub fn standard_reported_fidelity_level_predicate() -> Symbol {
309    standard_symbol("reported-fidelity-level")
310}
311
312/// Run `harness` against `profile`, gated on [`standard_test_capability`].
313///
314/// Each test publishes a test-run record and claims; a failed test lowers the
315/// level of any badge it affects. Returns a [`StandardTestReport`].
316///
317/// [`standard_test_capability`]: crate::standard_test_capability
318pub fn standard_test_stub(
319    cx: &mut Cx,
320    harness: &ConformanceHarness,
321    profile: &LanguageProfile,
322) -> Result<StandardTestReport> {
323    cx.require(&standard_test_capability())?;
324    let mut organs = Vec::with_capacity(profile.organs.len());
325    let mut failed_badges = BTreeMap::<Symbol, Ref>::new();
326
327    for organ in &profile.organs {
328        let mut tests = Vec::new();
329        for test in harness.tests_for_organ(&organ.organ) {
330            let outcome = test.run(cx, profile)?;
331            let evidence = publish_test_run(cx, profile, &organ.organ, test, &outcome)?;
332            if outcome.is_fail()
333                && let Some(badge) = &test.affected_badge
334            {
335                failed_badges.insert(badge.clone(), evidence.clone());
336            }
337            tests.push(ConformanceTestReport {
338                test: test.symbol.clone(),
339                passed: outcome.passed,
340                detail: outcome.detail,
341                evidence,
342            });
343        }
344        organs.push(OrganTestReport {
345            organ: organ.organ.clone(),
346            tests,
347        });
348    }
349
350    let reported_badges = lowered_badges(profile, &failed_badges);
351    publish_reported_badges(cx, &reported_badges)?;
352    Ok(StandardTestReport {
353        profile: profile.symbol.clone(),
354        organs,
355        reported_badges,
356    })
357}
358
359fn lowered_badges(
360    profile: &LanguageProfile,
361    failed_badges: &BTreeMap<Symbol, Ref>,
362) -> Vec<FidelityBadge> {
363    profile
364        .fidelity_badges
365        .iter()
366        .map(|badge| {
367            let mut reported = badge.clone();
368            if let Some(evidence) = failed_badges.get(&badge.badge) {
369                reported.level = reported.level.saturating_sub(1);
370                reported.evidence = evidence.clone();
371            }
372            reported
373        })
374        .collect()
375}
376
377fn publish_test_run(
378    cx: &mut Cx,
379    profile: &LanguageProfile,
380    organ: &Symbol,
381    test: &ConformanceTestCase,
382    outcome: &ConformanceOutcome,
383) -> Result<Ref> {
384    let evidence = test_run_ref(cx, profile, organ, test, outcome)?;
385    let status = outcome.status_symbol();
386    insert_observed_once(
387        cx,
388        evidence.clone(),
389        card_kind_predicate(),
390        Ref::Symbol(standard_test_run_kind()),
391    )?;
392    insert_observed_once(
393        cx,
394        evidence.clone(),
395        card_tests_predicate(),
396        Ref::Symbol(test.symbol.clone()),
397    )?;
398    insert_observed_once(
399        cx,
400        evidence.clone(),
401        standard_test_profile_predicate(),
402        Ref::Symbol(profile.symbol.clone()),
403    )?;
404    insert_observed_once(
405        cx,
406        evidence.clone(),
407        standard_test_organ_predicate(),
408        Ref::Symbol(organ.clone()),
409    )?;
410    insert_observed_once(
411        cx,
412        evidence.clone(),
413        standard_test_case_predicate(),
414        Ref::Symbol(test.symbol.clone()),
415    )?;
416    insert_observed_once(
417        cx,
418        evidence.clone(),
419        standard_test_status_predicate(),
420        Ref::Symbol(status),
421    )?;
422    insert_observed_once(
423        cx,
424        Ref::Symbol(profile.symbol.clone()),
425        standard_test_result_predicate(),
426        evidence.clone(),
427    )?;
428    insert_observed_once(
429        cx,
430        Ref::Symbol(organ.clone()),
431        standard_test_result_predicate(),
432        evidence.clone(),
433    )?;
434    insert_observed_once(
435        cx,
436        Ref::Symbol(profile.symbol.clone()),
437        standard_evidence_predicate(),
438        evidence.clone(),
439    )?;
440    Ok(evidence)
441}
442
443fn publish_reported_badges(cx: &mut Cx, badges: &[FidelityBadge]) -> Result<()> {
444    let mut seen = BTreeSet::new();
445    for badge in badges {
446        if !seen.insert((badge.subject.clone(), badge.badge.clone())) {
447            continue;
448        }
449        let evidence = vec![badge.evidence.clone()];
450        insert_observed_with_evidence_once(
451            cx,
452            badge.subject.clone(),
453            standard_reported_fidelity_predicate(),
454            Ref::Symbol(badge.badge.clone()),
455            evidence.clone(),
456        )?;
457        insert_observed_with_evidence_once(
458            cx,
459            badge.subject.clone(),
460            standard_reported_fidelity_level_predicate(),
461            Ref::Symbol(Symbol::qualified(
462                "standard/fidelity-level",
463                badge.level.to_string(),
464            )),
465            evidence,
466        )?;
467    }
468    Ok(())
469}
470
471fn test_run_ref(
472    cx: &mut Cx,
473    profile: &LanguageProfile,
474    organ: &Symbol,
475    test: &ConformanceTestCase,
476    outcome: &ConformanceOutcome,
477) -> Result<Ref> {
478    let mut fields = vec![
479        (
480            Symbol::new("profile"),
481            Datum::Symbol(profile.symbol.clone()),
482        ),
483        (Symbol::new("organ"), Datum::Symbol(organ.clone())),
484        (Symbol::new("test"), Datum::Symbol(test.symbol.clone())),
485        (Symbol::new("passed"), Datum::Bool(outcome.passed)),
486        (
487            Symbol::new("status"),
488            Datum::Symbol(outcome.status_symbol()),
489        ),
490    ];
491    if let Some(detail) = &outcome.detail {
492        fields.push((Symbol::new("detail"), Datum::String(detail.clone())));
493    }
494    cx.datum_store_mut()
495        .intern(Datum::Node {
496            tag: standard_test_run_kind(),
497            fields,
498        })
499        .map(Ref::Content)
500}
501
502fn insert_observed_once(cx: &mut Cx, subject: Ref, predicate: Symbol, object: Ref) -> Result<()> {
503    insert_observed_with_evidence_once(cx, subject, predicate, object, Vec::new())
504}
505
506fn insert_observed_with_evidence_once(
507    cx: &mut Cx,
508    subject: Ref,
509    predicate: Symbol,
510    object: Ref,
511    evidence: Vec<Ref>,
512) -> Result<()> {
513    let exists = !cx
514        .query_facts(ClaimPattern::exact(
515            subject.clone(),
516            predicate.clone(),
517            object.clone(),
518        ))?
519        .is_empty();
520    if !exists {
521        cx.insert_fact(
522            Claim::public(subject, predicate, object)
523                .with_kind(ClaimKind::Observed)
524                .with_evidence(evidence),
525        )?;
526    }
527    Ok(())
528}
529
530fn standard_symbol(name: &str) -> Symbol {
531    Symbol::qualified("standard", name.to_owned())
532}