blob: adec579c3ba9853f9cecdf9a5f4d1c85ee9a6d4f [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
43 match args.command {
44 Commands::Run => (),
45 }
46
David Brownc32ad202024-04-11 09:31:26 -060047 let workflow_text = fs::read_to_string(&args.workflow)?;
David Brown48a4ec32021-01-04 17:02:27 -070048 let workflow = YamlLoader::load_from_str(&workflow_text)?;
David Brown2bc26852019-01-14 21:54:04 +000049
50 let ncpus = num_cpus::get();
51 let limiter = Arc::new(Semaphore::new(ncpus as isize));
52
David Brown91de33d2021-03-05 15:43:44 -070053 let matrix = Matrix::from_yaml(&workflow);
David Brown2bc26852019-01-14 21:54:04 +000054
55 let mut children = vec![];
56 let state = State::new(matrix.envs.len());
57 let st2 = state.clone();
58 let _status = thread::spawn(move || {
59 loop {
60 thread::sleep(Duration::new(15, 0));
61 st2.lock().unwrap().status();
62 }
63 });
64 for env in matrix.envs {
65 let state = state.clone();
66 let limiter = limiter.clone();
67
68 let child = thread::spawn(move || {
69 let _run = limiter.access();
70 state.lock().unwrap().start(&env);
71 let out = env.run();
72 state.lock().unwrap().done(&env, out);
73 });
74 children.push(child);
75 }
76
77 for child in children {
78 child.join().unwrap();
79 }
80
David Brown91de33d2021-03-05 15:43:44 -070081 println!();
David Brown2bc26852019-01-14 21:54:04 +000082
83 Ok(())
84}
85
David Brown87983372024-04-11 08:58:48 -060086/// The main Cli.
87#[derive(Debug, Parser)]
88#[command(name = "ptest")]
89#[command(about = "Run MCUboot CI tests stand alone")]
90struct Cli {
David Brownc32ad202024-04-11 09:31:26 -060091 /// The workflow file to use.
92 #[arg(short, long, default_value = "../.github/workflows/sim.yaml")]
93 workflow: String,
94
David Brown87983372024-04-11 08:58:48 -060095 #[command(subcommand)]
96 command: Commands,
97}
98
99#[derive(Debug, Subcommand)]
100enum Commands {
101 /// Runs the tests.
102 Run,
103}
104
David Brown2bc26852019-01-14 21:54:04 +0000105/// State, for printing status.
106struct State {
107 running: HashSet<String>,
108 done: HashSet<String>,
109 total: usize,
110}
111
David Brownddd390a2021-01-12 12:04:56 -0700112/// Result of a test run.
113struct TestResult {
114 /// Was this run successful.
115 success: bool,
116
117 /// The captured output.
118 output: Vec<u8>,
119}
120
David Brown2bc26852019-01-14 21:54:04 +0000121impl State {
122 fn new(total: usize) -> Arc<Mutex<State>> {
123 Arc::new(Mutex::new(State {
124 running: HashSet::new(),
125 done: HashSet::new(),
David Brown91de33d2021-03-05 15:43:44 -0700126 total,
David Brown2bc26852019-01-14 21:54:04 +0000127 }))
128 }
129
130 fn start(&mut self, fs: &FeatureSet) {
131 let key = fs.textual();
132 if self.running.contains(&key) || self.done.contains(&key) {
133 warn!("Duplicate: {:?}", key);
134 }
135 debug!("Starting: {} ({} running)", key, self.running.len() + 1);
136 self.running.insert(key);
137 self.status();
138 }
139
David Brownddd390a2021-01-12 12:04:56 -0700140 fn done(&mut self, fs: &FeatureSet, output: Result<TestResult>) {
David Brown2bc26852019-01-14 21:54:04 +0000141 let key = fs.textual();
142 self.running.remove(&key);
143 self.done.insert(key.clone());
144 match output {
David Brownddd390a2021-01-12 12:04:56 -0700145 Ok(output) => {
146 if !output.success || log_all() {
147 // Write the output into a file.
148 let mut count = 1;
149 let (mut fd, logname) = loop {
150 let base = if output.success { "success" } else { "failure" };
151 let name = format!("./{}-{:04}.log", base, count);
152 count += 1;
153 match OpenOptions::new()
154 .create_new(true)
155 .write(true)
156 .open(&name)
157 {
158 Ok(file) => break (file, name),
159 Err(ref err) if err.kind() == ErrorKind::AlreadyExists => continue,
160 Err(err) => {
161 error!("Unable to write log file to current directory: {:?}", err);
162 return;
163 }
David Brown2bc26852019-01-14 21:54:04 +0000164 }
David Brownddd390a2021-01-12 12:04:56 -0700165 };
166 fd.write_all(&output.output).unwrap();
167 if !output.success {
168 error!("Failure {} log:{:?} ({} running)", key, logname,
169 self.running.len());
David Brown2bc26852019-01-14 21:54:04 +0000170 }
David Brownddd390a2021-01-12 12:04:56 -0700171 }
David Brown2bc26852019-01-14 21:54:04 +0000172 }
173 Err(err) => {
David Brownddd390a2021-01-12 12:04:56 -0700174 error!("Unable to run test {:?} ({:?}", key, err);
David Brown2bc26852019-01-14 21:54:04 +0000175 }
176 }
177 self.status();
178 }
179
180 fn status(&self) {
181 let running = self.running.len();
182 let done = self.done.len();
183 print!(" {} running ({}/{}/{} done)\r", running, done, running + done, self.total);
184 stdout().flush().unwrap();
185 }
186}
187
David Brown48a4ec32021-01-04 17:02:27 -0700188/// The extracted configurations from the workflow config
David Brown2bc26852019-01-14 21:54:04 +0000189#[derive(Debug)]
190struct Matrix {
191 envs: Vec<FeatureSet>,
192}
193
194#[derive(Debug, Eq, Hash, PartialEq)]
195struct FeatureSet {
196 // The environment variable to set.
197 env: String,
198 // The successive values to set it to.
199 values: Vec<String>,
200}
201
202impl Matrix {
David Brown91de33d2021-03-05 15:43:44 -0700203 fn from_yaml(yaml: &[Yaml]) -> Matrix {
David Brown2bc26852019-01-14 21:54:04 +0000204 let mut envs = vec![];
205
206 let mut all_tests = HashSet::new();
207
208 for y in yaml {
209 let m = match lookup_matrix(y) {
210 Some (m) => m,
211 None => continue,
212 };
213 for elt in m {
David Brown48a4ec32021-01-04 17:02:27 -0700214 let elt = match elt.as_str() {
215 None => {
216 warn!("Unexpected yaml: {:?}", elt);
217 continue;
218 }
219 Some(e) => e,
220 };
David Brown91de33d2021-03-05 15:43:44 -0700221 let fset = FeatureSet::decode(elt);
David Brown2bc26852019-01-14 21:54:04 +0000222
David Brown48a4ec32021-01-04 17:02:27 -0700223 if false {
224 // Respect the groupings in the `.workflow.yml` file.
225 envs.push(fset);
226 } else {
227 // Break each test up so we can run more in
228 // parallel.
229 let env = fset.env.clone();
230 for val in fset.values {
231 if !all_tests.contains(&val) {
232 all_tests.insert(val.clone());
233 envs.push(FeatureSet {
234 env: env.clone(),
235 values: vec![val],
236 });
237 } else {
238 warn!("Duplicate: {:?}: {:?}", env, val);
David Brown2bc26852019-01-14 21:54:04 +0000239 }
240 }
241 }
242 }
243 }
244
David Brown91de33d2021-03-05 15:43:44 -0700245 Matrix {
246 envs,
247 }
David Brown2bc26852019-01-14 21:54:04 +0000248 }
249}
250
251impl FeatureSet {
David Brown91de33d2021-03-05 15:43:44 -0700252 fn decode(text: &str) -> FeatureSet {
David Brown48a4ec32021-01-04 17:02:27 -0700253 // The github workflow is just a space separated set of values.
254 let values: Vec<_> = text
255 .split(',')
256 .map(|s| s.to_string())
257 .collect();
David Brown91de33d2021-03-05 15:43:44 -0700258 FeatureSet {
David Brown48a4ec32021-01-04 17:02:27 -0700259 env: "MULTI_FEATURES".to_string(),
David Brown91de33d2021-03-05 15:43:44 -0700260 values,
261 }
David Brown2bc26852019-01-14 21:54:04 +0000262 }
263
264 /// Run a test for this given feature set. Output is captured and will be returned if there is
265 /// an error. Each will be run successively, and the first failure will be returned.
266 /// Otherwise, it returns None, which means everything worked.
David Brownddd390a2021-01-12 12:04:56 -0700267 fn run(&self) -> Result<TestResult> {
268 let mut output = vec![];
269 let mut success = true;
David Brown2bc26852019-01-14 21:54:04 +0000270 for v in &self.values {
David Brownddd390a2021-01-12 12:04:56 -0700271 let cmdout = Command::new("bash")
David Brownb899f792019-03-14 15:21:06 -0600272 .arg("./ci/sim_run.sh")
David Brown2bc26852019-01-14 21:54:04 +0000273 .current_dir("..")
274 .env(&self.env, v)
275 .output()?;
David Brownddd390a2021-01-12 12:04:56 -0700276 // Grab the output for logging, etc.
277 writeln!(&mut output, "Test {} {}",
278 if cmdout.status.success() { "success" } else { "FAILURE" },
279 self.textual())?;
280 writeln!(&mut output, "time: {}", Local::now().to_rfc3339())?;
281 writeln!(&mut output, "----------------------------------------")?;
282 writeln!(&mut output, "stdout:")?;
283 output.extend(&cmdout.stdout);
284 writeln!(&mut output, "----------------------------------------")?;
285 writeln!(&mut output, "stderr:")?;
286 output.extend(&cmdout.stderr);
287 if !cmdout.status.success() {
288 success = false;
David Brown2bc26852019-01-14 21:54:04 +0000289 }
290 }
David Brownddd390a2021-01-12 12:04:56 -0700291 Ok(TestResult { success, output })
David Brown2bc26852019-01-14 21:54:04 +0000292 }
293
294 /// Convert this feature set into a textual representation
295 fn textual(&self) -> String {
296 use std::fmt::Write;
297
298 let mut buf = String::new();
299
300 write!(&mut buf, "{}:", self.env).unwrap();
301 for v in &self.values {
302 write!(&mut buf, " {}", v).unwrap();
303 }
304
305 buf
306 }
307}
308
309fn lookup_matrix(y: &Yaml) -> Option<&Vec<Yaml>> {
David Brown48a4ec32021-01-04 17:02:27 -0700310 let jobs = Yaml::String("jobs".to_string());
311 let environment = Yaml::String("environment".to_string());
312 let strategy = Yaml::String("strategy".to_string());
David Brown2bc26852019-01-14 21:54:04 +0000313 let matrix = Yaml::String("matrix".to_string());
David Brown48a4ec32021-01-04 17:02:27 -0700314 let features = Yaml::String("features".to_string());
315 y
316 .as_hash()?.get(&jobs)?
317 .as_hash()?.get(&environment)?
318 .as_hash()?.get(&strategy)?
319 .as_hash()?.get(&matrix)?
320 .as_hash()?.get(&features)?
321 .as_vec()
David Brown2bc26852019-01-14 21:54:04 +0000322}
David Brownddd390a2021-01-12 12:04:56 -0700323
324/// Query if we should be logging all tests and not only failures.
325fn log_all() -> bool {
326 env::var("PTEST_LOG_ALL").is_ok()
327}