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