diff --git a/crates/cli/src/cli/comp.rs b/crates/cli/src/cli/comp.rs index 46a30cd..4f0f399 100644 --- a/crates/cli/src/cli/comp.rs +++ b/crates/cli/src/cli/comp.rs @@ -5,6 +5,7 @@ use clap::{Arg, ArgAction, ArgMatches, Command}; use crate::comp::{add, create, finish, list, remove, rename, solve, test}; use crate::config::get_settings; +use crate::problem::run::{RunnableCategory, RunnableFile}; use crate::util::get_project_root; pub fn cli() -> Command { @@ -189,7 +190,10 @@ pub fn exec(args: &ArgMatches) -> Result<()> { .context("Competition name is required")?; let solution_lang = cmd.try_get_one::("lang")?; - solve::solve(&settings, &problems_dir, comp_name, solution_lang)?; + let solution_file = + RunnableFile::new(&settings, RunnableCategory::Solution, None, solution_lang)?; + + solve::solve(&settings, &problems_dir, comp_name, solution_file)?; } Some(("test", cmd)) => { let comp_name = cmd @@ -197,7 +201,10 @@ pub fn exec(args: &ArgMatches) -> Result<()> { .context("Competition name is required")?; let solution_lang = cmd.try_get_one::("lang")?; - test::test(&settings, &problems_dir, comp_name, solution_lang)?; + let solution_file = + RunnableFile::new(&settings, RunnableCategory::Solution, None, solution_lang)?; + + test::test(&settings, &problems_dir, comp_name, solution_file)?; } _ => {} } diff --git a/crates/cli/src/cli/problem.rs b/crates/cli/src/cli/problem.rs index a664f9d..e8fc17b 100644 --- a/crates/cli/src/cli/problem.rs +++ b/crates/cli/src/cli/problem.rs @@ -79,6 +79,10 @@ pub fn cli() -> Command { .long("generator-file") .help("Name of the generator file") .action(ArgAction::Set), + Arg::new("generator-lang") + .long("generator-lang") + .help("Language of the generator file (e.g. cpp, py)") + .action(ArgAction::Set), Arg::new("problem") .short('p') .long("problem") @@ -94,6 +98,10 @@ pub fn cli() -> Command { .long("file") .help("Name of the generator file") .action(ArgAction::Set), + Arg::new("lang") + .long("lang") + .help("Language of the generator file (e.g. cpp, py)") + .action(ArgAction::Set), Arg::new("problem") .short('p') .long("problem") @@ -190,7 +198,7 @@ pub fn exec(args: &ArgMatches) -> Result<()> { let mut solution_files: Vec = Vec::new(); for f in files { let solution_file = - RunnableFile::new(&settings, RunnableCategory::Solution, Some(f)); + RunnableFile::new(&settings, RunnableCategory::Solution, Some(f), None); solution_files.push(solution_file?); } @@ -231,7 +239,7 @@ pub fn exec(args: &ArgMatches) -> Result<()> { let mut solution_files: Vec = Vec::new(); for f in files { let solution_file = - RunnableFile::new(&settings, RunnableCategory::Solution, Some(f)); + RunnableFile::new(&settings, RunnableCategory::Solution, Some(f), None); solution_files.push(solution_file?); } @@ -239,6 +247,7 @@ pub fn exec(args: &ArgMatches) -> Result<()> { &settings, RunnableCategory::Generator, cmd.try_get_one::("generator-file")?, + cmd.try_get_one::("generator-lang")?, )?; let fuzz_args = fuzz::FuzzArgs { @@ -260,6 +269,7 @@ pub fn exec(args: &ArgMatches) -> Result<()> { &settings, RunnableCategory::Generator, cmd.try_get_one::("file")?, + cmd.try_get_one::("lang")?, )?; let test_name = cmd @@ -281,16 +291,14 @@ pub fn exec(args: &ArgMatches) -> Result<()> { None => &get_problem_from_cwd(&problems_dir)?, }; - let solution_file = cmd.try_get_one::("file")?.map(|f| f.as_str()); - let solution_lang = cmd.try_get_one::("lang")?; - - solve::solve( + let solution_file = RunnableFile::new( &settings, - &problems_dir, - problem_name, - solution_file, - solution_lang, + RunnableCategory::Solution, + cmd.try_get_one::("file")?, + cmd.try_get_one::("lang")?, )?; + + solve::solve(&settings, &problems_dir, problem_name, &solution_file)?; } Some(("test", cmd)) => { let problem_name = match cmd.try_get_one::("problem")? { @@ -298,16 +306,14 @@ pub fn exec(args: &ArgMatches) -> Result<()> { None => &get_problem_from_cwd(&problems_dir)?, }; - let solution_file = cmd.try_get_one::("file")?.map(|f| f.as_str()); - let solution_lang = cmd.try_get_one::("lang")?; - - test::test( + let solution_file = RunnableFile::new( &settings, - &problems_dir, - problem_name, - solution_file, - solution_lang, + RunnableCategory::Solution, + cmd.try_get_one::("file")?, + cmd.try_get_one::("lang")?, )?; + + test::test(&settings, &problems_dir, problem_name, &solution_file)?; } _ => {} } diff --git a/crates/cli/src/comp/solve.rs b/crates/cli/src/comp/solve.rs index bb6e2b4..af355f5 100644 --- a/crates/cli/src/comp/solve.rs +++ b/crates/cli/src/comp/solve.rs @@ -5,6 +5,7 @@ use anyhow::{bail, Context, Result}; use serde_json::from_reader; use crate::config::Settings; +use crate::problem::run::RunnableFile; use crate::problem::solve::solve as problem_solve; use super::{Competitions, COMPETITIONS_FILE}; @@ -13,7 +14,7 @@ pub fn solve( settings: &Settings, problems_dir: &Path, comp_name: &str, - solution_lang: Option<&String>, + solution_file: RunnableFile, ) -> Result<()> { let comp_file_path = problems_dir.join(COMPETITIONS_FILE); if !fs::exists(&comp_file_path)? { @@ -34,8 +35,7 @@ pub fn solve( settings, problems_dir, problem_name.as_str(), - None, - solution_lang, + &solution_file, )?; } diff --git a/crates/cli/src/comp/test.rs b/crates/cli/src/comp/test.rs index e5eb60a..4650714 100644 --- a/crates/cli/src/comp/test.rs +++ b/crates/cli/src/comp/test.rs @@ -5,6 +5,7 @@ use anyhow::{bail, Context, Result}; use serde_json::from_reader; use crate::config::Settings; +use crate::problem::run::RunnableFile; use crate::problem::test::test as problem_test; use super::{Competitions, COMPETITIONS_FILE}; @@ -13,7 +14,7 @@ pub fn test( settings: &Settings, problems_dir: &Path, comp_name: &str, - solution_lang: Option<&String>, + solution_file: RunnableFile, ) -> Result<()> { let comp_file_path = problems_dir.join(COMPETITIONS_FILE); if !fs::exists(&comp_file_path)? { @@ -34,8 +35,7 @@ pub fn test( settings, problems_dir, problem_name.as_str(), - None, - solution_lang, + &solution_file, )?; } diff --git a/crates/cli/src/problem/run.rs b/crates/cli/src/problem/run.rs index 09d6948..723eabe 100644 --- a/crates/cli/src/problem/run.rs +++ b/crates/cli/src/problem/run.rs @@ -38,26 +38,46 @@ pub struct RunnableFile { impl RunnableFile { /// Sets the file name if given, and infers the language from the file - /// extension. Otherwise defaults to the category name and the default - /// language from the settings. + /// extension. If the language is provided but not the file name, the + /// default language file is used for that category. + /// + /// If neither is provided, it defaults to the category name + /// and the default language from the settings. pub fn new( settings: &Settings, category: RunnableCategory, name: Option<&String>, + language: Option<&String>, ) -> Result { - let lang: String; - let filename: String; - if name.is_none() { - lang = match category { - RunnableCategory::Solution => settings.problem.default_lang.clone(), - RunnableCategory::Generator => settings.problem.default_generator_lang.clone(), - }; - filename = format!("{category}.{lang}"); - } else { - lang = - get_lang_from_extension(name.context("Failed to get filename of runnable file")?)?; - filename = name.unwrap().to_string(); - } + let (filename, lang) = match (name, language) { + (Some(name), Some(lang)) => { + let file_lang = get_lang_from_extension(name) + .context("Failed to get language from file extension")?; + if file_lang != *lang { + bail!( + "Language from file extension ({file_lang}) does not match provided language ({lang})" + ); + } + (name.to_string(), lang.to_string()) + } + (Some(name), None) => { + let lang = get_lang_from_extension(name) + .context("Failed to get language from file extension")?; + (name.to_string(), lang) + } + (None, Some(lang)) => { + let filename = format!("{category}.{lang}"); + (filename, lang.to_string()) + } + (None, None) => { + let lang = match category { + RunnableCategory::Solution => settings.problem.default_lang.clone(), + RunnableCategory::Generator => settings.problem.default_generator_lang.clone(), + }; + let filename = format!("{category}.{lang}"); + (filename, lang) + } + }; Ok(Self { name: filename, diff --git a/crates/cli/src/problem/solve.rs b/crates/cli/src/problem/solve.rs index 0f2d002..d63a014 100644 --- a/crates/cli/src/problem/solve.rs +++ b/crates/cli/src/problem/solve.rs @@ -1,12 +1,11 @@ -use std::ffi::OsStr; -use std::fs::{self, File}; +use std::fs::File; +use std::io::Write; use std::path::Path; -use anyhow::{bail, Context, Result}; -use normpath::PathExt; -use subprocess::Exec; +use anyhow::{Context, Result}; use super::sync_mappings::get_problem; +use crate::problem::run::{RunCommand, RunnableFile}; use crate::util::get_project_root; use crate::{config::Settings, util::get_input_files_in_directory}; @@ -15,113 +14,41 @@ pub fn solve( settings: &Settings, problems_dir: &Path, problem_name: &str, - solution_file_name: Option<&str>, - solution_lang: Option<&String>, + solution_file: &RunnableFile, ) -> Result<()> { let project_root = get_project_root()?; - let problem = project_root.join(get_problem(problems_dir, problem_name)?); + let problem_path = project_root.join(get_problem(problems_dir, problem_name)?); - let solution_lang = solution_lang.unwrap_or(&settings.problem.default_lang); - let mut solution_file = problem.join(format!("solutions/solution.{solution_lang}")); - if solution_file_name.is_some() { - solution_file = problem.join(format!( - "solutions/{}", - solution_file_name.context("Failed to get solution file name")? - )); - } - solution_file = solution_file.normalize()?.into(); - - if !fs::exists(&solution_file).expect("Failed to check if path exists") { - bail!("Solution file does not exist: {:?}", solution_file); - } - - eprintln!("Using solution file at: {}", solution_file.display()); - - let bin_file = problem.join("solutions/solution.out"); - let script_file = problem.join(format!("solutions/solution.{solution_lang}")); - - let lang_settings = settings - .problem - .solution - .get(solution_lang) - .context(format!( - "Could not get settings for language `{solution_lang}`" - ))?; - - let compile_command = lang_settings.compile_command.clone(); + let run_command = RunCommand::new( + settings, + &problem_path, + solution_file, + problem_path.join("solutions/solution.out"), + problem_path.join(format!("{solution_file}")), + )?; - // Check if the solution file is a script (if it needs compilation or not) - let needs_compilation = compile_command.is_some(); - let compile_command = compile_command.unwrap_or_default(); - if needs_compilation && compile_command.is_empty() { - bail!("compile_command specified in the settings, but array is empty"); - } - - if needs_compilation { - let mut cmd_iter = compile_command.iter(); - let mut final_cmd = Exec::cmd(cmd_iter.next().context("Failed to get command")?); - for c in cmd_iter { - // Replace strings where necessary - final_cmd = match c.as_str() { - "@in_file" => final_cmd.arg(&solution_file), - "@bin_file" => final_cmd.arg(&bin_file), - _ => final_cmd.arg(c), - } - } - eprint!("Compiling the solution file... "); - // Run the compile command - final_cmd.join()?; - eprintln!("Done"); - } - - let run_command = lang_settings.run_command.clone().unwrap_or_default(); - if run_command.is_empty() { - bail!("No run command specified in the settings. It must be specified!"); - } - let cmd_iter = run_command.iter(); - let test_files = get_input_files_in_directory(problem.join("tests"))?; + let test_files = get_input_files_in_directory(problem_path.join("tests"))?; eprintln!("Running the solution file for each test case..."); // Run the file for every test input and generate the corresponding output for test_file in test_files { - let input_file_path = problem.join(format!("tests/{test_file}")); - let output_file_path = problem.join(format!( + let input_file_path = problem_path.join(format!("tests/{test_file}")); + let output_file_path = problem_path.join(format!( "tests/{}.out", test_file .strip_suffix(".in") .context("Failed to strip suffix of test file")? )); - let mut cmd_iter_clone = cmd_iter.clone(); - let cmd = cmd_iter_clone.next().context("Failed to get command")?; - let mut final_cmd = Exec::cmd(match cmd.as_str() { - "@bin_file" => bin_file.as_os_str(), - "@script_file" => script_file.as_os_str(), - _ => OsStr::new(cmd), - }); - - for c in cmd_iter_clone { - // Replace strings where necessary - final_cmd = match c.as_str() { - "@bin_file" => final_cmd.arg(&bin_file), - "@script_file" => final_cmd.arg(&script_file), - _ => final_cmd.arg(c), - } - } + let result = run_command.get_result(Some(&input_file_path))?; + let mut output_file = File::create(output_file_path)?; + output_file.write_all(result.output.as_bytes())?; - let input_file = File::open(input_file_path)?; - let output_file = File::create(output_file_path)?; - - final_cmd = final_cmd.stdin(input_file).stdout(output_file); - final_cmd.capture()?; eprintln!(" - generated output for test file: {test_file}"); } eprintln!("Finished generating outputs for all test cases"); - // Delete the compiled run file, if it exists - if bin_file.exists() { - fs::remove_file(bin_file)?; - } + run_command.cleanup()?; Ok(()) } diff --git a/crates/cli/src/problem/test.rs b/crates/cli/src/problem/test.rs index 3933828..da8af82 100644 --- a/crates/cli/src/problem/test.rs +++ b/crates/cli/src/problem/test.rs @@ -1,14 +1,12 @@ -use std::ffi::OsStr; -use std::fs::{self, File}; +use std::fs::File; use std::io::Read; use std::path::Path; -use std::time::{Duration, Instant}; +use std::time::Duration; -use anyhow::{bail, Context, Result}; -use normpath::PathExt; -use subprocess::{Exec, Redirection}; +use anyhow::{Context, Result}; use crate::config::Settings; +use crate::problem::run::{RunCommand, RunnableFile}; use crate::util::{get_input_files_in_directory, get_project_root}; use super::sync_mappings::get_problem; @@ -18,71 +16,20 @@ pub fn test( settings: &Settings, problems_dir: &Path, problem_name: &str, - solution_file_name: Option<&str>, - solution_lang: Option<&String>, + solution_file: &RunnableFile, ) -> Result<()> { let project_root = get_project_root()?; - let problem = project_root.join(get_problem(problems_dir, problem_name)?); - - let solution_lang = solution_lang.unwrap_or(&settings.problem.default_lang); - let mut solution_file = problem.join(format!("solutions/solution.{solution_lang}")); - if solution_file_name.is_some() { - solution_file = problem.join(format!( - "solutions/{}", - solution_file_name.context("Failed to get solution file name")? - )); - } - solution_file = solution_file.normalize()?.into(); - - if !fs::exists(&solution_file).expect("Failed to check if path exists") { - bail!("Solution file does not exist: {:?}", solution_file); - } - - eprintln!("Using solution file at: {}", solution_file.display()); - - let bin_file = problem.join("solutions/solution.out"); - let script_file = problem.join(format!("solutions/solution.{solution_lang}")); - - let lang_settings = settings - .problem - .solution - .get(solution_lang) - .context(format!( - "Could not get settings for language `{solution_lang}`" - ))?; - - let compile_command = lang_settings.compile_command.clone(); + let problem_path = project_root.join(get_problem(problems_dir, problem_name)?); - // Check if the solution file is a script (if it needs compilation or not) - let needs_compilation = compile_command.is_some(); - let compile_command = compile_command.unwrap_or_default(); - if needs_compilation && compile_command.is_empty() { - bail!("compile_command specified in the settings, but array is empty"); - } - - if needs_compilation { - let mut cmd_iter = compile_command.iter(); - let mut final_cmd = Exec::cmd(cmd_iter.next().context("Failed to get command")?); - for c in cmd_iter { - // Replace strings where necessary - final_cmd = match c.as_str() { - "@in_file" => final_cmd.arg(&solution_file), - "@bin_file" => final_cmd.arg(&bin_file), - _ => final_cmd.arg(c), - } - } - eprint!("Compiling the solution file... "); - // Run the compile command - final_cmd.join()?; - eprintln!("Done"); - } + let run_command = RunCommand::new( + settings, + &problem_path, + solution_file, + problem_path.join("solutions/solution.out"), + problem_path.join(format!("{solution_file}")), + )?; - let run_command = lang_settings.run_command.clone().unwrap_or_default(); - if run_command.is_empty() { - bail!("No run command specified in the settings. It must be specified!"); - } - let cmd_iter = run_command.iter(); - let test_files = get_input_files_in_directory(problem.join("tests"))?; + let test_files = get_input_files_in_directory(problem_path.join("tests"))?; eprintln!("Running the solution file for each test case..."); @@ -91,38 +38,19 @@ pub fn test( let mut total_time: Duration = Duration::new(0, 0); for test_file in test_files { - let input_file_path = problem.join(format!("tests/{test_file}")); - let output_file_path = problem.join(format!( + let input_file_path = problem_path.join(format!("tests/{test_file}")); + let output_file_path = problem_path.join(format!( "tests/{}.out", test_file .strip_suffix(".in") .context("Failed to strip suffix of test file")? )); - let mut cmd_iter_clone = cmd_iter.clone(); - let cmd = cmd_iter_clone.next().context("Failed to get command")?; - let mut final_cmd = Exec::cmd(match cmd.as_str() { - "@bin_file" => bin_file.as_os_str(), - "@script_file" => script_file.as_os_str(), - _ => OsStr::new(cmd), - }); - - for c in cmd_iter_clone { - // Replace strings where necessary - final_cmd = match c.as_str() { - "@bin_file" => final_cmd.arg(&bin_file), - "@script_file" => final_cmd.arg(&script_file), - _ => final_cmd.arg(c), - } - } + let result = run_command.get_result(Some(&input_file_path))?; - let input_file = File::open(input_file_path)?; let mut output_file = File::open(output_file_path)?; - - let start_time = Instant::now(); - final_cmd = final_cmd.stdin(input_file).stdout(Redirection::Pipe); - let out_str = final_cmd.capture()?.stdout_str(); - let elapsed_time = start_time.elapsed(); + let out_str = result.output; + let elapsed_time = result.elapsed_time; // Compare the output with the expected output let expected: &mut Vec = &mut Vec::new(); @@ -150,10 +78,7 @@ pub fn test( total_time.as_secs_f64() ); - // Delete the compiled run file, if it exists - if bin_file.exists() { - fs::remove_file(bin_file)?; - } + run_command.cleanup()?; Ok(()) }