ui_test/
build_manager.rs

1//! Auxiliary and dependency builder. Extendable to custom builds.
2
3use crate::{
4    per_test_config::TestConfig,
5    status_emitter::{RevisionStyle, TestStatus},
6    test_result::{TestResult, TestRun},
7    Config, Errored,
8};
9use color_eyre::eyre::Result;
10use crossbeam_channel::{bounded, Sender};
11use std::{
12    collections::{hash_map::Entry, HashMap},
13    ffi::OsString,
14    sync::{Arc, OnceLock, RwLock},
15};
16
17/// A build shared between all tests of the same `BuildManager`
18pub trait Build {
19    /// Runs the build and returns command line args to add to the test so it can find
20    /// the built things.
21    fn build(&self, build_manager: &BuildManager) -> Result<Vec<OsString>, Errored>;
22    /// Must uniquely describe the build, as it is used for checking that a value
23    /// has already been cached.
24    fn description(&self) -> String;
25}
26
27/// Deduplicates builds
28pub struct BuildManager {
29    #[allow(clippy::type_complexity)]
30    cache: RwLock<HashMap<String, Arc<OnceLock<Result<Vec<OsString>, ()>>>>>,
31    pub(crate) config: Config,
32    new_job_submitter: Sender<NewJob>,
33}
34
35/// Type of closure that is used to run individual tests.
36pub type NewJob = Box<dyn Send + for<'a> FnOnce(&'a Sender<TestRun>) -> Result<()>>;
37
38impl BuildManager {
39    /// Create a new `BuildManager` for a specific `Config`. Each `Config` needs
40    /// to have its own.
41    pub fn new(config: Config, new_job_submitter: Sender<NewJob>) -> Self {
42        Self {
43            cache: Default::default(),
44            config,
45            new_job_submitter,
46        }
47    }
48
49    /// Create a new `BuildManager` that cannot create new sub-jobs.
50    pub fn one_off(config: Config) -> Self {
51        Self::new(config, bounded(0).0)
52    }
53
54    /// Lazily add more jobs after a test has finished. These are added to the queue
55    /// as normally, but nested below the test.
56    pub fn add_new_job(
57        &self,
58        mut config: TestConfig,
59        job: impl Send + 'static + FnOnce(&mut TestConfig) -> TestResult,
60    ) {
61        if self.aborted() {
62            return;
63        }
64        self.new_job_submitter
65            .send(Box::new(move |sender| {
66                let result = job(&mut config);
67                let result = TestRun {
68                    result,
69                    status: config.status,
70                    abort_check: config.config.abort_check,
71                };
72                Ok(sender.send(result)?)
73            }))
74            .unwrap()
75    }
76
77    /// This function will block until the build is done and then return the arguments
78    /// that need to be passed in order to build the dependencies.
79    /// The error is only reported once, all follow up invocations of the same build will
80    /// have a generic error about a previous build failing.
81    pub fn build(
82        &self,
83        what: impl Build,
84        status: &dyn TestStatus,
85    ) -> Result<Vec<OsString>, Errored> {
86        let description = what.description();
87        // Fast path without much contention.
88        if let Some(res) = self
89            .cache
90            .read()
91            .unwrap()
92            .get(&description)
93            .and_then(|o| o.get())
94        {
95            return res.clone().map_err(|()| Errored {
96                command: format!("{description:?}"),
97                errors: vec![],
98                stderr: b"previous build failed".to_vec(),
99                stdout: vec![],
100            });
101        }
102        let mut lock = self.cache.write().unwrap();
103        let once = match lock.entry(description) {
104            Entry::Occupied(entry) => {
105                if let Some(res) = entry.get().get() {
106                    return res.clone().map_err(|()| Errored {
107                        command: format!("{:?}", what.description()),
108                        errors: vec![],
109                        stderr: b"previous build failed".to_vec(),
110                        stdout: vec![],
111                    });
112                }
113                entry.get().clone()
114            }
115            Entry::Vacant(entry) => {
116                let once = Arc::new(OnceLock::new());
117                entry.insert(once.clone());
118                once
119            }
120        };
121        drop(lock);
122
123        let mut err = None;
124        once.get_or_init(|| {
125            let description = what.description();
126            let build = status.for_revision(&description, RevisionStyle::Separate);
127            let res = what.build(self).map_err(|e| err = Some(e));
128            build.done(
129                &res.as_ref()
130                    .map(|_| crate::test_result::TestOk::Ok)
131                    .map_err(|()| Errored {
132                        command: description,
133                        errors: vec![],
134                        stderr: vec![],
135                        stdout: vec![],
136                    }),
137                self.aborted(),
138            );
139            res
140        })
141        .clone()
142        .map_err(|()| {
143            err.unwrap_or_else(|| Errored {
144                command: what.description(),
145                errors: vec![],
146                stderr: b"previous build failed".to_vec(),
147                stdout: vec![],
148            })
149        })
150    }
151
152    /// The `Config` used for all builds.
153    pub fn config(&self) -> &Config {
154        &self.config
155    }
156
157    /// Whether the build was cancelled
158    pub fn aborted(&self) -> bool {
159        self.config.abort_check.aborted()
160    }
161}