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
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
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 ")
                // Also skip the last line that looks like `##[error]Process completed with exit code 2.`
                || line.starts_with("##[error]Process completed with exit code")
        })
        .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);
    }

    const TEST_NOT_TRIMMED_YOCTO_ERROR_SUMMARY: &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.21665
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-16 12:45:43 - ERROR    - Command "/app/yocto/poky/bitbake/bin/bitbake -c build test-template-ci-xilinx-image package-index" failed with error 1
error: Recipe `in-container-build-ci-image` failed on line 31 with exit code 2
error: Recipe `run-in-docker` failed with exit code 2
error: Recipe `build-ci-image` failed with exit code 2"#;

    const TEST_EXPECT_TRIMMED_YOCTO_ERROR_SUMMARY: &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.21665
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-16 12:45:43 - ERROR    - Command "/app/yocto/poky/bitbake/bin/bitbake -c build test-template-ci-xilinx-image package-index" failed with error 1
"#;

    #[test]
    pub fn test_trim_yocto_error_summary() {
        let trimmed = trim_trailing_just_recipes(TEST_NOT_TRIMMED_YOCTO_ERROR_SUMMARY).unwrap();
        eprintln!("{trimmed}");
        assert_eq!(trimmed, TEST_EXPECT_TRIMMED_YOCTO_ERROR_SUMMARY);
    }
}