buildlog-consultant 0.0.35

buildlog parser and analyser
Documentation
use pyo3::prelude::*;
use std::borrow::Cow;
use std::collections::HashMap;

pub trait Match: Send + Sync + std::fmt::Debug {
    fn line(&self) -> String;

    fn origin(&self) -> Origin;

    fn offset(&self) -> usize;

    fn lineno(&self) -> usize {
        self.offset() + 1
    }
}

#[derive(Clone, Debug)]
pub struct Origin(String);

impl ToString for Origin {
    fn to_string(&self) -> String {
        self.0.clone()
    }
}

pub struct SingleLineMatch {
    pub origin: Origin,
    pub offset: usize,
    pub line: String,
}

impl Match for SingleLineMatch {
    fn line(&self) -> String {
        self.line.clone()
    }

    fn origin(&self) -> Origin {
        self.origin.clone()
    }

    fn offset(&self) -> usize {
        self.offset
    }
}

impl std::fmt::Debug for SingleLineMatch {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}:{}: {}", self.origin.0, self.lineno(), self.line)
    }
}

pub struct MultiLineMatch {
    pub origin: Origin,
    pub offsets: Vec<usize>,
    pub lines: Vec<String>,
}

impl MultiLineMatch {
    pub fn new(origin: Origin, offsets: Vec<usize>, lines: Vec<String>) -> Self {
        assert!(!offsets.is_empty());
        assert!(offsets.len() == lines.len());
        Self {
            origin,
            offsets,
            lines,
        }
    }
}

impl Match for MultiLineMatch {
    fn line(&self) -> String {
        self.lines[0].clone()
    }

    fn origin(&self) -> Origin {
        self.origin.clone()
    }

    fn offset(&self) -> usize {
        self.offsets[0]
    }

    fn lineno(&self) -> usize {
        self.offset() + 1
    }
}

impl std::fmt::Debug for MultiLineMatch {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}:{}: {}", self.origin.0, self.lineno(), self.line())
    }
}

pub trait Problem: std::fmt::Display + Send + Sync {
    fn kind(&self) -> Cow<str>;

    fn json(&self) -> serde_json::Value;
}

pub struct PyMatch(PyObject);

impl Match for PyMatch {
    fn line(&self) -> String {
        Python::with_gil(|py| {
            let line = self.0.getattr(py, "line").unwrap();
            line.extract::<String>(py).unwrap()
        })
    }

    fn origin(&self) -> Origin {
        Python::with_gil(|py| {
            let origin = self.0.getattr(py, "origin").unwrap();
            let origin = origin.extract::<String>(py).unwrap();
            Origin(origin)
        })
    }

    fn offset(&self) -> usize {
        Python::with_gil(|py| {
            let offset = self.0.getattr(py, "offset").unwrap();
            offset.extract::<usize>(py).unwrap()
        })
    }
}

impl std::fmt::Debug for PyMatch {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        Python::with_gil(|py| {
            let s = self
                .0
                .call_method0(py, "__repr__")
                .unwrap()
                .extract::<String>(py)
                .unwrap();
            write!(f, "{}", s)
        })
    }
}

impl PartialEq for PyMatch {
    fn eq(&self, other: &Self) -> bool {
        Python::with_gil(|py| {
            let eq = self
                .0
                .call_method1(py, "__eq__", (other.0.clone(),))
                .unwrap();
            eq.extract::<bool>(py).unwrap()
        })
    }
}

#[derive(Clone, Debug)]
pub struct PyProblem(PyObject);

fn py_to_json(py: Python, json: PyObject) -> PyResult<serde_json::Value> {
    if let Ok(s) = json.extract::<String>(py) {
        Ok(serde_json::Value::String(s))
    } else if let Ok(n) = json.extract::<i64>(py) {
        Ok(serde_json::Value::Number(n.into()))
    } else if let Ok(b) = json.extract::<bool>(py) {
        Ok(serde_json::Value::Bool(b))
    } else if let Ok(l) = json.extract::<Vec<PyObject>>(py) {
        let mut v = Vec::new();
        for x in l {
            v.push(py_to_json(py, x)?);
        }
        Ok(serde_json::Value::Array(v))
    } else if let Ok(d) = json.extract::<HashMap<String, PyObject>>(py) {
        let mut m = serde_json::Map::new();
        for (k, v) in d {
            let v = py_to_json(py, v)?;
            m.insert(k, v);
        }
        Ok(serde_json::Value::Object(m))
    } else {
        Err(PyErr::new::<pyo3::exceptions::PyTypeError, _>(
            "unsupported type",
        ))
    }
}

impl Problem for PyProblem {
    fn kind(&self) -> Cow<str> {
        Python::with_gil(|py| {
            let kind = self.0.getattr(py, "kind").unwrap();
            kind.extract::<String>(py).unwrap().into()
        })
    }

    fn json(&self) -> serde_json::Value {
        Python::with_gil(|py| {
            let json = self.0.getattr(py, "json").unwrap();
            py_to_json(py, json).unwrap()
        })
    }
}

impl PartialEq for PyProblem {
    fn eq(&self, other: &Self) -> bool {
        Python::with_gil(|py| {
            let eq = self
                .0
                .call_method1(py, "__eq__", (other.0.clone(),))
                .unwrap();
            eq.extract::<bool>(py).unwrap()
        })
    }
}

impl std::fmt::Display for PyProblem {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        Python::with_gil(|py| {
            let s = self
                .0
                .call_method0(py, "__str__")
                .unwrap()
                .extract::<String>(py)
                .unwrap();
            write!(f, "{}", s)
        })
    }
}

pub fn find_build_failure_description(
    lines: &[&str],
) -> (Option<Box<dyn Match>>, Option<Box<dyn Problem>>) {
    Python::with_gil(|py| {
        let module = py.import("buildlog_consultant.common").unwrap();
        let find_build_failure_description =
            module.getattr("find_build_failure_description").unwrap();
        let result = find_build_failure_description
            .call1((lines.to_vec(),))
            .unwrap();
        let (m, p) = result
            .extract::<(Option<PyObject>, Option<PyObject>)>()
            .unwrap();
        (
            m.map(|m| Box::new(PyMatch(m)) as Box<dyn Match>),
            p.map(|p| Box::new(PyProblem(p)) as Box<dyn Problem>),
        )
    })
}

#[cfg(test)]
mod test {
    #[test]
    fn test_simple() {
        let (m, p) = super::find_build_failure_description(&[
            "make[1]: *** No rule to make target 'nno.autopgen.bin', needed by 'dan-nno.autopgen.bin'.  Stop."]);
        assert!(m.is_some());
        assert!(p.is_some());
    }
}

pub mod common;

pub mod r#match;