1 //! 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 Workflow 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 //!
10 //! For now, we assume all of the features are listed under
11 //! jobs->environment->strategy->matric->features
12
13 use chrono::Local;
14 use log::{debug, error, warn};
15 use std::{
16 collections::HashSet,
17 env,
18 fs::{self, OpenOptions},
19 io::{ErrorKind, stdout, Write},
20 process::Command,
21 result,
22 sync::{
23 Arc,
24 Mutex,
25 },
26 thread,
27 time::Duration,
28 };
29 use std_semaphore::Semaphore;
30 use yaml_rust::{
31 Yaml,
32 YamlLoader,
33 };
34
35 type Result<T> = result::Result<T, failure::Error>;
36
main() -> Result<()>37 fn main() -> Result<()> {
38 env_logger::init();
39
40 let workflow_text = fs::read_to_string("../.github/workflows/sim.yaml")?;
41 let workflow = YamlLoader::load_from_str(&workflow_text)?;
42
43 let ncpus = num_cpus::get();
44 let limiter = Arc::new(Semaphore::new(ncpus as isize));
45
46 let matrix = Matrix::from_yaml(&workflow);
47
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
74 println!();
75
76 Ok(())
77 }
78
79 /// State, for printing status.
80 struct State {
81 running: HashSet<String>,
82 done: HashSet<String>,
83 total: usize,
84 }
85
86 /// Result of a test run.
87 struct TestResult {
88 /// Was this run successful.
89 success: bool,
90
91 /// The captured output.
92 output: Vec<u8>,
93 }
94
95 impl State {
new(total: usize) -> Arc<Mutex<State>>96 fn new(total: usize) -> Arc<Mutex<State>> {
97 Arc::new(Mutex::new(State {
98 running: HashSet::new(),
99 done: HashSet::new(),
100 total,
101 }))
102 }
103
start(&mut self, fs: &FeatureSet)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
done(&mut self, fs: &FeatureSet, output: Result<TestResult>)114 fn done(&mut self, fs: &FeatureSet, output: Result<TestResult>) {
115 let key = fs.textual();
116 self.running.remove(&key);
117 self.done.insert(key.clone());
118 match output {
119 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 }
138 }
139 };
140 fd.write_all(&output.output).unwrap();
141 if !output.success {
142 error!("Failure {} log:{:?} ({} running)", key, logname,
143 self.running.len());
144 }
145 }
146 }
147 Err(err) => {
148 error!("Unable to run test {:?} ({:?}", key, err);
149 }
150 }
151 self.status();
152 }
153
status(&self)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
162 /// The extracted configurations from the workflow config
163 #[derive(Debug)]
164 struct Matrix {
165 envs: Vec<FeatureSet>,
166 }
167
168 #[derive(Debug, Eq, Hash, PartialEq)]
169 struct FeatureSet {
170 // The environment variable to set.
171 env: String,
172 // The successive values to set it to.
173 values: Vec<String>,
174 }
175
176 impl Matrix {
from_yaml(yaml: &[Yaml]) -> Matrix177 fn from_yaml(yaml: &[Yaml]) -> Matrix {
178 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 {
188 let elt = match elt.as_str() {
189 None => {
190 warn!("Unexpected yaml: {:?}", elt);
191 continue;
192 }
193 Some(e) => e,
194 };
195 let fset = FeatureSet::decode(elt);
196
197 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);
213 }
214 }
215 }
216 }
217 }
218
219 Matrix {
220 envs,
221 }
222 }
223 }
224
225 impl FeatureSet {
decode(text: &str) -> FeatureSet226 fn decode(text: &str) -> FeatureSet {
227 // 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();
232 FeatureSet {
233 env: "MULTI_FEATURES".to_string(),
234 values,
235 }
236 }
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.
run(&self) -> Result<TestResult>241 fn run(&self) -> Result<TestResult> {
242 let mut output = vec![];
243 let mut success = true;
244 for v in &self.values {
245 let cmdout = Command::new("bash")
246 .arg("./ci/sim_run.sh")
247 .current_dir("..")
248 .env(&self.env, v)
249 .output()?;
250 // 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;
263 }
264 }
265 Ok(TestResult { success, output })
266 }
267
268 /// Convert this feature set into a textual representation
textual(&self) -> String269 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
lookup_matrix(y: &Yaml) -> Option<&Vec<Yaml>>283 fn lookup_matrix(y: &Yaml) -> Option<&Vec<Yaml>> {
284 let jobs = Yaml::String("jobs".to_string());
285 let environment = Yaml::String("environment".to_string());
286 let strategy = Yaml::String("strategy".to_string());
287 let matrix = Yaml::String("matrix".to_string());
288 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()
296 }
297
298 /// Query if we should be logging all tests and not only failures.
log_all() -> bool299 fn log_all() -> bool {
300 env::var("PTEST_LOG_ALL").is_ok()
301 }
302