cairo_lang_test_utils/
lib.rs

1#![cfg(feature = "testing")]
2
3pub mod parse_test_file;
4use std::fs;
5use std::path::Path;
6use std::str::FromStr;
7use std::sync::{Mutex, MutexGuard};
8
9use cairo_lang_utils::ordered_hash_map::OrderedHashMap;
10use cairo_lang_utils::require;
11pub use parse_test_file::parse_test_file;
12
13/// Returns the content of the relevant test file.
14fn get_expected_contents(path: &Path) -> String {
15    fs::read_to_string(path).unwrap_or_else(|_| panic!("Could not read file: '{path:?}'"))
16}
17
18/// Overrides the test file data.
19fn set_contents(path: &Path, content: String) {
20    fs::write(path, content).unwrap_or_else(|_| panic!("Could not write file: '{path:?}'"));
21}
22
23/// Compares content to examples content, or overrides it if the `CAIRO_FIX_TESTS` environment
24/// value is set to `1`.
25pub fn compare_contents_or_fix_with_path(path: &Path, content: String) {
26    let is_fix_mode = std::env::var("CAIRO_FIX_TESTS") == Ok("1".into());
27    if is_fix_mode {
28        set_contents(path, content);
29    } else {
30        pretty_assertions::assert_eq!(content, get_expected_contents(path));
31    }
32}
33
34/// Locks the given mutex, and prints an informative error on failure.
35pub fn test_lock<'a, T: ?Sized + 'a>(m: &'a Mutex<T>) -> MutexGuard<'a, T> {
36    match m.lock() {
37        Ok(guard) => guard,
38        // Allow other test to take the lock if it was poisoned by a thread that panicked.
39        Err(poisoned) => poisoned.into_inner(),
40    }
41}
42
43/// Returns an error string according to the extracted `ExpectDiagnostics`.
44/// Returns None on success.
45pub fn verify_diagnostics_expectation(
46    args: &OrderedHashMap<String, String>,
47    diagnostics: &str,
48) -> Option<String> {
49    let expect_diagnostics = args.get("expect_diagnostics")?;
50    require(expect_diagnostics != "*")?;
51
52    let expect_diagnostics = expect_diagnostics_input_input(expect_diagnostics);
53    let has_diagnostics = !diagnostics.trim().is_empty();
54    // TODO(Gil): This is a bit of a hack, try and get the original diagnostics from the test.
55    let has_errors = diagnostics.lines().any(|line| line.starts_with("error: "));
56    match expect_diagnostics {
57        ExpectDiagnostics::Any => {
58            if !has_diagnostics {
59                return Some(
60                    "`expect_diagnostics` is true, but no diagnostics were generated.\n"
61                        .to_string(),
62                );
63            }
64        }
65        ExpectDiagnostics::Warnings => {
66            if !has_diagnostics {
67                return Some(
68                    "`expect_diagnostics` is 'warnings_only', but no diagnostics were generated.\n"
69                        .to_string(),
70                );
71            } else if has_errors {
72                return Some(
73                    "`expect_diagnostics` is 'warnings_only', but errors were generated.\n"
74                        .to_string(),
75                );
76            }
77        }
78        ExpectDiagnostics::None => {
79            if has_diagnostics {
80                return Some(
81                    "`expect_diagnostics` is false, but diagnostics were generated:\n".to_string(),
82                );
83            }
84        }
85    };
86    None
87}
88
89/// The expected diagnostics for a test.
90enum ExpectDiagnostics {
91    /// Any diagnostics (warnings or errors) are expected.
92    Any,
93    /// Only warnings are expected.
94    Warnings,
95    /// No diagnostics are expected.
96    None,
97}
98
99/// Translates a string test input to bool ("false" -> false, "true" -> true). Panics if invalid.
100/// Ignores case.
101fn expect_diagnostics_input_input(input: &str) -> ExpectDiagnostics {
102    let input = input.to_lowercase();
103    match input.as_str() {
104        "false" => ExpectDiagnostics::None,
105        "true" => ExpectDiagnostics::Any,
106        "warnings_only" => ExpectDiagnostics::Warnings,
107        _ => panic!("Expected 'true', 'false' or 'warnings', actual: {input}"),
108    }
109}
110
111/// Translates a string test input to bool ("false" -> false, "true" -> true). Panics if invalid.
112/// Ignores case.
113pub fn bool_input(input: &str) -> bool {
114    let input = input.to_lowercase();
115    bool::from_str(&input).unwrap_or_else(|_| panic!("Expected 'true' or 'false', actual: {input}"))
116}
117
118/// Parses a test input that may be a file input. If the input starts with ">>> file: " it reads the
119/// file and returns the file path and content, otherwise, it returns the input and a default dummy
120/// path.
121pub fn get_direct_or_file_content(input: &str) -> (String, String) {
122    if let Some(path) = input.strip_prefix(">>> file: ") {
123        (
124            path.to_string(),
125            fs::read_to_string(path).unwrap_or_else(|_| panic!("Could not read file: '{path}'")),
126        )
127    } else {
128        ("dummy_file.cairo".to_string(), input.to_string())
129    }
130}