1use crate::cuda_utils::get_cuda_mig_device_uuids;
2use crate::filesystem_utils::{
3 copy_dir, copy_dir_with_ignore, copy_dir_with_rsync, is_command_installed,
4 wait_until_file_ready,
5};
6use crate::handlebars_helper::*;
7use chrono::Local;
8use crossbeam_channel::{unbounded, Receiver, Sender};
9use handlebars::Handlebars;
10use indicatif::{MultiProgress, ProgressBar, ProgressFinish, ProgressStyle};
11use serde_json::{json, Value as JsonValue};
12use std::borrow::Cow;
13use std::collections::HashSet;
14use std::env;
15use std::error::Error;
16use std::io::Write;
17use std::path::{Path, PathBuf};
18use std::process::Command;
19use std::sync::OnceLock;
20use std::sync::{
21 atomic::{AtomicBool, Ordering},
22 Arc,
23};
24use std::thread::JoinHandle;
25use std::time::{Duration, Instant};
26use tempfile::tempdir;
27use uuid::Uuid;
28
29#[derive(PartialEq, Copy, Clone)]
31pub enum CompliationErrorHandlingMethod {
32 Ignore,
34 Collect,
36 Panic,
38}
39
40#[derive(PartialEq, Copy, Clone)]
42pub enum RunMethod {
43 No,
45 InPlace,
47 OutOfPlace(usize),
49 Exclusive(usize),
51}
52
53static CUDA_DEVICE_UUIDS: OnceLock<Vec<String>> = OnceLock::new();
54
55fn get_cuda_device_uuid(id: usize) -> Option<String> {
56 let cuda_device_uuids = CUDA_DEVICE_UUIDS.get_or_init(|| get_cuda_mig_device_uuids());
57 if id < cuda_device_uuids.len() {
58 Some(cuda_device_uuids[id].clone())
59 } else {
60 None
61 }
62}
63
64pub struct Parabuilder {
66 project_path: PathBuf,
67 workspaces_path: PathBuf,
68 template_file: PathBuf,
69 target_files: Vec<PathBuf>,
70 target_files_base: Vec<String>,
71 init_bash_script: String,
72 compile_bash_script: String,
73 run_bash_script: String,
74 build_workers: usize,
75 run_method: RunMethod,
76 temp_target_path_dir: PathBuf,
77 run_func_data: RunFunc,
78 data_queue_receiver: Option<Receiver<(usize, JsonValue)>>,
79 compilation_error_handling_method: CompliationErrorHandlingMethod,
80 auto_gather_array_data: bool,
81 in_place_template: bool,
82 disable_progress_bar: bool,
83 mpb: MultiProgress,
84 no_cache: bool,
85 without_rsync: bool,
86 enable_cppflags: bool,
87 autosave_interval: u64,
88 autosave_dir: PathBuf,
89 continue_from_start_time: Option<String>,
90}
91
92fn run_func_data_pre_(
93 workspace_path: &PathBuf,
94 run_script: &str,
95 data: &JsonValue,
96 _: &mut JsonValue,
97) -> Result<(bool, JsonValue), Box<dyn Error>> {
98 let workspace_id = workspace_path
99 .file_name()
100 .unwrap()
101 .to_str()
102 .unwrap()
103 .split('_')
104 .last()
105 .unwrap();
106 let mut output = Command::new("bash");
107 output
108 .arg("-c")
109 .arg(run_script)
110 .env("PARABUILD_ID", workspace_id);
111 if let Some(mig_uuid) = get_cuda_device_uuid(workspace_id.parse().unwrap()) {
112 output.env("CUDA_VISIBLE_DEVICES", mig_uuid);
113 }
114 output.current_dir(&workspace_path);
115 let output = output.output().unwrap();
116 let stdout = String::from_utf8(output.stdout).unwrap();
117 let stderr = String::from_utf8(output.stderr).unwrap();
118 let this_data = json! {
119 {
120 "status": match output.status.code() {
121 Some(code) => code,
122 None => -1
123 },
124 "stdout": stdout,
125 "stderr": stderr,
126 "data": data
127 }
128 };
129 Ok((output.status.success(), this_data))
130}
131
132fn run_func_data_post_(
133 this_data: JsonValue,
134 run_data: &mut JsonValue,
135) -> Result<JsonValue, Box<dyn Error>> {
136 if run_data.is_null() {
137 *run_data = JsonValue::Array(vec![this_data.clone()]);
138 } else {
139 run_data.as_array_mut().unwrap().push(this_data.clone());
140 }
141 Ok(this_data)
142}
143
144fn run_func_data_panic_on_error(
145 workspace_path: &PathBuf,
146 run_script: &str,
147 data: &JsonValue,
148 run_data: &mut JsonValue,
149 stop_flag: &Arc<AtomicBool>,
150) -> Result<JsonValue, Box<dyn Error>> {
151 let (success, this_data) = run_func_data_pre_(workspace_path, run_script, data, run_data)?;
152 if !success {
153 Err(format!("stderr: {}", this_data["stderr"]).as_str())?;
154 }
155 if stop_flag.load(Ordering::Relaxed) {
156 Ok(JsonValue::Null)
157 } else {
158 run_func_data_post_(this_data, run_data)
159 }
160}
161
162fn run_func_data_ignore_on_error(
163 workspace_path: &PathBuf,
164 run_script: &str,
165 data: &JsonValue,
166 run_data: &mut JsonValue,
167 stop_flag: &Arc<AtomicBool>,
168) -> Result<JsonValue, Box<dyn Error>> {
169 let (_, this_data) = run_func_data_pre_(workspace_path, run_script, data, run_data)?;
170 if stop_flag.load(Ordering::Relaxed) {
171 Ok(JsonValue::Null)
172 } else {
173 run_func_data_post_(this_data, run_data)
174 }
175}
176
177type RunFunc = fn(
178 &PathBuf,
179 &str,
180 &JsonValue,
181 &mut JsonValue,
182 &Arc<AtomicBool>,
183) -> Result<JsonValue, Box<dyn Error>>;
184
185pub const PANIC_ON_ERROR_DEFAULT_RUN_FUNC: RunFunc = run_func_data_panic_on_error;
187
188pub const IGNORE_ON_ERROR_DEFAULT_RUN_FUNC: RunFunc = run_func_data_ignore_on_error;
190
191impl Parabuilder {
192 pub const TEMP_TARGET_PATH_DIR: &'static str = "targets";
193
194 pub fn new<P, Q, R, S>(
195 project_path: P,
196 workspaces_path: Q,
197 template_file: R,
198 target_files: &[S],
199 ) -> Self
200 where
201 P: AsRef<Path>,
202 Q: AsRef<Path>,
203 R: AsRef<Path>,
204 S: AsRef<Path>,
205 {
206 let project_path = project_path.as_ref().to_path_buf();
207 let workspaces_path = workspaces_path.as_ref().to_path_buf();
208 let template_file = template_file.as_ref().to_path_buf();
209 let target_files: Vec<PathBuf> = target_files
210 .into_iter()
211 .map(|target_file| target_file.as_ref().to_path_buf())
212 .collect();
213 let target_files_base = target_files
214 .iter()
215 .map(|target_file| {
216 target_file
217 .file_name()
218 .unwrap()
219 .to_string_lossy()
220 .to_string()
221 })
222 .collect();
223
224 let default_run_bash_script = if target_files.len() > 0 {
225 format!(
226 r#"
227 ./{}
228 "#,
229 target_files[0].to_string_lossy()
230 )
231 } else {
232 "".to_string()
233 };
234
235 let temp_target_path_dir = workspaces_path.join(Self::TEMP_TARGET_PATH_DIR);
236 let init_bash_script = r#"
237 cmake -B build -S . -DPARABUILD=ON
238 "#;
239 let compile_bash_script = r#"
240 cmake --build build --target all -- -B
241 "#;
242 let build_workers = 1;
243 Self {
244 project_path,
245 workspaces_path,
246 template_file,
247 target_files,
248 target_files_base,
249 init_bash_script: init_bash_script.to_string(),
250 compile_bash_script: compile_bash_script.to_string(),
251 run_bash_script: default_run_bash_script,
252 build_workers,
253 run_method: RunMethod::Exclusive(1),
254 temp_target_path_dir,
255 run_func_data: IGNORE_ON_ERROR_DEFAULT_RUN_FUNC,
256 data_queue_receiver: None,
257 compilation_error_handling_method: CompliationErrorHandlingMethod::Collect,
258 auto_gather_array_data: true,
259 in_place_template: false,
260 disable_progress_bar: false,
261 mpb: MultiProgress::new(),
262 no_cache: false,
263 without_rsync: false,
264 enable_cppflags: false,
265 autosave_interval: 0,
266 autosave_dir: PathBuf::from(".parabuild/autosave"),
267 continue_from_start_time: None,
268 }
269 }
270
271 pub fn init_bash_script(mut self, init_bash_script: &str) -> Self {
272 self.init_bash_script = init_bash_script.to_string();
273 self
274 }
275
276 pub fn compile_bash_script(mut self, compile_bash_script: &str) -> Self {
277 self.compile_bash_script = compile_bash_script.to_string();
278 self
279 }
280
281 pub fn run_bash_script(mut self, run_bash_script: &str) -> Self {
282 self.run_bash_script = run_bash_script.to_string();
283 self
284 }
285
286 pub fn build_workers(mut self, build_workers: usize) -> Self {
287 self.build_workers = build_workers;
288 self
289 }
290
291 pub fn run_workers(mut self, run_workers: isize) -> Self {
292 if run_workers > 0 {
293 self.run_method = RunMethod::OutOfPlace(run_workers as usize);
294 } else if run_workers == 0 {
295 self.run_method = RunMethod::No;
296 } else if run_workers < 0 {
297 self.run_method = RunMethod::Exclusive(-run_workers as usize);
298 }
299 self
300 }
301
302 pub fn run_workers_exclusive(mut self, run_workers: isize) -> Self {
303 self.run_method = RunMethod::Exclusive(run_workers as usize);
304 self
305 }
306
307 pub fn run_method(mut self, run_method: RunMethod) -> Self {
308 self.run_method = run_method;
309 self
310 }
311
312 pub fn run_func(mut self, run_func: RunFunc) -> Self {
313 self.run_func_data = run_func;
314 self
315 }
316
317 pub fn compilation_error_handling_method(
318 mut self,
319 compilation_error_handling_method: CompliationErrorHandlingMethod,
320 ) -> Self {
321 self.compilation_error_handling_method = compilation_error_handling_method;
322 self
323 }
324
325 pub fn auto_gather_array_data(mut self, auto_gather_array_data: bool) -> Self {
326 self.auto_gather_array_data = auto_gather_array_data;
327 self
328 }
329
330 pub fn in_place_template(mut self, in_place_template: bool) -> Self {
331 self.in_place_template = in_place_template;
332 self
333 }
334
335 pub fn disable_progress_bar(mut self, disable_progress_bar: bool) -> Self {
336 self.disable_progress_bar = disable_progress_bar;
337 self
338 }
339
340 pub fn no_cache(mut self, no_cache: bool) -> Self {
341 self.no_cache = no_cache;
342 self
343 }
344
345 pub fn without_rsync(mut self, without_rsync: bool) -> Self {
346 self.without_rsync = without_rsync;
347 self
348 }
349
350 pub fn enable_cppflags(mut self, enable_cppflags: bool) -> Self {
351 self.enable_cppflags = enable_cppflags;
352 self
353 }
354
355 pub fn autosave_interval(mut self, autosave_interval: u64) -> Self {
356 self.autosave_interval = autosave_interval;
357 self
358 }
359
360 pub fn autosave_dir<S: AsRef<Path>>(mut self, autosave_dir: S) -> Self {
361 self.autosave_dir = autosave_dir.as_ref().to_path_buf();
362 self
363 }
364
365 pub fn set_datas(&mut self, datas: Vec<JsonValue>) -> Result<(), Box<dyn Error>> {
367 if self.data_queue_receiver.is_some() {
368 return Err("Data queue receiver is already initialized".into());
369 }
370 let (data_queue_sender, data_queue_receiver) = unbounded();
371 self.data_queue_receiver = Some(data_queue_receiver);
372 for id_data in datas.into_iter().enumerate() {
373 data_queue_sender.send(id_data).unwrap();
374 }
375 Ok(())
376 }
377
378 pub fn set_datas_with_processed_data_ids_set(
380 &mut self,
381 datas: Vec<JsonValue>,
382 processed_data_ids_set: HashSet<usize>,
383 ) -> Result<(), Box<dyn Error>> {
384 if self.data_queue_receiver.is_some() {
385 return Err("Data queue receiver is already initialized".into());
386 }
387 let (data_queue_sender, data_queue_receiver) = unbounded();
388 self.data_queue_receiver = Some(data_queue_receiver);
389 for id_data in datas.into_iter().enumerate() {
390 if !processed_data_ids_set.contains(&id_data.0) {
391 data_queue_sender.send(id_data).unwrap();
392 }
393 }
394 Ok(())
395 }
396
397 pub fn get_data_queue_sender(&mut self) -> Result<Sender<(usize, JsonValue)>, Box<dyn Error>> {
398 if self.data_queue_receiver.is_some() {
399 return Err("Data queue receiver is already initialized".into());
400 }
401 let (data_queue_sender, data_queue_receiver) = unbounded();
402 self.data_queue_receiver = Some(data_queue_receiver);
403 Ok(data_queue_sender)
404 }
405
406 pub fn init_workspace(&self) -> Result<(), Box<dyn Error>> {
408 if !is_command_installed("rsync") {
409 if !self.without_rsync {
410 return Err("rsync is not installed, set `without_rsync` to true to ignore".into());
411 }
412 }
413 let out_of_place_run_workers = match self.run_method {
414 RunMethod::OutOfPlace(run_workers) => run_workers,
415 RunMethod::Exclusive(run_workers) => run_workers,
416 _ => 0,
417 };
418 let workspaces_path = if self.workspaces_path.is_absolute() {
419 self.workspaces_path.clone()
420 } else {
421 env::current_dir().unwrap().join(&self.workspaces_path)
422 };
423 if self.no_cache {
424 if self.workspaces_path.exists() {
425 std::fs::remove_dir_all(&self.workspaces_path).unwrap();
426 }
427 }
428 std::fs::create_dir_all(&workspaces_path).unwrap();
429 let mut project_path = self.project_path.clone();
430 let move_to_temp_dir = workspaces_path
431 .starts_with(std::fs::canonicalize(&self.project_path).unwrap())
432 && self.without_rsync;
433 let mut build_handles = vec![];
434 if move_to_temp_dir {
435 self.add_spinner("copying to temp dir");
436 project_path = tempdir().unwrap().into_path();
437 copy_dir_with_ignore(&self.project_path, &project_path).unwrap();
438 }
439 for (i, destination) in (0..self.build_workers).map(|i| (i, format!("workspace_{}", i))) {
440 let source = project_path.clone();
441 let destination = self.workspaces_path.join(destination);
442 let init_bash_script = self.init_bash_script.clone();
443 let mpb = self.mpb.clone();
444 let disable_progress_bar = self.disable_progress_bar;
445 let without_rsync = self.without_rsync;
446 let handle = std::thread::spawn(move || {
447 let sp = Self::add_spinner2(
448 disable_progress_bar,
449 &mpb,
450 format!("init workspace {}: copying", i),
451 );
452 if move_to_temp_dir {
453 copy_dir(&source, &destination).unwrap();
454 } else {
455 if without_rsync {
456 copy_dir_with_ignore(&source, &destination).unwrap();
457 } else {
458 copy_dir_with_rsync(&source, &destination).unwrap();
459 }
460 }
461 sp.set_message(format!("init workspace {}: init", i));
462 Command::new("bash")
463 .arg("-c")
464 .arg(&init_bash_script)
465 .current_dir(&destination)
466 .output()
467 .unwrap();
468 });
469 build_handles.push(handle);
470 }
471 let mut run_handles = vec![];
472 if out_of_place_run_workers > 0 {
473 std::fs::create_dir_all(self.workspaces_path.join(Self::TEMP_TARGET_PATH_DIR)).unwrap();
475 for (i, destination) in
476 (0..out_of_place_run_workers).map(|i| (i, format!("workspace_exe_{}", i)))
477 {
478 let source = project_path.clone();
479 let destination = self.workspaces_path.join(destination);
480 let init_bash_script = self.init_bash_script.clone();
481 let mpb = self.mpb.clone();
484 let disable_progress_bar = self.disable_progress_bar;
485 let without_rsync = self.without_rsync;
486 let handle = std::thread::spawn(move || {
487 let sp = Self::add_spinner2(
488 disable_progress_bar,
489 &mpb,
490 format!("init workspace_run {}: copying", i),
491 );
492 if move_to_temp_dir {
493 copy_dir(&source, &destination).unwrap();
494 } else {
495 if without_rsync {
496 copy_dir_with_ignore(&source, &destination).unwrap();
497 } else {
498 copy_dir_with_rsync(&source, &destination).unwrap();
499 }
500 }
501 sp.set_message(format!("init workspace_run {}: init", i));
502 match Command::new("bash")
503 .arg("-c")
504 .arg(&init_bash_script)
505 .current_dir(&destination)
506 .output()
507 {
508 Ok(output) => {
509 if !output.status.success() {
510 panic!(
511 "Init bash script failed in workspace_run_{}: {:?}",
512 i, output
513 );
514 }
515 }
516 Err(e) => {
517 panic!("Init bash script failed in workspace_run_{}: {:?}", i, e);
518 }
519 }
520 });
529 run_handles.push(handle);
530 }
531 }
532
533 for handle in build_handles {
534 handle.join().unwrap();
535 }
536 for handle in run_handles {
537 handle.join().unwrap();
538 }
539
540 std::fs::create_dir_all(&self.temp_target_path_dir).unwrap();
541
542 Ok(())
543 }
544
545 fn latest_folder<P: AsRef<Path>>(dir: P) -> Option<PathBuf> {
546 if !dir.as_ref().exists() {
547 return None;
548 }
549 let mut latest = None;
550 let mut latest_time = None;
551 for entry in std::fs::read_dir(dir).unwrap() {
552 let entry = entry.unwrap();
553 let path = entry.path();
554 let metadata = entry.metadata().unwrap();
555 if metadata.is_dir() {
556 let time = metadata.modified().unwrap();
557 if latest_time.is_none() || time > latest_time.unwrap() {
558 latest = Some(path);
559 latest_time = Some(time);
560 }
561 }
562 }
563 latest
564 }
565
566 pub fn autosave_load(&mut self, start_time: String) -> (JsonValue, Vec<JsonValue>, Vec<usize>) {
568 let autosave_dir = if start_time.is_empty() {
569 let latest = Self::latest_folder(&self.autosave_dir);
570 if latest.is_none() {
571 panic!("No autosave data found, consider running without --continue");
572 }
573 latest.unwrap()
574 } else {
575 self.autosave_dir.join(start_time)
576 };
577 println!("Loading autosave data from {:?}", autosave_dir);
578 self.continue_from_start_time = Some(
579 autosave_dir
580 .file_name()
581 .unwrap()
582 .to_str()
583 .unwrap()
584 .to_string(),
585 );
586 let datas = std::fs::read_dir(&autosave_dir).unwrap().into_iter().fold(
587 (vec![], vec![], vec![]),
588 |(mut run_datas_array, mut compile_error_datas_array, mut processed_data_ids_array),
589 entry| {
590 let entry = entry.unwrap();
591 let path = entry.path();
592 let run_datas_file = path.join("run_datas.json");
593 let compile_error_datas_file = path.join("compile_error_datas.json");
594 let processed_data_ids_file = path.join("processed_data_ids.json");
595 let run_datas: JsonValue =
596 serde_json::from_reader(std::fs::File::open(&run_datas_file).unwrap()).unwrap();
597 let compile_error_datas: Vec<JsonValue> = serde_json::from_reader(
598 std::fs::File::open(&compile_error_datas_file).unwrap(),
599 )
600 .unwrap();
601 let processed_data_ids: Vec<usize> =
602 serde_json::from_reader(std::fs::File::open(&processed_data_ids_file).unwrap())
603 .unwrap();
604 run_datas_array.push(run_datas);
605 compile_error_datas_array.extend(compile_error_datas);
606 processed_data_ids_array.extend(processed_data_ids);
607 (
608 run_datas_array,
609 compile_error_datas_array,
610 processed_data_ids_array,
611 )
612 },
613 );
614 self.gather_data(datas.0, datas.1, datas.2).unwrap()
615 }
616
617 fn autosave_save<P: AsRef<Path>>(
619 autosave_dir: P,
620 start_time: &str,
621 run_datas: &JsonValue,
622 compile_error_datas: &Vec<JsonValue>,
623 processed_data_ids: &Vec<usize>,
624 workspace_id: Uuid,
625 ) {
626 let autosave_dir = autosave_dir
628 .as_ref()
629 .to_path_buf()
630 .join(start_time)
631 .join(workspace_id.to_string());
632 if !autosave_dir.exists() {
633 std::fs::create_dir_all(&autosave_dir).expect("Failed to create autosave dir");
634 }
635 let run_datas_file = autosave_dir.join("run_datas.json");
636 let run_datas_file1 = autosave_dir.join("run_datas.json.1");
637 let compile_error_datas_file = autosave_dir.join("compile_error_datas.json");
638 let compile_error_datas_file1 = autosave_dir.join("compile_error_datas.json.1");
639 let processed_data_ids_file = autosave_dir.join("processed_data_ids.json");
640 let processed_data_ids_file1 = autosave_dir.join("processed_data_ids.json.1");
641 if run_datas_file.exists() {
642 std::fs::rename(&run_datas_file, &run_datas_file1).unwrap();
643 }
644 if compile_error_datas_file.exists() {
645 std::fs::rename(&compile_error_datas_file, &compile_error_datas_file1).unwrap();
646 }
647 if processed_data_ids_file.exists() {
648 std::fs::rename(&processed_data_ids_file, &processed_data_ids_file1).unwrap();
649 }
650 let run_datas_file = std::fs::File::create(&run_datas_file).unwrap();
651 let compile_error_datas_file = std::fs::File::create(&compile_error_datas_file).unwrap();
652 let processed_data_ids_file = std::fs::File::create(&processed_data_ids_file).unwrap();
653 serde_json::to_writer(run_datas_file, &run_datas).unwrap();
654 serde_json::to_writer(compile_error_datas_file, &compile_error_datas).unwrap();
655 serde_json::to_writer(processed_data_ids_file, &processed_data_ids).unwrap();
656 }
657
658 pub fn run(&self) -> Result<(JsonValue, Vec<JsonValue>, Vec<usize>), Box<dyn Error>> {
660 let start_time = if let Some(start_time) = &self.continue_from_start_time {
661 start_time.clone()
662 } else {
663 Local::now().format("%Y-%m-%d_%H-%M-%S").to_string()
664 };
665 println!("Start from: {}", start_time);
666 if !self.data_queue_receiver.is_some() {
667 return Err("Data queue receiver is not initialized".into());
668 }
669 if !is_command_installed("bash") {
670 return Err("bash is not installed".into());
671 }
672 if !is_command_installed("lsof") {
673 return Err("lsof is not installed, which may lead to strange problems that are difficult to reproduce".into());
674 }
675 let mut build_handles = vec![];
676 let mut run_handles = Vec::new();
677 let (executable_queue_sender, executable_queue_receiver) = unbounded();
678 let data_size = self.data_queue_receiver.as_ref().unwrap().len() as u64;
679 let build_pb = self.add_progress_bar("Building", data_size, "All builds done");
680 let run_pb = if !matches!(self.run_method, RunMethod::No) {
681 if matches!(self.run_method, RunMethod::Exclusive(_)) {
682 self.add_progress_bar("Waiting to run (exclusive)", data_size, "All runs done")
683 } else {
684 self.add_progress_bar("Running", data_size, "All runs done")
685 }
686 } else {
687 ProgressBar::hidden()
688 };
689 let stop_flag = Arc::new(AtomicBool::new(false));
690 if !cfg!(test) {
691 ctrlc::set_handler({
692 let stop_flag = Arc::clone(&stop_flag);
693 move || {
694 println!("Ctrl-C received, stopping...");
695 stop_flag.store(true, Ordering::Relaxed);
696 }
697 })
698 .expect("Error setting Ctrl-C handler");
699 }
700 build_pb.tick();
701 run_pb.tick();
702 let spawn_build_workers = || {
703 for i in 0..self.build_workers {
704 let workspace_path = self.workspaces_path.join(format!("workspace_{}", i));
705 let build_handle = self.build_worker(
706 workspace_path,
707 executable_queue_sender.clone(),
708 build_pb.clone(),
709 run_pb.clone(),
710 Arc::clone(&stop_flag),
711 start_time.clone(),
712 );
713 build_handles.push(build_handle);
714 }
715 drop(executable_queue_sender);
716 };
717 let spawn_run_workers = || {
718 let run_workers = match self.run_method {
719 RunMethod::OutOfPlace(run_workers) => run_workers,
720 RunMethod::Exclusive(run_workers) => run_workers,
721 _ => 0,
722 };
723 for i in 0..run_workers {
724 let workspace_path = self.workspaces_path.join(format!("workspace_exe_{}", i));
725 let run_handle = self.run_worker(
726 workspace_path,
727 executable_queue_receiver.clone(),
728 run_pb.clone(),
729 Arc::clone(&stop_flag),
730 start_time.clone(),
731 );
732 run_handles.push(run_handle);
733 }
734 drop(executable_queue_receiver);
735 };
736 let gather_build_handlers =
737 |build_handles: Vec<JoinHandle<(JsonValue, Vec<JsonValue>, Vec<usize>)>>| {
738 build_handles.into_iter().fold(
739 (vec![], vec![], vec![]),
740 |(
741 mut run_datas_array,
742 mut compile_error_datas_array,
743 mut processed_data_ids_array,
744 ),
745 handle| {
746 let (run_datas, compile_error_datas, processed_data_ids) =
747 handle.join().unwrap();
748 run_datas_array.push(run_datas);
749 compile_error_datas_array.extend(compile_error_datas);
750 processed_data_ids_array.extend(processed_data_ids);
751 (
752 run_datas_array,
753 compile_error_datas_array,
754 processed_data_ids_array,
755 )
756 },
757 )
758 };
759 let gather_run_handlers = |run_handles: Vec<JoinHandle<(JsonValue, Vec<usize>)>>| {
760 run_handles.into_iter().fold(
761 (vec![], vec![]),
762 |(mut run_datas_array, mut processed_data_ids_array), handle| {
763 let (run_datas, processed_data_ids) = handle.join().unwrap();
764 run_datas_array.push(run_datas);
765 processed_data_ids_array.extend(processed_data_ids);
766 (run_datas_array, processed_data_ids_array)
767 },
768 )
769 };
770 spawn_build_workers();
771 drop(build_pb);
772 match self.run_method {
773 RunMethod::No | RunMethod::InPlace => {
774 let (run_datas, compile_error_datas, processed_data_ids) =
775 gather_build_handlers(build_handles);
776 self.gather_data(run_datas, compile_error_datas, processed_data_ids)
777 }
778 RunMethod::Exclusive(_) => {
779 let (_, compile_error_datas, mut processed_data_ids) =
780 gather_build_handlers(build_handles);
781 run_pb.set_message("Running");
782 spawn_run_workers();
783 let (run_datas, run_processed_data_ids) = gather_run_handlers(run_handles);
784 processed_data_ids.extend(run_processed_data_ids);
785 self.gather_data(run_datas, compile_error_datas, processed_data_ids)
786 }
787 RunMethod::OutOfPlace(_) => {
788 spawn_run_workers();
789 let (_, compile_error_datas, mut processed_data_ids) =
790 gather_build_handlers(build_handles);
791 let (run_datas, run_processed_data_ids) = gather_run_handlers(run_handles);
792 processed_data_ids.extend(run_processed_data_ids);
793 self.gather_data(run_datas, compile_error_datas, processed_data_ids)
794 }
795 }
796 }
797
798 fn build_worker(
799 &self,
800 workspace_path: PathBuf,
801 executable_queue_sender: Sender<(usize, JsonValue)>,
802 build_pb: ProgressBar,
803 run_pb: ProgressBar,
804 stop_flag: Arc<AtomicBool>,
805 start_time: String,
806 ) -> std::thread::JoinHandle<(JsonValue, Vec<JsonValue>, Vec<usize>)> {
807 let template_path = self.project_path.join(&self.template_file);
808 let targets_path: Vec<PathBuf> = self
809 .target_files
810 .iter()
811 .map(|target_file| workspace_path.join(target_file).to_path_buf())
812 .collect();
813 let compile_bash_script = self.compile_bash_script.clone();
814 let template_output_file = if self.in_place_template {
815 self.template_file.clone()
816 } else {
817 self.template_file.with_extension("")
818 };
819 let target_files_base = self.target_files_base.clone();
820 let temp_target_path_dir = self.temp_target_path_dir.clone();
821 let data_queue_receiver = self.data_queue_receiver.as_ref().unwrap().clone();
822 let run_method = self.run_method;
823 let run_func = self.run_func_data;
824 let compilation_error_handling_method = self.compilation_error_handling_method;
825
826 let template_output_path = workspace_path.join(&template_output_file);
827 let mut handlebars = Handlebars::new();
828 if template_path.exists() && template_path.is_file() {
829 handlebars.register_helper("default", Box::new(default_value_helper));
830 handlebars
831 .register_template_string("tpl", std::fs::read_to_string(&template_path).unwrap())
832 .unwrap();
833 }
834 let mut run_data = JsonValue::Null;
835 let mut compile_error_datas = Vec::new();
836 let run_bash_script = self.run_bash_script.clone();
837 let enable_cppflags = self.enable_cppflags;
838 let disable_progress_bar = self.disable_progress_bar;
839 let mpb = self.mpb.clone();
840 let autosave_dir = self.autosave_dir.clone();
841 let autosave_interval = self.autosave_interval;
842 std::thread::spawn(move || {
843 let uuid = Uuid::new_v4();
844 let mut processed_data_ids = Vec::new();
845 let sp = Self::add_spinner2(
846 disable_progress_bar || !matches!(run_method, RunMethod::InPlace),
847 &mpb,
848 serde_json::to_string_pretty(&JsonValue::Null).unwrap(),
849 );
850 let mut autosave_last_time = Instant::now();
851 for (i, data) in data_queue_receiver.iter() {
852 let mut cppflags_val = "-DPARABUILD=ON ".to_string();
853 if enable_cppflags {
854 for (key, value) in data.as_object().unwrap().iter() {
856 cppflags_val.push_str(&format!("-D{}={} ", key, value));
857 }
858 }
859 if handlebars.has_template("tpl") {
860 let mut template_output = std::fs::File::create(&template_output_path)
861 .expect(format!("Failed to create {:?}", template_output_path).as_str());
862 handlebars
863 .render_to_write("tpl", &data, &template_output)
864 .expect(format!("Failed to render {:?}", template_output_path).as_str());
865 template_output.flush().unwrap();
866 }
867 let mut output = Command::new("bash");
868 let mut output = output
869 .arg("-c")
870 .arg(&compile_bash_script)
871 .current_dir(&workspace_path);
872 if enable_cppflags {
873 output = output.env("CPPFLAGS", cppflags_val);
874 }
875 let output = output.output();
876 build_pb.inc(1);
877 if output.is_err() || output.is_ok() && !output.as_ref().unwrap().status.success() {
878 if stop_flag.load(Ordering::Relaxed) {
879 } else {
881 processed_data_ids.push(i);
882 if compilation_error_handling_method
883 == CompliationErrorHandlingMethod::Panic
884 {
885 if let Ok(output) = output {
886 panic!(
887 "Compilation script failed in data: {:?} with output: {:?}",
888 data, output
889 );
890 } else {
891 panic!("Compilation script failed in data: {:?}", data);
892 }
893 } else {
894 if !matches!(run_method, RunMethod::No) {
895 run_pb.inc(1);
896 }
897 match compilation_error_handling_method {
898 CompliationErrorHandlingMethod::Collect => {
899 compile_error_datas.push(data.clone());
900 continue;
901 }
902 CompliationErrorHandlingMethod::Ignore => {
903 continue;
904 }
905 CompliationErrorHandlingMethod::Panic => {
906 panic!("Compilation script failed in data: {:?}", data);
907 }
908 }
909 }
910 }
911 }
912 if stop_flag.load(Ordering::Relaxed) {
913 Self::autosave_save(
914 &autosave_dir,
915 &start_time,
916 &run_data,
917 &compile_error_datas,
918 &processed_data_ids,
919 uuid,
920 );
921 break;
922 }
923 match run_method {
924 RunMethod::InPlace => {
925 let last_data = run_func(
927 &std::fs::canonicalize(&workspace_path).unwrap(),
928 &run_bash_script,
929 &data,
930 &mut run_data,
931 &stop_flag,
932 )
933 .unwrap();
934 sp.set_message(serde_json::to_string_pretty(&last_data).unwrap());
935 run_pb.inc(1);
936 }
937 RunMethod::No | RunMethod::Exclusive(_) | RunMethod::OutOfPlace(_) => {
938 for (target_path, target_file_base) in
939 targets_path.iter().zip(target_files_base.iter())
940 {
941 let to_target_executable_path_file =
942 format!("{}_{}", &target_file_base, i);
943 let to_target_executable_path =
944 temp_target_path_dir.join(&to_target_executable_path_file);
945 std::fs::copy(&target_path, &to_target_executable_path).unwrap();
946 }
947 match run_method {
948 RunMethod::No => {
949 let to_metadata_path =
950 temp_target_path_dir.join(format!("data_{}.json", i));
951 std::fs::write(&to_metadata_path, data.to_string()).unwrap();
952 }
953 RunMethod::OutOfPlace(_) | RunMethod::Exclusive(_) => {
954 executable_queue_sender.send((i, data.clone())).unwrap();
955 }
956 _ => panic!("Unexpected run method"),
957 }
958 }
959 }
960 if stop_flag.load(Ordering::Relaxed) {
961 Self::autosave_save(
962 &autosave_dir,
963 &start_time,
964 &run_data,
965 &compile_error_datas,
966 &processed_data_ids,
967 uuid,
968 );
969 break;
970 }
971 match run_method {
972 RunMethod::InPlace | RunMethod::No => {
973 processed_data_ids.push(i);
974 }
975 _ => {}
976 }
977 if autosave_interval > 0
978 && autosave_last_time.elapsed().as_secs() > autosave_interval
979 {
980 Self::autosave_save(
981 &autosave_dir,
982 &start_time,
983 &run_data,
984 &compile_error_datas,
985 &processed_data_ids,
986 uuid,
987 );
988 autosave_last_time = Instant::now();
989 }
990 }
991 (run_data, compile_error_datas, processed_data_ids)
992 })
993 }
994
995 fn run_worker(
996 &self,
997 workspace_path: PathBuf,
998 executable_queue_receiver: Receiver<(usize, JsonValue)>,
999 run_pb: ProgressBar,
1000 stop_flag: Arc<AtomicBool>,
1001 start_time: String,
1002 ) -> std::thread::JoinHandle<(JsonValue, Vec<usize>)> {
1003 let uuid = Uuid::new_v4();
1004 let targets_path: Vec<PathBuf> = self
1005 .target_files
1006 .iter()
1007 .map(|target_file| workspace_path.join(target_file).to_path_buf())
1008 .collect();
1009 let target_files_base = self.target_files_base.clone();
1010 let run_func = self.run_func_data;
1011 let mut run_data = JsonValue::Null;
1012 let disable_progress_bar = self.disable_progress_bar;
1013 let mpb = self.mpb.clone();
1014 let run_bash_script = self.run_bash_script.clone();
1015 let temp_target_path_dir = self.temp_target_path_dir.clone();
1016 let autosave_dir = self.autosave_dir.clone();
1017 let autosave_interval = self.autosave_interval;
1018 std::thread::spawn(move || {
1019 let mut processed_data_ids = Vec::new();
1020 let mut autosave_last_time = Instant::now();
1021 let sp = Self::add_spinner2(
1022 disable_progress_bar,
1023 &mpb,
1024 serde_json::to_string_pretty(&JsonValue::Null).unwrap(),
1025 );
1026 for (i, data) in executable_queue_receiver.iter() {
1027 for (target_path, target_file_base) in
1028 targets_path.iter().zip(target_files_base.iter())
1029 {
1030 let to_target_path_file = format!("{}_{}", &target_file_base, i);
1031 let to_target_executable_path = temp_target_path_dir.join(&to_target_path_file);
1032 std::fs::rename(&to_target_executable_path, &target_path).unwrap();
1033 }
1034 for target_path in targets_path.iter() {
1035 wait_until_file_ready(&target_path).unwrap();
1036 }
1037 let last_data = run_func(
1038 &std::fs::canonicalize(&workspace_path).unwrap(),
1039 &run_bash_script,
1040 &data,
1041 &mut run_data,
1042 &stop_flag,
1043 )
1044 .unwrap();
1045 if stop_flag.load(Ordering::Relaxed) {
1046 Self::autosave_save(
1047 &autosave_dir,
1048 &start_time,
1049 &run_data,
1050 &vec![],
1051 &processed_data_ids,
1052 uuid,
1053 );
1054 break;
1055 }
1056 sp.set_message(serde_json::to_string_pretty(&last_data).unwrap());
1057 run_pb.inc(1);
1058 processed_data_ids.push(i);
1059 if autosave_interval > 0
1060 && autosave_last_time.elapsed().as_secs() > autosave_interval
1061 {
1062 Self::autosave_save(
1063 &autosave_dir,
1064 &start_time,
1065 &run_data,
1066 &vec![],
1067 &processed_data_ids,
1068 uuid,
1069 );
1070 autosave_last_time = Instant::now();
1071 }
1072 }
1073 (run_data, processed_data_ids)
1074 })
1075 }
1076
1077 pub fn gather_data(
1078 &self,
1079 run_data_array: Vec<JsonValue>,
1080 compile_error_datas: Vec<JsonValue>,
1081 processed_data_ids: Vec<usize>,
1082 ) -> Result<(JsonValue, Vec<JsonValue>, Vec<usize>), Box<dyn Error>> {
1083 let run_data_array: Vec<JsonValue> = run_data_array
1084 .into_iter()
1085 .filter(|item| !item.is_null())
1086 .collect();
1087 let run_datas = if self.run_method == RunMethod::No {
1088 JsonValue::Null
1089 } else if self.auto_gather_array_data && run_data_array.iter().all(|item| item.is_array()) {
1090 let mut run_data = Vec::new();
1091 for run_data_item in run_data_array {
1092 run_data.extend(run_data_item.as_array().unwrap().iter().cloned());
1093 }
1094 JsonValue::Array(run_data)
1095 } else {
1096 JsonValue::Array(run_data_array)
1098 };
1099 Ok((run_datas, compile_error_datas, processed_data_ids))
1100 }
1101
1102 fn add_progress_bar<S: Into<String>, F: Into<Cow<'static, str>>>(
1103 &self,
1104 message: S,
1105 total: u64,
1106 finish_message: F,
1107 ) -> ProgressBar {
1108 if self.disable_progress_bar {
1109 return ProgressBar::hidden();
1110 }
1111 let sty = ProgressStyle::with_template(
1112 "[{elapsed_precise} ETA: {eta_precise}] [{per_sec}] {bar:40.cyan/blue} {pos:>7}/{len:7} {msg}",
1113 )
1114 .unwrap();
1115 self.mpb.add(
1116 ProgressBar::new(total)
1117 .with_message(message.into())
1118 .with_style(sty)
1119 .with_finish(ProgressFinish::AbandonWithMessage(finish_message.into())),
1120 )
1121 }
1122
1123 fn add_spinner<S: Into<String>>(&self, message: S) -> ProgressBar {
1124 if self.disable_progress_bar {
1125 return ProgressBar::hidden();
1126 }
1127 let sp = self
1128 .mpb
1129 .add(ProgressBar::new_spinner().with_message(message.into()));
1130 sp.enable_steady_tick(Duration::from_millis(100));
1131 sp
1132 }
1133
1134 fn add_spinner2<S: Into<String>>(
1135 disable_progress_bar: bool,
1136 mpb: &MultiProgress,
1137 message: S,
1138 ) -> ProgressBar {
1139 if disable_progress_bar {
1140 return ProgressBar::hidden();
1141 }
1142 let sp = mpb.add(ProgressBar::new_spinner().with_message(message.into()));
1143 sp.enable_steady_tick(Duration::from_millis(100));
1144 sp
1145 }
1146}
1147
1148#[cfg(test)]
1149mod tests {
1150 use super::*;
1151 use serde_json::json;
1153
1154 const EXAMPLE_PROJECT: &str = crate::test_constants::EXAMPLE_CMAKE_PROJECT_PATH;
1155 const EXAMPLE_TEMPLATE_FILE: &str = "src/main.cpp.template";
1156 const EXAMPLE_IN_PLACE_TEMPLATE_FILE: &str = "src/main.cpp";
1157 const EXAMPLE_TARGET_EXECUTABLE_FILE: &str = "build/main";
1158 const EXAMPLE_INIT_BASH_SCRIPT: &str = r#"
1159 cmake -B build -S .
1160 "#;
1161 const EXAMPLE_IN_PLACE_INIT_BASH_SCRIPT: &str = r#"
1162 cmake -B build -S . -DPARABUILD=ON
1163 "#;
1164 const EXAMPLE_COMPILE_BASH_SCRIPT: &str = r#"
1165 cmake --build build --target all -- -B
1166 "#;
1167
1168 #[test]
1169 fn test_workspaces_under_project_path() {
1170 let example_project_path = std::fs::canonicalize(EXAMPLE_PROJECT).unwrap();
1171 let workspaces_path = PathBuf::from("workspaces_under_project_path");
1172 let parabuilder = Parabuilder::new(
1173 ".",
1174 &workspaces_path,
1175 example_project_path.join("src/main.cpp.template"),
1176 &["build/main"],
1177 )
1178 .init_bash_script(
1179 format!(
1180 r#"
1181 cmake -B build -S {}
1182 "#,
1183 EXAMPLE_PROJECT
1184 )
1185 .as_str(),
1186 )
1187 .compile_bash_script(
1188 r#"
1189 cmake --build build --target all -- -B
1190 "#,
1191 )
1192 .disable_progress_bar(true)
1193 .no_cache(true);
1194 parabuilder.init_workspace().unwrap();
1195 assert!(workspaces_path
1196 .join(format!(
1197 "workspace_0/{}/src/main.cpp.template",
1198 EXAMPLE_PROJECT
1199 ))
1200 .exists());
1201 assert!(workspaces_path.join("workspace_0/build").exists());
1202 std::fs::remove_dir_all(workspaces_path).unwrap();
1203 }
1204
1205 const SINGLETHREADED_N: i64 = 20;
1206 const MULTITHREADED_N: i64 = 100;
1207
1208 fn parabuild_tester(
1209 name: &str,
1210 size: i64,
1211 build_workers: usize,
1212 run_method: RunMethod,
1213 in_place_template: bool,
1214 is_makefile_project: bool,
1215 ) {
1216 let project = if !is_makefile_project {
1217 EXAMPLE_PROJECT
1218 } else {
1219 crate::test_constants::EXAMPLE_MAKEFILE_PROJECT_PATH
1220 };
1221 let mut datas = (1..=size)
1222 .map(|i| json!({"N": i}))
1223 .collect::<Vec<JsonValue>>();
1224 if size == 0 {
1225 datas.push(json!({}));
1226 }
1227 let error_data = json!({"N": "a"});
1228 datas.push(error_data.clone());
1229 let workspaces_path = PathBuf::from(format!("tests/workspaces_{}", name));
1230 let mut parabuilder = Parabuilder::new(
1231 project,
1232 &workspaces_path,
1233 if !is_makefile_project {
1234 if in_place_template {
1235 EXAMPLE_IN_PLACE_TEMPLATE_FILE
1236 } else {
1237 EXAMPLE_TEMPLATE_FILE
1238 }
1239 } else {
1240 ""
1241 },
1242 if !is_makefile_project {
1243 &[EXAMPLE_TARGET_EXECUTABLE_FILE]
1244 } else {
1245 &["main"]
1246 },
1247 )
1248 .init_bash_script(if !is_makefile_project {
1249 if in_place_template {
1250 EXAMPLE_IN_PLACE_INIT_BASH_SCRIPT
1251 } else {
1252 EXAMPLE_INIT_BASH_SCRIPT
1253 }
1254 } else {
1255 ""
1256 })
1257 .compile_bash_script(if !is_makefile_project {
1258 EXAMPLE_COMPILE_BASH_SCRIPT
1259 } else {
1260 "make -B"
1261 })
1262 .build_workers(build_workers)
1263 .run_method(run_method)
1264 .run_func(PANIC_ON_ERROR_DEFAULT_RUN_FUNC)
1265 .disable_progress_bar(true)
1266 .compilation_error_handling_method(CompliationErrorHandlingMethod::Collect)
1267 .in_place_template(in_place_template)
1268 .auto_gather_array_data(true)
1269 .enable_cppflags(is_makefile_project);
1270
1271 parabuilder.set_datas(datas).unwrap();
1272 parabuilder.init_workspace().unwrap();
1273 let (run_data, compile_error_datas, processed_data_ids) = parabuilder.run().unwrap();
1274 assert!(
1275 compile_error_datas == vec![error_data],
1276 "got: {:?} {:?}",
1277 run_data,
1278 compile_error_datas
1279 );
1280 assert!(
1281 processed_data_ids.len() == if size == 0 { 2 } else { size as usize + 1 }, "got: {:?}",
1283 processed_data_ids
1284 );
1285 if matches!(run_method, RunMethod::No) {
1286 assert!(run_data.is_null(), "got: {}", run_data);
1287 for i in 0..size {
1288 assert!(workspaces_path
1289 .join(format!("{}/main_{}", Parabuilder::TEMP_TARGET_PATH_DIR, i))
1290 .exists());
1291 assert!(workspaces_path
1292 .join(format!(
1293 "{}/data_{}.json",
1294 Parabuilder::TEMP_TARGET_PATH_DIR,
1295 i
1296 ))
1297 .exists());
1298 }
1299 } else {
1300 let ground_truth = if size > 0 {
1301 (1..=size).sum::<i64>()
1302 } else {
1303 42
1304 };
1305 assert!(run_data.is_array());
1306 let sum = run_data.as_array().unwrap().iter().fold(0, |acc, item| {
1307 acc + item["stdout"]
1308 .as_str()
1309 .unwrap()
1310 .trim()
1311 .parse::<i64>()
1312 .unwrap()
1313 });
1314 assert!(
1315 sum == ground_truth,
1316 "expected: {}, got: {}, run_data: {}",
1317 ground_truth,
1318 sum,
1319 run_data
1320 );
1321 }
1322 std::fs::remove_dir_all(workspaces_path).unwrap();
1323 }
1324
1325 #[test]
1326 fn test_singlethreaded_parabuild_without_run() {
1327 parabuild_tester(
1328 "test_singlethreaded_parabuild_without_run",
1329 SINGLETHREADED_N,
1330 1,
1331 RunMethod::No,
1332 false,
1333 false,
1334 );
1335 }
1336
1337 #[test]
1338 fn test_singlethreaded_parabuild_in_place_run() {
1339 parabuild_tester(
1340 "test_singlethreaded_parabuild_in_place_run",
1341 SINGLETHREADED_N,
1342 1,
1343 RunMethod::InPlace,
1344 false,
1345 false,
1346 );
1347 }
1348
1349 #[test]
1350 fn test_multithreaded_parabuild_without_run() {
1351 parabuild_tester(
1352 "test_multithreaded_parabuild_without_run",
1353 MULTITHREADED_N,
1354 4,
1355 RunMethod::No,
1356 false,
1357 false,
1358 );
1359 }
1360
1361 #[test]
1362 fn test_multithreaded_parabuild_in_place_run() {
1363 parabuild_tester(
1364 "test_multithreaded_parabuild_in_place_run",
1365 MULTITHREADED_N,
1366 4,
1367 RunMethod::InPlace,
1368 false,
1369 false,
1370 );
1371 }
1372
1373 #[test]
1374 fn test_multithreaded_parabuild_out_of_place_single_run() {
1375 parabuild_tester(
1376 "test_multithreaded_parabuild_out_of_place_single_run",
1377 MULTITHREADED_N,
1378 4,
1379 RunMethod::OutOfPlace(1),
1380 false,
1381 false,
1382 );
1383 }
1384
1385 #[test]
1386 fn test_multithreaded_parabuild_out_of_place_run() {
1387 parabuild_tester(
1388 "test_multithreaded_parabuild_out_of_place_run",
1389 MULTITHREADED_N,
1390 4,
1391 RunMethod::OutOfPlace(2),
1392 false,
1393 false,
1394 );
1395 }
1396
1397 #[test]
1398 fn test_multithreaded_parabuild_exclusive_run() {
1399 parabuild_tester(
1400 "test_multithreaded_parabuild_exclusive_run",
1401 MULTITHREADED_N,
1402 4,
1403 RunMethod::Exclusive(2),
1404 false,
1405 false,
1406 );
1407 }
1408
1409 #[test]
1410 fn test_multithreaded_parabuild_out_of_place_run_in_place_template() {
1411 parabuild_tester(
1412 "test_multithreaded_parabuild_out_of_place_run_in_place_template",
1413 MULTITHREADED_N,
1414 4,
1415 RunMethod::OutOfPlace(2),
1416 true,
1417 false,
1418 );
1419 }
1420
1421 #[test]
1422 fn test_multithreaded_parabuild_out_of_place_run_default_template() {
1423 parabuild_tester(
1424 "test_multithreaded_parabuild_out_of_place_run_default_template",
1425 0,
1426 4,
1427 RunMethod::OutOfPlace(2),
1428 true,
1429 false,
1430 );
1431 }
1432
1433 #[test]
1434 fn test_multithreaded_parabuild_makefile_project() {
1435 parabuild_tester(
1436 "test_multithreaded_parabuild_makefile_project",
1437 10,
1438 4,
1439 RunMethod::OutOfPlace(2),
1440 true,
1441 true,
1442 );
1443 }
1444
1445 }