Skip to main content

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