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