Skip to main content

pipedash_plugin_jenkins/
log_dialect.rs

1use pipedash_plugin_api::{
2    LineMarker,
3    LogDialect,
4    ParsedLine,
5};
6
7const PIPELINE_PREFIX: &[u8] = b"[Pipeline] ";
8const BODY_START: &[u8] = b"{ (";
9const STAGE_END: &[u8] = b"// stage";
10const SHELL_STEP: &[u8] = b"sh";
11const PARALLEL_BRANCH: &[u8] = b"Branch: ";
12const ERROR_PREFIX: &[u8] = b"ERROR: ";
13const FAILED_BUILDS: [&[u8]; 2] = [b"Finished: FAILURE", b"Finished: UNSTABLE"];
14const TRACED_COMMAND: &[u8] = b"+ ";
15
16pub struct JenkinsLogDialect;
17
18impl LogDialect for JenkinsLogDialect {
19    fn parse<'a>(&self, line: &'a [u8]) -> ParsedLine<'a> {
20        let (marker, body) = match line.strip_prefix(PIPELINE_PREFIX) {
21            Some(step) => classify_step(line, step),
22            None => (classify_output(line), line),
23        };
24        ParsedLine {
25            timestamp: None,
26            marker,
27            body,
28        }
29    }
30
31    fn infers_errors(&self) -> bool {
32        false
33    }
34}
35
36fn classify_step<'a>(line: &'a [u8], step: &'a [u8]) -> (LineMarker, &'a [u8]) {
37    if step == STAGE_END {
38        (LineMarker::GroupEnd, &[])
39    } else if step == SHELL_STEP {
40        (LineMarker::Command, line)
41    } else if let Some(name) = stage_name(step) {
42        (LineMarker::SectionStart { open: false }, name)
43    } else {
44        (LineMarker::Plain, line)
45    }
46}
47
48fn stage_name(step: &[u8]) -> Option<&[u8]> {
49    step.strip_prefix(BODY_START)?
50        .strip_suffix(b")")
51        .filter(|name| !name.is_empty() && !name.starts_with(PARALLEL_BRANCH))
52}
53
54fn classify_output(line: &[u8]) -> LineMarker {
55    if line.starts_with(ERROR_PREFIX) || FAILED_BUILDS.contains(&line) {
56        LineMarker::Error
57    } else if line.starts_with(TRACED_COMMAND) {
58        LineMarker::Command
59    } else {
60        LineMarker::Plain
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    fn parse(line: &[u8]) -> ParsedLine<'_> {
69        JenkinsLogDialect.parse(line)
70    }
71
72    #[test]
73    fn stage_body_opening_starts_a_section_named_after_the_stage() {
74        let cases: [(&[u8], &[u8]); 4] = [
75            (b"[Pipeline] { (Build)", b"Build"),
76            (
77                b"[Pipeline] { (Declarative: Checkout SCM)",
78                b"Declarative: Checkout SCM",
79            ),
80            (b"[Pipeline] { (Deploy (prod))", b"Deploy (prod)"),
81            (b"[Pipeline] { (build & test)", b"build & test"),
82        ];
83        for (line, name) in cases {
84            let parsed = parse(line);
85            assert_eq!(
86                parsed.marker,
87                LineMarker::SectionStart { open: false },
88                "{line:?}"
89            );
90            assert_eq!(parsed.body, name, "{line:?}");
91        }
92    }
93
94    #[test]
95    fn stage_step_close_ends_the_group() {
96        let parsed = parse(b"[Pipeline] // stage");
97        assert_eq!(parsed.marker, LineMarker::GroupEnd);
98    }
99
100    #[test]
101    fn other_pipeline_lines_stay_plain() {
102        let lines: [&[u8]; 12] = [
103            b"[Pipeline] stage",
104            b"[Pipeline] {",
105            b"[Pipeline] }",
106            b"[Pipeline] // node",
107            b"[Pipeline] // stages",
108            b"[Pipeline] echo",
109            b"[Pipeline] node",
110            b"[Pipeline] Start of Pipeline",
111            b"[Pipeline] { (Branch: unit)",
112            b"[Pipeline] { ()",
113            b"[Pipeline] { (unclosed",
114            b"[Pipeline] shell",
115        ];
116        for line in lines {
117            let parsed = parse(line);
118            assert_eq!(parsed.marker, LineMarker::Plain, "{line:?}");
119            assert_eq!(parsed.body, line, "{line:?}");
120        }
121    }
122
123    #[test]
124    fn shell_step_and_traced_commands_are_commands() {
125        let lines: [&[u8]; 3] = [b"[Pipeline] sh", b"+ make test", b"+ echo \"a + b\""];
126        for line in lines {
127            let parsed = parse(line);
128            assert_eq!(parsed.marker, LineMarker::Command, "{line:?}");
129            assert_eq!(parsed.body, line, "{line:?}");
130        }
131    }
132
133    #[test]
134    fn plus_without_a_space_is_output() {
135        for line in [b"+make".as_slice(), b"++ nested", b"a + b", b"+"] {
136            assert_eq!(parse(line).marker, LineMarker::Plain, "{line:?}");
137        }
138    }
139
140    #[test]
141    fn error_lines_and_failed_results_are_errors() {
142        let lines: [&[u8]; 4] = [
143            b"ERROR: script returned exit code 1",
144            b"ERROR: Build step failed with exception",
145            b"Finished: FAILURE",
146            b"Finished: UNSTABLE",
147        ];
148        for line in lines {
149            let parsed = parse(line);
150            assert_eq!(parsed.marker, LineMarker::Error, "{line:?}");
151            assert_eq!(parsed.body, line, "{line:?}");
152        }
153    }
154
155    #[test]
156    fn other_results_and_look_alikes_are_output() {
157        let lines: [&[u8]; 5] = [
158            b"Finished: SUCCESS",
159            b"Finished: ABORTED",
160            b"Finished: FAILURE twice",
161            b"ERROR:",
162            b"error: lowercase",
163        ];
164        for line in lines {
165            assert_eq!(parse(line).marker, LineMarker::Plain, "{line:?}");
166        }
167    }
168
169    #[test]
170    fn freestyle_console_has_no_sections() {
171        let lines: [&[u8]; 6] = [
172            b"Started by user alice",
173            b"Running as SYSTEM",
174            b"Building in workspace /var/jenkins_home/workspace/app",
175            b"[app] $ /bin/sh -xe /tmp/jenkins123.sh",
176            b"Hello from the build",
177            b"Finished: SUCCESS",
178        ];
179        for line in lines {
180            let parsed = parse(line);
181            assert_eq!(parsed.marker, LineMarker::Plain, "{line:?}");
182            assert_eq!(parsed.body, line, "{line:?}");
183            assert_eq!(parsed.timestamp, None);
184        }
185    }
186}