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

            
17
use crate::parse2::{NetdocParseable, NetdocParseableFields, ParseInput, parse_netdoc};
18
use derive_deftly::Deftly;
19
use itertools::chain;
20
use std::any::{Any, TypeId};
21
use std::collections::HashMap;
22
use std::fmt::Display;
23
use 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]
29
macro_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.
47
310
pub 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
310
    if a == b {
51
310
        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
310
}
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`].
81
162
pub fn regsub(update: &mut String, re: &str, repl: impl regex::Replacer) {
82
162
    *update = regex::Regex::new(&format!("(?m){re}"))
83
162
        .expect(re)
84
162
        .replace_all(update, repl)
85
162
        .to_string();
86
162
}
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
98
34
pub 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
34
    eprintln!("\n&&&&&&& input test case\n{input_doc}");
111
34
    let doc = chain!(
112
34
        ["parse-testcase-from-netdoc-intro\n"],
113
34
        input_doc
114
34
            .lines()
115
212
            .map(|l| l.split_once('#').map(|(l, _)| l).unwrap_or(l).trim())
116
212
            .filter(|l| !l.is_empty())
117
150
            .flat_map(|l| [l, "\n"]),
118
    )
119
34
    .collect::<String>();
120

            
121
34
    eprintln!(
122
        "---- tidied \n{}----",
123
34
        doc.split_inclusive('\n')
124
            // show line numbers in case of parse errors, what a faff
125
34
            .enumerate()
126
184
            .map(|(lno, l)| format!("| {:5} {l}", lno + 1))
127
34
            .collect::<String>()
128
    );
129

            
130
34
    let pinput = ParseInput::new(&doc, "<input doc>");
131
34
    let case: Document<T> = parse_netdoc(&pinput).expect("parse failed");
132

            
133
34
    case.fields
134
34
}
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.
146
1496
pub fn parse_test_document<D>(text: &'static str) -> &'static D
147
1496
where
148
1496
    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
1496
    let mut memo = MEMO.lock().unwrap_or_else(|poison| poison.into_inner());
159
1496
    let key = (text, TypeId::of::<D>());
160

            
161
1496
    let map_entry = memo.get_or_insert_default().entry(key).or_insert_with(|| {
162
380
        let mut input = ParseInput::new(text, "<test document>");
163

            
164
        #[cfg(feature = "retain-unknown")]
165
380
        input.retain_unknown_values();
166

            
167
380
        let doc = parse_netdoc::<D>(&input).expect(text);
168

            
169
380
        Box::leak(Box::new(doc))
170
380
    });
171

            
172
1496
    (*map_entry as &dyn Any)
173
1496
        .downcast_ref::<D>()
174
1496
        .expect("wrong type is impossible")
175
1496
}