pipedash_plugin_azure_devops/
log_dialect.rs1use pipedash_plugin_actions::ActionsLogDialect;
2use pipedash_plugin_api::{
3 LineMarker,
4 LogDialect,
5 ParsedLine,
6};
7
8const SECTION: &[u8] = b"##[section]";
9const TASK_START: &[u8] = b"Starting: ";
10const TASK_END: &[u8] = b"Finishing: ";
11
12pub struct AzureDevOpsLogDialect;
13
14impl LogDialect for AzureDevOpsLogDialect {
15 fn parse<'a>(&self, line: &'a [u8]) -> ParsedLine<'a> {
16 let parsed = ActionsLogDialect.parse(line);
17 match (parsed.marker, parsed.body.strip_prefix(SECTION)) {
18 (LineMarker::Plain, Some(title)) => ParsedLine {
19 marker: if title.starts_with(TASK_END) {
20 LineMarker::Plain
21 } else {
22 LineMarker::SectionStart { open: false }
23 },
24 body: title,
25 ..parsed
26 },
27 _ => parsed,
28 }
29 }
30
31 fn is_step_start(&self, line: &ParsedLine<'_>) -> bool {
32 line.marker == (LineMarker::SectionStart { open: false })
33 && line.body.starts_with(TASK_START)
34 }
35}
36
37#[cfg(test)]
38mod tests {
39 use super::*;
40
41 const STAMP: &str = "2024-01-01T10:00:00.1234567Z";
42
43 fn parse(line: &str) -> ParsedLine<'_> {
44 AzureDevOpsLogDialect.parse(line.as_bytes())
45 }
46
47 fn stamped(text: &str) -> String {
48 format!("{STAMP} {text}")
49 }
50
51 #[test]
52 fn timestamp_has_seven_fraction_digits_and_a_z_suffix() {
53 let line = stamped("plain output");
54 let parsed = parse(&line);
55
56 assert_eq!(
57 parsed.timestamp.unwrap().to_rfc3339(),
58 "2024-01-01T10:00:00.123456700+00:00"
59 );
60 assert_eq!(parsed.marker, LineMarker::Plain);
61 assert_eq!(parsed.body, b"plain output");
62 }
63
64 #[test]
65 fn line_without_a_timestamp_keeps_its_text() {
66 let parsed = parse("##[error]no stamp");
67
68 assert_eq!(parsed.timestamp, None);
69 assert_eq!(parsed.marker, LineMarker::Error);
70 assert_eq!(parsed.body, b"no stamp");
71 }
72
73 #[test]
74 fn byte_order_mark_at_the_start_of_a_page_is_dropped() {
75 let line = format!("\u{feff}{}", stamped("##[section]Starting: Initialize job"));
76 let parsed = parse(&line);
77
78 assert!(parsed.timestamp.is_some());
79 assert_eq!(parsed.marker, LineMarker::SectionStart { open: false });
80 assert_eq!(parsed.body, b"Starting: Initialize job");
81 }
82
83 #[test]
84 fn section_starts_a_collapsed_section_titled_by_the_text() {
85 for title in [
86 "Starting: Initialize job",
87 "Async Command Start: DetectDockerContainer",
88 ] {
89 let line = stamped(&format!("##[section]{title}"));
90 let parsed = parse(&line);
91
92 assert_eq!(parsed.marker, LineMarker::SectionStart { open: false });
93 assert_eq!(parsed.body, title.as_bytes());
94 assert!(parsed.timestamp.is_some());
95 }
96 }
97
98 #[test]
99 fn finishing_a_task_stays_a_plain_line_inside_its_section() {
100 let line = stamped("##[section]Finishing: Checkout acme/app@main to s");
101 let parsed = parse(&line);
102
103 assert_eq!(parsed.marker, LineMarker::Plain);
104 assert_eq!(parsed.body, b"Finishing: Checkout acme/app@main to s");
105 assert!(parsed.timestamp.is_some());
106 }
107
108 #[test]
109 fn group_and_endgroup_open_and_close_a_group() {
110 let open = stamped("##[group]Run tests");
111 let close = stamped("##[endgroup]");
112
113 let opened = parse(&open);
114 let closed = parse(&close);
115
116 assert_eq!(opened.marker, LineMarker::GroupStart);
117 assert_eq!(opened.body, b"Run tests");
118 assert_eq!(closed.marker, LineMarker::GroupEnd);
119 assert_eq!(closed.body, b"");
120 }
121
122 #[test]
123 fn error_warning_command_and_debug_keep_their_text() {
124 let cases = [
125 ("##[error]Bash exited with code '1'.", LineMarker::Error),
126 ("##[warning]Deprecated task", LineMarker::Warning),
127 ("##[command]git fetch --prune", LineMarker::Command),
128 ("##[debug]Evaluating: variables", LineMarker::Debug),
129 ];
130 for (text, marker) in cases {
131 let line = stamped(text);
132 let parsed = parse(&line);
133
134 assert_eq!(parsed.marker, marker, "{text}");
135 assert_eq!(
136 parsed.body,
137 text.split_once(']').unwrap().1.as_bytes(),
138 "{text}"
139 );
140 assert!(parsed.timestamp.is_some(), "{text}");
141 }
142 }
143
144 #[test]
145 fn task_output_commands_without_the_hashes_are_commands() {
146 let line = stamped("[command]/usr/bin/git version");
147 let parsed = parse(&line);
148
149 assert_eq!(parsed.marker, LineMarker::Command);
150 assert_eq!(parsed.body, b"/usr/bin/git version");
151 }
152
153 #[test]
154 fn only_a_starting_section_starts_a_step() {
155 let starts = |text: &str| AzureDevOpsLogDialect.is_step_start(&parse(&stamped(text)));
156
157 assert!(starts("##[section]Starting: Initialize job"));
158 assert!(starts("##[section]Starting: CmdLine"));
159 assert!(!starts("##[section]Finishing: CmdLine"));
160 assert!(!starts("##[section]Async Command Start: Foo"));
161 assert!(!starts("Starting: CmdLine"));
162 assert!(!starts("##[group]Starting: CmdLine"));
163 assert!(!starts("##[error]Starting: CmdLine"));
164 assert!(!starts("##[debug]Starting: CmdLine"));
165 }
166
167 #[test]
168 fn section_marker_in_the_middle_of_a_line_is_output() {
169 let line = stamped("echo ##[section]not a marker");
170 let parsed = parse(&line);
171
172 assert_eq!(parsed.marker, LineMarker::Plain);
173 assert_eq!(parsed.body, b"echo ##[section]not a marker");
174 }
175}