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 clap::{Parser, Subcommand};
15 use log::{debug, error, warn};
16 use std::{
17 collections::HashSet,
18 env,
19 fs::{self, OpenOptions},
20 io::{ErrorKind, stdout, Write},
21 process::Command,
22 result,
23 sync::{
24 Arc,
25 Mutex,
26 },
27 thread,
28 time::Duration,
29 };
30 use std_semaphore::Semaphore;
31 use yaml_rust::{
32 Yaml,
33 YamlLoader,
34 };
35
36 type Result<T> = result::Result<T, failure::Error>;
37
main() -> Result<()>38 fn main() -> Result<()> {
39 env_logger::init();
40
41 let args = Cli::parse();
42
43 let workflow_text = fs::read_to_string(&args.workflow)?;
44 let workflow = YamlLoader::load_from_str(&workflow_text)?;
45
46 let ncpus = num_cpus::get();
47 let limiter = Arc::new(Semaphore::new(ncpus as isize));
48
49 let matrix = Matrix::from_yaml(&workflow);
50
51 let matrix = if args.test.len() == 0 { matrix } else {
52 matrix.only(&args.test)
53 };
54
55 match args.command {
56 Commands::List => {
57 matrix.show();
58 return Ok(());
59 }
60 Commands::Run => (),
61 }
62
63 let mut children = vec![];
64 let state = State::new(matrix.envs.len());
65 let st2 = state.clone();
66 let _status = thread::spawn(move || {
67 loop {
68 thread::sleep(Duration::new(15, 0));
69 st2.lock().unwrap().status();
70 }
71 });
72 for env in matrix.envs {
73 let state = state.clone();
74 let limiter = limiter.clone();
75
76 let child = thread::spawn(move || {
77 let _run = limiter.access();
78 state.lock().unwrap().start(&env);
79 let out = env.run();
80 state.lock().unwrap().done(&env, out);
81 });
82 children.push(child);
83 }
84
85 for child in children {
86 child.join().unwrap();
87 }
88
89 println!();
90
91 Ok(())
92 }
93
94 /// The main Cli.
95 #[derive(Debug, Parser)]
96 #[command(name = "ptest")]
97 #[command(about = "Run MCUboot CI tests stand alone")]
98 struct Cli {
99 /// The workflow file to use.
100 #[arg(short, long, default_value = "../.github/workflows/sim.yaml")]
101 workflow: String,
102
103 /// The tests to run (defaults to all)
104 #[arg(short, long)]
105 test: Vec<usize>,
106
107 #[command(subcommand)]
108 command: Commands,
109 }
110
111 #[derive(Debug, Subcommand)]
112 enum Commands {
113 /// Runs the tests.
114 Run,
115 /// List available tests.
116 List,
117 }
118
119 /// State, for printing status.
120 struct State {
121 running: HashSet<String>,
122 done: HashSet<String>,
123 total: usize,
124 }
125
126 /// Result of a test run.
127 struct TestResult {
128 /// Was this run successful.
129 success: bool,
130
131 /// The captured output.
132 output: Vec<u8>,
133 }
134
135 impl State {
new(total: usize) -> Arc<Mutex<State>>136 fn new(total: usize) -> Arc<Mutex<State>> {
137 Arc::new(Mutex::new(State {
138 running: HashSet::new(),
139 done: HashSet::new(),
140 total,
141 }))
142 }
143
start(&mut self, fs: &FeatureSet)144 fn start(&mut self, fs: &FeatureSet) {
145 let key = fs.textual();
146 if self.running.contains(&key) || self.done.contains(&key) {
147 warn!("Duplicate: {:?}", key);
148 }
149 debug!("Starting: {} ({} running)", key, self.running.len() + 1);
150 self.running.insert(key);
151 self.status();
152 }
153
done(&mut self, fs: &FeatureSet, output: Result<TestResult>)154 fn done(&mut self, fs: &FeatureSet, output: Result<TestResult>) {
155 let key = fs.textual();
156 self.running.remove(&key);
157 self.done.insert(key.clone());
158 match output {
159 Ok(output) => {
160 if !output.success || log_all() {
161 // Write the output into a file.
162 let mut count = 1;
163 let (mut fd, logname) = loop {
164 let base = if output.success { "success" } else { "failure" };
165 let name = format!("./{}-{:04}.log", base, count);
166 count += 1;
167 match OpenOptions::new()
168 .create_new(true)
169 .write(true)
170 .open(&name)
171 {
172 Ok(file) => break (file, name),
173 Err(ref err) if err.kind() == ErrorKind::AlreadyExists => continue,
174 Err(err) => {
175 error!("Unable to write log file to current directory: {:?}", err);
176 return;
177 }
178 }
179 };
180 fd.write_all(&output.output).unwrap();
181 if !output.success {
182 error!("Failure {} log:{:?} ({} running)", key, logname,
183 self.running.len());
184 }
185 }
186 }
187 Err(err) => {
188 error!("Unable to run test {:?} ({:?}", key, err);
189 }
190 }
191 self.status();
192 }
193
status(&self)194 fn status(&self) {
195 let running = self.running.len();
196 let done = self.done.len();
197 print!(" {} running ({}/{}/{} done)\r", running, done, running + done, self.total);
198 stdout().flush().unwrap();
199 }
200 }
201
202 /// The extracted configurations from the workflow config
203 #[derive(Debug)]
204 struct Matrix {
205 envs: Vec<FeatureSet>,
206 }
207
208 #[derive(Debug, Eq, Hash, PartialEq)]
209 struct FeatureSet {
210 // The environment variable to set.
211 env: String,
212 // The successive values to set it to.
213 values: Vec<String>,
214 }
215
216 impl Matrix {
from_yaml(yaml: &[Yaml]) -> Matrix217 fn from_yaml(yaml: &[Yaml]) -> Matrix {
218 let mut envs = vec![];
219
220 let mut all_tests = HashSet::new();
221
222 for y in yaml {
223 let m = match lookup_matrix(y) {
224 Some (m) => m,
225 None => continue,
226 };
227 for elt in m {
228 let elt = match elt.as_str() {
229 None => {
230 warn!("Unexpected yaml: {:?}", elt);
231 continue;
232 }
233 Some(e) => e,
234 };
235 let fset = FeatureSet::decode(elt);
236
237 if false {
238 // Respect the groupings in the `.workflow.yml` file.
239 envs.push(fset);
240 } else {
241 // Break each test up so we can run more in
242 // parallel.
243 let env = fset.env.clone();
244 for val in fset.values {
245 if !all_tests.contains(&val) {
246 all_tests.insert(val.clone());
247 envs.push(FeatureSet {
248 env: env.clone(),
249 values: vec![val],
250 });
251 } else {
252 warn!("Duplicate: {:?}: {:?}", env, val);
253 }
254 }
255 }
256 }
257 }
258
259 Matrix {
260 envs,
261 }
262 }
263
264 /// Print out all of the feature sets.
show(&self)265 fn show(&self) {
266 for (i, feature) in self.envs.iter().enumerate() {
267 println!("{:3}. {}", i + 1, feature.simple_textual());
268 }
269 }
270
271 /// Replace this matrix with one that only has the chosen tests in it. Note
272 /// that the original order is preserved, not that given in `pick`.
only(self, pick: &[usize]) -> Self273 fn only(self, pick: &[usize]) -> Self {
274 let pick: HashSet<usize> = pick.iter().cloned().collect();
275 let envs: Vec<_> = self
276 .envs
277 .into_iter()
278 .enumerate()
279 .filter(|(ind, _)| pick.contains(&(ind + 1)))
280 .map(|(_, item)| item)
281 .collect();
282 Matrix { envs }
283 }
284 }
285
286 impl FeatureSet {
decode(text: &str) -> FeatureSet287 fn decode(text: &str) -> FeatureSet {
288 // The github workflow is just a space separated set of values.
289 let values: Vec<_> = text
290 .split(',')
291 .map(|s| s.to_string())
292 .collect();
293 FeatureSet {
294 env: "MULTI_FEATURES".to_string(),
295 values,
296 }
297 }
298
299 /// Run a test for this given feature set. Output is captured and will be returned if there is
300 /// an error. Each will be run successively, and the first failure will be returned.
301 /// Otherwise, it returns None, which means everything worked.
run(&self) -> Result<TestResult>302 fn run(&self) -> Result<TestResult> {
303 let mut output = vec![];
304 let mut success = true;
305 for v in &self.values {
306 let cmdout = Command::new("bash")
307 .arg("./ci/sim_run.sh")
308 .current_dir("..")
309 .env(&self.env, v)
310 .output()?;
311 // Grab the output for logging, etc.
312 writeln!(&mut output, "Test {} {}",
313 if cmdout.status.success() { "success" } else { "FAILURE" },
314 self.textual())?;
315 writeln!(&mut output, "time: {}", Local::now().to_rfc3339())?;
316 writeln!(&mut output, "----------------------------------------")?;
317 writeln!(&mut output, "stdout:")?;
318 output.extend(&cmdout.stdout);
319 writeln!(&mut output, "----------------------------------------")?;
320 writeln!(&mut output, "stderr:")?;
321 output.extend(&cmdout.stderr);
322 if !cmdout.status.success() {
323 success = false;
324 }
325 }
326 Ok(TestResult { success, output })
327 }
328
329 /// Convert this feature set into a textual representation
textual(&self) -> String330 fn textual(&self) -> String {
331 use std::fmt::Write;
332
333 let mut buf = String::new();
334
335 write!(&mut buf, "{}:", self.env).unwrap();
336 for v in &self.values {
337 write!(&mut buf, " {}", v).unwrap();
338 }
339
340 buf
341 }
342
343 /// Generate a simpler textual representation, without showing the environment.
simple_textual(&self) -> String344 fn simple_textual(&self) -> String {
345 use std::fmt::Write;
346
347 let mut buf = String::new();
348 for v in &self.values {
349 write!(&mut buf, " {}", v).unwrap();
350 }
351
352 buf
353 }
354 }
355
lookup_matrix(y: &Yaml) -> Option<&Vec<Yaml>>356 fn lookup_matrix(y: &Yaml) -> Option<&Vec<Yaml>> {
357 let jobs = Yaml::String("jobs".to_string());
358 let environment = Yaml::String("environment".to_string());
359 let strategy = Yaml::String("strategy".to_string());
360 let matrix = Yaml::String("matrix".to_string());
361 let features = Yaml::String("features".to_string());
362 y
363 .as_hash()?.get(&jobs)?
364 .as_hash()?.get(&environment)?
365 .as_hash()?.get(&strategy)?
366 .as_hash()?.get(&matrix)?
367 .as_hash()?.get(&features)?
368 .as_vec()
369 }
370
371 /// Query if we should be logging all tests and not only failures.
log_all() -> bool372 fn log_all() -> bool {
373 env::var("PTEST_LOG_ALL").is_ok()
374 }
375