Skip to main content

tor_netdoc/
test_support.rs

1//! Support functions for testing, also expoeted
2// @@ begin test lint list maintained by maint/add_warning @@
3#![allow(clippy::bool_assert_comparison)]
4#![allow(clippy::clone_on_copy)]
5#![allow(clippy::dbg_macro)]
6#![allow(clippy::mixed_attributes_style)]
7#![allow(clippy::print_stderr)]
8#![allow(clippy::print_stdout)]
9#![allow(clippy::single_char_pattern)]
10#![allow(clippy::unwrap_used)]
11#![allow(clippy::unchecked_time_subtraction)]
12#![allow(clippy::useless_vec)]
13#![allow(clippy::needless_pass_by_value)]
14#![allow(clippy::string_slice)] // See arti#2571
15//! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
16
17use crate::parse2::{NetdocParseable, NetdocParseableFields, ParseInput, parse_netdoc};
18use derive_deftly::Deftly;
19use itertools::chain;
20use std::any::{Any, TypeId};
21use std::collections::HashMap;
22use std::fmt::Display;
23use std::sync::Mutex;
24
25/// Assert that `$a = $b`; if not, panic with a unidiff
26//
27// implementation is in fn assert_eq_or_diff, at the bottom of the file
28#[macro_export]
29macro_rules! assert_eq_or_diff {
30    { $a:expr, $b:expr $(,)? } => {
31        assert_eq_or_diff!($a, $b, "")
32    };
33    { $a:expr, $b:expr , $($message:tt)*} => {
34        $crate::assert_eq_or_diff(
35            &$a,
36            stringify!($a),
37            &$b,
38            stringify!($b),
39            &format_args!($($message)*),
40        )
41    };
42}
43
44/// Assert that `a = b`; if not, panic with a unidiff mentioning `a_what`, `b_what` and `message`
45///
46/// Normally it is more convenient to use the [`assert_eq_or_diff!`] macro.
47pub fn assert_eq_or_diff(a: &str, a_what: &str, b: &str, b_what: &str, message: &dyn Display) {
48    use imara_diff::{Algorithm, BasicLineDiffPrinter, Diff, InternedInput, UnifiedDiffConfig};
49
50    if a == b {
51        return;
52    }
53    let input = InternedInput::new(a, b);
54    let mut diff = Diff::compute(Algorithm::Histogram, &input);
55    diff.postprocess_lines(&input);
56    panic!(
57        // rustdoc insists on this unhelpful formatting
58        "===== document {a_what} =====
59{a}
60===== document {b_what} =====
61{b}
62===== diff ====
63{}
64===== documents differ: {a_what} != {b_what} =====
65{message}
66",
67        diff.unified_diff(
68            &BasicLineDiffPrinter(&input.interner),
69            UnifiedDiffConfig::default(),
70            &input,
71        ),
72    );
73}
74
75/// Substitute all `re` in `update` with `repl`
76///
77/// **Note** that `re` is processed in "multiple lines mode"
78/// (`(?m)` is prepended.)
79///
80/// Convenience wrapper around [`regex::Regex::replace_all`].
81pub fn regsub(update: &mut String, re: &str, repl: impl regex::Replacer) {
82    *update = regex::Regex::new(&format!("(?m){re}"))
83        .expect(re)
84        .replace_all(update, repl)
85        .to_string();
86}
87
88/// Parse a test case from a netdoc-style test case string
89///
90/// `T` must be `NetdocParseableFields`.
91/// (a surrounding document type with an intro item will be used internally.)
92///
93/// The input string is preprocessed:
94///
95///  - `#`-comments are stripped
96///  - each line is trimmed (so the input can be inden ted)
97///  - blank lines are removed
98pub fn parse_testcase_from_netdoc<T: NetdocParseableFields>(input_doc: &str) -> T {
99    #[derive(Deftly)]
100    #[derive_deftly(NetdocParseable)]
101    struct Document<T: NetdocParseableFields> {
102        /// Intro item, not present in test case doc strings
103        #[allow(unused)]
104        parse_testcase_from_netdoc_intro: (),
105
106        #[deftly(netdoc(flatten))]
107        fields: T,
108    }
109
110    eprintln!("\n&&&&&&& input test case\n{input_doc}");
111    let doc = chain!(
112        ["parse-testcase-from-netdoc-intro\n"],
113        input_doc
114            .lines()
115            .map(|l| l.split_once('#').map(|(l, _)| l).unwrap_or(l).trim())
116            .filter(|l| !l.is_empty())
117            .flat_map(|l| [l, "\n"]),
118    )
119    .collect::<String>();
120
121    eprintln!(
122        "---- tidied \n{}----",
123        doc.split_inclusive('\n')
124            // show line numbers in case of parse errors, what a faff
125            .enumerate()
126            .map(|(lno, l)| format!("| {:5} {l}", lno + 1))
127            .collect::<String>()
128    );
129
130    let pinput = ParseInput::new(&doc, "<input doc>");
131    let case: Document<T> = parse_netdoc(&pinput).expect("parse failed");
132
133    case.fields
134}
135
136/// Parse `text` as document type `D`, for use in testing
137///
138/// Parses with retain unknown values enabled, iff the corresponding cargo feature is enabled.
139///
140/// Memoises the parsing (so, leaking the parsed document),
141/// so that we don't reparse these documents once for each test case that uses them.
142///
143/// # Panics
144///
145/// Panics if the parsing fails.
146pub fn parse_test_document<D>(text: &'static str) -> &'static D
147where
148    D: NetdocParseable + Sync,
149{
150    /// Keys in the memo table
151    type MemoKey = (&'static str, TypeId);
152
153    /// Memo table
154    static MEMO: Mutex<Option<HashMap<MemoKey, &'static (dyn Any + Sync)>>> = Mutex::new(None);
155
156    // With this locking strategy, we can run only one actual parser at once.
157    // That seems fine for testing.
158    let mut memo = MEMO.lock().unwrap_or_else(|poison| poison.into_inner());
159    let key = (text, TypeId::of::<D>());
160
161    let map_entry = memo.get_or_insert_default().entry(key).or_insert_with(|| {
162        let mut input = ParseInput::new(text, "<test document>");
163
164        #[cfg(feature = "retain-unknown")]
165        input.retain_unknown_values();
166
167        let doc = parse_netdoc::<D>(&input).expect(text);
168
169        Box::leak(Box::new(doc))
170    });
171
172    (*map_entry as &dyn Any)
173        .downcast_ref::<D>()
174        .expect("wrong type is impossible")
175}