cpast/
lib.rs

1//! # cpast - Code Testing and Analysis Tool
2//!
3//! `cpast` is a versatile code testing and analysis tool that empowers users in competitive programming and coding practice. It allows testing correct and incorrect code files against a custom language generator called `clex_gen`. This crate supports various programming languages, such as Python, C++, C, Rust, Ruby, JavaScript, and Java, and enables users to specify the number of iterations for testing code against random input values.
4//!
5//! ## Main Modules
6//!
7//! - `lang_runner`: Module for language-runner-related functionalities and handling the storage and management of code programs.
8//! - `utils`: Utility module with miscellaneous functions.
9//!
10//! ## Introduction
11//!
12//! The crate provides solutions to common challenges faced by competitive programmers and coding enthusiasts, such as verifying code correctness, efficient testing under time constraints, and quick debugging to improve code performance.
13//!
14//! ## Usage
15//!
16//! To get started with `cpast`, users can use the provided functions:
17//!
18//! - `compile_and_test`: Compiles and tests code against a custom language generator.
19//!
20//! ## Example
21//!
22//! ```rust, no_run
23//! use cpast::compile_and_test;
24//!
25//! async fn compile() {
26//!     compile_and_test("correct.cpp".to_string(), "incorrect.rs".to_string(), "(N[1,10]) (?:N){\\1}".to_string(), 100, false, false, false).await.unwrap();
27//! }
28//! ```
29//!
30//! For more details on usage and advanced features, refer to the README.
31//!
32
33use ccode_runner::lang_runner::language_name::LanguageName;
34use ccode_runner::lang_runner::runner::Language;
35use colored::Colorize;
36use error_types::cli_error::CliErrorType;
37use futures::future::join_all;
38use std::io;
39use std::path::Path;
40use std::sync::Arc;
41
42use ccode_runner::lang_runner::program_store::ProgramStore;
43use ccode_runner::lang_runner::runner_error_types::RunnerErrorType;
44use clex_gen::clex_language::clex_error_type::ClexErrorType;
45use clex_gen::clex_language::{code_generator, lexer, parser};
46use std::sync::atomic::{AtomicBool, Ordering};
47
48pub(crate) mod error_types;
49
50pub const DEFAULT_FAIL_EXIT_CODE: i32 = 1;
51
52#[derive(thiserror::Error, Debug)]
53pub enum GenericCpastError {
54    #[error("{0}")]
55    ClexError(#[from] ClexErrorType),
56
57    #[error("{0}")]
58    RunnerError(#[from] Box<RunnerErrorType>),
59
60    #[error("{0}")]
61    CliError(#[from] CliErrorType),
62}
63
64pub enum CodeOrPath {
65    /// Source Code, Clex
66    Code(String, LanguageName),
67    /// File Path
68    Path(String),
69}
70
71/// Compile and test code against custom language generator.
72///
73/// # Arguments
74///
75/// * `correct_binding` - The source code file path containing correct code.
76/// * `test_binding` - The source code file path containing incorrect code for testing.
77/// * `language` - The custom language generator code for test generation.
78/// * `iterations` - The number of test iterations to run.
79/// * `no_stop` - Whether to stop after a failing testcase is found or not.
80/// * `do_force_compile` - Whether to forcefully recompile files, even though it is updated
81/// * `debug` - Whether to print debug information or not analogous to CPAST_DEBUG=1.
82///
83/// # Example
84///
85/// ```rust,no_run
86/// async fn compile() {
87///     cpast::compile_and_test("correct.cpp".to_string(), "incorrect.rs".to_string(), "(N[1,10]) (?:N){\\1}".to_string(), 100, false, false, false).await.unwrap();
88/// }
89/// ```
90pub async fn compile_and_test(
91    correct_binding: CodeOrPath,
92    test_binding: String,
93    language: String,
94    iterations: usize,
95    no_stop: bool,
96    do_force_compile: bool,
97    debug: bool,
98) -> Result<(), GenericCpastError> {
99    let store = match correct_binding {
100        CodeOrPath::Code(correct_code, correct_lang) => {
101            // CURRENTLY UNSTABLE
102            eprintln!(
103                "{}",
104                "Using --problem_url is unstable at this moment. It may or may not work!"
105                    .bold()
106                    .red()
107            );
108            let correct_lang_instance =
109                Language::new_from_text(&correct_code, correct_lang, do_force_compile)?;
110            let test_lang_instance = Language::new(Path::new(&test_binding), do_force_compile)?;
111            ProgramStore::new_from_language(correct_lang_instance, test_lang_instance)?
112        }
113        CodeOrPath::Path(correct_path) => ProgramStore::new(
114            Path::new(&correct_path),
115            Path::new(&test_binding),
116            do_force_compile,
117        )?,
118    };
119    let store = Arc::new(store);
120
121    let mut token = lexer::Tokens::new(language);
122    token.scan_tokens()?;
123    let mut parser = parser::Parser::new_from_tokens(token);
124    parser.parser()?;
125    let generator = code_generator::Generator::new(&parser);
126    let generator = Arc::new(generator);
127
128    let has_failed = Arc::new(AtomicBool::new(false));
129    // let semaphore = Arc::new(Semaphore::new(100)); // Limit concurrency to 64
130
131    if debug {
132        eprintln!("{}", "Debug mode enabled!".bold().yellow());
133        eprintln!(
134            "{}\n",
135            "[INFO] Using multi-threading to speed up the process, testcase order might vary!"
136                .bright_blue()
137        );
138    }
139
140    let tasks = (1..=iterations)
141        .map(|iter| {
142            let has_failed_clone = Arc::clone(&has_failed);
143            let store_clone = Arc::clone(&store);
144            let generator_clone = Arc::clone(&generator);
145            // let semaphore_clone = Arc::clone(&semaphore);
146
147            tokio::spawn(async move {
148                // let permit = semaphore_clone.acquire().await.unwrap(); // Acquire a permit
149                process_test_case(
150                    no_stop,
151                    debug,
152                    iter,
153                    has_failed_clone,
154                    store_clone,
155                    generator_clone,
156                )
157                .await;
158                // drop(permit);
159            })
160        })
161        .collect::<Vec<_>>();
162
163    join_all(tasks).await;
164
165    if no_stop {
166        println!(
167            "\n{}",
168            "Test case generation & matching done!".bold().bright_blue()
169        );
170    }
171
172    if !has_failed.load(Ordering::Relaxed) {
173        println!("{}", "🐣 Vohoo! No testcases have failed!".bold().green());
174    }
175
176    Ok(())
177}
178
179async fn process_test_case(
180    no_stop: bool,
181    debug: bool,
182    iter: usize,
183    has_failed_clone: Arc<AtomicBool>,
184    store_clone: Arc<ProgramStore>,
185    generator_clone: Arc<code_generator::Generator>,
186) {
187    if !no_stop && has_failed_clone.load(Ordering::Relaxed) {
188        return;
189    }
190
191    match generator_clone.generate_testcases() {
192        Err(err) => {
193            eprintln!("{}", err);
194            has_failed_clone.store(true, Ordering::Relaxed);
195        }
196        Ok(output_text) => match store_clone.run_codes_and_compare_output(&output_text) {
197            Ok((true, _, _)) => {
198                if !no_stop && debug {
199                    eprintln!("{}", format!("Testcase {} ran successfully!", iter).green());
200                }
201            }
202            Ok((false, expected, actual)) => {
203                println!(
204                    "{}\n{}\n{}\n==============================\n{}\n{}\n==============================\n{}\n{}",
205                    format!("Testcase {} failed!", iter).red(),
206                    "INPUT".underline(),
207                    &output_text.cyan(),
208                    "EXPECTED OUTPUT".underline(),
209                    expected.green(),
210                    "ACTUAL OUTPUT".underline(),
211                    actual.red()
212                );
213                has_failed_clone.store(true, Ordering::Relaxed);
214            }
215            Err(err) => {
216                eprintln!("{}", format!("Error matching the file! {}", err).red());
217                if let RunnerErrorType::ProgramRunError(run_err) = *err {
218                    if let Some(io_err) = run_err.downcast_ref::<io::Error>() {
219                        if io_err.kind() == io::ErrorKind::BrokenPipe {
220                            eprintln!("Broken pipe detected!");
221                            eprintln!(
222                                "This usually happens when your clex is incorrect and it doesn't generate what your codes are expecting!"
223                            );
224                            eprintln!("Please check your clex and try again!");
225                        }
226                    }
227                }
228
229                has_failed_clone.store(true, Ordering::Relaxed);
230            }
231        },
232    }
233}