1use serde::Serialize;
36
37use crate::fixture::{OmenaFixtureExpectationKindV0, OmenaFixtureExpectationV0, OmenaFixtureV0};
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
44#[serde(rename_all = "camelCase")]
45pub struct OmenaFixtureDiagnosticV0 {
46 pub code: String,
48}
49
50impl OmenaFixtureDiagnosticV0 {
51 pub fn new(code: impl Into<String>) -> Self {
53 Self { code: code.into() }
54 }
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
64#[serde(rename_all = "camelCase")]
65pub struct OmenaFixtureBoundaryStateV0 {
66 pub reference: String,
68 pub state: String,
70}
71
72impl OmenaFixtureBoundaryStateV0 {
73 pub fn new(reference: impl Into<String>, state: impl Into<String>) -> Self {
75 Self {
76 reference: reference.into(),
77 state: state.into(),
78 }
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
92#[serde(rename_all = "camelCase")]
93pub struct OmenaFixtureCascadeV0 {
94 pub winner_id: String,
96 pub witness_ids: Vec<String>,
102}
103
104impl OmenaFixtureCascadeV0 {
105 pub fn new(
110 winner_id: impl Into<String>,
111 witness_ids: impl IntoIterator<Item = String>,
112 ) -> Self {
113 let winner_id = winner_id.into();
114 let mut witnesses = vec![winner_id.clone()];
115 for id in witness_ids {
116 if !witnesses.contains(&id) {
117 witnesses.push(id);
118 }
119 }
120 Self {
121 winner_id,
122 witness_ids: witnesses,
123 }
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
129#[serde(rename_all = "camelCase")]
130pub struct OmenaFixtureExpectationOutcomeV0 {
131 pub key: String,
133 pub kind: OmenaFixtureExpectationKindV0,
135 pub evaluated: bool,
137 pub satisfied: bool,
139 pub detail: String,
141}
142
143pub fn evaluate_omena_fixture_v0(
153 fixture: &OmenaFixtureV0,
154 diagnostics: &[OmenaFixtureDiagnosticV0],
155 boundary_states: &[OmenaFixtureBoundaryStateV0],
156 cascades: &[OmenaFixtureCascadeV0],
157) -> Vec<OmenaFixtureExpectationOutcomeV0> {
158 fixture
159 .expectations
160 .iter()
161 .map(|expectation| evaluate_one(expectation, diagnostics, boundary_states, cascades))
162 .collect()
163}
164
165pub fn evaluate_omena_fixture_v0_with<F>(
175 fixture: &OmenaFixtureV0,
176 boundary_states: &[OmenaFixtureBoundaryStateV0],
177 cascades: &[OmenaFixtureCascadeV0],
178 mut produce_diagnostics: F,
179) -> Vec<OmenaFixtureExpectationOutcomeV0>
180where
181 F: FnMut(&crate::fixture::OmenaFixtureFileV0) -> Vec<OmenaFixtureDiagnosticV0>,
182{
183 let diagnostics = fixture
184 .files
185 .iter()
186 .flat_map(&mut produce_diagnostics)
187 .collect::<Vec<_>>();
188 evaluate_omena_fixture_v0(fixture, &diagnostics, boundary_states, cascades)
189}
190
191fn evaluate_one(
192 expectation: &OmenaFixtureExpectationV0,
193 diagnostics: &[OmenaFixtureDiagnosticV0],
194 boundary_states: &[OmenaFixtureBoundaryStateV0],
195 cascades: &[OmenaFixtureCascadeV0],
196) -> OmenaFixtureExpectationOutcomeV0 {
197 let kind = expectation.kind();
198 match kind {
199 OmenaFixtureExpectationKindV0::Diagnostic => {
200 evaluate_diagnostic(expectation, diagnostics, kind)
201 }
202 OmenaFixtureExpectationKindV0::NoDiagnostic => {
203 evaluate_no_diagnostic(expectation, diagnostics, kind)
204 }
205 OmenaFixtureExpectationKindV0::Count => evaluate_count(expectation, diagnostics, kind),
206 OmenaFixtureExpectationKindV0::BoundaryState => {
207 evaluate_boundary_state(expectation, boundary_states, kind)
208 }
209 OmenaFixtureExpectationKindV0::CascadeOutcome => {
210 evaluate_cascade_outcome(expectation, cascades, kind)
211 }
212 OmenaFixtureExpectationKindV0::CascadeWitness => {
213 evaluate_cascade_witness(expectation, cascades, kind)
214 }
215 OmenaFixtureExpectationKindV0::Product
216 | OmenaFixtureExpectationKindV0::Assertion
217 | OmenaFixtureExpectationKindV0::Unknown => deferred(
218 expectation,
219 kind,
220 "product-owned expectation family is not engine-evaluated by the testkit",
221 ),
222 }
223}
224
225fn expectation_diagnostic_code(expectation: &OmenaFixtureExpectationV0) -> Option<String> {
231 if let Some(code) = code_from_key_tail(&expectation.key) {
232 return Some(code);
233 }
234 code_from_value_body(&expectation.value)
235}
236
237fn code_from_key_tail(key: &str) -> Option<String> {
238 let tail = key.split_whitespace().nth(1)?;
239 let code = tail.split(':').next().unwrap_or(tail);
241 if code.is_empty() {
242 None
243 } else {
244 Some(code.to_string())
245 }
246}
247
248fn code_from_value_body(value: &str) -> Option<String> {
249 value.lines().find_map(|line| {
250 line.trim()
251 .strip_prefix("code:")
252 .map(|code| code.trim().to_string())
253 .filter(|code| !code.is_empty())
254 })
255}
256
257fn count_diagnostics_with_code(diagnostics: &[OmenaFixtureDiagnosticV0], code: &str) -> usize {
258 diagnostics
259 .iter()
260 .filter(|diagnostic| diagnostic.code == code)
261 .count()
262}
263
264fn evaluate_diagnostic(
265 expectation: &OmenaFixtureExpectationV0,
266 diagnostics: &[OmenaFixtureDiagnosticV0],
267 kind: OmenaFixtureExpectationKindV0,
268) -> OmenaFixtureExpectationOutcomeV0 {
269 let Some(code) = expectation_diagnostic_code(expectation) else {
270 return malformed(
271 expectation,
272 kind,
273 "diagnostic expectation is missing a code",
274 );
275 };
276 let observed = count_diagnostics_with_code(diagnostics, &code);
277 let satisfied = observed > 0;
278 let detail = if satisfied {
279 format!("diagnostic `{code}` present ({observed} occurrence(s))")
280 } else {
281 format!("diagnostic `{code}` expected but absent")
282 };
283 outcome(expectation, kind, satisfied, detail)
284}
285
286fn evaluate_no_diagnostic(
287 expectation: &OmenaFixtureExpectationV0,
288 diagnostics: &[OmenaFixtureDiagnosticV0],
289 kind: OmenaFixtureExpectationKindV0,
290) -> OmenaFixtureExpectationOutcomeV0 {
291 let Some(code) = expectation_diagnostic_code(expectation) else {
292 return malformed(
293 expectation,
294 kind,
295 "no-diagnostic expectation is missing a code",
296 );
297 };
298 let observed = count_diagnostics_with_code(diagnostics, &code);
299 let satisfied = observed == 0;
300 let detail = if satisfied {
301 format!("diagnostic `{code}` correctly absent")
302 } else {
303 format!("diagnostic `{code}` present but expected absent ({observed} occurrence(s))")
304 };
305 outcome(expectation, kind, satisfied, detail)
306}
307
308fn evaluate_count(
309 expectation: &OmenaFixtureExpectationV0,
310 diagnostics: &[OmenaFixtureDiagnosticV0],
311 kind: OmenaFixtureExpectationKindV0,
312) -> OmenaFixtureExpectationOutcomeV0 {
313 let Some((code, expected)) = parse_count_target(&expectation.key) else {
314 return malformed(
315 expectation,
316 kind,
317 "count expectation must be `count <code>:<n>`",
318 );
319 };
320 let observed = count_diagnostics_with_code(diagnostics, &code);
321 let satisfied = observed == expected;
322 let detail = format!("diagnostic `{code}` count expected {expected}, observed {observed}");
323 outcome(expectation, kind, satisfied, detail)
324}
325
326fn parse_count_target(key: &str) -> Option<(String, usize)> {
327 let tail = key.split_whitespace().nth(1)?;
328 let (code, count) = tail.split_once(':')?;
329 if code.is_empty() {
330 return None;
331 }
332 let expected = count.trim().parse::<usize>().ok()?;
333 Some((code.to_string(), expected))
334}
335
336fn evaluate_boundary_state(
337 expectation: &OmenaFixtureExpectationV0,
338 boundary_states: &[OmenaFixtureBoundaryStateV0],
339 kind: OmenaFixtureExpectationKindV0,
340) -> OmenaFixtureExpectationOutcomeV0 {
341 let Some((reference, expected_state)) = parse_boundary_target(&expectation.key) else {
342 return malformed(
343 expectation,
344 kind,
345 "boundary-state expectation must be `boundary-state <ref> <state>`",
346 );
347 };
348 let Some(actual) = boundary_states
349 .iter()
350 .find(|state| state.reference == reference)
351 else {
352 return outcome(
353 expectation,
354 kind,
355 false,
356 format!("boundary reference `{reference}` not present in resolver output"),
357 );
358 };
359 let satisfied = actual.state.eq_ignore_ascii_case(&expected_state);
362 let detail = if satisfied {
363 format!("boundary `{reference}` state `{}` matches", actual.state)
364 } else {
365 format!(
366 "boundary `{reference}` expected `{expected_state}`, observed `{}`",
367 actual.state
368 )
369 };
370 outcome(expectation, kind, satisfied, detail)
371}
372
373fn parse_boundary_target(key: &str) -> Option<(String, String)> {
374 let mut parts = key.split_whitespace();
375 parts.next()?; let reference = parts.next()?;
377 let state = parts.next()?;
378 if reference.is_empty() || state.is_empty() {
379 return None;
380 }
381 Some((reference.to_string(), state.to_string()))
382}
383
384fn cascade_target_id(key: &str) -> Option<String> {
387 let id = key.split_whitespace().nth(1)?;
388 if id.is_empty() {
389 None
390 } else {
391 Some(id.to_string())
392 }
393}
394
395fn evaluate_cascade_outcome(
396 expectation: &OmenaFixtureExpectationV0,
397 cascades: &[OmenaFixtureCascadeV0],
398 kind: OmenaFixtureExpectationKindV0,
399) -> OmenaFixtureExpectationOutcomeV0 {
400 let Some(expected_winner) = cascade_target_id(&expectation.key) else {
401 return malformed(
402 expectation,
403 kind,
404 "cascade-outcome expectation must be `cascade-outcome <declaration-id>`",
405 );
406 };
407 let satisfied = cascades
408 .iter()
409 .any(|cascade| cascade.winner_id == expected_winner);
410 let detail = if satisfied {
411 format!("cascade winner `{expected_winner}` matches")
412 } else {
413 let observed = cascades
414 .iter()
415 .map(|cascade| cascade.winner_id.as_str())
416 .collect::<Vec<_>>()
417 .join(", ");
418 format!("cascade winner `{expected_winner}` expected, observed winners [{observed}]")
419 };
420 outcome(expectation, kind, satisfied, detail)
421}
422
423fn evaluate_cascade_witness(
424 expectation: &OmenaFixtureExpectationV0,
425 cascades: &[OmenaFixtureCascadeV0],
426 kind: OmenaFixtureExpectationKindV0,
427) -> OmenaFixtureExpectationOutcomeV0 {
428 let Some(expected_witness) = cascade_target_id(&expectation.key) else {
429 return malformed(
430 expectation,
431 kind,
432 "cascade-witness expectation must be `cascade-witness <declaration-id>`",
433 );
434 };
435 let satisfied = cascades
436 .iter()
437 .any(|cascade| cascade.witness_ids.iter().any(|id| id == &expected_witness));
438 let detail = if satisfied {
439 format!("cascade witness `{expected_witness}` participated in the cascade")
440 } else {
441 format!("cascade witness `{expected_witness}` expected but never participated")
442 };
443 outcome(expectation, kind, satisfied, detail)
444}
445
446fn outcome(
447 expectation: &OmenaFixtureExpectationV0,
448 kind: OmenaFixtureExpectationKindV0,
449 satisfied: bool,
450 detail: impl Into<String>,
451) -> OmenaFixtureExpectationOutcomeV0 {
452 OmenaFixtureExpectationOutcomeV0 {
453 key: expectation.key.clone(),
454 kind,
455 evaluated: true,
456 satisfied,
457 detail: detail.into(),
458 }
459}
460
461fn malformed(
462 expectation: &OmenaFixtureExpectationV0,
463 kind: OmenaFixtureExpectationKindV0,
464 detail: impl Into<String>,
465) -> OmenaFixtureExpectationOutcomeV0 {
466 outcome(expectation, kind, false, detail)
469}
470
471fn deferred(
472 expectation: &OmenaFixtureExpectationV0,
473 kind: OmenaFixtureExpectationKindV0,
474 detail: impl Into<String>,
475) -> OmenaFixtureExpectationOutcomeV0 {
476 OmenaFixtureExpectationOutcomeV0 {
477 key: expectation.key.clone(),
478 kind,
479 evaluated: false,
480 satisfied: false,
481 detail: detail.into(),
482 }
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488 use crate::fixture::parse_omena_fixture_v0;
489
490 fn diag(code: &str) -> OmenaFixtureDiagnosticV0 {
491 OmenaFixtureDiagnosticV0::new(code)
492 }
493
494 fn boundary(reference: &str, state: &str) -> OmenaFixtureBoundaryStateV0 {
495 OmenaFixtureBoundaryStateV0::new(reference, state)
496 }
497
498 fn cascade(winner: &str, witnesses: &[&str]) -> OmenaFixtureCascadeV0 {
499 OmenaFixtureCascadeV0::new(winner, witnesses.iter().map(|id| id.to_string()))
500 }
501
502 const DIAGNOSTIC_FIXTURE: &str = r#"//- src/Card.module.scss dialect:scss
503.card { color: red; }
504--- expect: diagnostic
505code: missingSassSymbol
506--- expect: no-diagnostic missingKeyframes
507--- expect: count missingSassSymbol:1
508"#;
509
510 #[test]
511 fn evaluates_passing_diagnostic_family() -> Result<(), String> {
512 let fixture = parse_omena_fixture_v0(DIAGNOSTIC_FIXTURE)?;
513 let diagnostics = [diag("missingSassSymbol")];
515 let outcomes = evaluate_omena_fixture_v0(&fixture, &diagnostics, &[], &[]);
516
517 assert_eq!(outcomes.len(), 3);
518 assert!(outcomes.iter().all(|outcome| outcome.evaluated));
519 assert!(
520 outcomes.iter().all(|outcome| outcome.satisfied),
521 "all diagnostic-family assertions should pass: {outcomes:?}"
522 );
523 Ok(())
524 }
525
526 #[test]
527 fn fails_when_expected_diagnostic_is_absent() -> Result<(), String> {
528 let fixture = parse_omena_fixture_v0(DIAGNOSTIC_FIXTURE)?;
529 let diagnostics = [diag("missingKeyframes")];
531 let outcomes = evaluate_omena_fixture_v0(&fixture, &diagnostics, &[], &[]);
532
533 let diagnostic = &outcomes[0];
534 assert_eq!(diagnostic.kind, OmenaFixtureExpectationKindV0::Diagnostic);
535 assert!(diagnostic.evaluated);
536 assert!(
537 !diagnostic.satisfied,
538 "absent expected diagnostic must fail: {diagnostic:?}"
539 );
540
541 let no_diagnostic = &outcomes[1];
543 assert_eq!(
544 no_diagnostic.kind,
545 OmenaFixtureExpectationKindV0::NoDiagnostic
546 );
547 assert!(!no_diagnostic.satisfied);
548
549 let count = &outcomes[2];
551 assert_eq!(count.kind, OmenaFixtureExpectationKindV0::Count);
552 assert!(!count.satisfied);
553 Ok(())
554 }
555
556 #[test]
557 fn correct_fixture_does_not_spuriously_fail() -> Result<(), String> {
558 let fixture = parse_omena_fixture_v0(
561 r#"//- src/Card.module.scss dialect:scss
562.card { color: red; }
563--- expect: count missingSassSymbol:2
564--- expect: no-diagnostic missingKeyframes
565--- expect: boundary-state ext-1 Resolved
566"#,
567 )?;
568 let diagnostics = [diag("missingSassSymbol"), diag("missingSassSymbol")];
569 let boundaries = [boundary("ext-1", "resolved")];
570 let outcomes = evaluate_omena_fixture_v0(&fixture, &diagnostics, &boundaries, &[]);
571
572 let live_failures = outcomes
573 .iter()
574 .filter(|outcome| outcome.evaluated && !outcome.satisfied)
575 .collect::<Vec<_>>();
576 assert!(
577 live_failures.is_empty(),
578 "correct fixture must not spuriously fail: {live_failures:?}"
579 );
580 Ok(())
581 }
582
583 #[test]
584 fn boundary_state_family_matches_lattice_case_insensitively() -> Result<(), String> {
585 let fixture = parse_omena_fixture_v0(
586 r#"//- src/Card.module.scss dialect:scss
587.card { color: red; }
588--- expect: boundary-state ext-1 Resolved
589--- expect: boundary-state ext-2 Partial
590"#,
591 )?;
592 let boundaries = [boundary("ext-1", "resolved"), boundary("ext-2", "stale")];
594 let outcomes = evaluate_omena_fixture_v0(&fixture, &[], &boundaries, &[]);
595
596 assert!(outcomes[0].satisfied, "{:?}", outcomes[0]);
597 assert!(!outcomes[1].satisfied, "{:?}", outcomes[1]);
598 Ok(())
599 }
600
601 #[test]
602 fn missing_boundary_reference_fails_evaluated() -> Result<(), String> {
603 let fixture = parse_omena_fixture_v0(
604 r#"//- src/Card.module.scss dialect:scss
605.card { color: red; }
606--- expect: boundary-state ext-9 Resolved
607"#,
608 )?;
609 let outcomes = evaluate_omena_fixture_v0(&fixture, &[], &[], &[]);
610
611 assert!(outcomes[0].evaluated);
612 assert!(!outcomes[0].satisfied);
613 assert!(outcomes[0].detail.contains("not present"));
614 Ok(())
615 }
616
617 const CASCADE_FIXTURE: &str = r#"//- src/Card.module.scss dialect:scss
618.card { color: red; }
619--- expect: cascade-outcome decl-1
620--- expect: cascade-witness decl-2
621"#;
622
623 #[test]
624 fn evaluates_passing_cascade_families() -> Result<(), String> {
625 let fixture = parse_omena_fixture_v0(CASCADE_FIXTURE)?;
626 let cascades = [cascade("decl-1", &["decl-2"])];
628 let outcomes = evaluate_omena_fixture_v0(&fixture, &[], &[], &cascades);
629
630 assert_eq!(outcomes.len(), 2);
631 let outcome_kinds = outcomes.iter().map(|o| o.kind).collect::<Vec<_>>();
632 assert_eq!(
633 outcome_kinds,
634 vec![
635 OmenaFixtureExpectationKindV0::CascadeOutcome,
636 OmenaFixtureExpectationKindV0::CascadeWitness,
637 ]
638 );
639 assert!(
640 outcomes.iter().all(|outcome| outcome.evaluated),
641 "cascade families are now evaluated, not deferred: {outcomes:?}"
642 );
643 assert!(
644 outcomes.iter().all(|outcome| outcome.satisfied),
645 "correct cascade fixture must pass: {outcomes:?}"
646 );
647 Ok(())
648 }
649
650 #[test]
651 fn fails_on_wrong_cascade_winner_and_absent_witness() -> Result<(), String> {
652 let fixture = parse_omena_fixture_v0(CASCADE_FIXTURE)?;
653 let cascades = [cascade("decl-9", &["decl-7"])];
655 let outcomes = evaluate_omena_fixture_v0(&fixture, &[], &[], &cascades);
656
657 let outcome = &outcomes[0];
658 assert_eq!(outcome.kind, OmenaFixtureExpectationKindV0::CascadeOutcome);
659 assert!(outcome.evaluated);
660 assert!(
661 !outcome.satisfied,
662 "wrong cascade winner must fail: {outcome:?}"
663 );
664
665 let witness = &outcomes[1];
666 assert_eq!(witness.kind, OmenaFixtureExpectationKindV0::CascadeWitness);
667 assert!(witness.evaluated);
668 assert!(
669 !witness.satisfied,
670 "absent cascade witness must fail: {witness:?}"
671 );
672 Ok(())
673 }
674
675 #[test]
676 fn cascade_winner_is_always_its_own_witness() -> Result<(), String> {
677 let fixture = parse_omena_fixture_v0(
680 r#"//- src/Card.module.scss dialect:scss
681.card { color: red; }
682--- expect: cascade-witness decl-1
683"#,
684 )?;
685 let cascades = [cascade("decl-1", &[])];
686 let outcomes = evaluate_omena_fixture_v0(&fixture, &[], &[], &cascades);
687
688 assert!(outcomes[0].evaluated);
689 assert!(outcomes[0].satisfied, "{:?}", outcomes[0]);
690 Ok(())
691 }
692
693 #[test]
694 fn cascade_family_fails_when_no_cascade_supplied() -> Result<(), String> {
695 let fixture = parse_omena_fixture_v0(CASCADE_FIXTURE)?;
698 let outcomes = evaluate_omena_fixture_v0(&fixture, &[], &[], &[]);
699
700 for outcome in &outcomes {
701 assert!(
702 outcome.evaluated,
703 "cascade family is now evaluated: {outcome:?}"
704 );
705 assert!(
706 !outcome.satisfied,
707 "cascade assertion with no engine cascade must fail: {outcome:?}"
708 );
709 }
710 Ok(())
711 }
712
713 #[test]
714 fn malformed_cascade_outcome_fails_as_evaluated() -> Result<(), String> {
715 let fixture = parse_omena_fixture_v0(
716 r#"//- src/Card.module.scss dialect:scss
717.card { color: red; }
718--- expect: cascade-outcome
719"#,
720 )?;
721 let cascades = [cascade("decl-1", &[])];
722 let outcomes = evaluate_omena_fixture_v0(&fixture, &[], &[], &cascades);
723
724 assert_eq!(
725 outcomes[0].kind,
726 OmenaFixtureExpectationKindV0::CascadeOutcome
727 );
728 assert!(outcomes[0].evaluated);
729 assert!(!outcomes[0].satisfied);
730 Ok(())
731 }
732
733 #[test]
734 fn injected_closure_variant_flattens_per_file_diagnostics() -> Result<(), String> {
735 let fixture = parse_omena_fixture_v0(DIAGNOSTIC_FIXTURE)?;
736 let outcomes = evaluate_omena_fixture_v0_with(&fixture, &[], &[], |file| {
739 if file.path.ends_with(".scss") {
740 vec![diag("missingSassSymbol")]
741 } else {
742 Vec::new()
743 }
744 });
745
746 assert!(
747 outcomes.iter().all(|outcome| outcome.satisfied),
748 "closure-fed evaluation should pass: {outcomes:?}"
749 );
750 Ok(())
751 }
752
753 #[test]
754 fn count_zero_passes_when_diagnostic_absent() -> Result<(), String> {
755 let fixture = parse_omena_fixture_v0(
756 r#"//- src/Card.module.scss dialect:scss
757.card { color: red; }
758--- expect: count unreachableDeclaration:0
759"#,
760 )?;
761 let outcomes = evaluate_omena_fixture_v0(&fixture, &[], &[], &[]);
762
763 assert!(outcomes[0].evaluated);
764 assert!(outcomes[0].satisfied, "{:?}", outcomes[0]);
765 Ok(())
766 }
767
768 #[test]
769 fn malformed_count_fails_as_evaluated() -> Result<(), String> {
770 let fixture = parse_omena_fixture_v0(
771 r#"//- src/Card.module.scss dialect:scss
772.card { color: red; }
773--- expect: count missingSassSymbol
774"#,
775 )?;
776 let outcomes = evaluate_omena_fixture_v0(&fixture, &[diag("missingSassSymbol")], &[], &[]);
777
778 assert_eq!(outcomes[0].kind, OmenaFixtureExpectationKindV0::Count);
779 assert!(outcomes[0].evaluated);
780 assert!(!outcomes[0].satisfied);
781 Ok(())
782 }
783}