From 3f23d266002939284f9b9547696fc35d724280bc Mon Sep 17 00:00:00 2001 From: Ben Date: Mon, 30 Oct 2023 18:53:37 +0000 Subject: [PATCH] feat: add support for exercises that depend on crates --- exercises/crates/mockall/mocks1/Cargo.toml | 11 +++ exercises/crates/mockall/mocks1/mocks1.rs | 45 +++++++++ info.toml | 7 ++ src/exercise.rs | 101 +++++++++++++++++---- src/run.rs | 4 +- src/verify.rs | 12 +-- 6 files changed, 155 insertions(+), 25 deletions(-) create mode 100644 exercises/crates/mockall/mocks1/Cargo.toml create mode 100644 exercises/crates/mockall/mocks1/mocks1.rs diff --git a/exercises/crates/mockall/mocks1/Cargo.toml b/exercises/crates/mockall/mocks1/Cargo.toml new file mode 100644 index 00000000..48082dae --- /dev/null +++ b/exercises/crates/mockall/mocks1/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "mocks1" +version = "0.0.1" +edition = "2021" + +[dependencies] +mockall = "0.11.4" + +[[bin]] +name = "mocks1" +path = "mocks1.rs" \ No newline at end of file diff --git a/exercises/crates/mockall/mocks1/mocks1.rs b/exercises/crates/mockall/mocks1/mocks1.rs new file mode 100644 index 00000000..9f2fefe2 --- /dev/null +++ b/exercises/crates/mockall/mocks1/mocks1.rs @@ -0,0 +1,45 @@ +// mocks1.rs +// +// Mockall is a powerful mock object library for Rust. It provides tools to create +// mock versions of almost any trait or struct. They can be used in unit tests as +// a stand-in for the real object. +// +// These tests each contain an expectation that defines some behaviour we expect on +// calls to the function "foo". Add the "foo" function call and get the tests to pass +// +// I AM NOT DONE + +use mockall::*; +use mockall::predicate::*; + +#[automock] +trait MyTrait { + fn foo(&self) -> bool; +} + +fn follow_path_from_trait(x: &dyn MyTrait) -> String { + if ??? { + String::from("Followed path A") + } + else { + String::from("Followed path B") + } +} + +#[test] +fn can_follow_path_a() { + let mut mock = MockMyTrait::new(); + mock.expect_foo() + .times(1) + .returning(||true); + assert_eq!(follow_path_from_trait(&mock), "Followed path A"); +} + +#[test] +fn can_follow_path_b() { + let mut mock = MockMyTrait::new(); + mock.expect_foo() + .times(1) + .returning(||false); + assert_eq!(follow_path_from_trait(&mock), "Followed path B"); +} \ No newline at end of file diff --git a/info.toml b/info.toml index 44f344a3..1c88d737 100644 --- a/info.toml +++ b/info.toml @@ -1319,3 +1319,10 @@ path = "exercises/23_conversions/as_ref_mut.rs" mode = "test" hint = """ Add `AsRef` or `AsMut` as a trait bound to the functions.""" + +[[exercises]] +name = "mocks1" +path = "exercises/crates/mockall/mocks1/Cargo.toml" +mode = "cratetest" +hint = """ +x.foo() needs to be called in the if conditional to get the tests to pass.""" \ No newline at end of file diff --git a/src/exercise.rs b/src/exercise.rs index 664b362b..0c64bdb2 100644 --- a/src/exercise.rs +++ b/src/exercise.rs @@ -1,5 +1,6 @@ use regex::Regex; use serde::Deserialize; +use serde_json::Value; use std::env; use std::fmt::{self, Display, Formatter}; use std::fs::{self, remove_file, File}; @@ -35,6 +36,10 @@ pub enum Mode { Test, // Indicates that the exercise should be linted with clippy Clippy, + // Indicates that the exercise should be compiled as a binary and requires a crate + CrateCompile, + // Indicates that the exercise should be compiled as a test harness and requires a crate + CrateTest, } #[derive(Deserialize)] @@ -50,7 +55,7 @@ pub struct Exercise { pub name: String, // The path to the file containing the exercise's source code pub path: PathBuf, - // The mode of the exercise (Test, Compile, or Clippy) + // The mode of the exercise (Test, Compile, Clippy, CrateCompile or CrateTest pub mode: Mode, // The hint text associated with the exercise pub hint: String, @@ -81,12 +86,13 @@ pub struct ContextLine { pub struct CompiledExercise<'a> { exercise: &'a Exercise, _handle: FileHandle, + pub stdout: String, } impl<'a> CompiledExercise<'a> { // Run the compiled exercise pub fn run(&self) -> Result { - self.exercise.run() + self.exercise.run(&self.stdout) } } @@ -164,6 +170,19 @@ path = "{}.rs""#, .args(RUSTC_COLOR_ARGS) .args(["--", "-D", "warnings", "-D", "clippy::float_cmp"]) .output() + }, + Mode::CrateCompile => { + Command::new("cargo") + .args(["build", "--manifest-path", self.path.to_str().unwrap(), "--target-dir", &temp_file()]) + .output() + }, + Mode::CrateTest => { + Command::new("cargo") + .args(["test", "--no-run"]) + .args(["--manifest-path", self.path.to_str().unwrap()]) + .args(["--target-dir", &temp_file()]) + .args(["--message-format", "json-render-diagnostics"]) + .output() } } .expect("Failed to run 'compile' command."); @@ -172,8 +191,10 @@ path = "{}.rs""#, Ok(CompiledExercise { exercise: self, _handle: FileHandle, + stdout: String::from_utf8_lossy(&cmd.stdout).to_string(), }) } else { + self.cleanup_temporary_dirs_by_mode(); clean(); Err(ExerciseOutput { stdout: String::from_utf8_lossy(&cmd.stdout).to_string(), @@ -182,26 +203,72 @@ path = "{}.rs""#, } } - fn run(&self) -> Result { + fn get_crate_test_filename(&self, stdout: &str) -> Result { + let json_objects = stdout.split("\n"); + for json_object in json_objects { + let parsed_json: Value = serde_json::from_str(json_object).unwrap(); + if parsed_json["target"]["kind"][0] == "bin" { + return Ok(String::from(parsed_json["filenames"][0].as_str().unwrap())); + } + } + Err(()) + } + + fn get_compiled_filename_by_mode(&self, compilation_stdout: &str) -> String { + match self.mode { + Mode::CrateCompile => temp_file() + "/debug/" + &self.name, + Mode::CrateTest => { + let get_filename_result = self.get_crate_test_filename(&compilation_stdout); + match get_filename_result { + Ok(filename) => filename, + Err(()) => panic!("Failed to get crate test filename") + } + }, + _ => temp_file() + } + } + + fn cleanup_temporary_dirs_by_mode(&self) { + match self.mode { + Mode::CrateCompile | Mode::CrateTest => fs::remove_dir_all(temp_file()) + .expect("Failed to cleanup temp build dir"), + _ => () + } + } + + fn run(&self, compilation_stdout: &str) -> Result { let arg = match self.mode { - Mode::Test => "--show-output", + Mode::Test | Mode::CrateTest => "--show-output", _ => "", }; - let cmd = Command::new(temp_file()) + + let filename = self.get_compiled_filename_by_mode(compilation_stdout); + + let command_output = Command::new(filename) .arg(arg) - .output() - .expect("Failed to run 'run' command"); - - let output = ExerciseOutput { - stdout: String::from_utf8_lossy(&cmd.stdout).to_string(), - stderr: String::from_utf8_lossy(&cmd.stderr).to_string(), + .output(); + let result = match command_output { + Ok(cmd) => { + let output = ExerciseOutput { + stdout: String::from_utf8_lossy(&cmd.stdout).to_string(), + stderr: String::from_utf8_lossy(&cmd.stderr).to_string(), + }; + + self.cleanup_temporary_dirs_by_mode(); + + if cmd.status.success() { + Ok(output) + } else { + Err(output) + } + }, + Err(msg) => { + self.cleanup_temporary_dirs_by_mode(); + println!("Error: {}", msg); + panic!("Failed to run 'run' command"); + } }; - - if cmd.status.success() { - Ok(output) - } else { - Err(output) - } + result } pub fn state(&self) -> State { diff --git a/src/run.rs b/src/run.rs index e0ada4c5..adc04638 100644 --- a/src/run.rs +++ b/src/run.rs @@ -11,8 +11,8 @@ use indicatif::ProgressBar; // the output from the test harnesses (if the mode of the exercise is test) pub fn run(exercise: &Exercise, verbose: bool) -> Result<(), ()> { match exercise.mode { - Mode::Test => test(exercise, verbose)?, - Mode::Compile => compile_and_run(exercise)?, + Mode::Test | Mode::CrateTest => test(exercise, verbose)?, + Mode::Compile | Mode::CrateCompile => compile_and_run(exercise)?, Mode::Clippy => compile_and_run(exercise)?, } Ok(()) diff --git a/src/verify.rs b/src/verify.rs index 8a2ad49f..9c56e67a 100644 --- a/src/verify.rs +++ b/src/verify.rs @@ -28,8 +28,8 @@ pub fn verify<'a>( for exercise in exercises { let compile_result = match exercise.mode { - Mode::Test => compile_and_test(exercise, RunMode::Interactive, verbose, success_hints), - Mode::Compile => compile_and_run_interactively(exercise, success_hints), + Mode::Test | Mode::CrateTest => compile_and_test(exercise, RunMode::Interactive, verbose, success_hints), + Mode::Compile | Mode::CrateCompile => compile_and_run_interactively(exercise, success_hints), Mode::Clippy => compile_only(exercise, success_hints), }; if !compile_result.unwrap_or(false) { @@ -164,8 +164,8 @@ fn prompt_for_completion( State::Pending(context) => context, }; match exercise.mode { - Mode::Compile => success!("Successfully ran {}!", exercise), - Mode::Test => success!("Successfully tested {}!", exercise), + Mode::Compile | Mode::CrateCompile => success!("Successfully ran {}!", exercise), + Mode::Test | Mode::CrateTest => success!("Successfully tested {}!", exercise), Mode::Clippy => success!("Successfully compiled {}!", exercise), } @@ -178,8 +178,8 @@ fn prompt_for_completion( }; let success_msg = match exercise.mode { - Mode::Compile => "The code is compiling!", - Mode::Test => "The code is compiling, and the tests pass!", + Mode::Compile | Mode::CrateCompile => "The code is compiling!", + Mode::Test | Mode::CrateTest => "The code is compiling, and the tests pass!", Mode::Clippy => clippy_success_msg, }; println!();