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