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}