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
150
use std::error::Error;
use strum::*;

#[derive(
    Debug, Default, Clone, PartialEq, Eq, PartialOrd, Ord, Copy, Display, EnumString, EnumIter,
)]
pub enum YoctoFailureKind {
    /// The 6 standard tasks in Yocto https://docs.yoctoproject.org/ref-manual/tasks.html
    #[strum(serialize = "do_build")]
    DoBuild,
    #[strum(serialize = "do_compile")]
    DoCompile,
    #[strum(serialize = "do_compile_ptest_base")]
    DoCompilePtestBase,
    #[strum(serialize = "do_configure")]
    DoConfigure,
    #[strum(serialize = "do_configure_ptest_base")]
    DoConfigurePtestBase,
    #[strum(serialize = "do_deploy")]
    DoDeploy,
    /// Other tasks
    #[strum(serialize = "do_fetch")]
    DoFetch,
    /// If it's a type of failure we're not familiar with or parsing fails, default to this
    #[default]
    #[strum(serialize = "misc")]
    Misc,
}

impl YoctoFailureKind {
    /// Takes in a yocto logfile filename such as `log.do_fetch.21616` and attempts to determine the type
    /// of yocto task the the logfile is associated with.
    ///
    /// # Example
    /// ```
    /// # use gh_workflow_parser::err_msg_parse::yocto_err::util::YoctoFailureKind;
    /// let kind = YoctoFailureKind::parse_from_logfilename("log.do_fetch.21616").unwrap();
    /// assert_eq!(kind, YoctoFailureKind::DoFetch);
    ///
    /// // Infallible if you're sure the filename is a yocto log but it might not be a known task
    /// let kind = YoctoFailureKind::parse_from_logfilename("log.some_custom_task.21616").unwrap_or_default();
    /// assert_eq!(kind, YoctoFailureKind::Misc);
    /// ```
    pub fn parse_from_logfilename(fname: &str) -> Result<Self, Box<dyn Error>> {
        for variant in YoctoFailureKind::iter() {
            let variant_as_str = variant.to_string();
            if fname.contains(&variant_as_str) {
                return Ok(variant);
            }
        }
        Err(format!("Could not determine task from input: {fname}").into())
    }
}

/// Find the `--- Error summary ---` section in the log and return the rest of the log.
pub fn yocto_error_summary(log: &str) -> Result<String, Box<dyn Error>> {
    const YOCTO_ERROR_SUMMARY_SIGNATURE: &str = "--- Error summary ---";
    let error_summary = log
        .split(YOCTO_ERROR_SUMMARY_SIGNATURE)
        .collect::<Vec<&str>>()
        .pop()
        .ok_or("No error summary found")?;
    Ok(error_summary.trim().to_string())
}

/// Trim the trailing `error: Recipe` lines from the error summary
/// This is to remove the noise of just recipe failures
pub fn trim_trailing_just_recipes(log: &str) -> Result<String, Box<dyn Error>> {
    let trimmed = log
        .lines()
        .rev()
        .skip_while(|line| line.starts_with("error: Recipe"))
        .collect::<Vec<&str>>()
        .iter()
        .rev()
        .fold(String::with_capacity(log.len()), |acc, line| {
            acc + line + "\n"
        });
    Ok(trimmed)
}

/// Find the kind of yocto failure in the string e.g. this would be `do_fetch`
/// ERROR: Logfile of failure stored in: /app/yocto/build/tmp/work/x86_64-linux/sqlite3-native/3.43.2/temp/log.do_fetch.21616
///
/// # Example
/// ```
/// use gh_workflow_parser::err_msg_parse::yocto_err::util::find_yocto_failure_log_str;
/// let log = r#"ERROR: Some error message
/// ERROR: Logfile of failure stored in: /app/yocto/build/tmp/work/x86_64-linux/sqlite3-native/3.43.2/temp/log.do_fetch.21616
/// ERROR: Some other error message"#;
///
/// let failure_log_str = find_yocto_failure_log_str(log).unwrap();
///
/// assert_eq!(failure_log_str, "ERROR: Logfile of failure stored in: /app/yocto/build/tmp/work/x86_64-linux/sqlite3-native/3.43.2/temp/log.do_fetch.21616");
/// ```
///
///
pub fn find_yocto_failure_log_str(log: &str) -> Result<&str, Box<dyn Error>> {
    let log_file_line = log
        .lines()
        .find(|line| line.contains("Logfile of failure stored in"))
        .ok_or("No log file line found")?;

    Ok(log_file_line)
}

#[cfg(test)]
mod tests {
    use super::*;
    use pretty_assertions::assert_eq;
    use std::str::FromStr;

    const ERROR_SUMMARY_TEST_STR: &str = r#"ERROR: sqlite3-native-3_3.43.2-r0 do_fetch: Bitbake Fetcher Error: MalformedUrl('${SOURCE_MIRROR_URL}')
    ERROR: Logfile of failure stored in: /app/yocto/build/tmp/work/x86_64-linux/sqlite3-native/3.43.2/temp/log.do_fetch.21616
    ERROR: Task (virtual:native:/app/yocto/build/../poky/meta/recipes-support/sqlite/sqlite3_3.43.2.bb:do_fetch) failed with exit code '1'

    2024-02-11 00:09:04 - ERROR    - Command "/app/yocto/poky/bitbake/bin/bitbake -c build test-template-ci-xilinx-image package-index" failed with error 1"#;

    #[test]
    fn test_determine_yocto_error_kind() {
        let task = "do_build";
        assert_eq!(
            YoctoFailureKind::from_str(task).unwrap(),
            YoctoFailureKind::DoBuild
        );
    }

    #[test]
    fn test_yocto_error_from_error_message() {
        // find the part of the string after
        let log_file_line = ERROR_SUMMARY_TEST_STR
            .lines()
            .find(|line| line.contains("Logfile of failure stored in"))
            .ok_or("No log file line found")
            .unwrap();
        dbg!(log_file_line);
        // Get the path stored after `Logfile of failure stored in: `
        let logfile_path = log_file_line
            .split("Logfile of failure stored in: ")
            .collect::<Vec<&str>>()
            .pop()
            .ok_or("No log file found");

        let path = std::path::PathBuf::from(logfile_path.unwrap());
        let fname = path.file_stem().unwrap().to_str().unwrap();

        let yocto_failure = YoctoFailureKind::parse_from_logfilename(fname).unwrap();
        assert_eq!(yocto_failure, YoctoFailureKind::DoFetch);
    }
}