blob: f6b860212c7a76109101b4c6a0440171978a7390 [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,
17 fs::{self, OpenOptions},
18 io::{ErrorKind, stdout, Write},
19 process::{Command, Output},
20 result,
21 sync::{
22 Arc,
23 Mutex,
24 },
25 thread,
26 time::Duration,
27};
28use std_semaphore::Semaphore;
29use yaml_rust::{
30 Yaml,
31 YamlLoader,
32};
33
34type Result<T> = result::Result<T, failure::Error>;
35
36fn main() -> Result<()> {
37 env_logger::init();
38
David Brown48a4ec32021-01-04 17:02:27 -070039 let workflow_text = fs::read_to_string("../.github/workflows/sim.yaml")?;
40 let workflow = YamlLoader::load_from_str(&workflow_text)?;
David Brown2bc26852019-01-14 21:54:04 +000041
42 let ncpus = num_cpus::get();
43 let limiter = Arc::new(Semaphore::new(ncpus as isize));
44
David Brown48a4ec32021-01-04 17:02:27 -070045 let matrix = Matrix::from_yaml(&workflow)?;
David Brown2bc26852019-01-14 21:54:04 +000046
47 let mut children = vec![];
48 let state = State::new(matrix.envs.len());
49 let st2 = state.clone();
50 let _status = thread::spawn(move || {
51 loop {
52 thread::sleep(Duration::new(15, 0));
53 st2.lock().unwrap().status();
54 }
55 });
56 for env in matrix.envs {
57 let state = state.clone();
58 let limiter = limiter.clone();
59
60 let child = thread::spawn(move || {
61 let _run = limiter.access();
62 state.lock().unwrap().start(&env);
63 let out = env.run();
64 state.lock().unwrap().done(&env, out);
65 });
66 children.push(child);
67 }
68
69 for child in children {
70 child.join().unwrap();
71 }
72
73 println!("");
74
75 Ok(())
76}
77
78/// State, for printing status.
79struct State {
80 running: HashSet<String>,
81 done: HashSet<String>,
82 total: usize,
83}
84
85impl State {
86 fn new(total: usize) -> Arc<Mutex<State>> {
87 Arc::new(Mutex::new(State {
88 running: HashSet::new(),
89 done: HashSet::new(),
90 total: total,
91 }))
92 }
93
94 fn start(&mut self, fs: &FeatureSet) {
95 let key = fs.textual();
96 if self.running.contains(&key) || self.done.contains(&key) {
97 warn!("Duplicate: {:?}", key);
98 }
99 debug!("Starting: {} ({} running)", key, self.running.len() + 1);
100 self.running.insert(key);
101 self.status();
102 }
103
104 fn done(&mut self, fs: &FeatureSet, output: Result<Option<Output>>) {
105 let key = fs.textual();
106 self.running.remove(&key);
107 self.done.insert(key.clone());
108 match output {
109 Ok(None) => {
110 // println!("Success {} ({} running)", key, self.running.len());
111 }
112 Ok(Some(output)) => {
113 // Write the output into a file.
114 let mut count = 1;
115 let (mut fd, logname) = loop {
116 let name = format!("./failure-{:04}.log", count);
117 count += 1;
118 match OpenOptions::new()
119 .create_new(true)
120 .write(true)
121 .open(&name)
122 {
123 Ok(file) => break (file, name),
124 Err(ref err) if err.kind() == ErrorKind::AlreadyExists => continue,
125 Err(err) => {
126 error!("Unable to write log file to current directory: {:?}", err);
127 return;
128 }
129 }
130 };
131 writeln!(&mut fd, "Test failure {}", key).unwrap();
132 writeln!(&mut fd, "time: {}", Local::now().to_rfc3339()).unwrap();
133 writeln!(&mut fd, "----------------------------------------").unwrap();
134 writeln!(&mut fd, "stdout:").unwrap();
135 fd.write_all(&output.stdout).unwrap();
136 writeln!(&mut fd, "----------------------------------------").unwrap();
137 writeln!(&mut fd, "\nstderr:").unwrap();
138 fd.write_all(&output.stderr).unwrap();
139 error!("Failure {} log:{:?} ({} running)", key, logname,
140 self.running.len());
141 }
142 Err(err) => {
143 error!("Unable to run test {:?} ({:?})", key, err);
144 }
145 }
146 self.status();
147 }
148
149 fn status(&self) {
150 let running = self.running.len();
151 let done = self.done.len();
152 print!(" {} running ({}/{}/{} done)\r", running, done, running + done, self.total);
153 stdout().flush().unwrap();
154 }
155}
156
David Brown48a4ec32021-01-04 17:02:27 -0700157/// The extracted configurations from the workflow config
David Brown2bc26852019-01-14 21:54:04 +0000158#[derive(Debug)]
159struct Matrix {
160 envs: Vec<FeatureSet>,
161}
162
163#[derive(Debug, Eq, Hash, PartialEq)]
164struct FeatureSet {
165 // The environment variable to set.
166 env: String,
167 // The successive values to set it to.
168 values: Vec<String>,
169}
170
171impl Matrix {
172 fn from_yaml(yaml: &[Yaml]) -> Result<Matrix> {
173 let mut envs = vec![];
174
175 let mut all_tests = HashSet::new();
176
177 for y in yaml {
178 let m = match lookup_matrix(y) {
179 Some (m) => m,
180 None => continue,
181 };
182 for elt in m {
David Brown48a4ec32021-01-04 17:02:27 -0700183 let elt = match elt.as_str() {
184 None => {
185 warn!("Unexpected yaml: {:?}", elt);
186 continue;
187 }
188 Some(e) => e,
189 };
190 let fset = match FeatureSet::decode(elt) {
191 Ok(fset) => fset,
192 Err(err) => {
193 warn!("Skipping: {:?}", err);
194 continue;
195 }
196 };
David Brown2bc26852019-01-14 21:54:04 +0000197
David Brown48a4ec32021-01-04 17:02:27 -0700198 if false {
199 // Respect the groupings in the `.workflow.yml` file.
200 envs.push(fset);
201 } else {
202 // Break each test up so we can run more in
203 // parallel.
204 let env = fset.env.clone();
205 for val in fset.values {
206 if !all_tests.contains(&val) {
207 all_tests.insert(val.clone());
208 envs.push(FeatureSet {
209 env: env.clone(),
210 values: vec![val],
211 });
212 } else {
213 warn!("Duplicate: {:?}: {:?}", env, val);
David Brown2bc26852019-01-14 21:54:04 +0000214 }
215 }
216 }
217 }
218 }
219
220 Ok(Matrix {
221 envs: envs,
222 })
223 }
224}
225
226impl FeatureSet {
227 fn decode(text: &str) -> Result<FeatureSet> {
David Brown48a4ec32021-01-04 17:02:27 -0700228 // The github workflow is just a space separated set of values.
229 let values: Vec<_> = text
230 .split(',')
231 .map(|s| s.to_string())
232 .collect();
233 Ok(FeatureSet {
234 env: "MULTI_FEATURES".to_string(),
235 values: values,
236 })
David Brown2bc26852019-01-14 21:54:04 +0000237 }
238
239 /// Run a test for this given feature set. Output is captured and will be returned if there is
240 /// an error. Each will be run successively, and the first failure will be returned.
241 /// Otherwise, it returns None, which means everything worked.
242 fn run(&self) -> Result<Option<Output>> {
243 for v in &self.values {
244 let output = Command::new("bash")
David Brownb899f792019-03-14 15:21:06 -0600245 .arg("./ci/sim_run.sh")
David Brown2bc26852019-01-14 21:54:04 +0000246 .current_dir("..")
247 .env(&self.env, v)
248 .output()?;
249 if !output.status.success() {
250 return Ok(Some(output));
251 }
252 }
253 return Ok(None);
254 }
255
256 /// Convert this feature set into a textual representation
257 fn textual(&self) -> String {
258 use std::fmt::Write;
259
260 let mut buf = String::new();
261
262 write!(&mut buf, "{}:", self.env).unwrap();
263 for v in &self.values {
264 write!(&mut buf, " {}", v).unwrap();
265 }
266
267 buf
268 }
269}
270
271fn lookup_matrix(y: &Yaml) -> Option<&Vec<Yaml>> {
David Brown48a4ec32021-01-04 17:02:27 -0700272 let jobs = Yaml::String("jobs".to_string());
273 let environment = Yaml::String("environment".to_string());
274 let strategy = Yaml::String("strategy".to_string());
David Brown2bc26852019-01-14 21:54:04 +0000275 let matrix = Yaml::String("matrix".to_string());
David Brown48a4ec32021-01-04 17:02:27 -0700276 let features = Yaml::String("features".to_string());
277 y
278 .as_hash()?.get(&jobs)?
279 .as_hash()?.get(&environment)?
280 .as_hash()?.get(&strategy)?
281 .as_hash()?.get(&matrix)?
282 .as_hash()?.get(&features)?
283 .as_vec()
David Brown2bc26852019-01-14 21:54:04 +0000284}