cairo_lang_test_utils/
lib.rs

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