buildlog_consultant/
brz.rs

1//! Module for parsing and analyzing Bazaar (brz) version control system logs.
2//!
3//! This module contains functions to identify and diagnose common issues in
4//! Bazaar/Breezy output, particularly in the context of Debian packaging.
5
6use crate::lines::Lines;
7use crate::problems::common::NoSpaceOnDevice;
8use crate::problems::debian::*;
9use crate::Problem;
10
11/// Searches for Bazaar (brz) build errors in log lines.
12///
13/// This function scans log lines for Bazaar errors and extracts problem information.
14///
15/// # Arguments
16/// * `lines` - Vector of log lines to analyze
17///
18/// # Returns
19/// An optional tuple containing:
20/// * An optional problem description
21/// * A string representation of the error
22pub fn find_brz_build_error(lines: Vec<&str>) -> Option<(Option<Box<dyn Problem>>, String)> {
23    for (i, line) in lines.enumerate_backward(None) {
24        if let Some(suffix) = line.strip_prefix("brz: ERROR: ") {
25            let mut rest = vec![suffix.to_string()];
26            for n in lines[i + 1..].iter() {
27                if n.starts_with(" ") {
28                    rest.push(n.to_string());
29                }
30            }
31            let reflowed = rest.join("\n");
32            let (err, line) = parse_brz_error(&reflowed, lines[..i].to_vec());
33            return Some((err, line.to_string()));
34        }
35    }
36    None
37}
38
39/// Extracts debcargo-specific failures from brz error output.
40///
41/// This function parses error output specific to debcargo failures,
42/// looking for patterns like "Couldn't find any crate matching".
43///
44/// # Arguments
45/// * `_` - The regex captures (unused)
46/// * `prior_lines` - Lines preceding the error to analyze for context
47///
48/// # Returns
49/// An optional problem description
50fn parse_debcargo_failure(_: &regex::Captures, prior_lines: Vec<&str>) -> Option<Box<dyn Problem>> {
51    const MORE_TAIL: &str = "\x1b[0m\n";
52    const MORE_HEAD1: &str = "\x1b[1;31mSomething failed: ";
53    const MORE_HEAD2: &str = "\x1b[1;31mdebcargo failed: ";
54    if let Some(extra) = prior_lines.last().unwrap().strip_suffix(MORE_TAIL) {
55        let mut extra = vec![extra];
56        for line in prior_lines[..prior_lines.len() - 1].iter().rev() {
57            if let Some(middle) = extra[0].strip_prefix(MORE_HEAD1) {
58                extra[0] = middle;
59                break;
60            }
61            if let Some(middle) = extra[0].strip_prefix(MORE_HEAD2) {
62                extra[0] = middle;
63                break;
64            }
65            extra.insert(0, line);
66        }
67        if extra.len() == 1 {
68            extra = vec![];
69        }
70        if extra
71            .last()
72            .and_then(|l| l.strip_prefix("Try `debcargo update` to update the crates.io index."))
73            .is_some()
74        {
75            if let Some((_, n)) = lazy_regex::regex_captures!(
76                r"Couldn't find any crate matching (.*)",
77                extra[extra.len() - 2].trim_end()
78            ) {
79                return Some(Box::new(MissingDebcargoCrate::from_string(n)));
80            } else {
81                return Some(Box::new(DpkgSourcePackFailed(
82                    extra[extra.len() - 2].to_owned(),
83                )));
84            }
85        } else if !extra.is_empty() {
86            if let Some((_, d, p)) = lazy_regex::regex_captures!(
87                r"Cannot represent prerelease part of dependency: (.*) Predicate \{ (.*) \}",
88                extra[0]
89            ) {
90                return Some(Box::new(DebcargoUnacceptablePredicate {
91                    cratename: d.to_owned(),
92                    predicate: p.to_owned(),
93                }));
94            } else if let Some((_, d, c)) = lazy_regex::regex_captures!(
95                r"Cannot represent prerelease part of dependency: (.*) Comparator \{ (.*) \}",
96                extra[0]
97            ) {
98                return Some(Box::new(DebcargoUnacceptableComparator {
99                    cratename: d.to_owned(),
100                    comparator: c.to_owned(),
101                }));
102            }
103        } else {
104            return Some(Box::new(DebcargoFailure(extra.join(""))));
105        }
106    }
107
108    Some(Box::new(DebcargoFailure(
109        "Debcargo failed to run".to_string(),
110    )))
111}
112
113macro_rules! regex_line_matcher {
114    ($re:expr, $f:expr) => {
115        (regex::Regex::new($re).unwrap(), $f)
116    };
117}
118
119lazy_static::lazy_static! {
120    static ref BRZ_ERRORS: Vec<(regex::Regex, fn(&regex::Captures, Vec<&str>) -> Option<Box<dyn Problem>>)> = vec![
121        regex_line_matcher!("Unable to find the needed upstream tarball for package (.*), version (.*)\\.",
122        |m, _| Some(Box::new(UnableToFindUpstreamTarball{package: m.get(1).unwrap().as_str().to_string(), version: m.get(2).unwrap().as_str().parse().unwrap()}))),
123        regex_line_matcher!("Unknown mercurial extra fields in (.*): b'(.*)'.", |m, _| Some(Box::new(UnknownMercurialExtraFields(m.get(2).unwrap().as_str().to_string())))),
124        regex_line_matcher!("UScan failed to run: In watchfile (.*), reading webpage (.*) failed: 429 too many requests\\.", |m, _| Some(Box::new(UScanTooManyRequests(m.get(2).unwrap().as_str().to_string())))),
125        regex_line_matcher!("UScan failed to run: OpenPGP signature did not verify..", |_, _| Some(Box::new(UpstreamPGPSignatureVerificationFailed))),
126        regex_line_matcher!(r"Inconsistency between source format and version: version is( not)? native, format is( not)? native\.", |m, _| Some(Box::new(InconsistentSourceFormat{version: m.get(1).is_some(), source_format: m.get(2).is_some()}))),
127        regex_line_matcher!(r"UScan failed to run: In (.*) no matching hrefs for version (.*) in watch line", |m, _| Some(Box::new(UScanRequestVersionMissing(m.get(2).unwrap().as_str().to_string())))),
128        regex_line_matcher!(r"UScan failed to run: In directory ., downloading (.*) failed: (.*)", |m, _| Some(Box::new(UScanFailed{url: m.get(1).unwrap().as_str().to_string(), reason: m.get(2).unwrap().as_str().to_string()}))),
129        regex_line_matcher!(r"UScan failed to run: In watchfile debian/watch, reading webpage\n  (.*) failed: (.*)", |m, _| Some(Box::new(UScanFailed{url: m.get(1).unwrap().as_str().to_string(), reason: m.get(2).unwrap().as_str().to_string()}))),
130        regex_line_matcher!(r"Unable to parse upstream metadata file (.*): (.*)", |m, _| Some(Box::new(UpstreamMetadataFileParseError{path: m.get(1).unwrap().as_str().to_string().into(), reason: m.get(2).unwrap().as_str().to_string()}))),
131        regex_line_matcher!(r"Debcargo failed to run\.", parse_debcargo_failure),
132        regex_line_matcher!(r"\[Errno 28\] No space left on device", |_, _| Some(Box::new(NoSpaceOnDevice)))
133    ];
134}
135
136/// Parses a brz error message to identify the specific problem.
137///
138/// This function analyzes a Bazaar error message line and the preceding context
139/// to determine the specific problem type.
140///
141/// # Arguments
142/// * `line` - The error message to parse
143/// * `prior_lines` - Vector of lines preceding the error message in the log
144///
145/// # Returns
146/// A tuple containing:
147/// * An optional problem description if the error is recognized
148/// * A string representation of the error
149pub fn parse_brz_error<'a>(
150    line: &'a str,
151    prior_lines: Vec<&'a str>,
152) -> (Option<Box<dyn Problem>>, String) {
153    let line = line.trim();
154    for (re, f) in BRZ_ERRORS.iter() {
155        if let Some(m) = re.captures(line) {
156            let err = f(&m, prior_lines);
157            let description = err.as_ref().unwrap().to_string();
158            return (err, description);
159        }
160    }
161    if let Some(suffix) = line.strip_prefix("UScan failed to run: ") {
162        return (
163            Some(Box::new(UScanError(suffix.to_owned()))),
164            line.to_string(),
165        );
166    }
167    if let Some(suffix) = line.strip_prefix("Unable to parse changelog: ") {
168        return (
169            Some(Box::new(ChangelogParseError(
170                suffix.to_string().to_string(),
171            ))),
172            line.to_string(),
173        );
174    }
175    return (None, line.split_once('\n').unwrap().0.to_string());
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    #[test]
182    fn test_inconsistent_source_format() {
183        let (err, line) = parse_brz_error(
184                "Inconsistency between source format and version: version is not native, format is native.",
185                vec![]);
186        assert_eq!(
187            line,
188            "Inconsistent source format between version and source format",
189        );
190        assert_eq!(
191            Some(Box::new(InconsistentSourceFormat {
192                version: true,
193                source_format: false
194            }) as Box<dyn Problem>),
195            err
196        );
197    }
198
199    #[test]
200    fn test_missing_debcargo_crate() {
201        let lines = vec![
202            "Using crate name: version-check, version 0.9.2   Updating crates.io index\n",
203            "\x1b[1;31mSomething failed: Couldn't find any crate matching version-check = 0.9.2\n",
204            "Try `debcargo update` to update the crates.io index.\x1b[0m\n",
205            "brz: ERROR: Debcargo failed to run.\n",
206        ];
207        let (err, line) = find_brz_build_error(lines).unwrap();
208        assert_eq!(
209            line,
210            "debcargo can't find crate version-check (version: 0.9.2)"
211        );
212        assert_eq!(
213            err,
214            Some(Box::new(MissingDebcargoCrate {
215                cratename: "version-check".to_string(),
216                version: Some("0.9.2".to_string())
217            }) as Box<dyn Problem>)
218        );
219    }
220
221    #[test]
222    fn test_missing_debcargo_crate2() {
223        let lines = vec![
224            "Running 'sbuild -A -s -v'\n",
225            "Building using working tree\n",
226            "Building package in merge mode\n",
227            "Using crate name: utf8parse, version 0.10.1+git20220116.1.dfac57e\n",
228            "    Updating crates.io index\n",
229            "    Updating crates.io index\n",
230            "\x1b[1;31mdebcargo failed: Couldn't find any crate matching utf8parse =0.10.1\n",
231            "Try `debcargo update` to update the crates.io index.\x1b[0m\n",
232            "brz: ERROR: Debcargo failed to run.\n",
233        ];
234        let (err, line) = find_brz_build_error(lines).unwrap();
235        assert_eq!(
236            line,
237            "debcargo can't find crate utf8parse (version: 0.10.1)"
238        );
239        assert_eq!(
240            err,
241            Some(Box::new(MissingDebcargoCrate {
242                cratename: "utf8parse".to_owned(),
243                version: Some("0.10.1".to_owned())
244            }) as Box<dyn Problem>)
245        );
246    }
247}