gh_workflow_parser/commands/
locate_failure_log.rs

1use crate::util::first_path_from_str;
2use std::{io, path::PathBuf};
3
4use super::BuildKind;
5
6/// Locate the specific failure log in a failed build/test/other from a log file
7///
8/// # Arguments
9///
10/// * `kind` - The [BuildKind] (e.g. Yocto)
11/// * `log_file` - Log file to search for the failure log (e.g. log.txt or read from stdin)
12///
13/// e.g. if you have the log of a failed Yocto build (stdout & stderr) stored in log.txt, you can run use
14/// `gh-workflow-parser locate-failure-log --kind Yocto log.txt` to get an absolute path to the failure log
15/// e.g. a log.do_fetch.1234 file
16pub fn locate_failure_log(
17    kind: BuildKind,
18    log_file: Option<&PathBuf>,
19) -> Result<(), Box<dyn std::error::Error>> {
20    let logfile_content: String = match log_file {
21        Some(file) => {
22            log::info!("Reading log file: {file:?}");
23            if !file.exists() {
24                return Err(format!("File: {file:?} does not exist",).into());
25            }
26            std::fs::read_to_string(file)?
27        },
28        None => {
29            log::info!("Reading log from stdin");
30            let stdin = io::stdin();
31            let mut handle = stdin.lock();
32            let mut buf = String::new();
33            io::Read::read_to_string(&mut handle, &mut buf)?;
34            buf
35        },
36    };
37
38    match kind {
39        BuildKind::Yocto => locate_yocto_failure_log(&logfile_content)?,
40        BuildKind::Other => todo!("This feature is not implemented yet!"),
41    }
42
43    Ok(())
44}
45
46/// Locate the specific failure log in a failed Yocto build from the contents of a log file
47///
48/// # Arguments
49/// * `logfile_content` - The contents of the log file
50///
51/// # Returns
52/// The absolute path to the failure log
53///
54/// # Errors
55/// Returns an error if the log file does not contain a failure log
56///
57/// # Example
58/// ```no_run
59/// # use gh_workflow_parser::commands::locate_failure_log::locate_yocto_failure_log;
60/// let logfile_content = r#"multi line
61/// test string foo/bar/baz.txt and other
62/// contents"#;
63/// locate_yocto_failure_log(logfile_content).unwrap();
64/// // Prints the absolute path to "foo/bar/baz.txt" to stdout
65/// ```
66///
67pub fn locate_yocto_failure_log(logfile_content: &str) -> Result<(), Box<dyn std::error::Error>> {
68    use crate::err_msg_parse::yocto_err::util;
69    use std::io::Write;
70
71    log::trace!("Finding failure log in log file contents: {logfile_content}");
72    let error_summary = util::yocto_error_summary(logfile_content)?;
73    let error_summary = util::trim_trailing_just_recipes(&error_summary)?;
74    log::trace!("Trimmed error summary: {error_summary}");
75    let log_file_line = util::find_yocto_failure_log_str(&error_summary)?;
76    let path = logfile_path_from_str(log_file_line)?;
77    // write to stdout
78    crate::macros::pipe_print!("{}", path.to_string_lossy())?;
79
80    Ok(())
81}
82
83/// Find the absolute path of the first path found in a string.
84///
85/// e.g. "foo yocto/test/bar.txt baz" returns the absolute path to "yocto/test/bar.txt"
86///
87/// Takes the following steps:
88/// 1. Find a (unix) path in the string
89/// 2. Check if the path exists then:
90/// - **Path exists:** check that it is a file, then get the absolute path and return it
91/// - **Path does not exist:** Attempt to find the file using the following steps:
92///      1. Remove the first `/` from the string and try the remaining string as a path
93///      2. Remove the next part of the string after the first `/` and try the remaining string as a path
94///      3. Repeat step 1-2 until we find a path that exists or there are no more `/` in the string
95///      4. If no path is found, return an error
96pub fn logfile_path_from_str(s: &str) -> Result<PathBuf, Box<dyn std::error::Error>> {
97    let path = first_path_from_str(s)?;
98    log::debug!("Searching for logfile from path: {path:?}");
99    if path.exists() {
100        return canonicalize_if_file(path);
101    }
102
103    let mut parts = path.components().collect::<Vec<_>>();
104    log::debug!("File not found, looking for file using parts: {parts:?}");
105    for _ in 0..parts.len() {
106        parts.remove(0);
107        let tmp_path = parts.iter().collect::<PathBuf>();
108        log::debug!("Looking for file at path: {tmp_path:?}");
109        if tmp_path.exists() {
110            return canonicalize_if_file(tmp_path);
111        }
112        // Then try the path from root (with '/' at the start)
113        let tmp_path_from_root = PathBuf::from("/").join(tmp_path);
114        log::debug!("Looking for file at path: {tmp_path_from_root:?}");
115        if tmp_path_from_root.exists() {
116            return canonicalize_if_file(tmp_path_from_root);
117        }
118    }
119
120    Err(format!("No file found at path: {s}").into())
121}
122
123/// Checks if the path is a file and returns the absolute path if it is
124/// # Errors
125/// Returns an error if the path is not a file
126fn canonicalize_if_file(path: PathBuf) -> Result<PathBuf, Box<dyn std::error::Error>> {
127    if path.is_file() {
128        return Ok(path.canonicalize()?);
129    }
130    Err(format!("No file found at path: {path:?}").into())
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use temp_dir::TempDir;
137
138    #[test]
139    fn test_logfile_path_from_str_simple() {
140        // Create a temporary file and write the test string to it
141        let dir = TempDir::new().unwrap();
142        let dir_file = dir.child("test.log");
143        let tmp_log_file = dir_file.as_path();
144        // The test log string is formatted with the path to the temporary file
145        let test_log_str = format!(
146            "ERROR: Logfile of failure stored in: /app{real_location}",
147            real_location = tmp_log_file.to_string_lossy()
148        );
149        std::fs::write(tmp_log_file, &test_log_str).unwrap();
150
151        // Get the path from the test string
152        let path = logfile_path_from_str(&test_log_str).unwrap();
153
154        // Check that the path is the same as the temporary file
155        assert_eq!(path, tmp_log_file);
156    }
157
158    #[test]
159    fn test_logfile_path_from_str() {
160        let dir = TempDir::new().unwrap();
161        let real_path_str =
162            r#"yocto/build/tmp/work/x86_64-linux/sqlite3-native/3.43.2/temp/log.do_fetch.21616"#;
163        // Create the whole path in the temp dir
164        let path_to_log = dir.path().join(real_path_str);
165        // Make the whole path
166        std::fs::create_dir_all(path_to_log.parent().unwrap()).unwrap();
167        // The test log string is formatted with the path to the temporary file
168        let test_log_str = format!(
169            r"other contents
170ERROR: Logfile of failure stored in: /app{real_location} other contents
171other contents",
172            real_location = &path_to_log.to_string_lossy()
173        );
174        // Create the file with the test string
175        std::fs::write(&path_to_log, &test_log_str).unwrap();
176
177        // Attempt to get the path from the test string
178        let path = logfile_path_from_str(&test_log_str).unwrap();
179        // Check that the path is the same as the temporary file
180        assert_eq!(path, path_to_log);
181    }
182}