compiletest/runtest/
debugger.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
use std::fmt::Write;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::{Path, PathBuf};

use crate::common::Config;
use crate::runtest::ProcRes;

/// Representation of information to invoke a debugger and check its output
pub(super) struct DebuggerCommands {
    /// Commands for the debuuger
    pub commands: Vec<String>,
    /// Lines to insert breakpoints at
    pub breakpoint_lines: Vec<usize>,
    /// Contains the source line number to check and the line itself
    check_lines: Vec<(usize, String)>,
    /// Source file name
    file: PathBuf,
}

impl DebuggerCommands {
    pub fn parse_from(
        file: &Path,
        config: &Config,
        debugger_prefixes: &[&str],
    ) -> Result<Self, String> {
        let directives = debugger_prefixes
            .iter()
            .map(|prefix| (format!("{prefix}-command"), format!("{prefix}-check")))
            .collect::<Vec<_>>();

        let mut breakpoint_lines = vec![];
        let mut commands = vec![];
        let mut check_lines = vec![];
        let mut counter = 0;
        let reader = BufReader::new(File::open(file).unwrap());
        for (line_no, line) in reader.lines().enumerate() {
            counter += 1;
            let line = line.map_err(|e| format!("Error while parsing debugger commands: {}", e))?;

            // Breakpoints appear on lines with actual code, typically at the end of the line.
            if line.contains("#break") {
                breakpoint_lines.push(counter);
                continue;
            }

            let Some(line) = line.trim_start().strip_prefix("//").map(str::trim_start) else {
                continue;
            };

            for &(ref command_directive, ref check_directive) in &directives {
                config
                    .parse_name_value_directive(&line, command_directive)
                    .map(|cmd| commands.push(cmd));

                config
                    .parse_name_value_directive(&line, check_directive)
                    .map(|cmd| check_lines.push((line_no, cmd)));
            }
        }

        Ok(Self { commands, breakpoint_lines, check_lines, file: file.to_owned() })
    }

    /// Given debugger output and lines to check, ensure that every line is
    /// contained in the debugger output. The check lines need to be found in
    /// order, but there can be extra lines between.
    pub fn check_output(&self, debugger_run_result: &ProcRes) -> Result<(), String> {
        // (src_lineno, ck_line)  that we did find
        let mut found = vec![];
        // (src_lineno, ck_line) that we couldn't find
        let mut missing = vec![];
        //  We can find our any current match anywhere after our last match
        let mut last_idx = 0;
        let dbg_lines: Vec<&str> = debugger_run_result.stdout.lines().collect();

        for (src_lineno, ck_line) in &self.check_lines {
            if let Some(offset) = dbg_lines
                .iter()
                .skip(last_idx)
                .position(|out_line| check_single_line(out_line, &ck_line))
            {
                last_idx += offset;
                found.push((src_lineno, dbg_lines[last_idx]));
            } else {
                missing.push((src_lineno, ck_line));
            }
        }

        if missing.is_empty() {
            Ok(())
        } else {
            let fname = self.file.file_name().unwrap().to_string_lossy();
            let mut msg = format!(
                "check directive(s) from `{}` not found in debugger output. errors:",
                self.file.display()
            );

            for (src_lineno, err_line) in missing {
                write!(msg, "\n    ({fname}:{num}) `{err_line}`", num = src_lineno + 1).unwrap();
            }

            if !found.is_empty() {
                let init = "\nthe following subset of check directive(s) was found successfully:";
                msg.push_str(init);
                for (src_lineno, found_line) in found {
                    write!(msg, "\n    ({fname}:{num}) `{found_line}`", num = src_lineno + 1)
                        .unwrap();
                }
            }

            Err(msg)
        }
    }
}

/// Check that the pattern in `check_line` applies to `line`. Returns `true` if they do match.
fn check_single_line(line: &str, check_line: &str) -> bool {
    // Allow check lines to leave parts unspecified (e.g., uninitialized
    // bits in the  wrong case of an enum) with the notation "[...]".
    let line = line.trim();
    let check_line = check_line.trim();
    let can_start_anywhere = check_line.starts_with("[...]");
    let can_end_anywhere = check_line.ends_with("[...]");

    let check_fragments: Vec<&str> =
        check_line.split("[...]").filter(|frag| !frag.is_empty()).collect();
    if check_fragments.is_empty() {
        return true;
    }

    let (mut rest, first_fragment) = if can_start_anywhere {
        let Some(pos) = line.find(check_fragments[0]) else {
            return false;
        };
        (&line[pos + check_fragments[0].len()..], 1)
    } else {
        (line, 0)
    };

    for current_fragment in &check_fragments[first_fragment..] {
        let Some(pos) = rest.find(current_fragment) else {
            return false;
        };
        rest = &rest[pos + current_fragment.len()..];
    }

    can_end_anywhere || rest.is_empty()
}