toster 1.1.0

A simple-as-toast tester for C++ solutions to competitive programming exercises
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
mod args;
mod test_result;
mod testing_utils;

use std::{fs, panic, process, thread};
use std::cmp::Ordering;
use std::fmt::Write as FmtWrite;
use std::fs::{File, read_dir};
use std::panic::PanicInfo;
use std::path::Path;
use std::sync::{Arc, atomic, Mutex, OnceLock};
use std::sync::atomic::{AtomicBool, AtomicUsize};
use std::time::{Duration, Instant};
use atomic_counter::{AtomicCounter, RelaxedCounter};
use clap::Parser;
use colored::Colorize;
use human_panic::{handle_dump, print_msg};
use indicatif::{ParallelProgressIterator, ProgressState, ProgressStyle};
use is_executable::is_executable;
use lazy_static::lazy_static;
use rayon::iter::IntoParallelRefIterator;
use rayon::prelude::*;
use tempfile::tempdir;
use which::which;
use args::Args;
use crate::test_result::{ExecutionError, TestResult};
use crate::testing_utils::{compile_cpp, generate_output_default, get_sio2jail, run_test};
use crate::TestResult::{Correct, Error, Incorrect, NoOutputFile};

lazy_static! {
    static ref SUCCESS_COUNT: RelaxedCounter = RelaxedCounter::new(0);
	static ref INCORRECT_COUNT: RelaxedCounter = RelaxedCounter::new(0);
    static ref TIMED_OUT_COUNT: RelaxedCounter = RelaxedCounter::new(0);
    static ref MEMORY_LIMIT_EXCEEDED_COUNT: RelaxedCounter = RelaxedCounter::new(0);
    static ref RUNTIME_ERROR_COUNT: RelaxedCounter = RelaxedCounter::new(0);
    static ref NO_OUTPUT_FILE_COUNT: RelaxedCounter = RelaxedCounter::new(0);
	static ref SIO2JAIL_ERROR_COUNT: RelaxedCounter = RelaxedCounter::new(0);

    static ref SLOWEST_TEST: Arc<Mutex<(f64, String)>> = Arc::new(Mutex::new((-1 as f64, String::new())));
    static ref MOST_MEMORY_USED: Arc<Mutex<(i64, String)>> = Arc::new(Mutex::new((-1, String::new())));
    static ref ERRORS: Arc<Mutex<Vec<TestResult>>> = Arc::new(Mutex::new(vec![]));
}

static TIME_BEFORE_TESTING: OnceLock<Instant> = OnceLock::new();
static TEST_COUNT: AtomicUsize = AtomicUsize::new(0);
static GENERATE: AtomicBool = AtomicBool::new(false);

static RECEIVED_CTRL_C: AtomicBool = AtomicBool::new(false);
static PANICKING: AtomicBool = AtomicBool::new(false);

fn format_error_counts() -> String {
	[
		(INCORRECT_COUNT.get(), if INCORRECT_COUNT.get() > 1 { "wrong answers" } else { "wrong answer" }, ),
		(TIMED_OUT_COUNT.get(), "timed out"),
		(MEMORY_LIMIT_EXCEEDED_COUNT.get(), "out of memory"),
		(RUNTIME_ERROR_COUNT.get(), if RUNTIME_ERROR_COUNT.get() > 1 { "runtime errors" } else { "runtime error" }),
		(NO_OUTPUT_FILE_COUNT.get(), if NO_OUTPUT_FILE_COUNT.get() > 1 { "without output files" } else { "without output file" }),
		(SIO2JAIL_ERROR_COUNT.get(), if SIO2JAIL_ERROR_COUNT.get() > 1 { "sio2jail errors" } else { "sio2jail error" })
	]
		.into_iter()
		.filter(|(count, _)| count > &0)
		.map(|(count, label)| format!("{} {}", count.to_string().red(), label.to_string().red()))
		.collect::<Vec<String>>()
		.join(", ")
}

fn print_output(stopped_early: bool) {
	let slowest_test_clone = Arc::clone(&SLOWEST_TEST);
	let errors_clone = Arc::clone(&ERRORS);
	let most_memory_clone = Arc::clone(&MOST_MEMORY_USED);
	let slowest_test_mutex = slowest_test_clone.lock().expect("Failed to acquire mutex!");
	let mut errors_mutex = errors_clone.lock().expect("Failed to acquire mutex!");
	let most_memory_mutex = most_memory_clone.lock().expect("Failed to acquire mutex!");

	if TIME_BEFORE_TESTING.get().is_none() {
		println!("{}", "Toster was stopped before testing could start".red());
		process::exit(0);
	}

	let testing_time = TIME_BEFORE_TESTING.get().unwrap().elapsed().as_secs_f64();
	let tested_count = SUCCESS_COUNT.get() + TIMED_OUT_COUNT.get() + INCORRECT_COUNT.get() + MEMORY_LIMIT_EXCEEDED_COUNT.get() + RUNTIME_ERROR_COUNT.get() + NO_OUTPUT_FILE_COUNT.get() + SIO2JAIL_ERROR_COUNT.get();
	let not_tested_count = &TEST_COUNT.load(atomic::Ordering::Acquire) - tested_count;

	let error_counts = format_error_counts();
	let error_text = format!("{}{}", if !error_counts.is_empty() {", "} else {""}, error_counts);
	let not_finished_text = if not_tested_count > 0 {format!(", {}", (not_tested_count.to_string() + " not finished").yellow())} else {"".to_string()};

	if stopped_early {
		println!();
	}

	let mut additional_info = String::new();
	if slowest_test_mutex.0 != -1 as f64 {
		additional_info = format!(" (Slowest test: {} at {:.3}s{})",
		                          slowest_test_mutex.1,
		                          slowest_test_mutex.0,
		                          if most_memory_mutex.0 != -1 { format!(", most memory used: {} at {}KiB", most_memory_mutex.1, most_memory_mutex.0) } else { String::new() }
		)
	}

	// Printing the output
	match GENERATE.load(atomic::Ordering::Acquire) {
		true => {
			println!("Generation {} {:.2}s{}\nResults: {}{}{}",
			         if stopped_early {"stopped after"} else {"finished in"},
			         testing_time,
			         additional_info,
			         format!("{} successful", SUCCESS_COUNT.get()).green(),
			         error_text,
			         not_finished_text
			);
		}
		false => {
			println!("Testing {} {:.2}s{}\nResults: {}{}{}",
			         if stopped_early {"stopped after"} else {"finished in"},
			         testing_time,
			         additional_info,
			         format!("{} correct", SUCCESS_COUNT.get()).green(),
			         error_text,
			         not_finished_text
			);
		}
	}

	// Sorting the errors by name
	errors_mutex.sort_unstable_by(|a, b| -> Ordering {
		return human_sort::compare(&a.test_name(), &b.test_name());
	});

	// Printing errors if necessary
	if !errors_mutex.is_empty() {
		println!("Errors were found in the following tests:");

		for test_error in errors_mutex.iter() {
			println!("{}", test_error.to_string());
		}
	}

	process::exit(0);
}

fn setup_panic() {
	match human_panic::PanicStyle::default() {
		human_panic::PanicStyle::Debug => {}
		human_panic::PanicStyle::Human => {
			let meta = human_panic::metadata!();

			panic::set_hook(Box::new(move |info: &PanicInfo| {
				if !PANICKING.load(atomic::Ordering::Acquire) {
					PANICKING.store(true, atomic::Ordering::Release);

					let file_path = handle_dump(&meta, info);
					print_msg(file_path, &meta)
						.expect("human-panic: printing error message to console failed");
					process::exit(0);
				}
				else {
					thread::sleep(Duration::from_secs(u64::MAX));
				}
			}));
		}
	}
}

fn main() {
	setup_panic();
	ctrlc::set_handler(move || {
		RECEIVED_CTRL_C.store(true, atomic::Ordering::Release);
		print_output(true)
	}).expect("Error setting Ctrl-C handler");

	let args = Args::parse();
	GENERATE.store(args.generate, atomic::Ordering::Release);
	let tempdir = tempdir().expect("Failed to create temporary directory!");
	let input_dir: String = args.io.clone().unwrap_or(args.r#in);
	let output_dir: String = args.io.clone().unwrap_or(args.out);

	#[allow(unused_assignments)]
	let mut sio2jail = false;
	#[allow(unused_assignments)]
	let mut memory_limit = 0;
	#[cfg(all(target_os = "linux", target_arch = "x86_64"))] {
		sio2jail = args.sio2jail;
		memory_limit = args.memory_limit.unwrap_or(0);
	}

	if memory_limit != 0 && !sio2jail {
		sio2jail = true;
	}
	if sio2jail && args.generate {
		println!("{}", "You can't have the --generate and --sio2jail flags on at the same time.".red());
		return;
	}
	if sio2jail && memory_limit == 0 {
		memory_limit = 1048576;
	}
	if sio2jail && get_sio2jail() == String::new() {
		return;
	}

	// Making sure that the input and output directories as well as the source code file exist
	if !args.io.is_none() && !Path::new(&args.io.unwrap()).is_dir() {
		println!("{}", "The input/output directory does not exist".red());
		return;
	}
	if !Path::new(&input_dir).is_dir() {
		println!("{}", "The input directory does not exist".red());
		return;
	}
	if !Path::new(&output_dir).is_dir() {
		if args.generate {
			fs::create_dir(&output_dir).expect("Failed to create output directory!");
		}
		else {
			println!("{}", "The output directory does not exist".red());
			return;
		}
	}
	if !Path::new(&args.filename).is_file() {
		println!("{}", "The provided file does not exist".red());
		return;
	}

	if !args.compile_command.contains("<IN>") || !args.compile_command.contains("<OUT>") {
		println!("{}", "The compile command is invalid:".red());

		if !args.compile_command.contains("<IN>") {
			println!("{}", "- The <IN> argument is missing (read \"toster -h\" for more info)".red());
		}
		if !args.compile_command.contains("<OUT>") {
			println!("{}", "- The <OUT> argument is missing (read \"toster -h\" for more info)".red());
		}

		return;
	}

	// Compiling
	let extension = Path::new(&args.filename).extension().expect("Couldn't get the extension of the provided file").to_str().expect("Couldn't get the extension of the provided file");
	let executable: String;
	if is_executable(&args.filename) && !(wsl::is_wsl() && (extension == "cpp" || extension == "cc" || extension == "cxx" || extension == "c")) {
		executable = tempdir.path().join(format!("{}.o", Path::new(&args.filename).file_name().expect("The provided filename is invalid!").to_str().expect("The provided filename is invalid!"))).to_str().expect("The provided filename is invalid!").to_string();
		fs::copy(&args.filename, &executable).expect("The provided filename is invalid!");
	}
	else {
		match compile_cpp(Path::new(&args.filename).to_path_buf(), &tempdir, args.compile_timeout, &args.compile_command) {
			Ok(result) => { executable = result }
			Err(error) => {
				println!("{}", "Compilation failed with the following errors:".red());
				println!("{}", error);
				return;
			}
		}
	}

	// Progress bar styling
	let style: ProgressStyle = ProgressStyle::with_template("[{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({eta})\n{correct} {incorrect} {ctrlc}")
		.expect("Progress bar creation failed!")
		.with_key("eta", |state: &ProgressState, w: &mut dyn FmtWrite| write!(w, "{:.1}s", state.eta().as_secs_f64()).expect("Displaying the progress bar failed!"))
		.progress_chars("#>-")
		.with_key("correct", |_state: &ProgressState, w: &mut dyn FmtWrite|
			write!(w, "{}", format!("{} {}", &SUCCESS_COUNT.get(), if GENERATE.load(atomic::Ordering::Acquire) { "successful" } else { if SUCCESS_COUNT.get() != 1 { "correct answers" } else { "correct answer" } }).green()).expect("Displaying the progress bar failed!")
		)
		.with_key("incorrect", |_state: &ProgressState, w: &mut dyn FmtWrite|
			write!(w, "{}", format_error_counts()).expect("Displaying the progress bar failed!")
		)
		.with_key("ctrlc", |_state: &ProgressState, w: &mut dyn FmtWrite|
			write!(w, "{}", "(Press Ctrl+C to stop testing and print current results)".bright_black()).expect("Displaying the progress bar Ctrl+C message failed!")
		);

	// Filtering out input files
	let mut input_files = read_dir(&input_dir).expect("Cannot open input directory!").collect::<Vec<_>>();
	input_files.retain(|input| {
		let input_path = input.as_ref().expect("Failed to acquire reference!").path();
		let extension = input_path.extension();

		return match extension {
			None => false,
			Some(ext) => ".".to_owned() + &ext.to_str().unwrap_or("") == args.in_ext
		};
	});
	TEST_COUNT.store(input_files.len(), atomic::Ordering::Release);

	if input_files.is_empty() {
		println!("{}", "There are no files in the input directory with the provided file extension".red());
		return;
	}

	// Testing for sio2jail errors
	if sio2jail {
		let true_location = which("true");
		if true_location.is_err() {
			println!("{}", "The executable for the \"true\" command could not be found".red());
			return;
		}

		let test_input = tempdir.path().join(format!("test.in")).to_str().expect("The provided filename is invalid!").to_string();
		let test_input_path = Path::new(&test_input);
		File::create(test_input_path).expect("Failed to create temporary file!");

		let random_input_file_entry = input_files.get(0).expect("Couldn't get random input file").as_ref().expect("Failed to acquire reference!");
		let random_test_name = random_input_file_entry.path().file_stem().expect("Couldn't get the name of a random input file").to_str().expect("Couldn't get the name of a random input file").to_string();

		let (test_result, _) = run_test(&true_location.unwrap().to_str().expect("The executable for the \"true\" command has an invalid path").to_string(), test_input_path, &output_dir, &random_test_name, &args.out_ext, &tempdir, &(1 as u64), true, 0);
		match test_result {
			Error { error: ExecutionError::Sio2jailError(error), .. } => {
				if error == "Exception occurred: System error occured: perf event open failed: Permission denied: error 13: Permission denied\n" {
					println!("{}", "You need to run the following command to use toster with sio2jail. You may also put this option in your /etc/sysctl.conf. This will make the setting persist across reboots.".red());
					println!("{}", "sudo sysctl -w kernel.perf_event_paranoid=-1".bright_black().italic());
				}
				else {
					println!("Sio2jail error: {}", error.red());
				}

				return;
			}
			_ => {}
		}
	}

	TIME_BEFORE_TESTING.set(Instant::now()).expect("Couldn't store timestamp before testing!");
	// Running tests / generating output
	input_files.par_iter().progress_with_style(style).for_each(|input| {
		let input_file_entry = input.as_ref().expect("Failed to acquire reference!");
		let input_file_path = input_file_entry.path();
		let input_file_path_str = input_file_path.to_string_lossy().to_string();
		let test_name = input_file_entry.path().file_stem().expect(&*format!("The input file {} is invalid!", input_file_path_str)).to_str().expect(&*format!("The input file {} is invalid!", input_file_path_str)).to_string();

		let mut test_time: f64 = f64::MAX;
		let mut test_memory: i64 = -1;
		if args.generate {
			let input_file = File::open(input_file_path).expect(&*format!("Could not open input file {}", input_file_path_str));
			let output_file_path = format!("{}/{}{}", &output_dir, test_name, args.out_ext);
			let output_file = File::create(Path::new(&output_file_path)).expect("Failed to create output file!");

			let (execution_result, execution_error) = generate_output_default(&executable, input_file, output_file, &args.timeout);
			if !RECEIVED_CTRL_C.load(atomic::Ordering::Acquire) {
				match execution_error {
					Ok(()) => {
						SUCCESS_COUNT.inc();
					}
					Err(error) => {
						match error {
							ExecutionError::TimedOut => { TIMED_OUT_COUNT.inc(); }
							ExecutionError::MemoryLimitExceeded => { MEMORY_LIMIT_EXCEEDED_COUNT.inc(); }
							ExecutionError::RuntimeError(_) => { RUNTIME_ERROR_COUNT.inc(); }
							ExecutionError::Sio2jailError(_) => {}
						}
						let clone = Arc::clone(&ERRORS);
						clone.lock().expect("Failed to acquire mutex!").push(Error { test_name: test_name.clone(), error });
					}
				}

				test_time = execution_result.time_seconds;
			}
			else {
				thread::sleep(Duration::from_secs(u64::MAX));
			}
		}
		else {
			let (test_result, execution_result) = run_test(&executable, input_file_path.as_path(), &output_dir, &test_name, &args.out_ext, &tempdir, &args.timeout, sio2jail, memory_limit);
			test_time = execution_result.time_seconds;
			test_memory = execution_result.memory_kilobytes.unwrap_or(-1);

			if !RECEIVED_CTRL_C.load(atomic::Ordering::Acquire) {
				match test_result {
					Correct { .. } => { SUCCESS_COUNT.inc(); }
					Incorrect { .. } => { INCORRECT_COUNT.inc(); }
					Error { error: ExecutionError::MemoryLimitExceeded, .. } => { MEMORY_LIMIT_EXCEEDED_COUNT.inc(); }
					Error { error: ExecutionError::TimedOut, .. } => { TIMED_OUT_COUNT.inc(); }
					Error { error: ExecutionError::RuntimeError(_), .. } => { RUNTIME_ERROR_COUNT.inc(); }
					Error { error: ExecutionError::Sio2jailError(_), .. } => { SIO2JAIL_ERROR_COUNT.inc(); }
					NoOutputFile { .. } => { NO_OUTPUT_FILE_COUNT.inc(); }
				}

				if !test_result.is_correct() {
					let clone = Arc::clone(&ERRORS);
					clone.lock().expect("Failed to acquire mutex!").push(test_result);
				}
			}
			else {
				thread::sleep(Duration::from_secs(u64::MAX));
			}
		}

		if !RECEIVED_CTRL_C.load(atomic::Ordering::Acquire) {
			let slowest_test_clone = Arc::clone(&SLOWEST_TEST);
			let mut slowest_test_mutex = slowest_test_clone.lock().expect("Failed to acquire mutex!");
			if test_time > slowest_test_mutex.0 {
				*slowest_test_mutex = (test_time, test_name.clone());
			}

			let most_memory_clone = Arc::clone(&MOST_MEMORY_USED);
			let mut most_memory_mutex = most_memory_clone.lock().expect("Failed to acquire mutex!");
			if test_memory > most_memory_mutex.0 {
				*most_memory_mutex = (test_memory, test_name.clone());
			}
		}
		else {
			thread::sleep(Duration::from_secs(u64::MAX));
		}
	});

	print_output(false)
}