diff --git a/src/compiler.rs b/src/compilation/compiler.rs similarity index 100% rename from src/compiler.rs rename to src/compilation/compiler.rs diff --git a/src/compilation/mod.rs b/src/compilation/mod.rs new file mode 100644 index 0000000..7b7eee9 --- /dev/null +++ b/src/compilation/mod.rs @@ -0,0 +1,6 @@ +mod compiler; +mod template; + +pub use compiler::P1XX; +pub use template::generate_main; +pub use compiler::CompilationError as Error; \ No newline at end of file diff --git a/src/template.rs b/src/compilation/template.rs similarity index 100% rename from src/template.rs rename to src/compilation/template.rs diff --git a/src/config.rs b/src/config.rs index e50ff80..5e34b2d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,8 +1,7 @@ use core::fmt; use std::{env, fs, io, path}; use configparser::ini; -use crate::connection_manager::Credentials; -use crate::{connection_manager, debug, ux}; +use crate::{debug, fetch, ux}; #[derive(Debug)] pub enum Error { @@ -41,7 +40,7 @@ pub struct Config { pub config_dir: path::PathBuf, pub cache_dir: path::PathBuf, pub tmp_dir: path::PathBuf, - pub credentials: Option + pub credentials: Option } impl Config { @@ -104,7 +103,7 @@ impl Config { let password = auth.get("password"); if let (Some(Some(email)), Some(Some(password))) = (email, password) { - self.credentials = Some(connection_manager::Credentials::new(email.as_bytes(), password.as_bytes())); + self.credentials = Some(fetch::Credentials::new(email.as_bytes(), password.as_bytes())); } } } diff --git a/src/download.rs b/src/download.rs deleted file mode 100644 index 09f3caa..0000000 --- a/src/download.rs +++ /dev/null @@ -1,126 +0,0 @@ -use std::fmt::Display; -use std::fmt::Formatter; -use std::path; -use std::fs; -use std::io; -use crate::{connection_manager, debug, problem, ux}; - -pub enum UnzipError { - CantReadFile(io::Error), - CantCreateFile(io::Error), - CantInflateFile(io::Error), - ZipError(zip::result::ZipError) -} - -impl Display for UnzipError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - UnzipError::CantReadFile(e) => write!(f, "Couldn't read the file: {}", e), - UnzipError::CantCreateFile(e) => write!(f, "Couldn't create a file: {}", e), - UnzipError::CantInflateFile(e) => write!(f, "Couldn't inflate a file: {}", e), - UnzipError::ZipError(e) => write!(f, "Zip raised an error: {:?}", e) - } - } -} - -fn unzip_samples(zip_path: &path::Path, output_folder: &path::Path) -> Result<(), UnzipError> { - debug!("Unzipping {} to {}", zip_path.to_string_lossy(), output_folder.to_string_lossy()); - - let zip_file = fs::File::open(zip_path) - .map_err(UnzipError::CantReadFile)?; - let mut archive = zip::ZipArchive::new(zip_file) - .map_err(UnzipError::ZipError)?; - - debug!("Creating output folder {}", output_folder.to_string_lossy()); - fs::create_dir_all(output_folder).map_err(UnzipError::CantCreateFile)?; - - for i in 0..archive.len() { - let mut file = archive.by_index(i).unwrap(); - let outpath = match filter_samples(&file) { - Some(path) => output_folder.join(path), - None => continue - }; - - if file.is_dir() { - debug!("Inflating folder {}", output_folder.to_string_lossy()); - fs::create_dir_all(outpath).map_err(UnzipError::CantCreateFile)?; - } else { - if let Some(parent) = outpath.parent() { - if !parent.exists() { - debug!("Inflating folder {}", parent.to_string_lossy()); - fs::create_dir_all(parent).map_err(UnzipError::CantCreateFile)?; - } - } - - debug!("Inflating file {}", outpath.to_string_lossy()); - let mut outfile = fs::File::create(&outpath).map_err(UnzipError::CantCreateFile)?; - io::copy(&mut file, &mut outfile).map_err(UnzipError::CantInflateFile)?; - } - } - Ok(()) -} - -fn filter_samples(zip_file: &zip::read::ZipFile) -> Option { - let path = zip_file.enclosed_name()?; - if let Some(filename) = path.file_name() { - if zip_file.is_file() && filename.to_string_lossy().starts_with("sample") { - Some(filename.into()) - } else { - None - } - } else { - None - } -} - -pub fn download_problem_zip(problem: &problem::Problem, connection: &mut connection_manager::ConnectionManager) -> (ux::TaskStatus, Option) { - let path = problem.work_dir.join("problem.zip"); - - if path.is_file() { - debug!("Problem zip already downloaded"); - (ux::TaskStatus::SkipGood, None) - } else if path.is_dir() { - debug!("The download path is a folder"); - (ux::TaskStatus::SkipBad, None) - } else { - match connection.get_file(&problem.zip_url, &path) { - Ok(()) => (ux::TaskStatus::Done, None), - Err(e) => (ux::TaskStatus::Fail, Some(e)), - } - } -} - -pub fn download_problem_main(problem: &problem::Problem, connection: &mut connection_manager::ConnectionManager) -> (ux::TaskStatus, Option) { - let path = problem.work_dir.join("main.cc"); - - if problem.has_main || path.is_file() { - debug!("Problem main.cc already downloaded or unnecessary"); - (ux::TaskStatus::SkipGood, None) - } else if path.is_dir() { - debug!("The download path is a folder"); - (ux::TaskStatus::SkipBad, None) - } else { - match connection.get_file(&problem.main_cc_url, &path) { - Ok(()) => (ux::TaskStatus::Done, None), - Err(e) => (ux::TaskStatus::Fail, Some(e)) - } - } -} - -pub fn unzip_problem_tests(problem: &problem::Problem) -> (ux::TaskStatus, Option) { - let zip_path = problem.work_dir.join("problem.zip"); - let tests_path = problem.work_dir.join("samples"); - - if tests_path.is_dir() { - debug!("Problem tests already extracted"); - (ux::TaskStatus::SkipGood, None) - } else if tests_path.is_file() || !zip_path.exists() { - debug!("Unable to extract problem tests"); - (ux::TaskStatus::SkipBad, None) - } else { - match unzip_samples(&zip_path, &tests_path) { - Ok(()) => (ux::TaskStatus::Done, None), - Err(e) => (ux::TaskStatus::Fail, Some(e)) - } - } -} \ No newline at end of file diff --git a/src/connection_manager.rs b/src/fetch/connection_manager.rs similarity index 79% rename from src/connection_manager.rs rename to src/fetch/connection_manager.rs index 5b65714..6b82f27 100644 --- a/src/connection_manager.rs +++ b/src/fetch/connection_manager.rs @@ -2,6 +2,7 @@ use std::{fmt, fs, io, path}; use std::io::Write; use curl::easy; use crate::{config, debug, warning}; +use crate::fetch::credentials; pub enum Error { CurlError(curl::Error), @@ -81,7 +82,7 @@ impl ConnectionManager { Ok(()) } - fn try_to_authenticate(&mut self, credentials: &Credentials) -> Result { + fn try_to_authenticate(&mut self, credentials: &credentials::Credentials) -> Result { if let Some(form) = credentials.build_form() { debug!("Attempting to authenticate"); self.handle.url("https://jutge.org/")?; @@ -112,40 +113,4 @@ impl ConnectionManager { Ok(!String::from_utf8_lossy(&response).contains("Did you sign in?")) } -} - -#[derive(Clone)] -pub struct Credentials { - email: Vec, - password: Vec -} - -impl Credentials { - pub fn new(email: &[u8], password: &[u8]) -> Credentials { - Credentials { - email: Vec::from(email), - password: Vec::from(password) - } - } - - fn build_form(&self) -> Option { - let mut form = easy::Form::new(); - - form.part("email").contents(self.email.as_slice()).add().ok()?; - form.part("password").contents(self.password.as_slice()).add().ok()?; - form.part("submit").contents(b"").add().ok()?; - - Some(form) - } -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn credentials_build_form_test() { - let credentials = Credentials::new(b"me@example.com", b"1234"); - assert!(credentials.build_form().is_some()); - } } \ No newline at end of file diff --git a/src/fetch/credentials.rs b/src/fetch/credentials.rs new file mode 100644 index 0000000..be55322 --- /dev/null +++ b/src/fetch/credentials.rs @@ -0,0 +1,37 @@ +use curl::easy; + +#[derive(Clone)] +pub struct Credentials { + email: Vec, + password: Vec +} + +impl Credentials { + pub fn new(email: &[u8], password: &[u8]) -> Credentials { + Credentials { + email: Vec::from(email), + password: Vec::from(password) + } + } + + pub fn build_form(&self) -> Option { + let mut form = easy::Form::new(); + + form.part("email").contents(self.email.as_slice()).add().ok()?; + form.part("password").contents(self.password.as_slice()).add().ok()?; + form.part("submit").contents(b"").add().ok()?; + + Some(form) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn credentials_build_form_test() { + let credentials = Credentials::new(b"me@example.com", b"1234"); + assert!(credentials.build_form().is_some()); + } +} \ No newline at end of file diff --git a/src/fetch/download.rs b/src/fetch/download.rs new file mode 100644 index 0000000..19bf5dc --- /dev/null +++ b/src/fetch/download.rs @@ -0,0 +1,54 @@ +use crate::{debug, problem, ux}; +use crate::fetch::{connection_manager, unzip}; + +pub fn download_problem_zip(problem: &problem::Problem, connection: &mut connection_manager::ConnectionManager) -> (ux::TaskStatus, Option) { + let path = problem.work_dir.join("problem.zip"); + + if path.is_file() { + debug!("Problem zip already downloaded"); + (ux::TaskStatus::SkipGood, None) + } else if path.is_dir() { + debug!("The download path is a folder"); + (ux::TaskStatus::SkipBad, None) + } else { + match connection.get_file(&problem.zip_url, &path) { + Ok(()) => (ux::TaskStatus::Done, None), + Err(e) => (ux::TaskStatus::Fail, Some(e)), + } + } +} + +pub fn download_problem_main(problem: &problem::Problem, connection: &mut connection_manager::ConnectionManager) -> (ux::TaskStatus, Option) { + let path = problem.work_dir.join("main.cc"); + + if problem.has_main || path.is_file() { + debug!("Problem main.cc already downloaded or unnecessary"); + (ux::TaskStatus::SkipGood, None) + } else if path.is_dir() { + debug!("The download path is a folder"); + (ux::TaskStatus::SkipBad, None) + } else { + match connection.get_file(&problem.main_cc_url, &path) { + Ok(()) => (ux::TaskStatus::Done, None), + Err(e) => (ux::TaskStatus::Fail, Some(e)) + } + } +} + +pub fn unzip_problem_tests(problem: &problem::Problem) -> (ux::TaskStatus, Option) { + let zip_path = problem.work_dir.join("problem.zip"); + let tests_path = problem.work_dir.join("samples"); + + if tests_path.is_dir() { + debug!("Problem tests already extracted"); + (ux::TaskStatus::SkipGood, None) + } else if tests_path.is_file() || !zip_path.exists() { + debug!("Unable to extract problem tests"); + (ux::TaskStatus::SkipBad, None) + } else { + match unzip::unzip_samples(&zip_path, &tests_path) { + Ok(()) => (ux::TaskStatus::Done, None), + Err(e) => (ux::TaskStatus::Fail, Some(e)) + } + } +} \ No newline at end of file diff --git a/src/fetch/mod.rs b/src/fetch/mod.rs new file mode 100644 index 0000000..0e03421 --- /dev/null +++ b/src/fetch/mod.rs @@ -0,0 +1,52 @@ +use std::fmt; +use crate::{config, error, problem, ux, warning}; + +mod download; +mod connection_manager; +mod credentials; +mod unzip; + +pub use credentials::Credentials as Credentials; + +pub fn fetch_resources(problem: &problem::Problem, config: &config::Config) -> Result<(bool, bool, bool), crate::Error> { + let mut connection = connection_manager::ConnectionManager::new(config) + .map_err(|e| crate::Error { + description: format!("Couldn't start the connection manager: {}", e), + exitcode: exitcode::IOERR + })?; + + let zip = execute_task("Downloading problem zip", || download::download_problem_zip(problem, &mut connection)); + let main_cc = execute_task("Downloading problem main.cc", || download::download_problem_main(problem, &mut connection)); + let tests = execute_task("Extracting tests", || download::unzip_problem_tests(problem)); + + if !zip { + warning!("Unable to retrieve tests!"); + } + + if !main_cc { + return Err( crate::Error { + description: String::from("Unable to retrieve the main.cc file, which is required to compile your binary!"), + exitcode: exitcode::IOERR + }); + } + + if !tests { + warning!("Unable to unzip tests!"); + } + + Ok((zip, main_cc, tests)) +} + +fn execute_task(name: &str, mut task: T) -> bool + where + T: FnMut() -> (ux::TaskStatus, Option) +{ + ux::show_task_status(name, ux::TaskType::Fetch, &ux::TaskStatus::InProgress); + let (status, err) = task(); + + ux::show_task_status(name, ux::TaskType::Fetch, &status); + if let Some(err) = err { + error!("The task [{}] returned the following error: {}", name, err); + } + status.is_ok() +} \ No newline at end of file diff --git a/src/fetch/unzip.rs b/src/fetch/unzip.rs new file mode 100644 index 0000000..5c50b58 --- /dev/null +++ b/src/fetch/unzip.rs @@ -0,0 +1,70 @@ +use std::{fmt, fs, io, path}; +use crate::debug; + +pub enum Error { + CantReadFile(io::Error), + CantCreateFile(io::Error), + CantInflateFile(io::Error), + ZipError(zip::result::ZipError) +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::CantReadFile(e) => write!(f, "Couldn't read the file: {}", e), + Error::CantCreateFile(e) => write!(f, "Couldn't create a file: {}", e), + Error::CantInflateFile(e) => write!(f, "Couldn't inflate a file: {}", e), + Error::ZipError(e) => write!(f, "Zip raised an error: {:?}", e) + } + } +} + +pub fn unzip_samples(zip_path: &path::Path, output_folder: &path::Path) -> Result<(), Error> { + debug!("Unzipping {} to {}", zip_path.to_string_lossy(), output_folder.to_string_lossy()); + + let zip_file = fs::File::open(zip_path) + .map_err(Error::CantReadFile)?; + let mut archive = zip::ZipArchive::new(zip_file) + .map_err(Error::ZipError)?; + + debug!("Creating output folder {}", output_folder.to_string_lossy()); + fs::create_dir_all(output_folder).map_err(Error::CantCreateFile)?; + + for i in 0..archive.len() { + let mut file = archive.by_index(i).unwrap(); + let outpath = match filter_samples(&file) { + Some(path) => output_folder.join(path), + None => continue + }; + + if file.is_dir() { + debug!("Inflating folder {}", output_folder.to_string_lossy()); + fs::create_dir_all(outpath).map_err(Error::CantCreateFile)?; + } else { + if let Some(parent) = outpath.parent() { + if !parent.exists() { + debug!("Inflating folder {}", parent.to_string_lossy()); + fs::create_dir_all(parent).map_err(Error::CantCreateFile)?; + } + } + + debug!("Inflating file {}", outpath.to_string_lossy()); + let mut outfile = fs::File::create(&outpath).map_err(Error::CantCreateFile)?; + io::copy(&mut file, &mut outfile).map_err(Error::CantInflateFile)?; + } + } + Ok(()) +} + +fn filter_samples(zip_file: &zip::read::ZipFile) -> Option { + let path = zip_file.enclosed_name()?; + if let Some(filename) = path.file_name() { + if zip_file.is_file() && filename.to_string_lossy().starts_with("sample") { + Some(filename.into()) + } else { + None + } + } else { + None + } +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 66912d9..e2dfbe9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,18 +1,13 @@ use std::{env, fmt, ops, path}; -use std::fmt::Display; use termion::{color, style}; -use crate::compiler::CompilationError; use crate::problem::Problem; -use crate::testsuite::TestSuite; mod problem; pub mod ux; -mod download; -mod testsuite; -mod template; -mod compiler; +mod testing; +mod compilation; +mod fetch; mod config; -mod connection_manager; #[cfg(test)] mod test_utils; @@ -47,7 +42,7 @@ pub fn run() -> Result { let problem = Problem::new(&config)?; debug!("Done! Problem details: {:?}", problem); - let (_zip, _main_cc, tests) = fetch_resources(&problem, &config)?; + let (_zip, _main_cc, tests) = fetch::fetch_resources(&problem, &config)?; let tests = [ load_tests("jutge.org", problem.work_dir.join("samples").as_path(), !tests), @@ -55,7 +50,7 @@ pub fn run() -> Result { ]; debug!("Generating sources..."); - let generated_sources = template::generate_main(&problem)?; + let generated_sources = compilation::generate_main(&problem)?; println!(); let binary = execute_compiler(&problem, generated_sources.as_path()); @@ -64,53 +59,10 @@ pub fn run() -> Result { Ok(show_veredict(binary, passed_tests, total_tests)) } -fn execute_task(name: &str, mut task: T) -> bool -where - T: FnMut() -> (ux::TaskStatus, Option) -{ - ux::show_task_status(name, ux::TaskType::Fetch, &ux::TaskStatus::InProgress); - let (status, err) = task(); - - ux::show_task_status(name, ux::TaskType::Fetch, &status); - if let Some(err) = err { - error!("The task [{}] returned the following error: {}", name, err); - } - status.is_ok() -} - -fn fetch_resources(problem: &Problem, config: &config::Config) -> Result<(bool, bool, bool), Error> { - let mut connection = connection_manager::ConnectionManager::new(config) - .map_err(|e| Error { - description: format!("Couldn't start the connection manager: {}", e), - exitcode: exitcode::IOERR - })?; - - let zip = execute_task("Downloading problem zip", || download::download_problem_zip(problem, &mut connection)); - let main_cc = execute_task("Downloading problem main.cc", || download::download_problem_main(problem, &mut connection)); - let tests = execute_task("Extracting tests", || download::unzip_problem_tests(problem)); - - if !zip { - warning!("Unable to retrieve tests!"); - } - - if !main_cc { - return Err( Error { - description: String::from("Unable to retrieve the main.cc file, which is required to compile your binary!"), - exitcode: exitcode::IOERR - }); - } - - if !tests { - warning!("Unable to unzip tests!"); - } - - Ok((zip, main_cc, tests)) -} - -fn load_tests(name: &str, dir: &path::Path, ignore_missing_dir: bool) -> Option { +fn load_tests(name: &str, dir: &path::Path, ignore_missing_dir: bool) -> Option { debug!("Loading {} tests...", name); - match TestSuite::from_dir(name, dir) { - Err(testsuite::Error::PathDoesntExist) if ignore_missing_dir => None, + match testing::TestSuite::from_dir(name, dir) { + Err(testing::Error::PathDoesntExist) if ignore_missing_dir => None, Err(e) => { error!("Error loading {} tests: {}", name, e); None }, Ok(testsuite) => Some(testsuite) } @@ -120,7 +72,7 @@ fn execute_compiler(problem: &Problem, generated_sources: &path::Path) -> bool { const TASK: &str = "Compilation"; ux::show_task_status(TASK, ux::TaskType::Test, &ux::TaskStatus::InProgress); - match compiler::P1XX.compile_problem(problem, generated_sources) { + match compilation::P1XX.compile_problem(problem, generated_sources) { Ok(()) => { ux::show_task_status(TASK, ux::TaskType::Test, &ux::TaskStatus::Pass); true @@ -128,7 +80,7 @@ fn execute_compiler(problem: &Problem, generated_sources: &path::Path) -> bool { Err(e) => { ux::show_task_status(TASK, ux::TaskType::Test, &ux::TaskStatus::Fail); match e.error { - CompilationError::CompilerError(stderr) => { + compilation::Error::CompilerError(stderr) => { ux::show_task_output(format!("Compilation output (pass {})", e.pass).as_str(), &stderr); } _ => error!("Compilation failed unexpectedly: {}", e) @@ -138,7 +90,7 @@ fn execute_compiler(problem: &Problem, generated_sources: &path::Path) -> bool { } } -fn run_tests(testsuites: &[Option], binary: &path::Path, skip_tests: bool) -> (usize, usize) { +fn run_tests(testsuites: &[Option], binary: &path::Path, skip_tests: bool) -> (usize, usize) { let mut passed: usize = 0; let mut total: usize = 0; @@ -180,4 +132,4 @@ mod test { assert_eq!(show_veredict(true, 0, 1), exitcode::DATAERR); assert_eq!(show_veredict(true, 1, 1), exitcode::OK); } -} \ No newline at end of file +} diff --git a/src/testing/diff_display.rs b/src/testing/diff_display.rs new file mode 100644 index 0000000..6b2c997 --- /dev/null +++ b/src/testing/diff_display.rs @@ -0,0 +1,72 @@ +use termion::color; +use crate::ux; + +pub struct DiffDisplay { + text: String, + side_width: usize, + left_color: &'static dyn color::Color, + right_color: &'static dyn color::Color +} + +impl DiffDisplay { + pub fn new(left_title: &str, right_title: &str, left_color: &'static dyn color::Color, right_color: &'static dyn color::Color) -> Self { + let side_width = ((ux::get_terminal_width() - 7) / 2) as usize; + let mut dd = DiffDisplay { text: String::new(), side_width, left_color, right_color }; + dd.draw_horizontal_line('╭', '┬', '╮'); + dd.write_centered_row(left_title, right_title); + dd.draw_horizontal_line('├', '┼', '┤'); + dd + } + + fn draw_horizontal_line(&mut self, left: char, mid: char, right: char) { + self.text.push_str(format!("{l}─{:─^w$}─{m}─{:─^w$}─{r}\n", "", "", + l = left, m = mid, r = right, w = self.side_width).as_str()); + } + + pub fn end(&mut self) { + self.draw_horizontal_line('╰', '┴', '╯'); + } + + fn write_centered_row(&mut self, left: &str, right: &str) { + let left = self.trim_line(left); + let right = self.trim_line(right); + self.text.push_str(format!("│ {l:^w$} │ {r:^w$} │\n", + l = left, r = right, w = self.side_width).as_str()); + } + + fn write_row(&mut self, left: &str, mid: char, right: &str, left_color: &dyn color::Color, right_color: &dyn color::Color) { + let left = self.trim_line(left); + let right = self.trim_line(right); + self.text.push_str(format!("│ {lc}{l:w$}{nc} {m} {rc}{r:w$}{nc} │\n", + l = left, m = mid, r = right, + lc = color::Fg(left_color), + rc = color::Fg(right_color), + nc = color::Fg(color::Reset), + w = self.side_width).as_str()); + } + + pub fn write_left(&mut self, left: &str) { + self.write_row(left, '<', "", self.left_color, self.right_color); + } + + pub fn write_right(&mut self, right: &str) { + self.write_row("", '>', right, self.left_color, self.right_color); + } + + pub fn write_both(&mut self, left: &str, right: &str) { + self.write_row(left, '│', right, &color::Reset, &color::Reset); + } + + fn trim_line(&self, line: &str) -> String { + if line.len() <= self.side_width { + line.to_owned() + } else { + let line = &line[0..self.side_width-3]; + line.to_owned() + "..." + } + } + + pub fn build(self) -> String { + self.text + } +} \ No newline at end of file diff --git a/src/testing/mod.rs b/src/testing/mod.rs new file mode 100644 index 0000000..be39cc8 --- /dev/null +++ b/src/testing/mod.rs @@ -0,0 +1,6 @@ +mod test; +mod testsuite; +mod diff_display; + +pub use testsuite::TestSuite; +pub use testsuite::Error as Error; \ No newline at end of file diff --git a/src/testing/test.rs b/src/testing/test.rs new file mode 100644 index 0000000..4eb35be --- /dev/null +++ b/src/testing/test.rs @@ -0,0 +1,94 @@ +use std::{fs, io, path, process}; +use std::io::Write; +use termion::color; +use crate::{debug, ux}; +use crate::testing::diff_display; + +pub struct Test { + inputs: String, + outputs: String +} + +pub struct TestResult { + pub status: ux::TaskStatus, + pub error: Option, + pub diff: String +} + +impl Test { + pub fn from_files(input_file: &path::Path, output_file: &path::Path) -> Option { + if let Ok(inputs) = fs::read_to_string(input_file) { + if let Ok(outputs) = fs::read_to_string(output_file) { + debug!("Found test: {} => {}", input_file.to_string_lossy(), output_file.to_string_lossy()); + return Some(Test { inputs, outputs }); + } + } + None + } + + pub fn run(&self, binary: &path::Path) -> TestResult { + debug!("Executing the binary"); + let process = process::Command::new(binary) + .stdin(process::Stdio::piped()) + .stdout(process::Stdio::piped()) + .stderr(process::Stdio::piped()) + .spawn(); + + let mut process = match process { + Ok(p) => p, + Err(e) => return TestResult { status: ux::TaskStatus::Fail, error: Some(e), diff: String::new() } + }; + + debug!("Sending inputs"); + match process.stdin.take() { + Some(mut stdin) => if let Err(e) = stdin.write(self.inputs.as_bytes()) { + return TestResult { status: ux::TaskStatus::Fail, error: Some(e), diff: String::new() } + }, + None => debug!("The input pipe was closed by the program!") + }; + + debug!("Waiting for the program to end"); + let output = match process.wait_with_output() { + Ok(o) => o, + Err(e) => return TestResult { status: ux::TaskStatus::Fail, error: Some(e), diff: String::new() } + }; + + debug!("Capturing output"); + let binary_output = String::from_utf8_lossy(&output.stdout).to_string(); + + debug!("Computing diff"); + let (pass, diff) = parse_diff(diff::lines(&self.outputs, &binary_output)); + let status = if pass { + ux::TaskStatus::Pass + } else { + ux::TaskStatus::Fail + }; + TestResult { status, error: None, diff } + } +} + +fn parse_diff(diff: Vec>) -> (bool, String) { + debug!("Parsing diff"); + let mut pass = true; + let mut dd = + diff_display::DiffDisplay::new("Expected output", "Your output", &color::Green, &color::Red); + + for line in diff { + match line { + diff::Result::Left(l) => { + pass = false; + dd.write_left(l); + } + diff::Result::Both(l, r) => { + dd.write_both(l, r); + } + diff::Result::Right(r) => { + pass = false; + dd.write_right(r); + } + } + } + + dd.end(); + (pass, dd.build()) +} \ No newline at end of file diff --git a/src/testing/testsuite.rs b/src/testing/testsuite.rs new file mode 100644 index 0000000..2510c37 --- /dev/null +++ b/src/testing/testsuite.rs @@ -0,0 +1,81 @@ +use std::fmt; +use std::path; +use std::fs; +use std::io; +use termion::style; +use crate::{error, ux}; +use crate::testing::test; + +pub enum Error { + PathDoesntExist, + PathIsNotADir, + CantReadDir(io::Error) +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Error::PathDoesntExist => write!(f, "The tests path doesn't exist!"), + Error::PathIsNotADir => write!(f, "The tests path isn't a directory!"), + Error::CantReadDir(e) => write!(f, "Couldn't read the tests directory: {}", e) + } + } +} + +pub struct TestSuite { + name: String, + tests: Vec +} + +impl TestSuite { + pub fn from_dir(name: &str, dir: &path::Path) -> Result { + if !dir.exists() { + Err(Error::PathDoesntExist) + } else if !dir.is_dir() { + Err(Error::PathIsNotADir) + } else { + let mut suite = TestSuite { name: name.to_owned(), tests: Vec::new() }; + let mut input_files: Vec = fs::read_dir(dir) + .map_err(Error::CantReadDir)? + .flatten() + .filter(|f| f.path().extension().unwrap_or_else(|| "".as_ref()) == "inp") + .collect(); + input_files.sort_by_key(|a| a.file_name()); + input_files.iter() + .map(|inp| (inp.path(), inp.path().with_extension("cor"))) + .for_each(|(inp, out)| + if let Some(test) = test::Test::from_files(inp.as_path(), out.as_path()) { + suite.tests.push(test); + }); + + Ok(suite) + } + } + + pub fn run(&self, binary: &path::Path, should_skip: bool) -> usize { + let mut pass_count: usize = 0; + for (i, test) in self.tests.iter().enumerate() { + let test_name = format!("{} test {}", self.name, i+1); + if should_skip { + ux::show_task_status(&test_name, ux::TaskType::Test, &ux::TaskStatus::SkipBad); + } else { + ux::show_task_status(&test_name, ux::TaskType::Test, &ux::TaskStatus::InProgress); + let result = test.run(binary); + ux::show_task_status(&test_name, ux::TaskType::Test, &result.status); + if let Some(e) = result.error { + error!("Error running test: {}", e); + } else if result.status.is_ok() { + pass_count += 1; + } else { + ux::show_task_output("Test diff", format!("{}{}", style::Reset, result.diff).as_str()); + } + } + } + + pass_count + } + + pub fn count(&self) -> usize { + self.tests.len() + } +} \ No newline at end of file diff --git a/src/testsuite.rs b/src/testsuite.rs deleted file mode 100644 index 4f25f97..0000000 --- a/src/testsuite.rs +++ /dev/null @@ -1,239 +0,0 @@ -use std::{fmt, process}; -use std::path; -use std::fs; -use std::io; -use std::io::{Write}; -use termion::{color, style}; -use crate::{debug, error, ux}; - -pub enum Error { - PathDoesntExist, - PathIsNotADir, - CantReadDir(io::Error) -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::PathDoesntExist => write!(f, "The tests path doesn't exist!"), - Error::PathIsNotADir => write!(f, "The tests path isn't a directory!"), - Error::CantReadDir(e) => write!(f, "Couldn't read the tests directory: {}", e) - } - } -} - -pub struct TestSuite { - name: String, - tests: Vec -} - -impl TestSuite { - pub fn from_dir(name: &str, dir: &path::Path) -> Result { - if !dir.exists() { - Err(Error::PathDoesntExist) - } else if !dir.is_dir() { - Err(Error::PathIsNotADir) - } else { - let mut suite = TestSuite { name: name.to_owned(), tests: Vec::new() }; - let mut input_files: Vec = fs::read_dir(dir) - .map_err(Error::CantReadDir)? - .flatten() - .filter(|f| f.path().extension().unwrap_or_else(|| "".as_ref()) == "inp") - .collect(); - input_files.sort_by_key(|a| a.file_name()); - input_files.iter() - .map(|inp| (inp.path(), inp.path().with_extension("cor"))) - .for_each(|(inp, out)| - if let Some(test) = Test::from_files(inp.as_path(), out.as_path()) { - suite.tests.push(test); - }); - - Ok(suite) - } - } - - pub fn run(&self, binary: &path::Path, should_skip: bool) -> usize { - let mut pass_count: usize = 0; - for (i, test) in self.tests.iter().enumerate() { - let test_name = format!("{} test {}", self.name, i+1); - if should_skip { - ux::show_task_status(&test_name, ux::TaskType::Test, &ux::TaskStatus::SkipBad); - } else { - ux::show_task_status(&test_name, ux::TaskType::Test, &ux::TaskStatus::InProgress); - let result = test.run(binary); - ux::show_task_status(&test_name, ux::TaskType::Test, &result.status); - if let Some(e) = result.error { - error!("Error running test: {}", e); - } else if result.status.is_ok() { - pass_count += 1; - } else { - ux::show_task_output("Test diff", format!("{}{}", style::Reset, result.diff).as_str()); - } - } - } - - pass_count - } - - pub fn count(&self) -> usize { - self.tests.len() - } -} - -struct Test { - inputs: String, - outputs: String -} - -struct TestResult { - status: ux::TaskStatus, - error: Option, - diff: String -} - -impl Test { - fn from_files(input_file: &path::Path, output_file: &path::Path) -> Option { - if let Ok(inputs) = fs::read_to_string(input_file) { - if let Ok(outputs) = fs::read_to_string(output_file) { - debug!("Found test: {} => {}", input_file.to_string_lossy(), output_file.to_string_lossy()); - return Some(Test { inputs, outputs }); - } - } - None - } - - fn run(&self, binary: &path::Path) -> TestResult { - debug!("Executing the binary"); - let process = process::Command::new(binary) - .stdin(process::Stdio::piped()) - .stdout(process::Stdio::piped()) - .stderr(process::Stdio::piped()) - .spawn(); - - let mut process = match process { - Ok(p) => p, - Err(e) => return TestResult { status: ux::TaskStatus::Fail, error: Some(e), diff: String::new() } - }; - - debug!("Sending inputs"); - match process.stdin.take() { - Some(mut stdin) => if let Err(e) = stdin.write(self.inputs.as_bytes()) { - return TestResult { status: ux::TaskStatus::Fail, error: Some(e), diff: String::new() } - }, - None => debug!("The input pipe was closed by the program!") - }; - - debug!("Waiting for the program to end"); - let output = match process.wait_with_output() { - Ok(o) => o, - Err(e) => return TestResult { status: ux::TaskStatus::Fail, error: Some(e), diff: String::new() } - }; - - debug!("Capturing output"); - let binary_output = String::from_utf8_lossy(&output.stdout).to_string(); - - debug!("Computing diff"); - let (pass, diff) = parse_diff(diff::lines(&self.outputs, &binary_output)); - let status = if pass { - ux::TaskStatus::Pass - } else { - ux::TaskStatus::Fail - }; - TestResult { status, error: None, diff } - } -} - -// TODO: This is a mess, fix it -fn parse_diff(diff: Vec>) -> (bool, String) { - debug!("Parsing diff"); - let mut pass = true; - let mut dd = - DiffDisplay::new("Expected output", "Your output", &color::Green, &color::Red); - - for line in diff { - match line { - diff::Result::Left(l) => { - pass = false; - dd.write_left(l); - } - diff::Result::Both(l, r) => { - dd.write_both(l, r); - } - diff::Result::Right(r) => { - pass = false; - dd.write_right(r); - } - } - } - - dd.end(); - (pass, dd.text) -} - - - -struct DiffDisplay { - text: String, - side_width: usize, - left_color: &'static dyn color::Color, - right_color: &'static dyn color::Color -} - -impl DiffDisplay { - fn new(left_title: &str, right_title: &str, left_color: &'static dyn color::Color, right_color: &'static dyn color::Color) -> Self { - let side_width = ((ux::get_terminal_width() - 7) / 2) as usize; - let mut dd = DiffDisplay { text: String::new(), side_width, left_color, right_color }; - dd.draw_horizontal_line('╭', '┬', '╮'); - dd.write_centered_row(left_title, right_title); - dd.draw_horizontal_line('├', '┼', '┤'); - dd - } - - fn draw_horizontal_line(&mut self, left: char, mid: char, right: char) { - self.text.push_str(format!("{l}─{:─^w$}─{m}─{:─^w$}─{r}\n", "", "", - l = left, m = mid, r = right, w = self.side_width).as_str()); - } - - fn end(&mut self) { - self.draw_horizontal_line('╰', '┴', '╯'); - } - - fn write_centered_row(&mut self, left: &str, right: &str) { - let left = self.trim_line(left); - let right = self.trim_line(right); - self.text.push_str(format!("│ {l:^w$} │ {r:^w$} │\n", - l = left, r = right, w = self.side_width).as_str()); - } - - fn write_row(&mut self, left: &str, mid: char, right: &str, left_color: &dyn color::Color, right_color: &dyn color::Color) { - let left = self.trim_line(left); - let right = self.trim_line(right); - self.text.push_str(format!("│ {lc}{l:w$}{nc} {m} {rc}{r:w$}{nc} │\n", - l = left, m = mid, r = right, - lc = color::Fg(left_color), - rc = color::Fg(right_color), - nc = color::Fg(color::Reset), - w = self.side_width).as_str()); - } - - fn write_left(&mut self, left: &str) { - self.write_row(left, '<', "", self.left_color, self.right_color); - } - - fn write_right(&mut self, right: &str) { - self.write_row("", '>', right, self.left_color, self.right_color); - } - - fn write_both(&mut self, left: &str, right: &str) { - self.write_row(left, '│', right, &color::Reset, &color::Reset); - } - - fn trim_line(&self, line: &str) -> String { - if line.len() <= self.side_width { - line.to_owned() - } else { - let line = &line[0..self.side_width-3]; - line.to_owned() + "..." - } - } -} \ No newline at end of file