Skip to content

Commit

Permalink
feat: add support for exercises that depend on crates
Browse files Browse the repository at this point in the history
  • Loading branch information
Ben2917 committed Nov 3, 2023
1 parent 02b1b5f commit 3f23d26
Show file tree
Hide file tree
Showing 6 changed files with 155 additions and 25 deletions.
11 changes: 11 additions & 0 deletions exercises/crates/mockall/mocks1/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[package]
name = "mocks1"
version = "0.0.1"
edition = "2021"

[dependencies]
mockall = "0.11.4"

[[bin]]
name = "mocks1"
path = "mocks1.rs"
45 changes: 45 additions & 0 deletions exercises/crates/mockall/mocks1/mocks1.rs
Original file line number Diff line number Diff line change
@@ -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");
}
7 changes: 7 additions & 0 deletions info.toml
Original file line number Diff line number Diff line change
Expand Up @@ -1319,3 +1319,10 @@ path = "exercises/23_conversions/as_ref_mut.rs"
mode = "test"
hint = """
Add `AsRef<str>` or `AsMut<u32>` 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."""
101 changes: 84 additions & 17 deletions src/exercise.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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)]
Expand All @@ -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,
Expand Down Expand Up @@ -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<ExerciseOutput, ExerciseOutput> {
self.exercise.run()
self.exercise.run(&self.stdout)
}
}

Expand Down Expand Up @@ -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.");
Expand All @@ -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(),
Expand All @@ -182,26 +203,72 @@ path = "{}.rs""#,
}
}

fn run(&self) -> Result<ExerciseOutput, ExerciseOutput> {
fn get_crate_test_filename(&self, stdout: &str) -> Result<String, ()> {
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<ExerciseOutput, ExerciseOutput> {
let arg = match self.mode {
Mode::Test => "--show-output",
Mode::Test | Mode::CrateTest => "--show-output",
_ => "",
};
let cmd = Command::new(temp_file())
.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(),
};
let filename = self.get_compiled_filename_by_mode(compilation_stdout);

if cmd.status.success() {
Ok(output)
} else {
Err(output)
}
let command_output = Command::new(filename)
.arg(arg)
.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");
}
};
result
}

pub fn state(&self) -> State {
Expand Down
4 changes: 2 additions & 2 deletions src/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(())
Expand Down
12 changes: 6 additions & 6 deletions src/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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),
}

Expand All @@ -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!();
Expand Down

0 comments on commit 3f23d26

Please sign in to comment.