buildlog-consultant 0.1.3

buildlog parser and analyser
Documentation
//! Module for parsing and analyzing Bazaar (brz) version control system logs.
//!
//! This module contains functions to identify and diagnose common issues in
//! Bazaar/Breezy output, particularly in the context of Debian packaging.

use crate::lines::Lines;
use crate::problems::common::NoSpaceOnDevice;
use crate::problems::debian::*;
use crate::Problem;

/// Type alias for brz error handler
pub type BrzErrorHandler = Vec<(
    regex::Regex,
    fn(&regex::Captures, Vec<&str>) -> Option<Box<dyn Problem>>,
)>;

/// Searches for Bazaar (brz) build errors in log lines.
///
/// This function scans log lines for Bazaar errors and extracts problem information.
///
/// # Arguments
/// * `lines` - Vector of log lines to analyze
///
/// # Returns
/// An optional tuple containing:
/// * An optional problem description
/// * A string representation of the error
pub fn find_brz_build_error(lines: Vec<&str>) -> Option<(Option<Box<dyn Problem>>, String)> {
    for (i, line) in lines.enumerate_backward(None) {
        if let Some(suffix) = line.strip_prefix("brz: ERROR: ") {
            let mut rest = vec![suffix.to_string()];
            rest.extend(
                lines[i + 1..]
                    .iter()
                    .filter(|n| n.starts_with(" "))
                    .map(|n| n.to_string()),
            );
            let reflowed = rest.join("\n");
            let (err, line) = parse_brz_error(&reflowed, lines[..i].to_vec());
            return Some((err, line.to_string()));
        }
    }
    None
}

/// Extracts debcargo-specific failures from brz error output.
///
/// This function parses error output specific to debcargo failures,
/// looking for patterns like "Couldn't find any crate matching".
///
/// # Arguments
/// * `_` - The regex captures (unused)
/// * `prior_lines` - Lines preceding the error to analyze for context
///
/// # Returns
/// An optional problem description
fn parse_debcargo_failure(_: &regex::Captures, prior_lines: Vec<&str>) -> Option<Box<dyn Problem>> {
    const MORE_TAIL: &str = "\x1b[0m\n";
    const MORE_HEAD1: &str = "\x1b[1;31mSomething failed: ";
    const MORE_HEAD2: &str = "\x1b[1;31mdebcargo failed: ";
    if let Some(extra) = prior_lines.last().unwrap().strip_suffix(MORE_TAIL) {
        let mut extra = vec![extra];
        for line in prior_lines[..prior_lines.len() - 1].iter().rev() {
            if let Some(middle) = extra[0].strip_prefix(MORE_HEAD1) {
                extra[0] = middle;
                break;
            }
            if let Some(middle) = extra[0].strip_prefix(MORE_HEAD2) {
                extra[0] = middle;
                break;
            }
            extra.insert(0, line);
        }
        if extra.len() == 1 {
            extra = vec![];
        }
        if extra
            .last()
            .and_then(|l| l.strip_prefix("Try `debcargo update` to update the crates.io index."))
            .is_some()
        {
            if let Some((_, n)) = lazy_regex::regex_captures!(
                r"Couldn't find any crate matching (.*)",
                extra[extra.len() - 2].trim_end()
            ) {
                return Some(Box::new(MissingDebcargoCrate::from_string(n)));
            } else {
                return Some(Box::new(DpkgSourcePackFailed(
                    extra[extra.len() - 2].to_owned(),
                )));
            }
        } else if !extra.is_empty() {
            if let Some((_, d, p)) = lazy_regex::regex_captures!(
                r"Cannot represent prerelease part of dependency: (.*) Predicate \{ (.*) \}",
                extra[0]
            ) {
                return Some(Box::new(DebcargoUnacceptablePredicate {
                    cratename: d.to_owned(),
                    predicate: p.to_owned(),
                }));
            } else if let Some((_, d, c)) = lazy_regex::regex_captures!(
                r"Cannot represent prerelease part of dependency: (.*) Comparator \{ (.*) \}",
                extra[0]
            ) {
                return Some(Box::new(DebcargoUnacceptableComparator {
                    cratename: d.to_owned(),
                    comparator: c.to_owned(),
                }));
            }
        } else {
            return Some(Box::new(DebcargoFailure(extra.join(""))));
        }
    }

    Some(Box::new(DebcargoFailure(
        "Debcargo failed to run".to_string(),
    )))
}

macro_rules! regex_line_matcher {
    ($re:expr, $f:expr) => {
        (regex::Regex::new($re).unwrap(), $f)
    };
}

lazy_static::lazy_static! {
    static ref BRZ_ERRORS: BrzErrorHandler = vec![
        regex_line_matcher!("Unable to find the needed upstream tarball for package (.*), version (.*)\\.",
        |m, _| Some(Box::new(UnableToFindUpstreamTarball{package: m.get(1).unwrap().as_str().to_string(), version: m.get(2).unwrap().as_str().parse().unwrap()}))),
        regex_line_matcher!("Unknown mercurial extra fields in (.*): b'(.*)'.", |m, _| Some(Box::new(UnknownMercurialExtraFields(m.get(2).unwrap().as_str().to_string())))),
        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())))),
        regex_line_matcher!("UScan failed to run: OpenPGP signature did not verify..", |_, _| Some(Box::new(UpstreamPGPSignatureVerificationFailed))),
        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()}))),
        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())))),
        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()}))),
        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()}))),
        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()}))),
        regex_line_matcher!(r"Debcargo failed to run\.", parse_debcargo_failure),
        regex_line_matcher!(r"\[Errno 28\] No space left on device", |_, _| Some(Box::new(NoSpaceOnDevice)))
    ];
}

/// Parses a brz error message to identify the specific problem.
///
/// This function analyzes a Bazaar error message line and the preceding context
/// to determine the specific problem type.
///
/// # Arguments
/// * `line` - The error message to parse
/// * `prior_lines` - Vector of lines preceding the error message in the log
///
/// # Returns
/// A tuple containing:
/// * An optional problem description if the error is recognized
/// * A string representation of the error
pub fn parse_brz_error<'a>(
    line: &'a str,
    prior_lines: Vec<&'a str>,
) -> (Option<Box<dyn Problem>>, String) {
    let line = line.trim();
    for (re, f) in BRZ_ERRORS.iter() {
        if let Some(m) = re.captures(line) {
            let err = f(&m, prior_lines);
            let description = err.as_ref().unwrap().to_string();
            return (err, description);
        }
    }
    if let Some(suffix) = line.strip_prefix("UScan failed to run: ") {
        return (
            Some(Box::new(UScanError(suffix.to_owned()))),
            line.to_string(),
        );
    }
    if let Some(suffix) = line.strip_prefix("Unable to parse changelog: ") {
        return (
            Some(Box::new(ChangelogParseError(
                suffix.to_string().to_string(),
            ))),
            line.to_string(),
        );
    }
    (None, line.split_once('\n').unwrap().0.to_string())
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_inconsistent_source_format() {
        let (err, line) = parse_brz_error(
                "Inconsistency between source format and version: version is not native, format is native.",
                vec![]);
        assert_eq!(
            line,
            "Inconsistent source format between version and source format",
        );
        assert_eq!(
            Some(Box::new(InconsistentSourceFormat {
                version: true,
                source_format: false
            }) as Box<dyn Problem>),
            err
        );
    }

    #[test]
    fn test_missing_debcargo_crate() {
        let lines = vec![
            "Using crate name: version-check, version 0.9.2   Updating crates.io index\n",
            "\x1b[1;31mSomething failed: Couldn't find any crate matching version-check = 0.9.2\n",
            "Try `debcargo update` to update the crates.io index.\x1b[0m\n",
            "brz: ERROR: Debcargo failed to run.\n",
        ];
        let (err, line) = find_brz_build_error(lines).unwrap();
        assert_eq!(
            line,
            "debcargo can't find crate version-check (version: 0.9.2)"
        );
        assert_eq!(
            err,
            Some(Box::new(MissingDebcargoCrate {
                cratename: "version-check".to_string(),
                version: Some("0.9.2".to_string())
            }) as Box<dyn Problem>)
        );
    }

    #[test]
    fn test_missing_debcargo_crate2() {
        let lines = vec![
            "Running 'sbuild -A -s -v'\n",
            "Building using working tree\n",
            "Building package in merge mode\n",
            "Using crate name: utf8parse, version 0.10.1+git20220116.1.dfac57e\n",
            "    Updating crates.io index\n",
            "    Updating crates.io index\n",
            "\x1b[1;31mdebcargo failed: Couldn't find any crate matching utf8parse =0.10.1\n",
            "Try `debcargo update` to update the crates.io index.\x1b[0m\n",
            "brz: ERROR: Debcargo failed to run.\n",
        ];
        let (err, line) = find_brz_build_error(lines).unwrap();
        assert_eq!(
            line,
            "debcargo can't find crate utf8parse (version: 0.10.1)"
        );
        assert_eq!(
            err,
            Some(Box::new(MissingDebcargoCrate {
                cratename: "utf8parse".to_owned(),
                version: Some("0.10.1".to_owned())
            }) as Box<dyn Problem>)
        );
    }
}