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> dynFnOnce(&'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}
162