Skip to main content

omena_testkit/
fixture.rs

1use serde::Serialize;
2use std::collections::BTreeSet;
3
4/// One reusable fixture seed consumed by the testkit boundary report.
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub struct OmenaTestkitFixtureSeedV0 {
7    /// Stable fixture label.
8    pub label: &'static str,
9    /// Fixture lane such as `sass-language` or `cascade-proof`.
10    pub lane: &'static str,
11    /// Raw `omena-fixture-v0` text.
12    pub raw: &'static str,
13    /// Product surfaces expected to consume this fixture.
14    pub expected_products: &'static [&'static str],
15    /// Promotion target for M4.
16    pub promotion_target: &'static str,
17}
18
19/// Parsed reusable fixture.
20#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
21#[serde(rename_all = "camelCase")]
22pub struct OmenaFixtureV0 {
23    /// Fixture grammar version.
24    pub schema_version: &'static str,
25    /// Parsed files.
26    pub files: Vec<OmenaFixtureFileV0>,
27    /// Parsed expectations.
28    pub expectations: Vec<OmenaFixtureExpectationV0>,
29}
30
31/// One file section in a reusable fixture.
32#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
33#[serde(rename_all = "camelCase")]
34pub struct OmenaFixtureFileV0 {
35    /// Workspace-relative file path.
36    pub path: String,
37    /// File-header metadata such as dialect or layer.
38    pub metadata: Vec<OmenaFixtureFileMetadataV0>,
39    /// Markers removed from the source while preserving clean-source offsets.
40    pub markers: Vec<OmenaFixtureMarkerV0>,
41    /// File text.
42    pub source: String,
43}
44
45/// One metadata key/value pair from a fixture file header.
46#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
47#[serde(rename_all = "camelCase")]
48pub struct OmenaFixtureFileMetadataV0 {
49    /// Metadata key.
50    pub key: String,
51    /// Metadata value.
52    pub value: String,
53}
54
55/// One marker extracted from fixture source text.
56#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
57#[serde(rename_all = "camelCase")]
58pub struct OmenaFixtureMarkerV0 {
59    /// Marker kind such as `cursor`, `namedPoint`, or `rangeStart`.
60    pub kind: &'static str,
61    /// Optional marker payload.
62    pub name: Option<String>,
63    /// Byte offset in the cleaned source.
64    pub byte_start: usize,
65    /// Byte end in the cleaned source.
66    pub byte_end: usize,
67}
68
69/// One expectation section in a reusable fixture.
70#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
71#[serde(rename_all = "camelCase")]
72pub struct OmenaFixtureExpectationV0 {
73    /// Expectation key.
74    pub key: String,
75    /// Expectation text.
76    pub value: String,
77}
78
79/// Known `omena-fixture-v0` expectation families.
80#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
81#[serde(rename_all = "camelCase")]
82pub enum OmenaFixtureExpectationKindV0 {
83    /// Product-surface expectation.
84    Product,
85    /// Free-form assertion text.
86    Assertion,
87    /// Expected diagnostic with code/range/message details in the body.
88    Diagnostic,
89    /// A diagnostic code or code pattern that must not appear.
90    NoDiagnostic,
91    /// Expected diagnostic count, usually `<code>:<n>`.
92    Count,
93    /// Expected cascade winner declaration id.
94    CascadeOutcome,
95    /// Expected declaration participating in a cascade witness.
96    CascadeWitness,
97    /// Expected external-reference boundary state.
98    BoundaryState,
99    /// Unknown or product-owned expectation family.
100    Unknown,
101}
102
103impl OmenaFixtureExpectationV0 {
104    /// Classify the expectation family without interpreting product-owned
105    /// expectation payloads.
106    pub fn kind(&self) -> OmenaFixtureExpectationKindV0 {
107        omena_fixture_expectation_kind_from_key(&self.key)
108    }
109}
110
111/// Classify a raw expectation key by its leading keyword.
112pub fn omena_fixture_expectation_kind_from_key(key: &str) -> OmenaFixtureExpectationKindV0 {
113    match key.split_whitespace().next().unwrap_or_default() {
114        "product" => OmenaFixtureExpectationKindV0::Product,
115        "assertion" => OmenaFixtureExpectationKindV0::Assertion,
116        "diagnostic" => OmenaFixtureExpectationKindV0::Diagnostic,
117        "no-diagnostic" => OmenaFixtureExpectationKindV0::NoDiagnostic,
118        "count" => OmenaFixtureExpectationKindV0::Count,
119        "cascade-outcome" => OmenaFixtureExpectationKindV0::CascadeOutcome,
120        "cascade-witness" => OmenaFixtureExpectationKindV0::CascadeWitness,
121        "boundary-state" => OmenaFixtureExpectationKindV0::BoundaryState,
122        _ => OmenaFixtureExpectationKindV0::Unknown,
123    }
124}
125
126/// Parsed fixture seed evidence.
127#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
128#[serde(rename_all = "camelCase")]
129pub struct OmenaTestkitFixtureSeedReportV0 {
130    /// Stable fixture label.
131    pub label: &'static str,
132    /// Fixture lane.
133    pub lane: &'static str,
134    /// Whether the fixture parses with `omena-fixture-v0`.
135    pub parses: bool,
136    /// Parse error when present.
137    pub parse_error: Option<String>,
138    /// Parsed file count.
139    pub file_count: usize,
140    /// Parsed expectation count.
141    pub expectation_count: usize,
142    /// Parsed file-header metadata count.
143    pub metadata_count: usize,
144    /// Parsed marker count.
145    pub marker_count: usize,
146    /// Expected product surfaces.
147    pub expected_products: Vec<&'static str>,
148    /// Promotion target for M4.
149    pub promotion_target: &'static str,
150    /// Per-expectation evaluation outcomes.
151    ///
152    /// Evaluated against an empty engine output here (the seed corpus is a
153    /// parse-only substrate with no engine wired): `no-diagnostic` and
154    /// `count <code>:0` families pass, while `diagnostic`/`boundary-state`/
155    /// `cascade-outcome`/`cascade-witness` families fail as absent. An
156    /// engine-backed consumer supplies real diagnostics, boundary states, and
157    /// cascade projections via
158    /// [`crate::fixture_eval::evaluate_omena_fixture_v0`].
159    pub expectation_outcomes: Vec<crate::fixture_eval::OmenaFixtureExpectationOutcomeV0>,
160}
161
162/// Fixture seed corpus summary.
163#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
164#[serde(rename_all = "camelCase")]
165pub struct OmenaTestkitFixtureSeedCorpusReportV0 {
166    /// Schema version.
167    pub schema_version: &'static str,
168    /// Product surface name.
169    pub product: &'static str,
170    /// Fixture grammar.
171    pub fixture_grammar: &'static str,
172    /// Fixture count.
173    pub fixture_count: usize,
174    /// Covered lane count.
175    pub lane_count: usize,
176    /// Parsed metadata count across all seed files.
177    pub metadata_count: usize,
178    /// Parsed marker count across all seed files.
179    pub marker_count: usize,
180    /// Whether every seed parses with the shared fixture grammar.
181    pub all_seeds_parse: bool,
182    /// Seed reports.
183    pub reports: Vec<OmenaTestkitFixtureSeedReportV0>,
184}
185
186/// Summarize any `omena-fixture-v0` fixture seed corpus.
187pub fn summarize_omena_testkit_fixture_seed_corpus(
188    seeds: &[OmenaTestkitFixtureSeedV0],
189) -> OmenaTestkitFixtureSeedCorpusReportV0 {
190    let reports = seeds
191        .iter()
192        .copied()
193        .map(report_fixture_seed)
194        .collect::<Vec<_>>();
195    let all_seeds_parse = reports.iter().all(|report| report.parses);
196    let lane_count = reports
197        .iter()
198        .map(|report| report.lane)
199        .collect::<BTreeSet<_>>()
200        .len();
201    let metadata_count = reports.iter().map(|report| report.metadata_count).sum();
202    let marker_count = reports.iter().map(|report| report.marker_count).sum();
203
204    OmenaTestkitFixtureSeedCorpusReportV0 {
205        schema_version: "0",
206        product: "omena-testkit.fixture-seed-corpus",
207        fixture_grammar: "omena-fixture-v0",
208        fixture_count: reports.len(),
209        lane_count,
210        metadata_count,
211        marker_count,
212        all_seeds_parse,
213        reports,
214    }
215}
216
217/// Parse a `omena-fixture-v0` workspace fixture.
218pub fn parse_omena_fixture_v0(raw: &str) -> Result<OmenaFixtureV0, String> {
219    let mut files = Vec::new();
220    let mut expectations = Vec::new();
221    let mut current_file: Option<OmenaFixtureFileV0> = None;
222    let mut current_expectation: Option<OmenaFixtureExpectationV0> = None;
223
224    for line in raw.lines() {
225        if let Some(header) = line
226            .strip_prefix("--- file:")
227            .or_else(|| line.strip_prefix("//-"))
228        {
229            finish_fixture_section(&mut files, current_file.take());
230            finish_fixture_section(&mut expectations, current_expectation.take());
231            let (path, metadata) = parse_omena_fixture_file_header(header.trim())?;
232            current_file = Some(OmenaFixtureFileV0 {
233                path,
234                metadata,
235                markers: Vec::new(),
236                source: String::new(),
237            });
238            continue;
239        }
240
241        if let Some(key) = line.strip_prefix("--- expect:") {
242            finish_fixture_section(&mut files, current_file.take());
243            finish_fixture_section(&mut expectations, current_expectation.take());
244            current_expectation = Some(OmenaFixtureExpectationV0 {
245                key: key.trim().to_string(),
246                value: String::new(),
247            });
248            continue;
249        }
250
251        if let Some(file) = current_file.as_mut() {
252            push_fixture_line(&mut file.source, line);
253        } else if let Some(expectation) = current_expectation.as_mut() {
254            push_fixture_line(&mut expectation.value, line);
255        } else if !line.trim().is_empty() {
256            return Err("fixture content must start with a file or expect marker".to_string());
257        }
258    }
259
260    finish_fixture_section(&mut files, current_file.take());
261    finish_fixture_section(&mut expectations, current_expectation.take());
262
263    for file in &mut files {
264        let (cleaned_source, markers) = extract_omena_fixture_markers(&file.source)?;
265        file.source = cleaned_source;
266        file.markers = markers;
267    }
268
269    if files.is_empty() {
270        return Err("fixture must contain at least one file section".to_string());
271    }
272    if expectations.is_empty() {
273        return Err("fixture must contain at least one expectation section".to_string());
274    }
275
276    Ok(OmenaFixtureV0 {
277        schema_version: "0",
278        files,
279        expectations,
280    })
281}
282
283fn report_fixture_seed(seed: OmenaTestkitFixtureSeedV0) -> OmenaTestkitFixtureSeedReportV0 {
284    match parse_omena_fixture_v0(seed.raw) {
285        Ok(fixture) => {
286            let metadata_count = fixture.files.iter().map(|file| file.metadata.len()).sum();
287            let marker_count = fixture.files.iter().map(|file| file.markers.len()).sum();
288            // The seed corpus has no engine wired, so evaluate against empty
289            // engine output: this still exercises the evaluator at the hook
290            // point so the assertions are run, not merely parsed.
291            let expectation_outcomes =
292                crate::fixture_eval::evaluate_omena_fixture_v0(&fixture, &[], &[], &[]);
293            OmenaTestkitFixtureSeedReportV0 {
294                label: seed.label,
295                lane: seed.lane,
296                parses: true,
297                parse_error: None,
298                file_count: fixture.files.len(),
299                expectation_count: fixture.expectations.len(),
300                metadata_count,
301                marker_count,
302                expected_products: seed.expected_products.to_vec(),
303                promotion_target: seed.promotion_target,
304                expectation_outcomes,
305            }
306        }
307        Err(error) => OmenaTestkitFixtureSeedReportV0 {
308            label: seed.label,
309            lane: seed.lane,
310            parses: false,
311            parse_error: Some(error),
312            file_count: 0,
313            expectation_count: 0,
314            metadata_count: 0,
315            marker_count: 0,
316            expected_products: seed.expected_products.to_vec(),
317            promotion_target: seed.promotion_target,
318            expectation_outcomes: Vec::new(),
319        },
320    }
321}
322
323fn finish_fixture_section<T>(sections: &mut Vec<T>, current: Option<T>) {
324    if let Some(section) = current {
325        sections.push(section);
326    }
327}
328
329fn push_fixture_line(buffer: &mut String, line: &str) {
330    if !buffer.is_empty() {
331        buffer.push('\n');
332    }
333    buffer.push_str(line);
334}
335
336fn parse_omena_fixture_file_header(
337    header: &str,
338) -> Result<(String, Vec<OmenaFixtureFileMetadataV0>), String> {
339    let mut parts = header.split_whitespace();
340    let path = parts
341        .next()
342        .ok_or_else(|| "fixture file header must include a path".to_string())?;
343    if path.contains(':') {
344        return Err("fixture file header path must precede metadata".to_string());
345    }
346
347    let mut metadata = Vec::new();
348    for part in parts {
349        let Some((key, value)) = part.split_once(':') else {
350            return Err(format!("fixture metadata `{part}` must use key:value"));
351        };
352        validate_omena_fixture_metadata(key, value)?;
353        metadata.push(OmenaFixtureFileMetadataV0 {
354            key: key.to_string(),
355            value: value.to_string(),
356        });
357    }
358
359    Ok((path.to_string(), metadata))
360}
361
362fn validate_omena_fixture_metadata(key: &str, value: &str) -> Result<(), String> {
363    if value.is_empty() {
364        return Err(format!("fixture metadata `{key}` must have a value"));
365    }
366    match key {
367        "dialect" => match value {
368            "css" | "scss" | "less" => Ok(()),
369            _ => Err("fixture dialect metadata must be css, scss, or less".to_string()),
370        },
371        "layer" | "composes-from" | "consumer-of" => Ok(()),
372        _ => Err(format!("fixture metadata key `{key}` is not supported")),
373    }
374}
375
376fn extract_omena_fixture_markers(
377    source: &str,
378) -> Result<(String, Vec<OmenaFixtureMarkerV0>), String> {
379    let mut cleaned = String::new();
380    let mut markers = Vec::new();
381    let mut cursor = 0;
382
383    while let Some(relative_start) = source[cursor..].find("/*") {
384        let start = cursor + relative_start;
385        cleaned.push_str(&source[cursor..start]);
386        let Some(relative_end) = source[start + 2..].find("*/") else {
387            return Err("fixture marker comment is unterminated".to_string());
388        };
389        let end = start + 2 + relative_end + 2;
390        let body = &source[start + 2..end - 2];
391        if let Some(marker) = parse_omena_fixture_marker(body, cleaned.len())? {
392            markers.push(marker);
393        } else {
394            cleaned.push_str(&source[start..end]);
395        }
396        cursor = end;
397    }
398
399    cleaned.push_str(&source[cursor..]);
400    Ok((cleaned, markers))
401}
402
403fn parse_omena_fixture_marker(
404    body: &str,
405    byte_offset: usize,
406) -> Result<Option<OmenaFixtureMarkerV0>, String> {
407    if body == "|" {
408        return Ok(Some(omena_fixture_marker("cursor", None, byte_offset)));
409    }
410    if let Some(name) = body.strip_prefix("at:") {
411        return Ok(Some(omena_fixture_marker(
412            "namedPoint",
413            Some(validate_omena_fixture_marker_payload("at", name)?),
414            byte_offset,
415        )));
416    }
417    if let Some(name) = body
418        .strip_prefix("</")
419        .and_then(|name| name.strip_suffix('>'))
420    {
421        return Ok(Some(omena_fixture_marker(
422            "rangeEnd",
423            Some(validate_omena_fixture_marker_payload("range end", name)?),
424            byte_offset,
425        )));
426    }
427    if let Some(name) = body
428        .strip_prefix('<')
429        .and_then(|name| name.strip_suffix('>'))
430    {
431        return Ok(Some(omena_fixture_marker(
432            "rangeStart",
433            Some(validate_omena_fixture_marker_payload("range start", name)?),
434            byte_offset,
435        )));
436    }
437    if let Some(name) = body.strip_prefix("name:") {
438        return Ok(Some(omena_fixture_marker(
439            "nameAnchor",
440            Some(validate_omena_fixture_marker_payload("name", name)?),
441            byte_offset,
442        )));
443    }
444    if let Some(target) = body.strip_prefix("from:") {
445        return Ok(Some(omena_fixture_marker(
446            "linkEnd",
447            Some(validate_omena_fixture_marker_payload("from", target)?),
448            byte_offset,
449        )));
450    }
451    Ok(None)
452}
453
454fn omena_fixture_marker(
455    kind: &'static str,
456    name: Option<String>,
457    byte_offset: usize,
458) -> OmenaFixtureMarkerV0 {
459    OmenaFixtureMarkerV0 {
460        kind,
461        name,
462        byte_start: byte_offset,
463        byte_end: byte_offset,
464    }
465}
466
467fn validate_omena_fixture_marker_payload(kind: &str, value: &str) -> Result<String, String> {
468    if value.is_empty() {
469        return Err(format!("fixture marker `{kind}` must have a value"));
470    }
471    Ok(value.to_string())
472}
473
474#[cfg(test)]
475mod tests {
476    use super::*;
477
478    const CROSS_LANGUAGE_FIXTURE: &str = r#"--- file: src/App.tsx
479import styles from "./Button.module.scss";
480styles.button;
481--- file: src/Button.module.scss
482.button { color: red; }
483--- expect: product
484omena-query.source-syntax-index
485--- expect: assertion
486shared fixture parser keeps source and style files in the same workspace fixture
487"#;
488
489    #[test]
490    fn parses_reusable_omena_fixture_v0_sections() -> Result<(), String> {
491        let fixture = parse_omena_fixture_v0(
492            r#"--- file: src/proof.css
493.a { color: red; }
494--- expect: product
495omena-transform-passes.cascade-proof-obligations
496--- expect: assertion
497proof obligations remain product-visible
498"#,
499        )?;
500
501        assert_eq!(fixture.schema_version, "0");
502        assert_eq!(fixture.files.len(), 1);
503        assert_eq!(fixture.files[0].path, "src/proof.css");
504        assert!(fixture.files[0].source.contains(".a"));
505        assert_eq!(fixture.expectations.len(), 2);
506        assert_eq!(fixture.expectations[0].key, "product");
507        assert_eq!(
508            fixture.expectations[0].value,
509            "omena-transform-passes.cascade-proof-obligations"
510        );
511
512        Ok(())
513    }
514
515    #[test]
516    fn keeps_source_and_style_files_in_one_workspace_fixture() -> Result<(), String> {
517        let fixture = parse_omena_fixture_v0(CROSS_LANGUAGE_FIXTURE)?;
518
519        assert_eq!(fixture.files.len(), 2);
520        assert_eq!(fixture.files[0].path, "src/App.tsx");
521        assert_eq!(fixture.files[1].path, "src/Button.module.scss");
522        assert!(
523            fixture
524                .expectations
525                .iter()
526                .any(|expectation| expectation.value == "omena-query.source-syntax-index")
527        );
528
529        Ok(())
530    }
531
532    #[test]
533    fn parses_omena_fixture_v0_metadata_and_markers() -> Result<(), String> {
534        let fixture = parse_omena_fixture_v0(
535            r#"//- src/Card.module.scss dialect:scss layer:style
536.card { color: /*|*/red; }
537.card/*at:selector*/ { color: blue; }
538.card { color: /*<colorRange>*/green/*</colorRange>*/; }
539.card { composes: item/*from:src/Base.module.scss#item*/; }
540--- expect: product
541omena-testkit.fixture-markers
542"#,
543        )?;
544
545        assert_eq!(fixture.files.len(), 1);
546        assert_eq!(fixture.files[0].path, "src/Card.module.scss");
547        assert_eq!(
548            fixture.files[0]
549                .metadata
550                .iter()
551                .map(|metadata| (metadata.key.as_str(), metadata.value.as_str()))
552                .collect::<Vec<_>>(),
553            vec![("dialect", "scss"), ("layer", "style")]
554        );
555        assert_eq!(
556            fixture.files[0]
557                .markers
558                .iter()
559                .map(|marker| (marker.kind, marker.name.as_deref()))
560                .collect::<Vec<_>>(),
561            vec![
562                ("cursor", None),
563                ("namedPoint", Some("selector")),
564                ("rangeStart", Some("colorRange")),
565                ("rangeEnd", Some("colorRange")),
566                ("linkEnd", Some("src/Base.module.scss#item"))
567            ]
568        );
569        assert!(fixture.files[0].source.contains(".card { color: red; }"));
570        assert!(!fixture.files[0].source.contains("/*|*/"));
571        assert_eq!(
572            fixture.files[0].markers[0].byte_start,
573            ".card { color: ".len()
574        );
575
576        Ok(())
577    }
578
579    #[test]
580    fn keeps_non_fixture_comments_in_source() -> Result<(), String> {
581        let fixture = parse_omena_fixture_v0(
582            r#"//- src/Card.module.css dialect:css
583.card { /* regular comment */ color: red; }
584--- expect: product
585omena-testkit.fixture-markers
586"#,
587        )?;
588
589        assert!(fixture.files[0].source.contains("/* regular comment */"));
590        assert!(fixture.files[0].markers.is_empty());
591
592        Ok(())
593    }
594
595    #[test]
596    fn classifies_m7_diagnostic_cascade_and_boundary_expectations() -> Result<(), String> {
597        let fixture = parse_omena_fixture_v0(
598            r#"//- src/Nested.module.scss dialect:scss
599.article {
600  &.box { &.fill { padding: 1px; } }
601}
602--- expect: diagnostic
603code: unreachableDeclaration
604range: colorRange
605--- expect: no-diagnostic unspecifiedCascadeTie
606--- expect: count unreachableDeclaration:0
607--- expect: cascade-outcome decl-1
608--- expect: cascade-witness decl-2
609--- expect: boundary-state ext-1 Resolved
610--- expect: boundary-state ext-2 Partial
611--- expect: boundary-state ext-3 Stale
612--- expect: boundary-state ext-4 Missing
613--- expect: boundary-state ext-5 Unresolved
614"#,
615        )?;
616
617        assert_eq!(
618            fixture
619                .expectations
620                .iter()
621                .map(OmenaFixtureExpectationV0::kind)
622                .collect::<Vec<_>>(),
623            vec![
624                OmenaFixtureExpectationKindV0::Diagnostic,
625                OmenaFixtureExpectationKindV0::NoDiagnostic,
626                OmenaFixtureExpectationKindV0::Count,
627                OmenaFixtureExpectationKindV0::CascadeOutcome,
628                OmenaFixtureExpectationKindV0::CascadeWitness,
629                OmenaFixtureExpectationKindV0::BoundaryState,
630                OmenaFixtureExpectationKindV0::BoundaryState,
631                OmenaFixtureExpectationKindV0::BoundaryState,
632                OmenaFixtureExpectationKindV0::BoundaryState,
633                OmenaFixtureExpectationKindV0::BoundaryState,
634            ]
635        );
636        assert_eq!(
637            fixture.expectations[1].key,
638            "no-diagnostic unspecifiedCascadeTie"
639        );
640        assert_eq!(
641            fixture.expectations[2].key,
642            "count unreachableDeclaration:0"
643        );
644        Ok(())
645    }
646
647    #[test]
648    fn rejects_unknown_fixture_metadata() {
649        let error = parse_omena_fixture_v0(
650            r#"//- src/Card.module.css unknown:value
651.card { color: red; }
652--- expect: product
653omena-testkit.fixture-markers
654"#,
655        )
656        .err();
657
658        assert_eq!(
659            error.as_deref(),
660            Some("fixture metadata key `unknown` is not supported")
661        );
662    }
663
664    #[test]
665    fn rejects_fixture_without_sections() {
666        let error = parse_omena_fixture_v0("plain text").err();
667
668        assert_eq!(
669            error.as_deref(),
670            Some("fixture content must start with a file or expect marker")
671        );
672    }
673
674    #[test]
675    fn rejects_fixture_without_expectations() {
676        let error = parse_omena_fixture_v0(
677            r#"--- file: src/Button.module.scss
678.button { color: red; }
679"#,
680        )
681        .err();
682
683        assert_eq!(
684            error.as_deref(),
685            Some("fixture must contain at least one expectation section")
686        );
687    }
688
689    #[test]
690    fn summarizes_external_fixture_seed_corpus() {
691        let seeds = [OmenaTestkitFixtureSeedV0 {
692            label: "external",
693            lane: "consumer",
694            raw: r#"--- file: src/input.css
695.x { color: red; }
696--- expect: product
697consumer.product
698"#,
699            expected_products: &["consumer.product"],
700            promotion_target: "omena-testkit/consumer",
701        }];
702
703        let report = summarize_omena_testkit_fixture_seed_corpus(&seeds);
704
705        assert_eq!(report.product, "omena-testkit.fixture-seed-corpus");
706        assert_eq!(report.fixture_count, 1);
707        assert_eq!(report.lane_count, 1);
708        assert!(report.all_seeds_parse);
709        assert_eq!(report.reports[0].file_count, 1);
710        assert_eq!(report.reports[0].expectation_count, 1);
711    }
712}