Skip to main content

marque_test_utils/
lib.rs

1//! Shared test utilities for the marque workspace.
2//!
3//! Provides uniform access to `tests/corpus/` fixtures from any crate's test suite.
4//! Add this crate as a `[dev-dependencies]` path dependency.
5
6use serde::Deserialize;
7use std::path::{Path, PathBuf};
8
9/// Root of the test corpus relative to the workspace root.
10const CORPUS_REL: &str = "tests/corpus";
11
12/// Returns the absolute path to the corpus root directory.
13///
14/// Resolves relative to `CARGO_MANIFEST_DIR`'s ancestor that contains `tests/corpus/`.
15/// Works from any crate in the workspace.
16pub fn corpus_root() -> PathBuf {
17    // Walk up from CARGO_MANIFEST_DIR until we find the workspace root
18    // (identified by the presence of tests/corpus/).
19    let manifest_dir =
20        std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set — run via cargo");
21    let mut dir = PathBuf::from(&manifest_dir);
22    loop {
23        let candidate = dir.join(CORPUS_REL);
24        if candidate.is_dir() {
25            return candidate;
26        }
27        if !dir.pop() {
28            panic!(
29                "could not find {CORPUS_REL}/ in any ancestor of {manifest_dir}; \
30                 is the workspace root missing tests/corpus/?"
31            );
32        }
33    }
34}
35
36/// Returns paths to all `.txt` fixture files under the given corpus subdirectory.
37pub fn fixtures_in(subdir: &str) -> Vec<PathBuf> {
38    let dir = corpus_root().join(subdir);
39    if !dir.is_dir() {
40        return Vec::new();
41    }
42    let mut paths: Vec<PathBuf> = std::fs::read_dir(&dir)
43        .expect("failed to read corpus directory")
44        .filter_map(|e| e.ok())
45        .map(|e| e.path())
46        .filter(|p| p.extension().is_some_and(|ext| ext == "txt"))
47        .collect();
48    paths.sort();
49    paths
50}
51
52/// Returns all invalid (known-bad) fixture paths.
53pub fn invalid_fixtures() -> Vec<PathBuf> {
54    fixtures_in("invalid")
55}
56
57/// Returns all valid (known-good) fixture paths.
58pub fn valid_fixtures() -> Vec<PathBuf> {
59    fixtures_in("valid")
60}
61
62/// Returns all prose corpus fixture paths.
63pub fn prose_fixtures() -> Vec<PathBuf> {
64    fixtures_in("prose")
65}
66
67/// Expected diagnostic from a `.expected.json` sidecar file.
68#[derive(Debug, Clone, Deserialize)]
69pub struct ExpectedDiagnostic {
70    pub rule: String,
71    pub span: ExpectedSpan,
72    #[serde(default)]
73    pub severity: Option<String>,
74}
75
76/// Expected byte span.
77#[derive(Debug, Clone, Deserialize)]
78pub struct ExpectedSpan {
79    pub start: usize,
80    pub end: usize,
81}
82
83/// Expected diagnostics loaded from a `.expected.json` file.
84#[derive(Debug, Clone, Deserialize)]
85pub struct ExpectedFixture {
86    pub diagnostics: Vec<ExpectedDiagnostic>,
87}
88
89/// Load the `.expected.json` sidecar for a given fixture path.
90///
91/// Given `tests/corpus/invalid/banner_abbrev.txt`, loads
92/// `tests/corpus/invalid/banner_abbrev.expected.json`.
93pub fn load_expected(fixture_path: &Path) -> ExpectedFixture {
94    let json_path = fixture_path.with_extension("expected.json");
95    if !json_path.exists() {
96        panic!(
97            "missing expected file for fixture: {} (expected {})",
98            fixture_path.display(),
99            json_path.display()
100        );
101    }
102    let content = std::fs::read_to_string(&json_path)
103        .unwrap_or_else(|e| panic!("failed to read {}: {e}", json_path.display()));
104    serde_json::from_str(&content)
105        .unwrap_or_else(|e| panic!("failed to parse {}: {e}", json_path.display()))
106}
107
108/// Load fixture text content as bytes.
109pub fn load_fixture(path: &Path) -> Vec<u8> {
110    std::fs::read(path).unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()))
111}