blob: 053448bf883f476c78cac1cf9620ab4e169f7c4e [file] [log] [blame]
David Brown2bc26852019-01-14 21:54:04 +00001//! Parallel testing.
2//!
3//! mcuboot simulator is strictly single threaded, as there is a lock around running the C startup
4//! code, because it contains numerous global variables.
5//!
David Brown48a4ec32021-01-04 17:02:27 -07006//! To help speed up testing, the Workflow configuration defines all of the configurations that can
David Brown2bc26852019-01-14 21:54:04 +00007//! be run in parallel. Fortunately, cargo works well this way, and these can be run by simply
8//! using subprocess for each particular thread.
David Brown48a4ec32021-01-04 17:02:27 -07009//!
10//! For now, we assume all of the features are listed under
11//! jobs->environment->strategy->matric->features
David Brown2bc26852019-01-14 21:54:04 +000012
13use chrono::Local;
David Brown2bc26852019-01-14 21:54:04 +000014use log::{debug, error, warn};
David Brown2bc26852019-01-14 21:54:04 +000015use std::{
16 collections::HashSet,
David Brownddd390a2021-01-12 12:04:56 -070017 env,
David Brown2bc26852019-01-14 21:54:04 +000018 fs::{self, OpenOptions},
19 io::{ErrorKind, stdout, Write},
David Brownddd390a2021-01-12 12:04:56 -070020 process::Command,
David Brown2bc26852019-01-14 21:54:04 +000021 result,
22 sync::{
23 Arc,
24 Mutex,
25 },
26 thread,
27 time::Duration,
28};
29use std_semaphore::Semaphore;
30use yaml_rust::{
31 Yaml,
32 YamlLoader,
33};
34
35type Result<T> = result::Result<T, failure::Error>;
36
37fn main() -> Result<()> {
38 env_logger::init();
39
David Brown48a4ec32021-01-04 17:02:27 -070040 let workflow_text = fs::read_to_string("../.github/workflows/sim.yaml")?;
41 let workflow = YamlLoader::load_from_str(&workflow_text)?;
David Brown2bc26852019-01-14 21:54:04 +000042
43 let ncpus = num_cpus::get();
44 let limiter = Arc::new(Semaphore::new(ncpus as isize));
45
David Brown91de33d2021-03-05 15:43:44 -070046 let matrix = Matrix::from_yaml(&workflow);
David Brown2bc26852019-01-14 21:54:04 +000047
48 let mut children = vec![];
49 let state = State::new(matrix.envs.len());
50 let st2 = state.clone();
51 let _status = thread::spawn(move || {
52 loop {
53 thread::sleep(Duration::new(15, 0));
54 st2.lock().unwrap().status();
55 }
56 });
57 for env in matrix.envs {
58 let state = state.clone();
59 let limiter = limiter.clone();
60
61 let child = thread::spawn(move || {
62 let _run = limiter.access();
63 state.lock().unwrap().start(&env);
64 let out = env.run();
65 state.lock().unwrap().done(&env, out);
66 });
67 children.push(child);
68 }
69
70 for child in children {
71 child.join().unwrap();
72 }
73
David Brown91de33d2021-03-05 15:43:44 -070074 println!();
David Brown2bc26852019-01-14 21:54:04 +000075
76 Ok(())
77}
78
79/// State, for printing status.
80struct State {
81 running: HashSet<String>,
82 done: HashSet<String>,
83 total: usize,
84}
85
David Brownddd390a2021-01-12 12:04:56 -070086/// Result of a test run.
87struct TestResult {
88 /// Was this run successful.
89 success: bool,
90
91 /// The captured output.
92 output: Vec<u8>,
93}
94
David Brown2bc26852019-01-14 21:54:04 +000095impl State {
96 fn new(total: usize) -> Arc<Mutex<State>> {
97 Arc::new(Mutex::new(State {
98 running: HashSet::new(),
99 done: HashSet::new(),
David Brown91de33d2021-03-05 15:43:44 -0700100 total,
David Brown2bc26852019-01-14 21:54:04 +0000101 }))
102 }
103
104 fn start(&mut self, fs: &FeatureSet) {
105 let key = fs.textual();
106 if self.running.contains(&key) || self.done.contains(&key) {
107 warn!("Duplicate: {:?}", key);
108 }
109 debug!("Starting: {} ({} running)", key, self.running.len() + 1);
110 self.running.insert(key);
111 self.status();
112 }
113
David Brownddd390a2021-01-12 12:04:56 -0700114 fn done(&mut self, fs: &FeatureSet, output: Result<TestResult>) {
David Brown2bc26852019-01-14 21:54:04 +0000115 let key = fs.textual();
116 self.running.remove(&key);
117 self.done.insert(key.clone());
118 match output {
David Brownddd390a2021-01-12 12:04:56 -0700119 Ok(output) => {
120 if !output.success || log_all() {
121 // Write the output into a file.
122 let mut count = 1;
123 let (mut fd, logname) = loop {
124 let base = if output.success { "success" } else { "failure" };
125 let name = format!("./{}-{:04}.log", base, count);
126 count += 1;
127 match OpenOptions::new()
128 .create_new(true)
129 .write(true)
130 .open(&name)
131 {
132 Ok(file) => break (file, name),
133 Err(ref err) if err.kind() == ErrorKind::AlreadyExists => continue,
134 Err(err) => {
135 error!("Unable to write log file to current directory: {:?}", err);
136 return;
137 }
David Brown2bc26852019-01-14 21:54:04 +0000138 }
David Brownddd390a2021-01-12 12:04:56 -0700139 };
140 fd.write_all(&output.output).unwrap();
141 if !output.success {
142 error!("Failure {} log:{:?} ({} running)", key, logname,
143 self.running.len());
David Brown2bc26852019-01-14 21:54:04 +0000144 }
David Brownddd390a2021-01-12 12:04:56 -0700145 }
David Brown2bc26852019-01-14 21:54:04 +0000146 }
147 Err(err) => {
David Brownddd390a2021-01-12 12:04:56 -0700148 error!("Unable to run test {:?} ({:?}", key, err);
David Brown2bc26852019-01-14 21:54:04 +0000149 }
150 }
151 self.status();
152 }
153
154 fn status(&self) {
155 let running = self.running.len();
156 let done = self.done.len();
157 print!(" {} running ({}/{}/{} done)\r", running, done, running + done, self.total);
158 stdout().flush().unwrap();
159 }
160}
161
David Brown48a4ec32021-01-04 17:02:27 -0700162/// The extracted configurations from the workflow config
David Brown2bc26852019-01-14 21:54:04 +0000163#[derive(Debug)]
164struct Matrix {
165 envs: Vec<FeatureSet>,
166}
167
168#[derive(Debug, Eq, Hash, PartialEq)]
169struct FeatureSet {
170 // The environment variable to set.
171 env: String,
172 // The successive values to set it to.
173 values: Vec<String>,
174}
175
176impl Matrix {
David Brown91de33d2021-03-05 15:43:44 -0700177 fn from_yaml(yaml: &[Yaml]) -> Matrix {
David Brown2bc26852019-01-14 21:54:04 +0000178 let mut envs = vec![];
179
180 let mut all_tests = HashSet::new();
181
182 for y in yaml {
183 let m = match lookup_matrix(y) {
184 Some (m) => m,
185 None => continue,
186 };
187 for elt in m {
David Brown48a4ec32021-01-04 17:02:27 -0700188 let elt = match elt.as_str() {
189 None => {
190 warn!("Unexpected yaml: {:?}", elt);
191 continue;
192 }
193 Some(e) => e,
194 };
David Brown91de33d2021-03-05 15:43:44 -0700195 let fset = FeatureSet::decode(elt);
David Brown2bc26852019-01-14 21:54:04 +0000196
David Brown48a4ec32021-01-04 17:02:27 -0700197 if false {
198 // Respect the groupings in the `.workflow.yml` file.
199 envs.push(fset);
200 } else {
201 // Break each test up so we can run more in
202 // parallel.
203 let env = fset.env.clone();
204 for val in fset.values {
205 if !all_tests.contains(&val) {
206 all_tests.insert(val.clone());
207 envs.push(FeatureSet {
208 env: env.clone(),
209 values: vec![val],
210 });
211 } else {
212 warn!("Duplicate: {:?}: {:?}", env, val);
David Brown2bc26852019-01-14 21:54:04 +0000213 }
214 }
215 }
216 }
217 }
218
David Brown91de33d2021-03-05 15:43:44 -0700219 Matrix {
220 envs,
221 }
David Brown2bc26852019-01-14 21:54:04 +0000222 }
223}
224
225impl FeatureSet {
David Brown91de33d2021-03-05 15:43:44 -0700226 fn decode(text: &str) -> FeatureSet {
David Brown48a4ec32021-01-04 17:02:27 -0700227 // The github workflow is just a space separated set of values.
228 let values: Vec<_> = text
229 .split(',')
230 .map(|s| s.to_string())
231 .collect();
David Brown91de33d2021-03-05 15:43:44 -0700232 FeatureSet {
David Brown48a4ec32021-01-04 17:02:27 -0700233 env: "MULTI_FEATURES".to_string(),
David Brown91de33d2021-03-05 15:43:44 -0700234 values,
235 }
David Brown2bc26852019-01-14 21:54:04 +0000236 }
237
238 /// Run a test for this given feature set. Output is captured and will be returned if there is
239 /// an error. Each will be run successively, and the first failure will be returned.
240 /// Otherwise, it returns None, which means everything worked.
David Brownddd390a2021-01-12 12:04:56 -0700241 fn run(&self) -> Result<TestResult> {
242 let mut output = vec![];
243 let mut success = true;
David Brown2bc26852019-01-14 21:54:04 +0000244 for v in &self.values {
David Brownddd390a2021-01-12 12:04:56 -0700245 let cmdout = Command::new("bash")
David Brownb899f792019-03-14 15:21:06 -0600246 .arg("./ci/sim_run.sh")
David Brown2bc26852019-01-14 21:54:04 +0000247 .current_dir("..")
248 .env(&self.env, v)
249 .output()?;
David Brownddd390a2021-01-12 12:04:56 -0700250 // Grab the output for logging, etc.
251 writeln!(&mut output, "Test {} {}",
252 if cmdout.status.success() { "success" } else { "FAILURE" },
253 self.textual())?;
254 writeln!(&mut output, "time: {}", Local::now().to_rfc3339())?;
255 writeln!(&mut output, "----------------------------------------")?;
256 writeln!(&mut output, "stdout:")?;
257 output.extend(&cmdout.stdout);
258 writeln!(&mut output, "----------------------------------------")?;
259 writeln!(&mut output, "stderr:")?;
260 output.extend(&cmdout.stderr);
261 if !cmdout.status.success() {
262 success = false;
David Brown2bc26852019-01-14 21:54:04 +0000263 }
264 }
David Brownddd390a2021-01-12 12:04:56 -0700265 Ok(TestResult { success, output })
David Brown2bc26852019-01-14 21:54:04 +0000266 }
267
268 /// Convert this feature set into a textual representation
269 fn textual(&self) -> String {
270 use std::fmt::Write;
271
272 let mut buf = String::new();
273
274 write!(&mut buf, "{}:", self.env).unwrap();
275 for v in &self.values {
276 write!(&mut buf, " {}", v).unwrap();
277 }
278
279 buf
280 }
281}
282
283fn lookup_matrix(y: &Yaml) -> Option<&Vec<Yaml>> {
David Brown48a4ec32021-01-04 17:02:27 -0700284 let jobs = Yaml::String("jobs".to_string());
285 let environment = Yaml::String("environment".to_string());
286 let strategy = Yaml::String("strategy".to_string());
David Brown2bc26852019-01-14 21:54:04 +0000287 let matrix = Yaml::String("matrix".to_string());
David Brown48a4ec32021-01-04 17:02:27 -0700288 let features = Yaml::String("features".to_string());
289 y
290 .as_hash()?.get(&jobs)?
291 .as_hash()?.get(&environment)?
292 .as_hash()?.get(&strategy)?
293 .as_hash()?.get(&matrix)?
294 .as_hash()?.get(&features)?
295 .as_vec()
David Brown2bc26852019-01-14 21:54:04 +0000296}
David Brownddd390a2021-01-12 12:04:56 -0700297
298/// Query if we should be logging all tests and not only failures.
299fn log_all() -> bool {
300 env::var("PTEST_LOG_ALL").is_ok()
301}