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}