Skip to main content

pipedash_plugin_azure_devops/
log_dialect.rs

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