Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for exercises that depend on crates #1752

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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."""
104 changes: 86 additions & 18 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 @@ -165,15 +171,32 @@ path = "{}.rs""#,
.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.");

if cmd.status.success() {
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 +205,71 @@ 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(),
};

if cmd.status.success() {
Ok(output)
} else {
Err(output)
}
let filename = self.get_compiled_filename_by_mode(compilation_stdout);

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
16 changes: 10 additions & 6 deletions src/verify.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,12 @@ 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 +168,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 +182,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