blob: e0e41a964ade2e625dc85c761388238a2d828fe7 [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 Brown87983372024-04-11 08:58:48 -060014use clap::{Parser, Subcommand};
David Brown2bc26852019-01-14 21:54:04 +000015use log::{debug, error, warn};
David Brown2bc26852019-01-14 21:54:04 +000016use std::{
17 collections::HashSet,
David Brownddd390a2021-01-12 12:04:56 -070018 env,
David Brown2bc26852019-01-14 21:54:04 +000019 fs::{self, OpenOptions},
20 io::{ErrorKind, stdout, Write},
David Brownddd390a2021-01-12 12:04:56 -070021 process::Command,
David Brown2bc26852019-01-14 21:54:04 +000022 result,
23 sync::{
24 Arc,
25 Mutex,
26 },
27 thread,
28 time::Duration,
29};
30use std_semaphore::Semaphore;
31use yaml_rust::{
32 Yaml,
33 YamlLoader,
34};
35
36type Result<T> = result::Result<T, failure::Error>;
37
38fn main() -> Result<()> {
39 env_logger::init();
40
David Brown87983372024-04-11 08:58:48 -060041 let args = Cli::parse();
42
David Brownc32ad202024-04-11 09:31:26 -060043 let workflow_text = fs::read_to_string(&args.workflow)?;
David Brown48a4ec32021-01-04 17:02:27 -070044 let workflow = YamlLoader::load_from_str(&workflow_text)?;
David Brown2bc26852019-01-14 21:54:04 +000045
46 let ncpus = num_cpus::get();
47 let limiter = Arc::new(Semaphore::new(ncpus as isize));
48
David Brown91de33d2021-03-05 15:43:44 -070049 let matrix = Matrix::from_yaml(&workflow);
David Brown2bc26852019-01-14 21:54:04 +000050
David Brown67fc1fc2024-04-11 09:31:26 -060051 match args.command {
52 Commands::List => {
53 matrix.show();
54 return Ok(());
55 }
56 Commands::Run => (),
57 }
58
David Brown2bc26852019-01-14 21:54:04 +000059 let mut children = vec![];
60 let state = State::new(matrix.envs.len());
61 let st2 = state.clone();
62 let _status = thread::spawn(move || {
63 loop {
64 thread::sleep(Duration::new(15, 0));
65 st2.lock().unwrap().status();
66 }
67 });
68 for env in matrix.envs {
69 let state = state.clone();
70 let limiter = limiter.clone();
71
72 let child = thread::spawn(move || {
73 let _run = limiter.access();
74 state.lock().unwrap().start(&env);
75 let out = env.run();
76 state.lock().unwrap().done(&env, out);
77 });
78 children.push(child);
79 }
80
81 for child in children {
82 child.join().unwrap();
83 }
84
David Brown91de33d2021-03-05 15:43:44 -070085 println!();
David Brown2bc26852019-01-14 21:54:04 +000086
87 Ok(())
88}
89
David Brown87983372024-04-11 08:58:48 -060090/// The main Cli.
91#[derive(Debug, Parser)]
92#[command(name = "ptest")]
93#[command(about = "Run MCUboot CI tests stand alone")]
94struct Cli {
David Brownc32ad202024-04-11 09:31:26 -060095 /// The workflow file to use.
96 #[arg(short, long, default_value = "../.github/workflows/sim.yaml")]
97 workflow: String,
98
David Brown87983372024-04-11 08:58:48 -060099 #[command(subcommand)]
100 command: Commands,
101}
102
103#[derive(Debug, Subcommand)]
104enum Commands {
105 /// Runs the tests.
106 Run,
David Brown67fc1fc2024-04-11 09:31:26 -0600107 /// List available tests.
108 List,
David Brown87983372024-04-11 08:58:48 -0600109}
110
David Brown2bc26852019-01-14 21:54:04 +0000111/// State, for printing status.
112struct State {
113 running: HashSet<String>,
114 done: HashSet<String>,
115 total: usize,
116}
117
David Brownddd390a2021-01-12 12:04:56 -0700118/// Result of a test run.
119struct TestResult {
120 /// Was this run successful.
121 success: bool,
122
123 /// The captured output.
124 output: Vec<u8>,
125}
126
David Brown2bc26852019-01-14 21:54:04 +0000127impl State {
128 fn new(total: usize) -> Arc<Mutex<State>> {
129 Arc::new(Mutex::new(State {
130 running: HashSet::new(),
131 done: HashSet::new(),
David Brown91de33d2021-03-05 15:43:44 -0700132 total,
David Brown2bc26852019-01-14 21:54:04 +0000133 }))
134 }
135
136 fn start(&mut self, fs: &FeatureSet) {
137 let key = fs.textual();
138 if self.running.contains(&key) || self.done.contains(&key) {
139 warn!("Duplicate: {:?}", key);
140 }
141 debug!("Starting: {} ({} running)", key, self.running.len() + 1);
142 self.running.insert(key);
143 self.status();
144 }
145
David Brownddd390a2021-01-12 12:04:56 -0700146 fn done(&mut self, fs: &FeatureSet, output: Result<TestResult>) {
David Brown2bc26852019-01-14 21:54:04 +0000147 let key = fs.textual();
148 self.running.remove(&key);
149 self.done.insert(key.clone());
150 match output {
David Brownddd390a2021-01-12 12:04:56 -0700151 Ok(output) => {
152 if !output.success || log_all() {
153 // Write the output into a file.
154 let mut count = 1;
155 let (mut fd, logname) = loop {
156 let base = if output.success { "success" } else { "failure" };
157 let name = format!("./{}-{:04}.log", base, count);
158 count += 1;
159 match OpenOptions::new()
160 .create_new(true)
161 .write(true)
162 .open(&name)
163 {
164 Ok(file) => break (file, name),
165 Err(ref err) if err.kind() == ErrorKind::AlreadyExists => continue,
166 Err(err) => {
167 error!("Unable to write log file to current directory: {:?}", err);
168 return;
169 }
David Brown2bc26852019-01-14 21:54:04 +0000170 }
David Brownddd390a2021-01-12 12:04:56 -0700171 };
172 fd.write_all(&output.output).unwrap();
173 if !output.success {
174 error!("Failure {} log:{:?} ({} running)", key, logname,
175 self.running.len());
David Brown2bc26852019-01-14 21:54:04 +0000176 }
David Brownddd390a2021-01-12 12:04:56 -0700177 }
David Brown2bc26852019-01-14 21:54:04 +0000178 }
179 Err(err) => {
David Brownddd390a2021-01-12 12:04:56 -0700180 error!("Unable to run test {:?} ({:?}", key, err);
David Brown2bc26852019-01-14 21:54:04 +0000181 }
182 }
183 self.status();
184 }
185
186 fn status(&self) {
187 let running = self.running.len();
188 let done = self.done.len();
189 print!(" {} running ({}/{}/{} done)\r", running, done, running + done, self.total);
190 stdout().flush().unwrap();
191 }
192}
193
David Brown48a4ec32021-01-04 17:02:27 -0700194/// The extracted configurations from the workflow config
David Brown2bc26852019-01-14 21:54:04 +0000195#[derive(Debug)]
196struct Matrix {
197 envs: Vec<FeatureSet>,
198}
199
200#[derive(Debug, Eq, Hash, PartialEq)]
201struct FeatureSet {
202 // The environment variable to set.
203 env: String,
204 // The successive values to set it to.
205 values: Vec<String>,
206}
207
208impl Matrix {
David Brown91de33d2021-03-05 15:43:44 -0700209 fn from_yaml(yaml: &[Yaml]) -> Matrix {
David Brown2bc26852019-01-14 21:54:04 +0000210 let mut envs = vec![];
211
212 let mut all_tests = HashSet::new();
213
214 for y in yaml {
215 let m = match lookup_matrix(y) {
216 Some (m) => m,
217 None => continue,
218 };
219 for elt in m {
David Brown48a4ec32021-01-04 17:02:27 -0700220 let elt = match elt.as_str() {
221 None => {
222 warn!("Unexpected yaml: {:?}", elt);
223 continue;
224 }
225 Some(e) => e,
226 };
David Brown91de33d2021-03-05 15:43:44 -0700227 let fset = FeatureSet::decode(elt);
David Brown2bc26852019-01-14 21:54:04 +0000228
David Brown48a4ec32021-01-04 17:02:27 -0700229 if false {
230 // Respect the groupings in the `.workflow.yml` file.
231 envs.push(fset);
232 } else {
233 // Break each test up so we can run more in
234 // parallel.
235 let env = fset.env.clone();
236 for val in fset.values {
237 if !all_tests.contains(&val) {
238 all_tests.insert(val.clone());
239 envs.push(FeatureSet {
240 env: env.clone(),
241 values: vec![val],
242 });
243 } else {
244 warn!("Duplicate: {:?}: {:?}", env, val);
David Brown2bc26852019-01-14 21:54:04 +0000245 }
246 }
247 }
248 }
249 }
250
David Brown91de33d2021-03-05 15:43:44 -0700251 Matrix {
252 envs,
253 }
David Brown2bc26852019-01-14 21:54:04 +0000254 }
David Brown67fc1fc2024-04-11 09:31:26 -0600255
256 /// Print out all of the feature sets.
257 fn show(&self) {
258 for (i, feature) in self.envs.iter().enumerate() {
259 println!("{:3}. {}", i, feature.simple_textual());
260 }
261 }
David Brown2bc26852019-01-14 21:54:04 +0000262}
263
264impl FeatureSet {
David Brown91de33d2021-03-05 15:43:44 -0700265 fn decode(text: &str) -> FeatureSet {
David Brown48a4ec32021-01-04 17:02:27 -0700266 // The github workflow is just a space separated set of values.
267 let values: Vec<_> = text
268 .split(',')
269 .map(|s| s.to_string())
270 .collect();
David Brown91de33d2021-03-05 15:43:44 -0700271 FeatureSet {
David Brown48a4ec32021-01-04 17:02:27 -0700272 env: "MULTI_FEATURES".to_string(),
David Brown91de33d2021-03-05 15:43:44 -0700273 values,
274 }
David Brown2bc26852019-01-14 21:54:04 +0000275 }
276
277 /// Run a test for this given feature set. Output is captured and will be returned if there is
278 /// an error. Each will be run successively, and the first failure will be returned.
279 /// Otherwise, it returns None, which means everything worked.
David Brownddd390a2021-01-12 12:04:56 -0700280 fn run(&self) -> Result<TestResult> {
281 let mut output = vec![];
282 let mut success = true;
David Brown2bc26852019-01-14 21:54:04 +0000283 for v in &self.values {
David Brownddd390a2021-01-12 12:04:56 -0700284 let cmdout = Command::new("bash")
David Brownb899f792019-03-14 15:21:06 -0600285 .arg("./ci/sim_run.sh")
David Brown2bc26852019-01-14 21:54:04 +0000286 .current_dir("..")
287 .env(&self.env, v)
288 .output()?;
David Brownddd390a2021-01-12 12:04:56 -0700289 // Grab the output for logging, etc.
290 writeln!(&mut output, "Test {} {}",
291 if cmdout.status.success() { "success" } else { "FAILURE" },
292 self.textual())?;
293 writeln!(&mut output, "time: {}", Local::now().to_rfc3339())?;
294 writeln!(&mut output, "----------------------------------------")?;
295 writeln!(&mut output, "stdout:")?;
296 output.extend(&cmdout.stdout);
297 writeln!(&mut output, "----------------------------------------")?;
298 writeln!(&mut output, "stderr:")?;
299 output.extend(&cmdout.stderr);
300 if !cmdout.status.success() {
301 success = false;
David Brown2bc26852019-01-14 21:54:04 +0000302 }
303 }
David Brownddd390a2021-01-12 12:04:56 -0700304 Ok(TestResult { success, output })
David Brown2bc26852019-01-14 21:54:04 +0000305 }
306
307 /// Convert this feature set into a textual representation
308 fn textual(&self) -> String {
309 use std::fmt::Write;
310
311 let mut buf = String::new();
312
313 write!(&mut buf, "{}:", self.env).unwrap();
314 for v in &self.values {
315 write!(&mut buf, " {}", v).unwrap();
316 }
317
318 buf
319 }
David Brown67fc1fc2024-04-11 09:31:26 -0600320
321 /// Generate a simpler textual representation, without showing the environment.
322 fn simple_textual(&self) -> String {
323 use std::fmt::Write;
324
325 let mut buf = String::new();
326 for v in &self.values {
327 write!(&mut buf, " {}", v).unwrap();
328 }
329
330 buf
331 }
David Brown2bc26852019-01-14 21:54:04 +0000332}
333
334fn lookup_matrix(y: &Yaml) -> Option<&Vec<Yaml>> {
David Brown48a4ec32021-01-04 17:02:27 -0700335 let jobs = Yaml::String("jobs".to_string());
336 let environment = Yaml::String("environment".to_string());
337 let strategy = Yaml::String("strategy".to_string());
David Brown2bc26852019-01-14 21:54:04 +0000338 let matrix = Yaml::String("matrix".to_string());
David Brown48a4ec32021-01-04 17:02:27 -0700339 let features = Yaml::String("features".to_string());
340 y
341 .as_hash()?.get(&jobs)?
342 .as_hash()?.get(&environment)?
343 .as_hash()?.get(&strategy)?
344 .as_hash()?.get(&matrix)?
345 .as_hash()?.get(&features)?
346 .as_vec()
David Brown2bc26852019-01-14 21:54:04 +0000347}
David Brownddd390a2021-01-12 12:04:56 -0700348
349/// Query if we should be logging all tests and not only failures.
350fn log_all() -> bool {
351 env::var("PTEST_LOG_ALL").is_ok()
352}