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