├── .gitignore
├── sio2jail
├── .idea
├── vcs.xml
├── .gitignore
├── misc.xml
└── modules.xml
├── src
├── generic_utils.rs
├── formatted_error.rs
├── temp_files.rs
├── prepare_input.rs
├── executor
│ ├── mod.rs
│ ├── simple.rs
│ └── sio2jail.rs
├── testing_utils.rs
├── test_errors.rs
├── checker.rs
├── compiler.rs
├── test_summary.rs
├── args.rs
└── main.rs
├── toster.iml
├── Cargo.toml
├── LICENSE
├── README.md
└── Cargo.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | /target
2 | in/*
3 | out/*
--------------------------------------------------------------------------------
/sio2jail:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/MikolajKolek/toster/HEAD/sio2jail
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 | # Editor-based HTTP Client requests
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 | /discord.xml
10 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/src/generic_utils.rs:
--------------------------------------------------------------------------------
1 | use std::thread;
2 | use std::time::Duration;
3 |
4 | #[deprecated(note = "This is not ideal, there must be a better way to implement it")]
5 | pub(crate) fn halt() -> ! {
6 | thread::sleep(Duration::from_secs(u64::MAX));
7 | unreachable!()
8 | }
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/toster.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/formatted_error.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::{Display, Formatter};
2 | use colored::Colorize;
3 |
4 | pub(crate) struct FormattedError(String);
5 |
6 | impl FormattedError {
7 | pub(crate) fn preformatted(formatted_string: String) -> FormattedError {
8 | FormattedError(formatted_string)
9 | }
10 |
11 | pub(crate) fn from_str(string: &str) -> FormattedError {
12 | FormattedError(string.red().to_string())
13 | }
14 | }
15 |
16 | impl Display for FormattedError {
17 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
18 | f.write_str(&self.0)
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "toster"
3 | description = "A simple-as-toast tester for C++ solutions to competitive programming exercises"
4 | repository = "https://github.com/MikolajKolek/toster"
5 | homepage = "https://github.com/MikolajKolek/toster"
6 | authors = ["Mikołaj Kołek", "Dominik Korsa"]
7 | readme = "README.md"
8 | license = "MIT"
9 | version = "1.2.1"
10 | edition = "2021"
11 | build = "build.rs"
12 |
13 | [dependencies]
14 | clap = { version = "4.5.4", features = ["derive"] }
15 | indicatif = { version = "0.17.8", features = ["rayon"] }
16 | rayon = "1.10.0"
17 | colored = "2.1.0"
18 | wait-timeout = "0.2.0"
19 | comfy-table = "7.1.1"
20 | tempfile = "3.10.1"
21 | terminal_size = "0.3.0"
22 | human-sort = "0.2.2"
23 | human-panic = "2.0.0"
24 | is_executable = "1.0.1"
25 | ctrlc = "3.4.4"
26 | directories = "5.0.1"
27 | which = "6.0.1"
28 |
29 | [target.'cfg(all(target_os = "linux", target_arch = "x86_64"))'.dependencies]
30 | command-fds = "0.3.0"
31 |
32 | [target.'cfg(target_os = "linux")'.dependencies]
33 | memfile = "0.3.2"
34 |
35 | [build-dependencies]
36 | directories = "5.0.1"
--------------------------------------------------------------------------------
/src/temp_files.rs:
--------------------------------------------------------------------------------
1 | use std::fs::File;
2 | use std::io;
3 | use std::process::Stdio;
4 |
5 | pub(crate) fn make_cloned_stdio(file: &File) -> Stdio {
6 | Stdio::from(file.try_clone().unwrap())
7 | }
8 |
9 | /// Creates a memfile using the `memfile` crate on Linux
10 | /// or a tempfile using the `tempfile` crate on other systems.
11 | ///
12 | /// These files should be deleted automatically when all file descriptors are closed
13 | ///
14 | /// Always returns a `File` struct
15 | pub(crate) fn create_temp_file() -> io::Result {
16 | #[cfg(target_os = "linux")]
17 | {
18 | // The file is deleted when all file descriptors are closed
19 | // https://man7.org/linux/man-pages/man2/memfd_create.2.html
20 | memfile::MemFile::create_default("toster temporary file")
21 | .map(|memfile| memfile.into_file())
22 | }
23 |
24 | #[cfg(not(target_os = "linux"))]
25 | {
26 | // tempfile() adds FILE_FLAG_DELETE_ON_CLOSE flag on Windows and TMPFILE on Unix
27 | // so the file should be deleted when all file descriptors are closed
28 | tempfile::tempfile()
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022-2024 Mikołaj Kołek
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/prepare_input.rs:
--------------------------------------------------------------------------------
1 | use std::fs::{File, read_dir};
2 | use std::path::{Path, PathBuf};
3 | use rayon::iter::{IndexedParallelIterator, IntoParallelIterator};
4 | use rayon::vec::IntoIter;
5 | use crate::formatted_error::FormattedError;
6 |
7 | pub(crate) enum TestInputSource {
8 | File(PathBuf)
9 | }
10 |
11 | impl TestInputSource {
12 | pub(crate) fn get_file(&self) -> File {
13 | match self {
14 | TestInputSource::File(path) => { File::open(path).expect("Failed to open input file") },
15 | }
16 | }
17 | }
18 |
19 | pub(crate) struct Test {
20 | pub(crate) test_name: String,
21 | pub(crate) input_source: TestInputSource,
22 | }
23 |
24 | pub(crate) struct TestingInputs> {
25 | pub(crate) test_count: usize,
26 | pub(crate) iterator: T,
27 | }
28 |
29 | pub(crate) fn prepare_file_inputs(input_dir: &Path, in_ext: &str) -> Result>, FormattedError> {
30 | let tests: Vec = read_dir(input_dir)
31 | .expect("Cannot open input directory")
32 | .map(|input| {
33 | input.expect("Failed to read contents of input directory").path()
34 | })
35 | .filter(|path| {
36 | return match path.extension() {
37 | None => false,
38 | Some(ext) => ".".to_owned() + ext.to_str().unwrap_or("") == in_ext
39 | };
40 | })
41 | .map(|file_path| {
42 | let test_name = file_path.file_stem().unwrap_or_else(|| panic!("The input file {} is invalid", file_path.display())).to_str().unwrap_or_else(|| panic!("The input file {} is invalid", file_path.display())).to_string();
43 | Test {
44 | test_name,
45 | input_source: TestInputSource::File(file_path)
46 | }
47 | })
48 | .collect();
49 |
50 | if tests.is_empty() {
51 | return Err(FormattedError::from_str("There are no files in the input directory with the provided file extension"));
52 | }
53 |
54 | let test_count = tests.len();
55 |
56 | Ok(TestingInputs { test_count, iterator: tests.into_par_iter() })
57 | }
--------------------------------------------------------------------------------
/src/executor/mod.rs:
--------------------------------------------------------------------------------
1 | pub(crate) mod simple;
2 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
3 | pub(crate) mod sio2jail;
4 |
5 | use std::fs::File;
6 | use std::io::{Read, Seek};
7 | use crate::executor::simple::SimpleExecutor;
8 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
9 | use crate::executor::sio2jail::Sio2jailExecutor;
10 | use crate::temp_files::create_temp_file;
11 | use crate::test_errors::{ExecutionError, ExecutionMetrics};
12 |
13 | pub(crate) trait TestExecutor: Sync + Send {
14 | /// Executes the program.
15 | ///
16 | /// Stdin is read from `input_file`, stderr is ignored.
17 | /// Stdout is written to `output_file`.
18 | /// `input_file` might not be read fully. `output_file` **is not** rewound.
19 | fn test_to_file(&self, input_file: &File, output_file: &File) -> (ExecutionMetrics, Result<(), ExecutionError>);
20 | }
21 |
22 | /// Creates a tempfile for stdout and executes the program.
23 | ///
24 | /// Returns execution metrics and output file (if there are no errors during execution).
25 | ///
26 | /// Stdin is read from `input_file`, stderr is ignored.
27 | /// `input_file` might not be read fully. Output file **is** rewound before returning.
28 | pub(crate) fn test_to_temp(executor: &impl TestExecutor, input_file: &File) -> (ExecutionMetrics, Result) {
29 | let mut stdout_memfile = create_temp_file().expect("Failed to create memfile");
30 | let (metrics, result) = executor.test_to_file(
31 | input_file,
32 | &stdout_memfile,
33 | );
34 | stdout_memfile.rewind().expect("Failed to rewind memfile");
35 | (metrics, result.map(|_| stdout_memfile))
36 | }
37 |
38 | pub(crate) enum AnyTestExecutor {
39 | Simple(SimpleExecutor),
40 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
41 | Sio2Jail(Sio2jailExecutor),
42 | }
43 |
44 | impl TestExecutor for AnyTestExecutor {
45 | fn test_to_file(&self, input_file: &File, output_file: &File) -> (ExecutionMetrics, Result<(), ExecutionError>) {
46 | match self {
47 | AnyTestExecutor::Simple(executor) => executor.test_to_file(input_file, output_file),
48 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
49 | AnyTestExecutor::Sio2Jail(executor) => executor.test_to_file(input_file, output_file),
50 | }
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/testing_utils.rs:
--------------------------------------------------------------------------------
1 | use std::cmp::max;
2 | use std::fs;
3 | use std::io::{Read, read_to_string};
4 | use std::path::Path;
5 | use comfy_table::{Attribute, Cell, Color, Table};
6 | use comfy_table::ContentArrangement::Dynamic;
7 | use terminal_size::{Height, Width};
8 | use crate::test_errors::TestError;
9 | use crate::test_errors::TestError::{Incorrect, NoOutputFile};
10 |
11 | pub(crate) fn compare_output(expected_output_path: &Path, actual_output: impl Read) -> Result<(), TestError> {
12 | if !expected_output_path.is_file() {
13 | return Err(NoOutputFile);
14 | }
15 | let expected_output = fs::read_to_string(expected_output_path).expect("Failed to read output file");
16 | let actual_output = read_to_string(actual_output).expect("Failed to read actual input");
17 |
18 | let expected_output = split_trim_end(&expected_output);
19 | let actual_output = split_trim_end(&actual_output);
20 |
21 | if actual_output != expected_output {
22 | return Err(Incorrect { error: generate_diff(&expected_output, &actual_output) });
23 | }
24 | Ok(())
25 | }
26 |
27 | fn split_trim_end(to_split: &str) -> Vec<&str> {
28 | let mut res = to_split
29 | .split('\n')
30 | .map(|line| line.trim_end())
31 | .collect::>();
32 |
33 | while res.last().is_some_and(|last| last.trim().is_empty()) {
34 | res.pop();
35 | }
36 |
37 | res
38 | }
39 |
40 | fn generate_diff(expected_split: &[&str], actual_split: &[&str]) -> String {
41 | let (Width(w), Height(_)) = terminal_size::terminal_size().unwrap_or((Width(40), Height(0)));
42 | let mut table = Table::new();
43 | table.set_content_arrangement(Dynamic).set_width(w).set_header(vec![
44 | Cell::new("Line").add_attribute(Attribute::Bold),
45 | Cell::new("Output file").add_attribute(Attribute::Bold).fg(Color::Green),
46 | Cell::new("Your program's output").add_attribute(Attribute::Bold).fg(Color::Red)
47 | ]);
48 |
49 | let mut row_count = 0;
50 | for i in 0..max(expected_split.len(), actual_split.len()) {
51 | let expected_line = expected_split.get(i).unwrap_or(&"");
52 | let actual_line = actual_split.get(i).unwrap_or(&"");
53 |
54 | if expected_line != actual_line {
55 | table.add_row(vec![
56 | Cell::new(i + 1),
57 | Cell::new(expected_line).fg(Color::Green),
58 | Cell::new(actual_line).fg(Color::Red)
59 | ]);
60 |
61 | row_count += 1;
62 | }
63 |
64 | if row_count >= 99 {
65 | table.add_row(vec![
66 | Cell::new("..."),
67 | Cell::new("..."),
68 | Cell::new("...")
69 | ]);
70 |
71 | break;
72 | }
73 | }
74 |
75 | table.to_string().replace('\r', "")
76 | }
77 |
--------------------------------------------------------------------------------
/src/executor/simple.rs:
--------------------------------------------------------------------------------
1 | use std::fs::File;
2 | use std::path::PathBuf;
3 | use std::process::{Child, Command, ExitStatus, Stdio};
4 | use std::time::{Duration, Instant};
5 | use crate::test_errors::{ExecutionError, ExecutionMetrics};
6 | use wait_timeout::ChildExt;
7 | use crate::executor::TestExecutor;
8 | use crate::test_errors::ExecutionError::{RuntimeError, TimedOut};
9 |
10 | #[cfg(unix)]
11 | use crate::generic_utils::halt;
12 | #[cfg(unix)]
13 | use std::os::unix::process::ExitStatusExt;
14 | use crate::temp_files::make_cloned_stdio;
15 |
16 | pub(crate) struct SimpleExecutor {
17 | pub(crate) timeout: Duration,
18 | pub(crate) executable_path: PathBuf,
19 | }
20 |
21 | impl SimpleExecutor {
22 | fn map_status_code(status: &ExitStatus) -> Result<(), ExecutionError> {
23 | match status.code() {
24 | Some(0) => Ok(()),
25 | Some(exit_code) => {
26 | Err(RuntimeError(format!("- the program returned a non-zero return code: {}", exit_code)))
27 | },
28 | None => {
29 | #[cfg(unix)]
30 | if status.signal().expect("The program returned an invalid status code") == 2 {
31 | halt();
32 | }
33 |
34 | Err(RuntimeError(format!("- the process was terminated with the following error:\n{}", status)))
35 | }
36 | }
37 | }
38 |
39 | fn wait_for_child(&self, mut child: Child) -> (ExecutionMetrics, Result<(), ExecutionError>) {
40 | let start_time = Instant::now();
41 | let status = child.wait_timeout(self.timeout).unwrap();
42 |
43 | match status {
44 | Some(status) => (
45 | ExecutionMetrics { time: Some(start_time.elapsed()), memory_kibibytes: None },
46 | SimpleExecutor::map_status_code(&status)
47 | ),
48 | None => {
49 | child.kill().unwrap();
50 | (ExecutionMetrics { time: Some(self.timeout), memory_kibibytes: None }, Err(TimedOut))
51 | }
52 | }
53 | }
54 | }
55 |
56 | impl TestExecutor for SimpleExecutor {
57 | fn test_to_file(&self, input_file: &File, output_file: &File) -> (ExecutionMetrics, Result<(), ExecutionError>) {
58 | let child = Command::new(&self.executable_path)
59 | .stdin(make_cloned_stdio(input_file))
60 | .stdout(make_cloned_stdio(output_file))
61 | .stderr(Stdio::null())
62 | .spawn().expect("Failed to spawn child");
63 |
64 | self.wait_for_child(child)
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/test_errors.rs:
--------------------------------------------------------------------------------
1 | use std::time::Duration;
2 | use colored::Colorize;
3 |
4 | pub struct ExecutionMetrics {
5 | pub(crate) memory_kibibytes: Option,
6 | pub(crate) time: Option,
7 | }
8 |
9 | impl ExecutionMetrics {
10 | // Currently only the sio2jail executor uses this constant,
11 | // which is not compiled on Windows builds
12 | #[allow(dead_code)]
13 | pub const NONE: ExecutionMetrics = ExecutionMetrics { memory_kibibytes: None, time: None };
14 | }
15 |
16 | pub enum TestError {
17 | Incorrect {
18 | error: String
19 | },
20 | ProgramError {
21 | error: ExecutionError
22 | },
23 | CheckerError {
24 | error: ExecutionError
25 | },
26 | NoOutputFile,
27 | Cancelled,
28 | }
29 |
30 | #[allow(unused)]
31 | #[derive(Debug)]
32 | pub enum ExecutionError {
33 | TimedOut,
34 | MemoryLimitExceeded,
35 | RuntimeError(String),
36 | Sio2jailError(String),
37 | PipeError,
38 | OutputNotUtf8,
39 | IncorrectCheckerFormat(String)
40 | }
41 |
42 | impl TestError {
43 | pub fn to_string(&self, test_name: &str) -> String {
44 | let mut result: String = String::new();
45 |
46 | match self {
47 | TestError::Incorrect { error } => {
48 | result.push_str(&format!("{}", format!("Test {}:\n", test_name).bold()));
49 | result.push_str(error);
50 | }
51 | TestError::ProgramError { error } => {
52 | result.push_str(&format!("{}", format!("Test {}:\n", test_name).bold()));
53 | result.push_str(&format!("{}", error.to_string().red()));
54 | }
55 | TestError::CheckerError { error } => {
56 | result.push_str(&format!("{}", format!("Test {} encountered a checker error:\n", test_name).bold()));
57 | result.push_str(&format!("{}", error.to_string().blue()));
58 | }
59 | TestError::NoOutputFile => {
60 | result.push_str(&format!("{}", format!("Test {}:\n", test_name).bold()));
61 | result.push_str(&format!("{}", "Output file does not exist".red()));
62 | }
63 | TestError::Cancelled => {
64 | result.push_str(&format!("{}", format!("Test {}:\n", test_name).bold()));
65 | result.push_str(&format!("{}", "Cancelled".yellow()));
66 | }
67 | }
68 |
69 | result
70 | }
71 | }
72 |
73 | impl ExecutionError {
74 | pub fn to_string(&self) -> String {
75 | match self {
76 | ExecutionError::TimedOut => "Timed out".to_string(),
77 | ExecutionError::MemoryLimitExceeded => "Memory limit exceeded".to_string(),
78 | ExecutionError::RuntimeError(error) => format!("Runtime error {}", error),
79 | ExecutionError::Sio2jailError(error) => format!("Sio2jail error: {}", error),
80 | ExecutionError::IncorrectCheckerFormat(error) => format!("The checker output didn't follow the Toster checker format - {}", error),
81 | ExecutionError::PipeError => "Failed to read program output".to_string(),
82 | ExecutionError::OutputNotUtf8 => "The output contained invalid characters".to_string(),
83 | }
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/checker.rs:
--------------------------------------------------------------------------------
1 | use std::fs::File;
2 | use std::io::{read_to_string, Seek, Write};
3 | use std::path::PathBuf;
4 | use std::io;
5 | use std::time::Duration;
6 | use colored::Colorize;
7 | use crate::executor::simple::SimpleExecutor;
8 | use crate::executor::test_to_temp;
9 | use crate::prepare_input::TestInputSource;
10 | use crate::temp_files::create_temp_file;
11 | use crate::test_errors::TestError;
12 | use crate::test_errors::ExecutionError::IncorrectCheckerFormat;
13 | use crate::test_errors::TestError::CheckerError;
14 |
15 | pub(crate) struct Checker {
16 | executor: SimpleExecutor
17 | }
18 |
19 | impl Checker {
20 | pub(crate) fn new(checker_executable: PathBuf, timeout: Duration) -> Self {
21 | Checker {
22 | executor: SimpleExecutor {
23 | executable_path: checker_executable,
24 | timeout,
25 | }
26 | }
27 | }
28 |
29 | fn parse_checker_output(output: &str) -> Result<(), TestError> {
30 | match output.chars().nth(0) {
31 | None => Err(CheckerError { error: IncorrectCheckerFormat("the checker returned an empty file".to_string()) }),
32 | Some('C') => Ok(()),
33 | Some('I') => {
34 | let checker_error = if output.len() > 1 { output.split_at(2).1.to_string() } else { String::new() };
35 | let error_message = format!("Incorrect output{}{}", if checker_error.trim().is_empty() { "" } else { ": " }, checker_error.trim()).red();
36 | Err(TestError::Incorrect {
37 | error: error_message.to_string(),
38 | })
39 | }
40 | Some(_) => Err(CheckerError { error: IncorrectCheckerFormat("the first character of the checker's output wasn't C or I".to_string()) })
41 | }
42 | }
43 |
44 | /// Creates a new temporary file for the checker input and writes the program input to it.
45 | /// The cursor is left at the end (not rewound).
46 | ///
47 | /// The program output should be appended to this file before calling check() on it,
48 | /// which can be done by passing the file as stdin to the tested program.
49 | pub(crate) fn prepare_checker_input(input_source: &TestInputSource) -> File {
50 | let mut input_memfile = create_temp_file().unwrap();
51 | io::copy(&mut input_source.get_file(), &mut input_memfile).unwrap();
52 | input_memfile.write_all("\n".as_bytes()).unwrap();
53 | input_memfile
54 | }
55 |
56 | /// Run checker on input file created using `prepare_checker_input()`.
57 | /// The program output should be appended to that file.
58 | /// `check()` will rewind `checker_input` before running checker.
59 | pub(crate) fn check(&self, mut checker_input: File) -> Result<(), TestError> {
60 | checker_input.rewind().unwrap();
61 |
62 | let (_, result) = test_to_temp(&self.executor, &checker_input);
63 | let output = match result {
64 | Ok(output) => output,
65 | Err(error) => {
66 | return Err(CheckerError { error });
67 | }
68 | };
69 | let output = read_to_string(output).expect("Failed to read checker output");
70 | Self::parse_checker_output(&output)
71 | }
72 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Toster
2 | [](https://github.com/MikolajKolek/toster/blob/master/LICENSE)
3 | [](https://crates.io/crates/toster)
4 | [](https://crates.io/crates/toster)
5 |
6 | A simple-as-toast tester for C++ solutions to competitive programming exercises
7 |
8 | # Usage
9 |
10 | ```
11 | Usage: toster [OPTIONS]
12 |
13 | Arguments:
14 | The name of the file containing the source code or the executable you want to test
15 |
16 | Options:
17 | -i, --in
18 | Input directory [default: in]
19 | --in-ext
20 | Input file extension [default: .in]
21 | -o, --out
22 | Output directory [default: out]
23 | --out-ext
24 | Output file extension [default: .out]
25 | --io
26 | The input and output directory (sets both -i and -o at once)
27 | -c, --checker
28 | The C++ source code or executable of a checker program that verifies if the tested program's output is correct instead of comparing it with given output files
29 | The checker must use the following protocol:
30 | - The checker receives the contents of the input file and the output of the tested program on stdin, separated by a single "\n" character
31 | - The checker outputs "C" if the output is correct, or "I " if the output is incorrect. The optional data can include any information useful for understanding why the output is wrong and will be shown when errors are displayed
32 | -t, --timeout
33 | The number of seconds after which a test or generation (or checker if you're using the --checker flag) times out if the program does not return. WARNING: if you're using the sio2jail flag, this timeout will still work based on time measured directly by toster, not time measured by sio2jail [default: 5]
34 | --compile-timeout
35 | The number of seconds after which compilation times out if it doesn't finish [default: 10]
36 | --compile-command
37 | The command used to compile the file. gets replaced with the path to the source code file, is the executable output location [default: "g++ -std=c++20 -O3 -static -o "]
38 | -s, --sio2jail
39 | Makes toster use sio2jail for measuring program runtime and memory use more accurately. By default limits memory use to 1 GiB. WARNING: enabling this flag can significantly slow down testing
40 | -m, --memory-limit
41 | Sets a memory limit (in KiB) for the executed program and enables the sio2jail flag. WARNING: enabling this flag can significantly slow down testing
42 | -g, --generate
43 | Makes toster generate output files in the output directory instead of comparing the program's output with the files in the output directory
44 | -h, --help
45 | Print help
46 | -V, --version
47 | Print version
48 | ```
49 |
50 | # Compiler
51 | If you're using the sio2jail feature and want to make sure that your toster measurements are exactly identical to those of sio2 on a contest, you need to make sure that you're using the same compiler version as the one used in sio. The compiler used in the [Polish Olympiad in Informatics](https://www.oi.edu.pl/) as of XXXI OI is G++ 12.2 (as detailed [here](https://www.oi.edu.pl/l/31oi_ustalenia_techniczne/)). If you want to install G++ 12.2, you can do so by building it from scratch (for example using [this](https://github.com/darrenjs/howto/blob/master/build_scripts/build_gcc_10.sh) script, only changing the version). You can also download prebuilt G++ versions made by me from here:
52 | - [G++ 10.2](https://mikolek.com/gcc-10.2)
53 | - [G++ 12.2](https://mikolek.com/gcc-12.2)
54 |
55 | # License
56 | Toster is licensed under the [MIT Licence](https://github.com/MikolajKolek/toster/blob/master/LICENSE)
57 |
58 | # Dependencies
59 | Toster uses [sio2jail](https://github.com/sio2project/sio2jail), a project available under the MIT licence
60 |
--------------------------------------------------------------------------------
/src/compiler.rs:
--------------------------------------------------------------------------------
1 | use std::{fs, io};
2 | use std::io::ErrorKind::NotFound;
3 | use std::io::{read_to_string, Seek};
4 | use std::path::{Path, PathBuf};
5 | use std::process::Command;
6 | use std::time::{Duration, Instant};
7 | use colored::Colorize;
8 | use is_executable::is_executable;
9 | use tempfile::TempDir;
10 | use wait_timeout::ChildExt;
11 | use crate::compiler::CompilerError::{CompilationError, InvalidExecutable};
12 | use crate::formatted_error::FormattedError;
13 | use crate::temp_files::{create_temp_file, make_cloned_stdio};
14 |
15 | pub(crate) enum CompilerError {
16 | InvalidExecutable(io::Error),
17 | CompilationError(String),
18 | }
19 |
20 | impl CompilerError {
21 | pub fn to_formatted(&self, is_checker: bool) -> FormattedError {
22 | FormattedError::preformatted(match self {
23 | InvalidExecutable(error) => {
24 | format!(
25 | "{}\n{}",
26 | format!(
27 | "The provided {} can't be executed",
28 | if is_checker { "checker" } else { "program" }
29 | ).red(),
30 | error
31 | )
32 | },
33 | CompilationError(error) => {
34 | format!(
35 | "{}\n{}",
36 | format!(
37 | "{} compilation failed with the following errors:",
38 | if is_checker { "Checker" } else { "Program" }
39 | ).red(),
40 | error
41 | )
42 | }
43 | })
44 | }
45 | }
46 |
47 | pub(crate) struct Compiler<'a> {
48 | pub(crate) tempdir: &'a TempDir,
49 | pub(crate) compile_timeout: Duration,
50 | pub(crate) compile_command: &'a str,
51 | }
52 |
53 | impl<'a> Compiler<'a> {
54 | fn is_source_file(path: &Path) -> bool {
55 | if let Some(extension) = path.extension().and_then(|extension| extension.to_str()) {
56 | return matches!(extension, "cpp" | "cc" | "cxx" | "c");
57 | }
58 | !is_executable(path)
59 | }
60 |
61 | fn compile_cpp(&self, source_path: &Path, executable_path: &Path) -> Result {
62 | let cmd = self.compile_command
63 | .replace("", source_path.to_str().expect("The provided filename is invalid"))
64 | .replace("", executable_path.to_str().expect("The provided filename is invalid"));
65 | let mut split_cmd = cmd.split(' ');
66 |
67 | let mut stderr = create_temp_file().expect("Failed to create memfile");
68 | let time_before_compilation = Instant::now();
69 | let child = Command::new(split_cmd.next().expect("The compile command is invalid"))
70 | .args(split_cmd)
71 | .stderr(make_cloned_stdio(&stderr))
72 | .spawn();
73 |
74 | let mut child = match child {
75 | Ok(child) => child,
76 | Err(error) if error.kind() == NotFound => { return Err("The compiler was not found".to_string()) }
77 | Err(error) => { return Err(error.to_string()) }
78 | };
79 | let result = child.wait_timeout(self.compile_timeout).unwrap();
80 |
81 | stderr.rewind().unwrap();
82 |
83 | match result {
84 | Some(status) => {
85 | if status.code().expect("The compiler returned an invalid status code") != 0 {
86 | let compilation_result = read_to_string(stderr).expect("Failed to read compiler output");
87 | return Err(compilation_result);
88 | }
89 | }
90 | None => {
91 | child.kill().unwrap();
92 | return Err("Compilation timed out".to_string());
93 | }
94 | }
95 | Ok(time_before_compilation.elapsed())
96 | }
97 |
98 | fn try_spawning_executable(executable_path: &PathBuf) -> io::Result<()> {
99 | Command::new(executable_path)
100 | .spawn()
101 | .map(|mut child| {
102 | child.kill().expect("Failed to kill executable");
103 | })
104 | }
105 |
106 | pub(crate) fn prepare_executable(
107 | &self,
108 | source_path: &Path,
109 | name: &'static str,
110 | ) -> Result<(PathBuf, Option), CompilerError> {
111 | debug_assert!(PathBuf::from(name).extension().is_none());
112 | let output_path = self.tempdir.path().join(format!("{}.o", name));
113 |
114 | if !Self::is_source_file(source_path) {
115 | fs::copy(source_path, &output_path).expect("The provided filename is invalid");
116 | if let Err(error) = Self::try_spawning_executable(&output_path) {
117 | return Err(InvalidExecutable(error));
118 | }
119 | return Ok((output_path, None));
120 | }
121 |
122 | match self.compile_cpp(source_path, &output_path) {
123 | Ok(compilation_time) => Ok((output_path, Some(compilation_time))),
124 | Err(error) => Err(CompilationError(error)),
125 | }
126 | }
127 | }
--------------------------------------------------------------------------------
/src/test_summary.rs:
--------------------------------------------------------------------------------
1 | use std::cmp::Ordering;
2 | use std::time::{Duration, Instant};
3 | use colored::Color::{Blue, Green, Red, Yellow};
4 | use colored::{Color, Colorize};
5 | use crate::test_errors::{ExecutionError, ExecutionMetrics, TestError};
6 | use crate::test_errors::TestError::*;
7 |
8 | pub(crate) struct TestSummary {
9 | pub(crate) generate_mode: bool,
10 | pub(crate) start_time: Instant,
11 |
12 | pub(crate) total: usize,
13 | pub(crate) processed: usize,
14 | pub(crate) success: usize,
15 | pub(crate) incorrect: usize,
16 | pub(crate) timed_out: usize,
17 | pub(crate) invalid_output: usize,
18 | pub(crate) memory_limit_exceeded: usize,
19 | pub(crate) runtime_error: usize,
20 | pub(crate) sio2jail_error: usize,
21 | pub(crate) checker_error: usize,
22 | pub(crate) no_output_file: usize,
23 |
24 | test_errors: Vec<(String, TestError)>,
25 |
26 | pub(crate) slowest_test: Option<(Duration, String)>,
27 | pub(crate) most_memory_used: Option<(u64, String)>,
28 | }
29 |
30 | struct CountPart<'a> {
31 | display_empty: bool,
32 | count: usize,
33 | singular: &'a str,
34 | plural: &'a str,
35 | color: Color,
36 | }
37 |
38 | impl<'a> CountPart<'a> {
39 | fn new(count: usize, text: &'a str) -> Self {
40 | CountPart {
41 | display_empty: false,
42 | count,
43 | singular: text,
44 | plural: text,
45 | color: Red
46 | }
47 | }
48 |
49 | fn with_plural(mut self, text: &'a str) -> Self {
50 | self.plural = text;
51 | self
52 | }
53 |
54 | fn display_empty(mut self) -> Self {
55 | self.display_empty = true;
56 | self
57 | }
58 |
59 | fn with_color(mut self, color: Color) -> Self {
60 | self.color = color;
61 | self
62 | }
63 |
64 | fn get_text(&self) -> &str {
65 | if self.count == 1 { self.singular }
66 | else { self.plural }
67 | }
68 | }
69 |
70 | impl TestSummary {
71 | pub(crate) fn new(generate_mode: bool, total_count: usize) -> Self {
72 | TestSummary {
73 | generate_mode,
74 | start_time: Instant::now(),
75 |
76 | total: total_count,
77 | processed: 0,
78 | incorrect: 0,
79 | timed_out: 0,
80 | invalid_output: 0,
81 | memory_limit_exceeded: 0,
82 | runtime_error: 0,
83 | sio2jail_error: 0,
84 | checker_error: 0,
85 | no_output_file: 0,
86 | success: 0,
87 |
88 | test_errors: vec![],
89 |
90 | slowest_test: None,
91 | most_memory_used: None,
92 | }
93 | }
94 |
95 | pub(crate) fn add_success(&mut self, metrics: &ExecutionMetrics, test_name: &str) {
96 | self.processed += 1;
97 | self.success += 1;
98 | self.add_metrics(metrics, test_name);
99 | }
100 |
101 | pub(crate) fn add_test_error(&mut self, error: TestError, test_name: String) {
102 | match &error {
103 | Incorrect { .. } => { self.incorrect += 1 }
104 | ProgramError { error: ExecutionError::TimedOut, .. } => { self.timed_out += 1 }
105 | ProgramError { error: ExecutionError::MemoryLimitExceeded, .. } => { self.memory_limit_exceeded += 1 }
106 | ProgramError { error: ExecutionError::RuntimeError(_), .. } => { self.runtime_error += 1 }
107 | ProgramError { error: ExecutionError::Sio2jailError(_), .. } => { self.sio2jail_error += 1 }
108 | ProgramError { error: ExecutionError::IncorrectCheckerFormat(_), .. } => { self.checker_error += 1 }
109 | ProgramError { error: ExecutionError::PipeError } => { self.invalid_output += 1 }
110 | ProgramError { error: ExecutionError::OutputNotUtf8 } => { self.invalid_output += 1 }
111 | CheckerError { .. } => { self.checker_error += 1 }
112 | NoOutputFile { .. } => { self.no_output_file += 1 }
113 | Cancelled => return,
114 | }
115 | self.processed += 1;
116 | self.test_errors.push((test_name, error));
117 | }
118 |
119 | fn add_metrics(&mut self, metrics: &ExecutionMetrics, test_name: &str) {
120 | if let Some(new_time) = &metrics.time {
121 | if self.slowest_test.as_ref().is_none_or(|(time, _)| new_time > time) {
122 | self.slowest_test = Some((*new_time, test_name.to_string()));
123 | }
124 | }
125 |
126 | if let Some(new_memory) = &metrics.memory_kibibytes {
127 | if self.most_memory_used.as_ref().is_none_or(|(memory, _)| new_memory > memory) {
128 | self.most_memory_used = Some((*new_memory, test_name.to_string()));
129 | }
130 | }
131 | }
132 |
133 | pub(crate) fn format_counts(&self, show_not_finished: bool) -> String {
134 | [
135 | CountPart::new(self.success, if self.generate_mode { "successful" } else { "correct" }).display_empty().with_color(Green),
136 | CountPart::new(self.incorrect, "wrong answer").with_plural("wrong answers"),
137 | CountPart::new(self.timed_out, "timed out"),
138 | CountPart::new(self.invalid_output, "invalid output").with_plural("invalid outputs"),
139 | CountPart::new(self.memory_limit_exceeded, "out of memory"),
140 | CountPart::new(self.runtime_error, "runtime error").with_plural("runtime errors"),
141 | CountPart::new(self.no_output_file, "without output file"),
142 | CountPart::new(self.sio2jail_error, "sio2jail error").with_plural("sio2jail errors"),
143 | CountPart::new(self.checker_error, "checker error").with_plural("checker errors").with_color(Blue),
144 | CountPart::new(if show_not_finished { self.total - self.processed } else { 0 }, "not finished").with_color(Yellow),
145 | ]
146 | .into_iter()
147 | .filter(|part| part.display_empty || part.count > 0)
148 | .map(|part| {
149 | format!("{} {}", part.count, part.get_text()).color(part.color).to_string()
150 | })
151 | .collect::>()
152 | .join(", ")
153 | }
154 |
155 | pub(crate) fn get_errors(&mut self) -> &Vec<(String, TestError)> {
156 | self.test_errors.sort_by(|a, b| -> Ordering {
157 | human_sort::compare(&a.0, &b.0)
158 | });
159 | &self.test_errors
160 | }
161 | }
--------------------------------------------------------------------------------
/src/args.rs:
--------------------------------------------------------------------------------
1 | use std::path::PathBuf;
2 | use std::time::Duration;
3 | use clap::Parser;
4 | use crate::args::ExecuteMode::{Simple};
5 |
6 | #[derive(Parser, Debug)]
7 | #[command(name = "Toster", version, about = "A simple-as-toast tester for C++ solutions to competitive programming exercises\nReport issues on the bugtracker at https://github.com/MikolajKolek/toster/issues", long_about = None)]
8 | pub struct Args {
9 | /// Input directory
10 | #[clap(short, long, value_parser, default_value = "in")]
11 | pub r#in: PathBuf,
12 |
13 | /// Input file extension
14 | #[clap(long, value_parser, default_value = ".in")]
15 | pub in_ext: String,
16 |
17 | /// Output directory
18 | #[clap(short, long, value_parser, default_value = "out")]
19 | pub out: PathBuf,
20 |
21 | /// Output file extension
22 | #[clap(long, value_parser, default_value = ".out")]
23 | pub out_ext: String,
24 |
25 | /// The input and output directory (sets both -i and -o at once)
26 | #[clap(long, value_parser)]
27 | pub io: Option,
28 |
29 | /// The C++ source code or executable of a checker program that verifies if the tested program's output is correct instead of comparing it with given output files
30 | /// The checker must use the following protocol:
31 | /// - The checker receives the contents of the input file and the output of the tested program on stdin, separated by a single "\n" character
32 | /// - The checker outputs "C" if the output is correct, or "I " if the output is incorrect. The optional data can include any information useful for understanding why the output is wrong and will be shown when errors are displayed
33 | #[clap(short, long, value_parser, verbatim_doc_comment)]
34 | pub checker: Option,
35 |
36 | /// The number of seconds after which a test or generation times out if the program does not return
37 | #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
38 | #[clap(short, long, value_parser, default_value = "5")]
39 | pub timeout: u64,
40 |
41 | /// The number of seconds after which a test or generation (or checker if you're using the --checker flag) times out if the program does not return. WARNING: if you're using the sio2jail flag, this timeout will still work based on time measured directly by toster, not time measured by sio2jail
42 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
43 | #[clap(short, long, value_parser, default_value = "5")]
44 | pub timeout: u64,
45 |
46 | /// The number of seconds after which compilation times out if it doesn't finish
47 | #[clap(long, value_parser, default_value = "10")]
48 | pub compile_timeout: u64,
49 |
50 | /// The command used to compile the file. gets replaced with the path to the source code file, is the executable output location.
51 | #[clap(long, value_parser, default_value = "g++ -std=c++20 -O3 -static -o ")]
52 | pub compile_command: String,
53 |
54 | /// Makes toster use sio2jail for measuring program runtime and memory use more accurately. By default limits memory use to 1 GiB. WARNING: enabling this flag can significantly slow down testing
55 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
56 | #[clap(short, long, action)]
57 | pub sio2jail: bool,
58 |
59 | /// Sets a memory limit (in KiB) for the executed program and enables the sio2jail flag. WARNING: enabling this flag can significantly slow down testing
60 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
61 | #[clap(short, long, value_parser)]
62 | pub memory_limit: Option,
63 |
64 | /// Makes toster generate output files in the output directory instead of comparing the program's output with the files in the output directory
65 | #[clap(short, long, action)]
66 | pub generate: bool,
67 |
68 | /// The name of the file containing the source code or the executable you want to test
69 | #[clap(value_parser)]
70 | pub filename: PathBuf
71 | }
72 |
73 | pub(crate) enum InputConfig {
74 | Directory {
75 | directory: PathBuf,
76 | ext: String,
77 | }
78 | }
79 |
80 | pub(crate) enum ExecuteMode {
81 | Simple,
82 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
83 | Sio2jail {
84 | memory_limit: u64,
85 | }
86 | }
87 |
88 | pub(crate) enum ActionType {
89 | Generate {
90 | output_directory: PathBuf,
91 | output_ext: String,
92 | },
93 | SimpleCompare {
94 | output_directory: PathBuf,
95 | output_ext: String,
96 | },
97 | Checker {
98 | path: PathBuf,
99 | }
100 | }
101 |
102 | pub(crate) struct ParsedConfig {
103 | pub(crate) source_path: PathBuf,
104 | pub(crate) compile_command: String,
105 | pub(crate) compile_timeout: Duration,
106 | pub(crate) execute_timeout: Duration,
107 | pub(crate) input: InputConfig,
108 | pub(crate) execute_mode: ExecuteMode,
109 | pub(crate) action_type: ActionType,
110 | }
111 |
112 | fn verify_compile_command(command: &str) -> Result<(), String> {
113 | let message = format!(
114 | "The compile command is invalid:\n{}\nRead \"toster -h\" for more info",
115 | match (command.contains(""), command.contains("")) {
116 | (true, true) => return Ok(()),
117 | (false, true) => "The argument is missing\n",
118 | (true, false) => "The argument is missing\n",
119 | (false, false) => "The and arguments are missing\n",
120 | }
121 | );
122 | Err(message)
123 | }
124 |
125 | impl TryFrom for ParsedConfig {
126 | type Error = String;
127 |
128 | fn try_from(args: Args) -> Result {
129 | if !args.filename.is_file() {
130 | return Err("The provided file does not exist".to_string());
131 | }
132 |
133 | let (input_directory, output_directory) = match args.io {
134 | Some(io) => {
135 | if !io.is_dir() {
136 | return Err("The input/output directory does not exist".to_string());
137 | }
138 | (io.clone(), io)
139 | },
140 | None => {
141 | if !args.r#in.is_dir() {
142 | return Err("The input directory does not exist".to_string());
143 | }
144 | (args.r#in, args.out)
145 | }
146 | };
147 |
148 | verify_compile_command(&args.compile_command)?;
149 |
150 | Ok(ParsedConfig {
151 | source_path: args.filename,
152 | compile_timeout: Duration::from_secs(args.compile_timeout),
153 | execute_timeout: Duration::from_secs(args.timeout),
154 | compile_command: args.compile_command,
155 | input: InputConfig::Directory {
156 | directory: input_directory,
157 | ext: args.in_ext,
158 | },
159 |
160 | action_type: match (args.generate, args.checker) {
161 | (true, Some(_)) => {
162 | return Err("You can't have the --generate and --checker flags on at the same time".to_string())
163 | },
164 | (true, None) => {
165 | if output_directory.exists() && !output_directory.is_dir() {
166 | return Err("The output path is not a directory".to_string())
167 | }
168 | ActionType::Generate {
169 | output_directory,
170 | output_ext: args.out_ext,
171 | }
172 | },
173 | (false, None) => {
174 | if !output_directory.is_dir() {
175 | return Err("The output directory does not exist".to_string())
176 | }
177 | ActionType::SimpleCompare {
178 | output_directory,
179 | output_ext: args.out_ext,
180 | }
181 | },
182 | (false, Some(checker_path)) => {
183 | if !checker_path.is_file() {
184 | return Err("The provided checker file does not exist".to_string());
185 | }
186 | ActionType::Checker {
187 | path: checker_path,
188 | }
189 | }
190 | },
191 |
192 | execute_mode: {
193 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))] {
194 | if let Some(memory_limit) = args.memory_limit {
195 | ExecuteMode::Sio2jail { memory_limit }
196 | } else if args.sio2jail {
197 | ExecuteMode::Sio2jail { memory_limit: 1024 * 1204 }
198 | } else {
199 | Simple
200 | }
201 | }
202 | #[cfg(not(all(target_os = "linux", target_arch = "x86_64")))]
203 | Simple
204 | }
205 | })
206 | }
207 | }
208 |
209 | impl ParsedConfig {
210 | pub(crate) fn generate_mode(&self) -> bool {
211 | matches!(self.action_type, ActionType::Generate { .. })
212 | }
213 | }
--------------------------------------------------------------------------------
/src/executor/sio2jail.rs:
--------------------------------------------------------------------------------
1 | use std::fs::File;
2 | use std::io::{read_to_string, Seek};
3 | use std::os::unix::process::ExitStatusExt;
4 | use std::path::{Path, PathBuf};
5 | use std::process::{Command, ExitStatus};
6 | use std::time::Duration;
7 | use colored::Colorize;
8 | use command_fds::{CommandFdExt, FdMapping};
9 | use directories::BaseDirs;
10 | use wait_timeout::ChildExt;
11 | use which::which;
12 | use crate::temp_files::{create_temp_file, make_cloned_stdio};
13 | use crate::executor::TestExecutor;
14 | use crate::formatted_error::FormattedError;
15 | use crate::generic_utils::halt;
16 | use crate::test_errors::{ExecutionError, ExecutionMetrics};
17 | use crate::test_errors::ExecutionError::{MemoryLimitExceeded, RuntimeError, Sio2jailError, TimedOut};
18 |
19 | pub(crate) struct Sio2jailExecutor {
20 | timeout: Duration,
21 | executable_path: PathBuf,
22 | sio2jail_path: PathBuf,
23 | memory_limit: u64,
24 | }
25 |
26 | struct Sio2jailOutput {
27 | status: ExitStatus,
28 | stderr: String,
29 | sio2jail_output: String,
30 | }
31 |
32 | impl Sio2jailExecutor {
33 | fn get_sio2jail_path() -> Result {
34 | let Some(binding) = BaseDirs::new() else {
35 | return Err(FormattedError::from_str(
36 | "No valid home directory path could be retrieved from the operating system. Sio2jail could not be found"
37 | ));
38 | };
39 | let Some(executable_dir) = binding.executable_dir() else {
40 | return Err(FormattedError::from_str(
41 | "Couldn't locate the user's executable directory. Sio2jail could not be found"
42 | ));
43 | };
44 |
45 | let result = executable_dir.join("sio2jail");
46 | if !result.exists() {
47 | return Err(FormattedError::from_str(
48 | &format!("Sio2jail could not be found at {}", result.display())
49 | ));
50 | }
51 | Ok(result)
52 | }
53 |
54 | fn run_sio2jail(&self, input_file: &File, output_file: &File, executable_path: &Path) -> Result {
55 | let mut sio2jail_output = create_temp_file().unwrap();
56 | let mut stderr = create_temp_file().unwrap();
57 |
58 | let mut child = Command::new(&self.sio2jail_path)
59 | .args(["-f", "3", "-o", "oiaug", "--mount-namespace", "off", "--pid-namespace", "off", "--uts-namespace", "off", "--ipc-namespace", "off", "--net-namespace", "off", "--capability-drop", "off", "--user-namespace", "off", "-m", &self.memory_limit.to_string(), "--", executable_path.to_str().unwrap() ])
60 | .fd_mappings(vec![FdMapping {
61 | parent_fd: sio2jail_output.try_clone().unwrap().into(),
62 | child_fd: 3
63 | }]).expect("Failed to redirect file descriptor 3")
64 | .stdout(make_cloned_stdio(output_file))
65 | .stderr(make_cloned_stdio(&stderr))
66 | .stdin(make_cloned_stdio(input_file))
67 | .spawn().expect("Failed to spawn sio2jail");
68 |
69 | let status = child.wait_timeout(self.timeout).unwrap();
70 | let Some(status) = status else {
71 | child.kill().unwrap();
72 | return Err(TimedOut);
73 | };
74 |
75 | sio2jail_output.rewind().unwrap();
76 | stderr.rewind().unwrap();
77 |
78 | Ok(Sio2jailOutput {
79 | status,
80 | stderr: read_to_string(stderr).unwrap(),
81 | sio2jail_output: read_to_string(sio2jail_output).unwrap(),
82 | })
83 | }
84 |
85 | fn test(&self) -> Result<(), FormattedError> {
86 | let Ok(true_command_location) = which("true") else {
87 | return Err(FormattedError::from_str("The executable for the \"true\" command could not be found"));
88 | };
89 |
90 | let null_file = File::open("/dev/null").expect("Opening /dev/null should not fail");
91 | let output = self.run_sio2jail(&null_file, &null_file, &true_command_location);
92 | let output = match output {
93 | Ok(output) => output,
94 | Err(error) => {
95 | return Err(FormattedError::from_str(&format!("Sio2jail error: {}", error.to_string())));
96 | }
97 | };
98 | if output.stderr == "Exception occurred: System error occured: perf event open failed: Permission denied: error 13: Permission denied\n" {
99 | return Err(FormattedError::preformatted(format!(
100 | "{}\n{}",
101 | "You need to run the following command to use toster with sio2jail.\n\
102 | You may also put this option in your /etc/sysctl.conf.\n\
103 | This will make the setting persist across reboots.".red(),
104 | "sudo sysctl -w kernel.perf_event_paranoid=-1".white()
105 | )));
106 | }
107 | if !output.stderr.is_empty() {
108 | return Err(FormattedError::from_str(&format!("Sio2jail error: {}", output.stderr)));
109 | }
110 | Ok(())
111 | }
112 |
113 | pub(crate) fn init_and_test(timeout: Duration, executable_path: PathBuf, memory_limit: u64) -> Result {
114 | let executor = Sio2jailExecutor {
115 | timeout,
116 | memory_limit,
117 | executable_path,
118 | sio2jail_path: Self::get_sio2jail_path()?,
119 | };
120 | executor.test()?;
121 | Ok(executor)
122 | }
123 | }
124 |
125 | impl TestExecutor for Sio2jailExecutor {
126 | fn test_to_file(&self, input_file: &File, output_file: &File) -> (ExecutionMetrics, Result<(), ExecutionError>) {
127 | let output = match self.run_sio2jail(input_file, output_file, &self.executable_path) {
128 | Err(TimedOut) => {
129 | return (ExecutionMetrics { time: Some(self.timeout), memory_kibibytes: None }, Err(TimedOut));
130 | }
131 | Err(error) => {
132 | return (ExecutionMetrics::NONE, Err(error));
133 | }
134 | Ok(output) => output
135 | };
136 |
137 | if !output.stderr.is_empty() {
138 | return if output.stderr == "terminate called after throwing an instance of 'std::bad_alloc'\n what(): std::bad_alloc\n" {
139 | (ExecutionMetrics { time: None, memory_kibibytes: Some(self.memory_limit) }, Err(MemoryLimitExceeded))
140 | } else {
141 | (ExecutionMetrics::NONE, Err(Sio2jailError(output.stderr)))
142 | }
143 | }
144 |
145 | let split: Vec<&str> = output.sio2jail_output.split_whitespace().collect();
146 | if split.len() < 6 {
147 | return (ExecutionMetrics::NONE, Err(Sio2jailError(format!("The sio2jail output is too short: {}", output.sio2jail_output))));
148 | }
149 | let sio2jail_status = split[0];
150 | let time = Duration::from_secs_f64(split[2].parse::().expect("Sio2jail returned an invalid runtime in the output") / 1000.0);
151 | let memory_kibibytes = split[4].parse::().expect("Sio2jail returned invalid memory usage in the output");
152 | let error_message = output.sio2jail_output.lines().nth(1);
153 |
154 | let metrics = ExecutionMetrics {
155 | time: Some(time),
156 | memory_kibibytes: Some(memory_kibibytes)
157 | };
158 |
159 | match output.status.code() {
160 | None => {
161 | #[cfg(unix)]
162 | if cfg!(unix) && output.status.signal().expect("Sio2jail returned an invalid status code") == 2 {
163 | halt();
164 | }
165 |
166 | return (metrics, Err(RuntimeError(format!("- the process was terminated with the following error:\n{}", output.status))))
167 | }
168 | Some(0) => {}
169 | Some(exit_code) => {
170 | return (metrics, Err(Sio2jailError(format!("Sio2jail returned an invalid status code: {}", exit_code))) );
171 | }
172 | }
173 |
174 | (ExecutionMetrics { time: Some(time), memory_kibibytes: Some(memory_kibibytes) }, match sio2jail_status {
175 | "OK" => Ok(()),
176 | "RE" | "RV" => Err(RuntimeError(error_message.map(|message| format!("- {}", message)).unwrap_or(String::new()))),
177 | "TLE" => Err(TimedOut),
178 | "MLE" => Err(MemoryLimitExceeded),
179 | "OLE" => Err(RuntimeError("- output limit exceeded".to_string())),
180 | _ => Err(Sio2jailError(format!("Sio2jail returned an invalid status in the output: {}", sio2jail_status)))
181 | })
182 | }
183 | }
184 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | mod args;
2 | mod test_errors;
3 | mod testing_utils;
4 | mod prepare_input;
5 | mod executor;
6 | mod generic_utils;
7 | mod test_summary;
8 | mod temp_files;
9 | mod checker;
10 | mod compiler;
11 | mod formatted_error;
12 |
13 | use std::{fs, panic};
14 | use std::fmt::Write as FmtWrite;
15 | use std::fs::File;
16 | use std::panic::PanicHookInfo;
17 | use std::path::PathBuf;
18 | use std::process::{exit, ExitCode};
19 | use std::sync::{Arc, Mutex};
20 | use std::sync::atomic::AtomicBool;
21 | use std::sync::atomic::Ordering::{Acquire, Release};
22 | use clap::Parser;
23 | use colored::Colorize;
24 | use human_panic::{handle_dump, print_msg};
25 | use indicatif::{ParallelProgressIterator, ProgressBar, ProgressState, ProgressStyle};
26 | use rayon::prelude::*;
27 | use tempfile::tempdir;
28 | use args::Args;
29 | use crate::args::{ActionType, InputConfig, ParsedConfig};
30 | use crate::args::ExecuteMode::*;
31 | use crate::checker::Checker;
32 | use crate::compiler::Compiler;
33 | use crate::executor::simple::SimpleExecutor;
34 | use crate::prepare_input::{prepare_file_inputs, Test, TestingInputs};
35 | use crate::executor::{AnyTestExecutor, test_to_temp, TestExecutor};
36 | use crate::test_errors::{ExecutionMetrics, TestError};
37 | use crate::test_errors::TestError::{Cancelled, ProgramError};
38 | use crate::test_summary::TestSummary;
39 | use crate::testing_utils::compare_output;
40 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
41 | use crate::executor::sio2jail::Sio2jailExecutor;
42 | use crate::formatted_error::FormattedError;
43 | use crate::generic_utils::halt;
44 |
45 | static RECEIVED_CTRL_C: AtomicBool = AtomicBool::new(false);
46 |
47 | fn print_output(stopped_early: bool, test_summary: &mut Option) {
48 | let Some(test_summary) = test_summary else {
49 | println!("{}", "Toster was stopped before testing could start".red());
50 | exit(0);
51 | };
52 |
53 | if stopped_early {
54 | println!();
55 | }
56 |
57 | let additional_info = match (&test_summary.slowest_test, &test_summary.most_memory_used) {
58 | (None, None) => "".to_string(),
59 | (Some((duration, slowest_test_name)), None) => format!(
60 | " (Slowest test: {} at {:.3}s)",
61 | slowest_test_name, duration.as_secs_f32(),
62 | ),
63 | (None, Some((memory, most_memory_test_name))) => format!(
64 | " (Most memory used: {} at {:.3}KiB)",
65 | most_memory_test_name, memory,
66 | ),
67 | (Some((duration, slowest_test_name)), Some((memory, most_memory_test_name))) => format!(
68 | " (Slowest test: {} at {:.3}s, most memory used: {} at {}KiB)",
69 | slowest_test_name, duration.as_secs_f32(),
70 | most_memory_test_name, memory,
71 | ),
72 | };
73 |
74 | println!(
75 | "{} {} {:.2}s{}\nResults: {}",
76 | if test_summary.generate_mode { "Generating" } else { "Testing" },
77 | if stopped_early {"stopped after"} else {"finished in"},
78 | test_summary.start_time.elapsed().as_secs_f64(),
79 | additional_info,
80 | test_summary.format_counts(true),
81 | );
82 |
83 | let incorrect_results = test_summary.get_errors();
84 | if !incorrect_results.is_empty() {
85 | println!("Errors were found in the following tests:");
86 |
87 | for (test_name, error) in incorrect_results.iter() {
88 | println!("{}", error.to_string(test_name));
89 | }
90 | }
91 |
92 | exit(0);
93 | }
94 |
95 | fn setup_panic() {
96 | let is_panicking = AtomicBool::new(false);
97 | match human_panic::PanicStyle::default() {
98 | human_panic::PanicStyle::Debug => {}
99 | human_panic::PanicStyle::Human => {
100 | let meta = human_panic::metadata!();
101 |
102 | panic::set_hook(Box::new(move |info: &PanicHookInfo| {
103 | if is_panicking.load(Acquire) {
104 | halt();
105 | }
106 | is_panicking.store(true, Release);
107 |
108 | let file_path = handle_dump(&meta, info);
109 | print_msg(file_path, &meta).expect("human-panic: printing error message to console failed");
110 | exit(0);
111 | }));
112 | },
113 | _ => {}
114 | }
115 | }
116 |
117 | fn check_ctrlc() -> Result<(), TestError> {
118 | if RECEIVED_CTRL_C.load(Acquire) { Err(Cancelled) }
119 | else { Ok(()) }
120 | }
121 |
122 | fn init_runner(executable: PathBuf, config: &ParsedConfig) -> Result {
123 | Ok(match config.execute_mode {
124 | Simple => AnyTestExecutor::Simple(SimpleExecutor {
125 | executable_path: executable,
126 | timeout: config.execute_timeout,
127 | }),
128 | #[cfg(all(target_os = "linux", target_arch = "x86_64"))]
129 | Sio2jail { memory_limit } => AnyTestExecutor::Sio2Jail(Sio2jailExecutor::init_and_test(
130 | config.execute_timeout,
131 | executable,
132 | memory_limit,
133 | )?),
134 | })
135 | }
136 |
137 | fn map_tests(
138 | inputs: TestingInputs,
139 | progress_bar: ProgressBar,
140 | test_summary: &Arc>>,
141 | callback: impl Fn(Test) -> Result + Sync
142 | ) where T: IndexedParallelIterator- {
143 | inputs.iterator.progress_with(progress_bar).try_for_each(|input| {
144 | let test_name = input.test_name.clone();
145 |
146 | let result = callback(input);
147 |
148 | let mut test_summary = test_summary.lock().expect("Failed to lock test summary mutex");
149 | let test_summary = test_summary.as_mut().unwrap();
150 | match result {
151 | Ok(metrics) => test_summary.add_success(&metrics, &test_name),
152 | Err(Cancelled) => return None,
153 | Err(error) => test_summary.add_test_error(error, test_name),
154 | };
155 | Some(())
156 | });
157 | }
158 |
159 | fn main() -> ExitCode {
160 | setup_panic();
161 |
162 | if let Err(error) = try_main() {
163 | println!("{}", error);
164 | return ExitCode::FAILURE;
165 | }
166 | ExitCode::SUCCESS
167 | }
168 |
169 | fn try_main() -> Result<(), FormattedError> {
170 | let config = ParsedConfig::try_from(Args::parse())
171 | .map_err(|error| FormattedError::from_str(&error))?;
172 | let test_summary: Arc>> = Arc::new(Mutex::new(None));
173 | {
174 | let test_summary = test_summary.clone();
175 | ctrlc::set_handler(move || {
176 | RECEIVED_CTRL_C.store(true, Release);
177 | print_output(true, &mut test_summary.lock().expect("Failed to lock test summary mutex"));
178 | }).expect("Error setting Ctrl-C handler");
179 | }
180 |
181 | let tempdir = tempdir().expect("Failed to create temporary directory");
182 |
183 | if let ActionType::Generate { output_directory, .. } = &config.action_type {
184 | if !output_directory.is_dir() {
185 | fs::create_dir_all(output_directory).expect("Failed to create output directory");
186 | }
187 | }
188 |
189 | let compiler = Compiler {
190 | tempdir: &tempdir,
191 | compile_timeout: config.compile_timeout,
192 | compile_command: &config.compile_command,
193 | };
194 |
195 | let executable = {
196 | let (executable, compilation_time) = compiler
197 | .prepare_executable(&config.source_path, "program")
198 | .map_err(|error| error.to_formatted(false))?;
199 | if let Some(compilation_time) = compilation_time {
200 | println!("{}", format!("Program compilation completed in {:.2}", compilation_time.as_secs_f32()).green());
201 | }
202 | executable
203 | };
204 |
205 | let checker_executable = if let ActionType::Checker { path } = &config.action_type {
206 | let (executable, compilation_time) = compiler
207 | .prepare_executable(path, "checker")
208 | .map_err(|error| error.to_formatted(true))?;
209 | if let Some(compilation_time) = compilation_time {
210 | println!("{}", format!("Checker compilation completed in {:.2}", compilation_time.as_secs_f32()).green());
211 | }
212 | Some(executable)
213 | } else { None };
214 |
215 | let runner = init_runner(executable, &config)?;
216 | let checker = checker_executable.map(|checker_executable| {
217 | Checker::new(checker_executable, config.execute_timeout)
218 | });
219 |
220 | // Progress bar styling
221 | let style: ProgressStyle = {
222 | let test_summary = test_summary.clone();
223 | ProgressStyle::with_template("[{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({eta})\n{counts} {ctrlc}")
224 | .expect("Progress bar creation failed")
225 | .with_key("eta", |state: &ProgressState, w: &mut dyn FmtWrite| write!(w, "{:.1}s", state.eta().as_secs_f64()).expect("Displaying the progress bar failed"))
226 | .progress_chars("#>-")
227 | .with_key("counts", move |_state: &ProgressState, w: &mut dyn FmtWrite| {
228 | write!(w, "{}", test_summary.lock().expect("Failed to lock test summary mutex").as_ref().unwrap().format_counts(false)).expect("Displaying the progress bar failed")
229 | })
230 | .with_key("ctrlc", |_state: &ProgressState, w: &mut dyn FmtWrite|
231 | write!(w, "{}", "(Press Ctrl+C to stop testing and print current results)".bright_black()).expect("Displaying the progress bar Ctrl+C message failed")
232 | )
233 | };
234 |
235 | let inputs = match &config.input {
236 | InputConfig::Directory { directory, ext } => {
237 | prepare_file_inputs(directory, ext)?
238 | },
239 | };
240 | *test_summary.lock().expect("Failed to lock test summary mutex") = Some(TestSummary::new(config.generate_mode(), inputs.test_count));
241 |
242 | let progress_bar = ProgressBar::new(inputs.test_count as u64).with_style(style);
243 |
244 | match config.action_type {
245 | ActionType::Generate { output_directory, output_ext } => {
246 | map_tests(inputs, progress_bar, &test_summary, |input| {
247 | check_ctrlc()?;
248 |
249 | let output_file_path = output_directory.join(format!("{}{}", input.test_name, &output_ext));
250 | let file = File::create(output_file_path).expect("Failed to create output file");
251 | check_ctrlc()?;
252 |
253 | let (metrics, result) = runner.test_to_file(&input.input_source.get_file(), &file);
254 | check_ctrlc()?;
255 |
256 | result.map_err(|error| ProgramError { error })?;
257 | Ok(metrics)
258 | });
259 | },
260 | ActionType::SimpleCompare { output_directory, output_ext } => {
261 | map_tests(inputs, progress_bar, &test_summary, |input| {
262 | check_ctrlc()?;
263 |
264 | let (metrics, result) = test_to_temp(&runner, &input.input_source.get_file());
265 | check_ctrlc()?;
266 |
267 | let result = result.map_err(|error| ProgramError { error })?;
268 | let output_file_path = output_directory.join(format!("{}{}", input.test_name, output_ext));
269 | compare_output(&output_file_path, result)?;
270 | check_ctrlc()?;
271 |
272 | Ok(metrics)
273 | });
274 | },
275 | ActionType::Checker { .. } => {
276 | let checker = checker.expect("Checker should be initialized");
277 | map_tests(inputs, progress_bar, &test_summary, |input| {
278 | check_ctrlc()?;
279 |
280 | let checker_input = Checker::prepare_checker_input(&input.input_source);
281 | check_ctrlc()?;
282 |
283 | let (metrics, result) = runner.test_to_file(
284 | &input.input_source.get_file(),
285 | &checker_input,
286 | );
287 | check_ctrlc()?;
288 |
289 | result.map_err(|error| ProgramError { error })?;
290 | checker.check(checker_input)?;
291 | check_ctrlc()?;
292 |
293 | Ok(metrics)
294 | })
295 | }
296 | }
297 |
298 | print_output(false, &mut test_summary.lock().expect("Failed to lock test summary mutex"));
299 | Ok(())
300 | }
301 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "addr2line"
7 | version = "0.21.0"
8 | source = "registry+https://github.com/rust-lang/crates.io-index"
9 | checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
10 | dependencies = [
11 | "gimli",
12 | ]
13 |
14 | [[package]]
15 | name = "adler"
16 | version = "1.0.2"
17 | source = "registry+https://github.com/rust-lang/crates.io-index"
18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
19 |
20 | [[package]]
21 | name = "anstream"
22 | version = "0.6.14"
23 | source = "registry+https://github.com/rust-lang/crates.io-index"
24 | checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
25 | dependencies = [
26 | "anstyle",
27 | "anstyle-parse",
28 | "anstyle-query",
29 | "anstyle-wincon",
30 | "colorchoice",
31 | "is_terminal_polyfill",
32 | "utf8parse",
33 | ]
34 |
35 | [[package]]
36 | name = "anstyle"
37 | version = "1.0.3"
38 | source = "registry+https://github.com/rust-lang/crates.io-index"
39 | checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46"
40 |
41 | [[package]]
42 | name = "anstyle-parse"
43 | version = "0.2.1"
44 | source = "registry+https://github.com/rust-lang/crates.io-index"
45 | checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
46 | dependencies = [
47 | "utf8parse",
48 | ]
49 |
50 | [[package]]
51 | name = "anstyle-query"
52 | version = "1.0.0"
53 | source = "registry+https://github.com/rust-lang/crates.io-index"
54 | checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
55 | dependencies = [
56 | "windows-sys 0.48.0",
57 | ]
58 |
59 | [[package]]
60 | name = "anstyle-wincon"
61 | version = "3.0.1"
62 | source = "registry+https://github.com/rust-lang/crates.io-index"
63 | checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628"
64 | dependencies = [
65 | "anstyle",
66 | "windows-sys 0.48.0",
67 | ]
68 |
69 | [[package]]
70 | name = "autocfg"
71 | version = "1.1.0"
72 | source = "registry+https://github.com/rust-lang/crates.io-index"
73 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
74 |
75 | [[package]]
76 | name = "backtrace"
77 | version = "0.3.69"
78 | source = "registry+https://github.com/rust-lang/crates.io-index"
79 | checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837"
80 | dependencies = [
81 | "addr2line",
82 | "cc",
83 | "cfg-if",
84 | "libc",
85 | "miniz_oxide",
86 | "object",
87 | "rustc-demangle",
88 | ]
89 |
90 | [[package]]
91 | name = "bitflags"
92 | version = "1.3.2"
93 | source = "registry+https://github.com/rust-lang/crates.io-index"
94 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
95 |
96 | [[package]]
97 | name = "bitflags"
98 | version = "2.4.0"
99 | source = "registry+https://github.com/rust-lang/crates.io-index"
100 | checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635"
101 |
102 | [[package]]
103 | name = "cc"
104 | version = "1.0.83"
105 | source = "registry+https://github.com/rust-lang/crates.io-index"
106 | checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0"
107 | dependencies = [
108 | "libc",
109 | ]
110 |
111 | [[package]]
112 | name = "cfg-if"
113 | version = "1.0.0"
114 | source = "registry+https://github.com/rust-lang/crates.io-index"
115 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
116 |
117 | [[package]]
118 | name = "cfg_aliases"
119 | version = "0.1.1"
120 | source = "registry+https://github.com/rust-lang/crates.io-index"
121 | checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e"
122 |
123 | [[package]]
124 | name = "clap"
125 | version = "4.5.4"
126 | source = "registry+https://github.com/rust-lang/crates.io-index"
127 | checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
128 | dependencies = [
129 | "clap_builder",
130 | "clap_derive",
131 | ]
132 |
133 | [[package]]
134 | name = "clap_builder"
135 | version = "4.5.2"
136 | source = "registry+https://github.com/rust-lang/crates.io-index"
137 | checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
138 | dependencies = [
139 | "anstream",
140 | "anstyle",
141 | "clap_lex",
142 | "strsim",
143 | ]
144 |
145 | [[package]]
146 | name = "clap_derive"
147 | version = "4.5.4"
148 | source = "registry+https://github.com/rust-lang/crates.io-index"
149 | checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
150 | dependencies = [
151 | "heck 0.5.0",
152 | "proc-macro2",
153 | "quote",
154 | "syn",
155 | ]
156 |
157 | [[package]]
158 | name = "clap_lex"
159 | version = "0.7.0"
160 | source = "registry+https://github.com/rust-lang/crates.io-index"
161 | checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
162 |
163 | [[package]]
164 | name = "colorchoice"
165 | version = "1.0.0"
166 | source = "registry+https://github.com/rust-lang/crates.io-index"
167 | checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
168 |
169 | [[package]]
170 | name = "colored"
171 | version = "2.1.0"
172 | source = "registry+https://github.com/rust-lang/crates.io-index"
173 | checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8"
174 | dependencies = [
175 | "lazy_static",
176 | "windows-sys 0.48.0",
177 | ]
178 |
179 | [[package]]
180 | name = "comfy-table"
181 | version = "7.1.1"
182 | source = "registry+https://github.com/rust-lang/crates.io-index"
183 | checksum = "b34115915337defe99b2aff5c2ce6771e5fbc4079f4b506301f5cf394c8452f7"
184 | dependencies = [
185 | "crossterm",
186 | "strum",
187 | "strum_macros",
188 | "unicode-width",
189 | ]
190 |
191 | [[package]]
192 | name = "command-fds"
193 | version = "0.3.0"
194 | source = "registry+https://github.com/rust-lang/crates.io-index"
195 | checksum = "7bb11bd1378bf3731b182997b40cefe00aba6a6cc74042c8318c1b271d3badf7"
196 | dependencies = [
197 | "nix 0.27.1",
198 | "thiserror",
199 | ]
200 |
201 | [[package]]
202 | name = "console"
203 | version = "0.15.7"
204 | source = "registry+https://github.com/rust-lang/crates.io-index"
205 | checksum = "c926e00cc70edefdc64d3a5ff31cc65bb97a3460097762bd23afb4d8145fccf8"
206 | dependencies = [
207 | "encode_unicode",
208 | "lazy_static",
209 | "libc",
210 | "unicode-width",
211 | "windows-sys 0.45.0",
212 | ]
213 |
214 | [[package]]
215 | name = "crossbeam-deque"
216 | version = "0.8.3"
217 | source = "registry+https://github.com/rust-lang/crates.io-index"
218 | checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
219 | dependencies = [
220 | "cfg-if",
221 | "crossbeam-epoch",
222 | "crossbeam-utils",
223 | ]
224 |
225 | [[package]]
226 | name = "crossbeam-epoch"
227 | version = "0.9.15"
228 | source = "registry+https://github.com/rust-lang/crates.io-index"
229 | checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
230 | dependencies = [
231 | "autocfg",
232 | "cfg-if",
233 | "crossbeam-utils",
234 | "memoffset",
235 | "scopeguard",
236 | ]
237 |
238 | [[package]]
239 | name = "crossbeam-utils"
240 | version = "0.8.16"
241 | source = "registry+https://github.com/rust-lang/crates.io-index"
242 | checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
243 | dependencies = [
244 | "cfg-if",
245 | ]
246 |
247 | [[package]]
248 | name = "crossterm"
249 | version = "0.27.0"
250 | source = "registry+https://github.com/rust-lang/crates.io-index"
251 | checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
252 | dependencies = [
253 | "bitflags 2.4.0",
254 | "crossterm_winapi",
255 | "libc",
256 | "parking_lot",
257 | "winapi",
258 | ]
259 |
260 | [[package]]
261 | name = "crossterm_winapi"
262 | version = "0.9.1"
263 | source = "registry+https://github.com/rust-lang/crates.io-index"
264 | checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b"
265 | dependencies = [
266 | "winapi",
267 | ]
268 |
269 | [[package]]
270 | name = "ctrlc"
271 | version = "3.4.4"
272 | source = "registry+https://github.com/rust-lang/crates.io-index"
273 | checksum = "672465ae37dc1bc6380a6547a8883d5dd397b0f1faaad4f265726cc7042a5345"
274 | dependencies = [
275 | "nix 0.28.0",
276 | "windows-sys 0.52.0",
277 | ]
278 |
279 | [[package]]
280 | name = "directories"
281 | version = "5.0.1"
282 | source = "registry+https://github.com/rust-lang/crates.io-index"
283 | checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35"
284 | dependencies = [
285 | "dirs-sys",
286 | ]
287 |
288 | [[package]]
289 | name = "dirs-sys"
290 | version = "0.4.1"
291 | source = "registry+https://github.com/rust-lang/crates.io-index"
292 | checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c"
293 | dependencies = [
294 | "libc",
295 | "option-ext",
296 | "redox_users",
297 | "windows-sys 0.48.0",
298 | ]
299 |
300 | [[package]]
301 | name = "either"
302 | version = "1.9.0"
303 | source = "registry+https://github.com/rust-lang/crates.io-index"
304 | checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
305 |
306 | [[package]]
307 | name = "encode_unicode"
308 | version = "0.3.6"
309 | source = "registry+https://github.com/rust-lang/crates.io-index"
310 | checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f"
311 |
312 | [[package]]
313 | name = "equivalent"
314 | version = "1.0.1"
315 | source = "registry+https://github.com/rust-lang/crates.io-index"
316 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
317 |
318 | [[package]]
319 | name = "errno"
320 | version = "0.3.9"
321 | source = "registry+https://github.com/rust-lang/crates.io-index"
322 | checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba"
323 | dependencies = [
324 | "libc",
325 | "windows-sys 0.52.0",
326 | ]
327 |
328 | [[package]]
329 | name = "fastrand"
330 | version = "2.1.0"
331 | source = "registry+https://github.com/rust-lang/crates.io-index"
332 | checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
333 |
334 | [[package]]
335 | name = "getrandom"
336 | version = "0.2.10"
337 | source = "registry+https://github.com/rust-lang/crates.io-index"
338 | checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
339 | dependencies = [
340 | "cfg-if",
341 | "libc",
342 | "wasi",
343 | ]
344 |
345 | [[package]]
346 | name = "gimli"
347 | version = "0.28.0"
348 | source = "registry+https://github.com/rust-lang/crates.io-index"
349 | checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
350 |
351 | [[package]]
352 | name = "hashbrown"
353 | version = "0.14.0"
354 | source = "registry+https://github.com/rust-lang/crates.io-index"
355 | checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
356 |
357 | [[package]]
358 | name = "heck"
359 | version = "0.4.1"
360 | source = "registry+https://github.com/rust-lang/crates.io-index"
361 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
362 |
363 | [[package]]
364 | name = "heck"
365 | version = "0.5.0"
366 | source = "registry+https://github.com/rust-lang/crates.io-index"
367 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
368 |
369 | [[package]]
370 | name = "home"
371 | version = "0.5.9"
372 | source = "registry+https://github.com/rust-lang/crates.io-index"
373 | checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
374 | dependencies = [
375 | "windows-sys 0.52.0",
376 | ]
377 |
378 | [[package]]
379 | name = "human-panic"
380 | version = "2.0.0"
381 | source = "registry+https://github.com/rust-lang/crates.io-index"
382 | checksum = "a4c5d0e9120f6bca6120d142c7ede1ba376dd6bf276d69dd3dbe6cbeb7824179"
383 | dependencies = [
384 | "anstream",
385 | "anstyle",
386 | "backtrace",
387 | "os_info",
388 | "serde",
389 | "serde_derive",
390 | "toml",
391 | "uuid",
392 | ]
393 |
394 | [[package]]
395 | name = "human-sort"
396 | version = "0.2.2"
397 | source = "registry+https://github.com/rust-lang/crates.io-index"
398 | checksum = "140a09c9305e6d5e557e2ed7cbc68e05765a7d4213975b87cb04920689cc6219"
399 |
400 | [[package]]
401 | name = "indexmap"
402 | version = "2.0.0"
403 | source = "registry+https://github.com/rust-lang/crates.io-index"
404 | checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
405 | dependencies = [
406 | "equivalent",
407 | "hashbrown",
408 | ]
409 |
410 | [[package]]
411 | name = "indicatif"
412 | version = "0.17.8"
413 | source = "registry+https://github.com/rust-lang/crates.io-index"
414 | checksum = "763a5a8f45087d6bcea4222e7b72c291a054edf80e4ef6efd2a4979878c7bea3"
415 | dependencies = [
416 | "console",
417 | "instant",
418 | "number_prefix",
419 | "portable-atomic",
420 | "rayon",
421 | "unicode-width",
422 | ]
423 |
424 | [[package]]
425 | name = "instant"
426 | version = "0.1.12"
427 | source = "registry+https://github.com/rust-lang/crates.io-index"
428 | checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
429 | dependencies = [
430 | "cfg-if",
431 | ]
432 |
433 | [[package]]
434 | name = "is_executable"
435 | version = "1.0.1"
436 | source = "registry+https://github.com/rust-lang/crates.io-index"
437 | checksum = "fa9acdc6d67b75e626ad644734e8bc6df893d9cd2a834129065d3dd6158ea9c8"
438 | dependencies = [
439 | "winapi",
440 | ]
441 |
442 | [[package]]
443 | name = "is_terminal_polyfill"
444 | version = "1.70.0"
445 | source = "registry+https://github.com/rust-lang/crates.io-index"
446 | checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
447 |
448 | [[package]]
449 | name = "lazy_static"
450 | version = "1.4.0"
451 | source = "registry+https://github.com/rust-lang/crates.io-index"
452 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
453 |
454 | [[package]]
455 | name = "libc"
456 | version = "0.2.154"
457 | source = "registry+https://github.com/rust-lang/crates.io-index"
458 | checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
459 |
460 | [[package]]
461 | name = "linux-raw-sys"
462 | version = "0.4.13"
463 | source = "registry+https://github.com/rust-lang/crates.io-index"
464 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c"
465 |
466 | [[package]]
467 | name = "lock_api"
468 | version = "0.4.11"
469 | source = "registry+https://github.com/rust-lang/crates.io-index"
470 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
471 | dependencies = [
472 | "autocfg",
473 | "scopeguard",
474 | ]
475 |
476 | [[package]]
477 | name = "log"
478 | version = "0.4.20"
479 | source = "registry+https://github.com/rust-lang/crates.io-index"
480 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
481 |
482 | [[package]]
483 | name = "memchr"
484 | version = "2.6.3"
485 | source = "registry+https://github.com/rust-lang/crates.io-index"
486 | checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c"
487 |
488 | [[package]]
489 | name = "memfile"
490 | version = "0.3.2"
491 | source = "registry+https://github.com/rust-lang/crates.io-index"
492 | checksum = "f64636fdb65a5f0740f920c4281f3dbb76a71e25e25914b6d27000739897d40e"
493 | dependencies = [
494 | "libc",
495 | ]
496 |
497 | [[package]]
498 | name = "memoffset"
499 | version = "0.9.0"
500 | source = "registry+https://github.com/rust-lang/crates.io-index"
501 | checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
502 | dependencies = [
503 | "autocfg",
504 | ]
505 |
506 | [[package]]
507 | name = "miniz_oxide"
508 | version = "0.7.1"
509 | source = "registry+https://github.com/rust-lang/crates.io-index"
510 | checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
511 | dependencies = [
512 | "adler",
513 | ]
514 |
515 | [[package]]
516 | name = "nix"
517 | version = "0.27.1"
518 | source = "registry+https://github.com/rust-lang/crates.io-index"
519 | checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
520 | dependencies = [
521 | "bitflags 2.4.0",
522 | "cfg-if",
523 | "libc",
524 | ]
525 |
526 | [[package]]
527 | name = "nix"
528 | version = "0.28.0"
529 | source = "registry+https://github.com/rust-lang/crates.io-index"
530 | checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
531 | dependencies = [
532 | "bitflags 2.4.0",
533 | "cfg-if",
534 | "cfg_aliases",
535 | "libc",
536 | ]
537 |
538 | [[package]]
539 | name = "number_prefix"
540 | version = "0.4.0"
541 | source = "registry+https://github.com/rust-lang/crates.io-index"
542 | checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
543 |
544 | [[package]]
545 | name = "object"
546 | version = "0.32.1"
547 | source = "registry+https://github.com/rust-lang/crates.io-index"
548 | checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
549 | dependencies = [
550 | "memchr",
551 | ]
552 |
553 | [[package]]
554 | name = "option-ext"
555 | version = "0.2.0"
556 | source = "registry+https://github.com/rust-lang/crates.io-index"
557 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
558 |
559 | [[package]]
560 | name = "os_info"
561 | version = "3.7.0"
562 | source = "registry+https://github.com/rust-lang/crates.io-index"
563 | checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e"
564 | dependencies = [
565 | "log",
566 | "serde",
567 | "winapi",
568 | ]
569 |
570 | [[package]]
571 | name = "parking_lot"
572 | version = "0.12.1"
573 | source = "registry+https://github.com/rust-lang/crates.io-index"
574 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
575 | dependencies = [
576 | "lock_api",
577 | "parking_lot_core",
578 | ]
579 |
580 | [[package]]
581 | name = "parking_lot_core"
582 | version = "0.9.9"
583 | source = "registry+https://github.com/rust-lang/crates.io-index"
584 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e"
585 | dependencies = [
586 | "cfg-if",
587 | "libc",
588 | "redox_syscall 0.4.1",
589 | "smallvec",
590 | "windows-targets 0.48.5",
591 | ]
592 |
593 | [[package]]
594 | name = "portable-atomic"
595 | version = "1.4.3"
596 | source = "registry+https://github.com/rust-lang/crates.io-index"
597 | checksum = "31114a898e107c51bb1609ffaf55a0e011cf6a4d7f1170d0015a165082c0338b"
598 |
599 | [[package]]
600 | name = "proc-macro2"
601 | version = "1.0.82"
602 | source = "registry+https://github.com/rust-lang/crates.io-index"
603 | checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
604 | dependencies = [
605 | "unicode-ident",
606 | ]
607 |
608 | [[package]]
609 | name = "quote"
610 | version = "1.0.33"
611 | source = "registry+https://github.com/rust-lang/crates.io-index"
612 | checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
613 | dependencies = [
614 | "proc-macro2",
615 | ]
616 |
617 | [[package]]
618 | name = "rayon"
619 | version = "1.10.0"
620 | source = "registry+https://github.com/rust-lang/crates.io-index"
621 | checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
622 | dependencies = [
623 | "either",
624 | "rayon-core",
625 | ]
626 |
627 | [[package]]
628 | name = "rayon-core"
629 | version = "1.12.1"
630 | source = "registry+https://github.com/rust-lang/crates.io-index"
631 | checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
632 | dependencies = [
633 | "crossbeam-deque",
634 | "crossbeam-utils",
635 | ]
636 |
637 | [[package]]
638 | name = "redox_syscall"
639 | version = "0.2.16"
640 | source = "registry+https://github.com/rust-lang/crates.io-index"
641 | checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
642 | dependencies = [
643 | "bitflags 1.3.2",
644 | ]
645 |
646 | [[package]]
647 | name = "redox_syscall"
648 | version = "0.4.1"
649 | source = "registry+https://github.com/rust-lang/crates.io-index"
650 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa"
651 | dependencies = [
652 | "bitflags 1.3.2",
653 | ]
654 |
655 | [[package]]
656 | name = "redox_users"
657 | version = "0.4.3"
658 | source = "registry+https://github.com/rust-lang/crates.io-index"
659 | checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
660 | dependencies = [
661 | "getrandom",
662 | "redox_syscall 0.2.16",
663 | "thiserror",
664 | ]
665 |
666 | [[package]]
667 | name = "rustc-demangle"
668 | version = "0.1.23"
669 | source = "registry+https://github.com/rust-lang/crates.io-index"
670 | checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
671 |
672 | [[package]]
673 | name = "rustix"
674 | version = "0.38.34"
675 | source = "registry+https://github.com/rust-lang/crates.io-index"
676 | checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
677 | dependencies = [
678 | "bitflags 2.4.0",
679 | "errno",
680 | "libc",
681 | "linux-raw-sys",
682 | "windows-sys 0.52.0",
683 | ]
684 |
685 | [[package]]
686 | name = "rustversion"
687 | version = "1.0.14"
688 | source = "registry+https://github.com/rust-lang/crates.io-index"
689 | checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
690 |
691 | [[package]]
692 | name = "scopeguard"
693 | version = "1.2.0"
694 | source = "registry+https://github.com/rust-lang/crates.io-index"
695 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
696 |
697 | [[package]]
698 | name = "serde"
699 | version = "1.0.188"
700 | source = "registry+https://github.com/rust-lang/crates.io-index"
701 | checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e"
702 | dependencies = [
703 | "serde_derive",
704 | ]
705 |
706 | [[package]]
707 | name = "serde_derive"
708 | version = "1.0.188"
709 | source = "registry+https://github.com/rust-lang/crates.io-index"
710 | checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2"
711 | dependencies = [
712 | "proc-macro2",
713 | "quote",
714 | "syn",
715 | ]
716 |
717 | [[package]]
718 | name = "serde_spanned"
719 | version = "0.6.4"
720 | source = "registry+https://github.com/rust-lang/crates.io-index"
721 | checksum = "12022b835073e5b11e90a14f86838ceb1c8fb0325b72416845c487ac0fa95e80"
722 | dependencies = [
723 | "serde",
724 | ]
725 |
726 | [[package]]
727 | name = "smallvec"
728 | version = "1.11.2"
729 | source = "registry+https://github.com/rust-lang/crates.io-index"
730 | checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970"
731 |
732 | [[package]]
733 | name = "strsim"
734 | version = "0.11.1"
735 | source = "registry+https://github.com/rust-lang/crates.io-index"
736 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
737 |
738 | [[package]]
739 | name = "strum"
740 | version = "0.26.2"
741 | source = "registry+https://github.com/rust-lang/crates.io-index"
742 | checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29"
743 |
744 | [[package]]
745 | name = "strum_macros"
746 | version = "0.26.2"
747 | source = "registry+https://github.com/rust-lang/crates.io-index"
748 | checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
749 | dependencies = [
750 | "heck 0.4.1",
751 | "proc-macro2",
752 | "quote",
753 | "rustversion",
754 | "syn",
755 | ]
756 |
757 | [[package]]
758 | name = "syn"
759 | version = "2.0.37"
760 | source = "registry+https://github.com/rust-lang/crates.io-index"
761 | checksum = "7303ef2c05cd654186cb250d29049a24840ca25d2747c25c0381c8d9e2f582e8"
762 | dependencies = [
763 | "proc-macro2",
764 | "quote",
765 | "unicode-ident",
766 | ]
767 |
768 | [[package]]
769 | name = "tempfile"
770 | version = "3.10.1"
771 | source = "registry+https://github.com/rust-lang/crates.io-index"
772 | checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
773 | dependencies = [
774 | "cfg-if",
775 | "fastrand",
776 | "rustix",
777 | "windows-sys 0.52.0",
778 | ]
779 |
780 | [[package]]
781 | name = "terminal_size"
782 | version = "0.3.0"
783 | source = "registry+https://github.com/rust-lang/crates.io-index"
784 | checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
785 | dependencies = [
786 | "rustix",
787 | "windows-sys 0.48.0",
788 | ]
789 |
790 | [[package]]
791 | name = "thiserror"
792 | version = "1.0.48"
793 | source = "registry+https://github.com/rust-lang/crates.io-index"
794 | checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7"
795 | dependencies = [
796 | "thiserror-impl",
797 | ]
798 |
799 | [[package]]
800 | name = "thiserror-impl"
801 | version = "1.0.48"
802 | source = "registry+https://github.com/rust-lang/crates.io-index"
803 | checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35"
804 | dependencies = [
805 | "proc-macro2",
806 | "quote",
807 | "syn",
808 | ]
809 |
810 | [[package]]
811 | name = "toml"
812 | version = "0.8.8"
813 | source = "registry+https://github.com/rust-lang/crates.io-index"
814 | checksum = "a1a195ec8c9da26928f773888e0742ca3ca1040c6cd859c919c9f59c1954ab35"
815 | dependencies = [
816 | "serde",
817 | "serde_spanned",
818 | "toml_datetime",
819 | "toml_edit",
820 | ]
821 |
822 | [[package]]
823 | name = "toml_datetime"
824 | version = "0.6.5"
825 | source = "registry+https://github.com/rust-lang/crates.io-index"
826 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
827 | dependencies = [
828 | "serde",
829 | ]
830 |
831 | [[package]]
832 | name = "toml_edit"
833 | version = "0.21.0"
834 | source = "registry+https://github.com/rust-lang/crates.io-index"
835 | checksum = "d34d383cd00a163b4a5b85053df514d45bc330f6de7737edfe0a93311d1eaa03"
836 | dependencies = [
837 | "indexmap",
838 | "serde",
839 | "serde_spanned",
840 | "toml_datetime",
841 | ]
842 |
843 | [[package]]
844 | name = "toster"
845 | version = "1.2.1"
846 | dependencies = [
847 | "clap",
848 | "colored",
849 | "comfy-table",
850 | "command-fds",
851 | "ctrlc",
852 | "directories",
853 | "human-panic",
854 | "human-sort",
855 | "indicatif",
856 | "is_executable",
857 | "memfile",
858 | "rayon",
859 | "tempfile",
860 | "terminal_size",
861 | "wait-timeout",
862 | "which",
863 | ]
864 |
865 | [[package]]
866 | name = "unicode-ident"
867 | version = "1.0.12"
868 | source = "registry+https://github.com/rust-lang/crates.io-index"
869 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
870 |
871 | [[package]]
872 | name = "unicode-width"
873 | version = "0.1.11"
874 | source = "registry+https://github.com/rust-lang/crates.io-index"
875 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
876 |
877 | [[package]]
878 | name = "utf8parse"
879 | version = "0.2.1"
880 | source = "registry+https://github.com/rust-lang/crates.io-index"
881 | checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
882 |
883 | [[package]]
884 | name = "uuid"
885 | version = "1.4.1"
886 | source = "registry+https://github.com/rust-lang/crates.io-index"
887 | checksum = "79daa5ed5740825c40b389c5e50312b9c86df53fccd33f281df655642b43869d"
888 | dependencies = [
889 | "getrandom",
890 | ]
891 |
892 | [[package]]
893 | name = "wait-timeout"
894 | version = "0.2.0"
895 | source = "registry+https://github.com/rust-lang/crates.io-index"
896 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6"
897 | dependencies = [
898 | "libc",
899 | ]
900 |
901 | [[package]]
902 | name = "wasi"
903 | version = "0.11.0+wasi-snapshot-preview1"
904 | source = "registry+https://github.com/rust-lang/crates.io-index"
905 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
906 |
907 | [[package]]
908 | name = "which"
909 | version = "6.0.1"
910 | source = "registry+https://github.com/rust-lang/crates.io-index"
911 | checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7"
912 | dependencies = [
913 | "either",
914 | "home",
915 | "rustix",
916 | "winsafe",
917 | ]
918 |
919 | [[package]]
920 | name = "winapi"
921 | version = "0.3.9"
922 | source = "registry+https://github.com/rust-lang/crates.io-index"
923 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
924 | dependencies = [
925 | "winapi-i686-pc-windows-gnu",
926 | "winapi-x86_64-pc-windows-gnu",
927 | ]
928 |
929 | [[package]]
930 | name = "winapi-i686-pc-windows-gnu"
931 | version = "0.4.0"
932 | source = "registry+https://github.com/rust-lang/crates.io-index"
933 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
934 |
935 | [[package]]
936 | name = "winapi-x86_64-pc-windows-gnu"
937 | version = "0.4.0"
938 | source = "registry+https://github.com/rust-lang/crates.io-index"
939 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
940 |
941 | [[package]]
942 | name = "windows-sys"
943 | version = "0.45.0"
944 | source = "registry+https://github.com/rust-lang/crates.io-index"
945 | checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
946 | dependencies = [
947 | "windows-targets 0.42.2",
948 | ]
949 |
950 | [[package]]
951 | name = "windows-sys"
952 | version = "0.48.0"
953 | source = "registry+https://github.com/rust-lang/crates.io-index"
954 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
955 | dependencies = [
956 | "windows-targets 0.48.5",
957 | ]
958 |
959 | [[package]]
960 | name = "windows-sys"
961 | version = "0.52.0"
962 | source = "registry+https://github.com/rust-lang/crates.io-index"
963 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
964 | dependencies = [
965 | "windows-targets 0.52.5",
966 | ]
967 |
968 | [[package]]
969 | name = "windows-targets"
970 | version = "0.42.2"
971 | source = "registry+https://github.com/rust-lang/crates.io-index"
972 | checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
973 | dependencies = [
974 | "windows_aarch64_gnullvm 0.42.2",
975 | "windows_aarch64_msvc 0.42.2",
976 | "windows_i686_gnu 0.42.2",
977 | "windows_i686_msvc 0.42.2",
978 | "windows_x86_64_gnu 0.42.2",
979 | "windows_x86_64_gnullvm 0.42.2",
980 | "windows_x86_64_msvc 0.42.2",
981 | ]
982 |
983 | [[package]]
984 | name = "windows-targets"
985 | version = "0.48.5"
986 | source = "registry+https://github.com/rust-lang/crates.io-index"
987 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
988 | dependencies = [
989 | "windows_aarch64_gnullvm 0.48.5",
990 | "windows_aarch64_msvc 0.48.5",
991 | "windows_i686_gnu 0.48.5",
992 | "windows_i686_msvc 0.48.5",
993 | "windows_x86_64_gnu 0.48.5",
994 | "windows_x86_64_gnullvm 0.48.5",
995 | "windows_x86_64_msvc 0.48.5",
996 | ]
997 |
998 | [[package]]
999 | name = "windows-targets"
1000 | version = "0.52.5"
1001 | source = "registry+https://github.com/rust-lang/crates.io-index"
1002 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
1003 | dependencies = [
1004 | "windows_aarch64_gnullvm 0.52.5",
1005 | "windows_aarch64_msvc 0.52.5",
1006 | "windows_i686_gnu 0.52.5",
1007 | "windows_i686_gnullvm",
1008 | "windows_i686_msvc 0.52.5",
1009 | "windows_x86_64_gnu 0.52.5",
1010 | "windows_x86_64_gnullvm 0.52.5",
1011 | "windows_x86_64_msvc 0.52.5",
1012 | ]
1013 |
1014 | [[package]]
1015 | name = "windows_aarch64_gnullvm"
1016 | version = "0.42.2"
1017 | source = "registry+https://github.com/rust-lang/crates.io-index"
1018 | checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
1019 |
1020 | [[package]]
1021 | name = "windows_aarch64_gnullvm"
1022 | version = "0.48.5"
1023 | source = "registry+https://github.com/rust-lang/crates.io-index"
1024 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
1025 |
1026 | [[package]]
1027 | name = "windows_aarch64_gnullvm"
1028 | version = "0.52.5"
1029 | source = "registry+https://github.com/rust-lang/crates.io-index"
1030 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
1031 |
1032 | [[package]]
1033 | name = "windows_aarch64_msvc"
1034 | version = "0.42.2"
1035 | source = "registry+https://github.com/rust-lang/crates.io-index"
1036 | checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
1037 |
1038 | [[package]]
1039 | name = "windows_aarch64_msvc"
1040 | version = "0.48.5"
1041 | source = "registry+https://github.com/rust-lang/crates.io-index"
1042 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
1043 |
1044 | [[package]]
1045 | name = "windows_aarch64_msvc"
1046 | version = "0.52.5"
1047 | source = "registry+https://github.com/rust-lang/crates.io-index"
1048 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
1049 |
1050 | [[package]]
1051 | name = "windows_i686_gnu"
1052 | version = "0.42.2"
1053 | source = "registry+https://github.com/rust-lang/crates.io-index"
1054 | checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
1055 |
1056 | [[package]]
1057 | name = "windows_i686_gnu"
1058 | version = "0.48.5"
1059 | source = "registry+https://github.com/rust-lang/crates.io-index"
1060 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
1061 |
1062 | [[package]]
1063 | name = "windows_i686_gnu"
1064 | version = "0.52.5"
1065 | source = "registry+https://github.com/rust-lang/crates.io-index"
1066 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
1067 |
1068 | [[package]]
1069 | name = "windows_i686_gnullvm"
1070 | version = "0.52.5"
1071 | source = "registry+https://github.com/rust-lang/crates.io-index"
1072 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
1073 |
1074 | [[package]]
1075 | name = "windows_i686_msvc"
1076 | version = "0.42.2"
1077 | source = "registry+https://github.com/rust-lang/crates.io-index"
1078 | checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
1079 |
1080 | [[package]]
1081 | name = "windows_i686_msvc"
1082 | version = "0.48.5"
1083 | source = "registry+https://github.com/rust-lang/crates.io-index"
1084 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
1085 |
1086 | [[package]]
1087 | name = "windows_i686_msvc"
1088 | version = "0.52.5"
1089 | source = "registry+https://github.com/rust-lang/crates.io-index"
1090 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
1091 |
1092 | [[package]]
1093 | name = "windows_x86_64_gnu"
1094 | version = "0.42.2"
1095 | source = "registry+https://github.com/rust-lang/crates.io-index"
1096 | checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
1097 |
1098 | [[package]]
1099 | name = "windows_x86_64_gnu"
1100 | version = "0.48.5"
1101 | source = "registry+https://github.com/rust-lang/crates.io-index"
1102 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
1103 |
1104 | [[package]]
1105 | name = "windows_x86_64_gnu"
1106 | version = "0.52.5"
1107 | source = "registry+https://github.com/rust-lang/crates.io-index"
1108 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
1109 |
1110 | [[package]]
1111 | name = "windows_x86_64_gnullvm"
1112 | version = "0.42.2"
1113 | source = "registry+https://github.com/rust-lang/crates.io-index"
1114 | checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
1115 |
1116 | [[package]]
1117 | name = "windows_x86_64_gnullvm"
1118 | version = "0.48.5"
1119 | source = "registry+https://github.com/rust-lang/crates.io-index"
1120 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
1121 |
1122 | [[package]]
1123 | name = "windows_x86_64_gnullvm"
1124 | version = "0.52.5"
1125 | source = "registry+https://github.com/rust-lang/crates.io-index"
1126 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
1127 |
1128 | [[package]]
1129 | name = "windows_x86_64_msvc"
1130 | version = "0.42.2"
1131 | source = "registry+https://github.com/rust-lang/crates.io-index"
1132 | checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
1133 |
1134 | [[package]]
1135 | name = "windows_x86_64_msvc"
1136 | version = "0.48.5"
1137 | source = "registry+https://github.com/rust-lang/crates.io-index"
1138 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
1139 |
1140 | [[package]]
1141 | name = "windows_x86_64_msvc"
1142 | version = "0.52.5"
1143 | source = "registry+https://github.com/rust-lang/crates.io-index"
1144 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
1145 |
1146 | [[package]]
1147 | name = "winsafe"
1148 | version = "0.0.19"
1149 | source = "registry+https://github.com/rust-lang/crates.io-index"
1150 | checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
1151 |
--------------------------------------------------------------------------------