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