compiletest/runtest/
debugger.rs

1use std::fmt::Write;
2use std::fs::File;
3use std::io::{BufRead, BufReader};
4
5use camino::{Utf8Path, Utf8PathBuf};
6
7use crate::directives::{LineNumber, line_directive};
8use crate::runtest::ProcRes;
9
10/// Representation of information to invoke a debugger and check its output
11pub(super) struct DebuggerCommands {
12    /// Commands for the debuuger
13    pub commands: Vec<String>,
14    /// Lines to insert breakpoints at
15    pub breakpoint_lines: Vec<LineNumber>,
16    /// Contains the source line number to check and the line itself
17    check_lines: Vec<(LineNumber, String)>,
18    /// Source file name
19    file: Utf8PathBuf,
20    /// The revision being tested, if any
21    revision: Option<String>,
22}
23
24impl DebuggerCommands {
25    pub fn parse_from(
26        file: &Utf8Path,
27        debugger_prefix: &str,
28        test_revision: Option<&str>,
29    ) -> Result<Self, String> {
30        let command_directive = format!("{debugger_prefix}-command");
31        let check_directive = format!("{debugger_prefix}-check");
32
33        let mut breakpoint_lines = vec![];
34        let mut commands = vec![];
35        let mut check_lines = vec![];
36        let reader = BufReader::new(File::open(file.as_std_path()).unwrap());
37        for (line_number, line) in LineNumber::enumerate().zip(reader.lines()) {
38            let line = line.map_err(|e| format!("Error while parsing debugger commands: {}", e))?;
39
40            // Breakpoints appear on lines with actual code, typically at the end of the line.
41            if line.contains("#break") {
42                breakpoint_lines.push(line_number);
43                continue;
44            }
45
46            let Some(directive) = line_directive(file, line_number, &line) else {
47                continue;
48            };
49
50            if !directive.applies_to_test_revision(test_revision) {
51                continue;
52            }
53
54            if directive.name == command_directive
55                && let Some(command) = directive.value_after_colon()
56            {
57                commands.push(command.to_string());
58            }
59            if directive.name == check_directive
60                && let Some(pattern) = directive.value_after_colon()
61            {
62                check_lines.push((line_number, pattern.to_string()));
63            }
64        }
65
66        Ok(Self {
67            commands,
68            breakpoint_lines,
69            check_lines,
70            file: file.to_path_buf(),
71            revision: test_revision.map(str::to_owned),
72        })
73    }
74
75    /// Given debugger output and lines to check, ensure that every line is
76    /// contained in the debugger output. The check lines need to be found in
77    /// order, but there can be extra lines between.
78    pub fn check_output(&self, debugger_run_result: &ProcRes) -> Result<(), String> {
79        // (src_lineno, ck_line)  that we did find
80        let mut found = vec![];
81        // (src_lineno, ck_line) that we couldn't find
82        let mut missing = vec![];
83        //  We can find our any current match anywhere after our last match
84        let mut last_idx = 0;
85        let dbg_lines: Vec<&str> = debugger_run_result.stdout.lines().collect();
86
87        for (src_lineno, ck_line) in &self.check_lines {
88            if let Some(offset) = dbg_lines
89                .iter()
90                .skip(last_idx)
91                .position(|out_line| check_single_line(out_line, &ck_line))
92            {
93                last_idx += offset;
94                found.push((src_lineno, dbg_lines[last_idx]));
95            } else {
96                missing.push((src_lineno, ck_line));
97            }
98        }
99
100        if missing.is_empty() {
101            Ok(())
102        } else {
103            let fname = self.file.file_name().unwrap();
104            let revision_suffix =
105                self.revision.as_ref().map_or(String::new(), |r| format!("#{}", r));
106            let mut msg = format!(
107                "check directive(s) from `{}{}` not found in debugger output. errors:",
108                self.file, revision_suffix
109            );
110
111            for (src_lineno, err_line) in missing {
112                write!(msg, "\n    ({fname}:{src_lineno}) `{err_line}`").unwrap();
113            }
114
115            if !found.is_empty() {
116                let init = "\nthe following subset of check directive(s) was found successfully:";
117                msg.push_str(init);
118                for (src_lineno, found_line) in found {
119                    write!(msg, "\n    ({fname}:{src_lineno}) `{found_line}`").unwrap();
120                }
121            }
122
123            Err(msg)
124        }
125    }
126}
127
128/// Check that the pattern in `check_line` applies to `line`. Returns `true` if they do match.
129fn check_single_line(line: &str, check_line: &str) -> bool {
130    // Allow check lines to leave parts unspecified (e.g., uninitialized
131    // bits in the  wrong case of an enum) with the notation "[...]".
132    let line = line.trim();
133    let check_line = check_line.trim();
134    let can_start_anywhere = check_line.starts_with("[...]");
135    let can_end_anywhere = check_line.ends_with("[...]");
136
137    let check_fragments: Vec<&str> =
138        check_line.split("[...]").filter(|frag| !frag.is_empty()).collect();
139    if check_fragments.is_empty() {
140        return true;
141    }
142
143    let (mut rest, first_fragment) = if can_start_anywhere {
144        let Some(pos) = line.find(check_fragments[0]) else {
145            return false;
146        };
147        (&line[pos + check_fragments[0].len()..], 1)
148    } else {
149        (line, 0)
150    };
151
152    for current_fragment in &check_fragments[first_fragment..] {
153        let Some(pos) = rest.find(current_fragment) else {
154            return false;
155        };
156        rest = &rest[pos + current_fragment.len()..];
157    }
158
159    can_end_anywhere || rest.is_empty()
160}