Skip to main content

flowey_lib_common/
publish_test_results.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Publish test results.
5//!
6//! - On ADO, this will hook into the backend's native JUnit handling.
7//! - On Github, this will publish artifacts containing the raw JUnit XML file
8//!   and any optional attachments.
9//! - When running locally, this will optionally copy the XML files and any
10//!   attachments to the provided artifact directory.
11
12use crate::_util::copy_dir_all;
13use crate::run_cargo_nextest_run::TestResults;
14use flowey::node::prelude::*;
15use std::collections::BTreeMap;
16
17flowey_request! {
18    pub struct Request {
19        /// Contains whether all passed and the path to a junit.xml file
20        ///
21        /// HACK: this is an optional since `flowey` doesn't (yet?) have any way
22        /// to perform conditional-requests, and there are instances where nodes
23        /// will only conditionally output JUnit XML.
24        ///
25        /// To keep making forward progress, I've tweaked this node to accept an
26        /// optional... but this ain't great.
27        pub test_results: ReadVar<TestResults>,
28        /// Brief string used when publishing the test.
29        /// Must be unique to the pipeline.
30        pub test_label: String,
31        /// Additional files or directories to upload.
32        ///
33        /// The boolean indicates whether the attachment is referenced in the
34        /// JUnit XML file. On backends with native JUnit attachment support,
35        /// these attachments will not be uploaded as distinct artifacts and
36        /// will instead be uploaded via the JUnit integration.
37        pub attachments: BTreeMap<String, (ReadVar<PathBuf>, bool)>,
38        /// Copy the xml file and attachments to the provided directory.
39        /// Only supported on local backend.
40        pub output_dir: Option<ReadVar<PathBuf>>,
41        /// Upload logs on success (logs are always uploaded on failure)
42        pub upload_logs_on_success: bool,
43        /// Side-effect confirming that the publish has succeeded
44        pub done: WriteVar<SideEffect>,
45    }
46}
47
48new_flow_node!(struct Node);
49
50impl FlowNode for Node {
51    type Request = Request;
52
53    fn imports(ctx: &mut ImportCtx<'_>) {
54        ctx.import::<crate::ado_task_publish_test_results::Node>();
55    }
56
57    fn emit(requests: Vec<Self::Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
58        let mut use_side_effects = Vec::new();
59        let mut resolve_side_effects = Vec::new();
60
61        for Request {
62            test_results,
63            test_label: label,
64            attachments,
65            output_dir,
66            upload_logs_on_success,
67            done,
68        } in requests
69        {
70            resolve_side_effects.push(done);
71
72            if output_dir.is_some() && !matches!(ctx.backend(), FlowBackend::Local) {
73                anyhow::bail!(
74                    "Copying to a custom output directory is only supported on local backend."
75                )
76            }
77
78            let step_name = format!("publish test results: {label} (JUnit XML)");
79            let artifact_name = format!("{label}-junit-xml");
80
81            let should_publish_junit_xml = test_results.map(ctx, move |r| {
82                r.junit_xml.is_some() && (upload_logs_on_success || !r.all_tests_passed)
83            });
84
85            match ctx.backend() {
86                FlowBackend::Ado => {
87                    let results_file = test_results.map(ctx, |p| p.junit_xml.unwrap_or_default());
88                    use_side_effects.push(ctx.reqv(|v| {
89                        crate::ado_task_publish_test_results::Request {
90                            step_name,
91                            format:
92                                crate::ado_task_publish_test_results::AdoTestResultsFormat::JUnit,
93                            results_file,
94                            test_title: label.clone(),
95                            condition: Some(should_publish_junit_xml),
96                            done: v,
97                        }
98                    }));
99                }
100                FlowBackend::Github => {
101                    let junit_xml = test_results.map(ctx, |p| {
102                        p.junit_xml
103                            .map(|p| p.absolute().expect("invalid path").display().to_string())
104                            .unwrap_or_default()
105                    });
106
107                    // Note: usually flowey's built-in artifact publishing API
108                    // should be used instead of this, but here we need to
109                    // manually upload the artifact now so that it is still
110                    // uploaded even if the pipeline fails.
111                    // actions/upload-artifact v7.0.1
112                    use_side_effects.push(
113                        ctx.emit_gh_step(
114                            step_name,
115                            "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a",
116                        )
117                        .condition(should_publish_junit_xml)
118                        .with("name", artifact_name)
119                        .with("path", junit_xml)
120                        .finish(ctx),
121                    );
122                }
123                FlowBackend::Local => {
124                    if let Some(output_dir) = output_dir.clone() {
125                        use_side_effects.push(ctx.emit_rust_step(step_name, |ctx| {
126                            let output_dir = output_dir.claim(ctx);
127                            let test_results = test_results.clone().claim(ctx);
128                            let should_publish_junit_xml = should_publish_junit_xml.claim(ctx);
129
130                            move |rt| {
131                                let output_dir = rt.read(output_dir);
132                                let test_results = rt.read(test_results);
133                                let should_publish_junit_xml = rt.read(should_publish_junit_xml);
134
135                                if let Some(junit_xml) = test_results.junit_xml
136                                    && should_publish_junit_xml
137                                {
138                                    fs_err::copy(
139                                        junit_xml,
140                                        output_dir.join(format!("{artifact_name}.xml")),
141                                    )?;
142                                }
143
144                                Ok(())
145                            }
146                        }));
147                    } else {
148                        use_side_effects.push(should_publish_junit_xml.into_side_effect());
149                    }
150                }
151            }
152
153            for (attachment_label, (attachment_path, publish_on_ado)) in attachments {
154                let step_name = format!("publish test results: {label} ({attachment_label})");
155                let artifact_name = format!("{label}-{attachment_label}");
156                let should_publish =
157                    attachment_path
158                        .zip(ctx, test_results.clone())
159                        .map(ctx, move |(p, r)| {
160                            (upload_logs_on_success || !r.all_tests_passed)
161                                && p.exists()
162                                && (p.is_file()
163                                    || p.read_dir()
164                                        .expect("failed to read attachment dir")
165                                        .next()
166                                        .is_some())
167                        });
168                let attachment_path_string = attachment_path.map(ctx, |p| {
169                    p.absolute().expect("invalid path").display().to_string()
170                });
171
172                match ctx.backend() {
173                    FlowBackend::Ado => {
174                        if publish_on_ado {
175                            let (published_read, published_write) = ctx.new_var();
176                            use_side_effects.push(published_read);
177
178                            // Note: usually flowey's built-in artifact publishing API
179                            // should be used instead of this, but here we need to
180                            // manually upload the artifact now so that it is still
181                            // uploaded even if the pipeline fails.
182                            ctx.emit_ado_step_with_condition(
183                                step_name.clone(),
184                                should_publish,
185                                |ctx| {
186                                    published_write.claim(ctx);
187                                    let attachment_path_string = attachment_path_string.claim(ctx);
188                                    move |rt| {
189                                        let path_var =
190                                            rt.get_var(attachment_path_string).as_raw_var_name();
191                                        // Artifact name includes the JobAttempt to
192                                        // differentiate between artifacts that were
193                                        // generated when rerunning failed jobs.
194                                        format!(
195                                            r#"
196                                            - publish: $({path_var})
197                                              artifact: {artifact_name}-$({})
198                                            "#,
199                                            AdoRuntimeVar::SYSTEM_JOB_ATTEMPT.as_raw_var_name()
200                                        )
201                                    }
202                                },
203                            );
204                        } else {
205                            use_side_effects.push(should_publish.into_side_effect());
206                            use_side_effects.push(attachment_path_string.into_side_effect());
207                        }
208                    }
209                    FlowBackend::Github => {
210                        // See above comment about manually publishing artifacts
211                        // actions/upload-artifact v7.0.1
212                        use_side_effects.push(
213                            ctx.emit_gh_step(
214                                step_name.clone(),
215                                "actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a",
216                            )
217                            .condition(should_publish)
218                            .with("name", artifact_name)
219                            .with("path", attachment_path_string)
220                            .finish(ctx),
221                        );
222                    }
223                    FlowBackend::Local => {
224                        if let Some(output_dir) = output_dir.clone() {
225                            use_side_effects.push(ctx.emit_rust_step(step_name, |ctx| {
226                                let output_dir = output_dir.claim(ctx);
227                                let should_publish = should_publish.claim(ctx);
228                                let attachment_path = attachment_path.claim(ctx);
229
230                                move |rt| {
231                                    let output_dir = rt.read(output_dir);
232                                    let should_publish = rt.read(should_publish);
233                                    let attachment_path = rt.read(attachment_path);
234
235                                    if should_publish {
236                                        copy_dir_all(
237                                            attachment_path,
238                                            output_dir.join(artifact_name),
239                                        )?;
240                                    }
241
242                                    Ok(())
243                                }
244                            }));
245                        } else {
246                            use_side_effects.push(should_publish.into_side_effect());
247                        }
248                        use_side_effects.push(attachment_path_string.into_side_effect());
249                    }
250                }
251            }
252        }
253        ctx.emit_side_effect_step(use_side_effects, resolve_side_effects);
254
255        Ok(())
256    }
257}