Skip to content

Commit 8ad5f9b

Browse files
committed
feat: Add a --nocapture option to display test harnesses' outputs
This new feature can be accessed by invoking rustlings with --nocapture. Both unit and integration tests added. closes rust-lang#262 BREAKING CHANGES: The following function take a new boolean argument: * `run` * `verify` * `test` * `compile_and_test`
1 parent 02a2fe4 commit 8ad5f9b

File tree

7 files changed

+113
-21
lines changed

7 files changed

+113
-21
lines changed

info.toml

+2-2
Original file line numberDiff line numberDiff line change
@@ -802,7 +802,7 @@ name = "try_from_into"
802802
path = "exercises/conversions/try_from_into.rs"
803803
mode = "test"
804804
hint = """
805-
Follow the steps provided right before the `From` implementation.
805+
Follow the steps provided right before the `TryFrom` implementation.
806806
You can also use the example at https://doc.rust-lang.org/std/convert/trait.TryFrom.html"""
807807

808808
[[exercises]]
@@ -819,4 +819,4 @@ mode = "test"
819819
hint = """
820820
The implementation of FromStr should return an Ok with a Person object,
821821
or an Err with a string if the string is not valid.
822-
This is a some like an `try_from_into` exercise."""
822+
This is almost like the `try_from_into` exercise."""

src/exercise.rs

+43-1
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,21 @@ const I_AM_DONE_REGEX: &str = r"(?m)^\s*///?\s*I\s+AM\s+NOT\s+DONE";
1111
const CONTEXT: usize = 2;
1212
const CLIPPY_CARGO_TOML_PATH: &str = "./exercises/clippy/Cargo.toml";
1313

14+
// Get a temporary file name that is hopefully unique to this process
15+
#[inline]
1416
fn temp_file() -> String {
1517
format!("./temp_{}", process::id())
1618
}
1719

20+
// The mode of the exercise.
1821
#[derive(Deserialize, Copy, Clone)]
1922
#[serde(rename_all = "lowercase")]
2023
pub enum Mode {
24+
// Indicates that the exercise should be compiled as a binary
2125
Compile,
26+
// Indicates that the exercise should be compiled as a test harness
2227
Test,
28+
// Indicates that the exercise should be linted with clippy
2329
Clippy,
2430
}
2531

@@ -28,41 +34,60 @@ pub struct ExerciseList {
2834
pub exercises: Vec<Exercise>,
2935
}
3036

37+
// A representation of a rustlings exercise.
38+
// This is deserialized from the accompanying info.toml file
3139
#[derive(Deserialize)]
3240
pub struct Exercise {
41+
// Name of the exercise
3342
pub name: String,
43+
// The path to the file containing the exercise's source code
3444
pub path: PathBuf,
45+
// The mode of the exercise (Test, Compile, or Clippy)
3546
pub mode: Mode,
47+
// The hint text associated with the exercise
3648
pub hint: String,
3749
}
3850

51+
// An enum to track of the state of an Exercise.
52+
// An Exercise can be either Done or Pending
3953
#[derive(PartialEq, Debug)]
4054
pub enum State {
55+
// The state of the exercise once it's been completed
4156
Done,
57+
// The state of the exercise while it's not completed yet
4258
Pending(Vec<ContextLine>),
4359
}
4460

61+
// The context information of a pending exercise
4562
#[derive(PartialEq, Debug)]
4663
pub struct ContextLine {
64+
// The source code that is still pending completion
4765
pub line: String,
66+
// The line number of the source code still pending completion
4867
pub number: usize,
68+
// Whether or not this is important
4969
pub important: bool,
5070
}
5171

72+
// The result of compiling an exercise
5273
pub struct CompiledExercise<'a> {
5374
exercise: &'a Exercise,
5475
_handle: FileHandle,
5576
}
5677

5778
impl<'a> CompiledExercise<'a> {
79+
// Run the compiled exercise
5880
pub fn run(&self) -> Result<ExerciseOutput, ExerciseOutput> {
5981
self.exercise.run()
6082
}
6183
}
6284

85+
// A representation of an already executed binary
6386
#[derive(Debug)]
6487
pub struct ExerciseOutput {
88+
// The textual contents of the standard output of the binary
6589
pub stdout: String,
90+
// The textual contents of the standard error of the binary
6691
pub stderr: String,
6792
}
6893

@@ -140,7 +165,11 @@ path = "{}.rs""#,
140165
}
141166

142167
fn run(&self) -> Result<ExerciseOutput, ExerciseOutput> {
143-
let cmd = Command::new(&temp_file())
168+
let arg = match self.mode {
169+
Mode::Test => "--show-output",
170+
_ => ""
171+
};
172+
let cmd = Command::new(&temp_file()).arg(arg)
144173
.output()
145174
.expect("Failed to run 'run' command");
146175

@@ -205,6 +234,7 @@ impl Display for Exercise {
205234
}
206235
}
207236

237+
#[inline]
208238
fn clean() {
209239
let _ignored = remove_file(&temp_file());
210240
}
@@ -280,4 +310,16 @@ mod test {
280310

281311
assert_eq!(exercise.state(), State::Done);
282312
}
313+
314+
#[test]
315+
fn test_exercise_with_output() {
316+
let exercise = Exercise {
317+
name: "finished_exercise".into(),
318+
path: PathBuf::from("tests/fixture/success/testSuccess.rs"),
319+
mode: Mode::Test,
320+
hint: String::new(),
321+
};
322+
let out = exercise.compile().unwrap().run().unwrap();
323+
assert!(out.stdout.contains("THIS TEST TOO SHALL PASS"));
324+
}
283325
}

src/main.rs

+10-10
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,9 @@ fn main() {
2828
.author("Olivia Hugger, Carol Nichols")
2929
.about("Rustlings is a collection of small exercises to get you used to writing and reading Rust code")
3030
.arg(
31-
Arg::with_name("verbose")
32-
.short("V")
33-
.long("verbose")
34-
.help("Show tests' standard output")
31+
Arg::with_name("nocapture")
32+
.long("nocapture")
33+
.help("Show outputs from the test exercises")
3534
)
3635
.subcommand(
3736
SubCommand::with_name("verify")
@@ -87,6 +86,7 @@ fn main() {
8786

8887
let toml_str = &fs::read_to_string("info.toml").unwrap();
8988
let exercises = toml::from_str::<ExerciseList>(toml_str).unwrap().exercises;
89+
let verbose = matches.is_present("nocapture");
9090

9191
if let Some(ref matches) = matches.subcommand_matches("run") {
9292
let name = matches.value_of("name").unwrap();
@@ -98,7 +98,7 @@ fn main() {
9898
std::process::exit(1)
9999
});
100100

101-
run(&exercise).unwrap_or_else(|_| std::process::exit(1));
101+
run(&exercise, verbose).unwrap_or_else(|_| std::process::exit(1));
102102
}
103103

104104
if let Some(ref matches) = matches.subcommand_matches("hint") {
@@ -116,10 +116,10 @@ fn main() {
116116
}
117117

118118
if matches.subcommand_matches("verify").is_some() {
119-
verify(&exercises).unwrap_or_else(|_| std::process::exit(1));
119+
verify(&exercises, verbose).unwrap_or_else(|_| std::process::exit(1));
120120
}
121121

122-
if matches.subcommand_matches("watch").is_some() && watch(&exercises).is_ok() {
122+
if matches.subcommand_matches("watch").is_some() && watch(&exercises, verbose).is_ok() {
123123
println!(
124124
"{emoji} All exercises completed! {emoji}",
125125
emoji = Emoji("🎉", "★")
@@ -161,7 +161,7 @@ fn spawn_watch_shell(failed_exercise_hint: &Arc<Mutex<Option<String>>>) {
161161
});
162162
}
163163

164-
fn watch(exercises: &[Exercise]) -> notify::Result<()> {
164+
fn watch(exercises: &[Exercise], verbose: bool) -> notify::Result<()> {
165165
/* Clears the terminal with an ANSI escape code.
166166
Works in UNIX and newer Windows terminals. */
167167
fn clear_screen() {
@@ -176,7 +176,7 @@ fn watch(exercises: &[Exercise]) -> notify::Result<()> {
176176
clear_screen();
177177

178178
let to_owned_hint = |t: &Exercise| t.hint.to_owned();
179-
let failed_exercise_hint = match verify(exercises.iter()) {
179+
let failed_exercise_hint = match verify(exercises.iter(), verbose) {
180180
Ok(_) => return Ok(()),
181181
Err(exercise) => Arc::new(Mutex::new(Some(to_owned_hint(exercise)))),
182182
};
@@ -191,7 +191,7 @@ fn watch(exercises: &[Exercise]) -> notify::Result<()> {
191191
.iter()
192192
.skip_while(|e| !filepath.ends_with(&e.path));
193193
clear_screen();
194-
match verify(pending_exercises) {
194+
match verify(pending_exercises, verbose) {
195195
Ok(_) => return Ok(()),
196196
Err(exercise) => {
197197
let mut failed_exercise_hint = failed_exercise_hint.lock().unwrap();

src/run.rs

+9-2
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,22 @@ use crate::exercise::{Exercise, Mode};
22
use crate::verify::test;
33
use indicatif::ProgressBar;
44

5-
pub fn run(exercise: &Exercise) -> Result<(), ()> {
5+
// Invoke the rust compiler on the path of the given exercise,
6+
// and run the ensuing binary.
7+
// The verbose argument helps determine whether or not to show
8+
// the output from the test harnesses (if the mode of the exercise is test)
9+
pub fn run(exercise: &Exercise, verbose: bool) -> Result<(), ()> {
610
match exercise.mode {
7-
Mode::Test => test(exercise)?,
11+
Mode::Test => test(exercise, verbose)?,
812
Mode::Compile => compile_and_run(exercise)?,
913
Mode::Clippy => compile_and_run(exercise)?,
1014
}
1115
Ok(())
1216
}
1317

18+
// Invoke the rust compiler on the path of the given exercise
19+
// and run the ensuing binary.
20+
// This is strictly for non-test binaries, so output is displayed
1421
fn compile_and_run(exercise: &Exercise) -> Result<(), ()> {
1522
let progress_bar = ProgressBar::new_spinner();
1623
progress_bar.set_message(format!("Compiling {}...", exercise).as_str());

src/verify.rs

+26-6
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,18 @@ use crate::exercise::{CompiledExercise, Exercise, Mode, State};
22
use console::style;
33
use indicatif::ProgressBar;
44

5-
pub fn verify<'a>(start_at: impl IntoIterator<Item = &'a Exercise>) -> Result<(), &'a Exercise> {
5+
// Verify that the provided container of Exercise objects
6+
// can be compiled and run without any failures.
7+
// Any such failures will be reported to the end user.
8+
// If the Exercise being verified is a test, the verbose boolean
9+
// determines whether or not the test harness outputs are displayed.
10+
pub fn verify<'a>(
11+
start_at: impl IntoIterator<Item = &'a Exercise>,
12+
verbose: bool
13+
) -> Result<(), &'a Exercise> {
614
for exercise in start_at {
715
let compile_result = match exercise.mode {
8-
Mode::Test => compile_and_test(&exercise, RunMode::Interactive),
16+
Mode::Test => compile_and_test(&exercise, RunMode::Interactive, verbose),
917
Mode::Compile => compile_and_run_interactively(&exercise),
1018
Mode::Clippy => compile_only(&exercise),
1119
};
@@ -21,11 +29,13 @@ enum RunMode {
2129
NonInteractive,
2230
}
2331

24-
pub fn test(exercise: &Exercise) -> Result<(), ()> {
25-
compile_and_test(exercise, RunMode::NonInteractive)?;
32+
// Compile and run the resulting test harness of the given Exercise
33+
pub fn test(exercise: &Exercise, verbose: bool) -> Result<(), ()> {
34+
compile_and_test(exercise, RunMode::NonInteractive, verbose)?;
2635
Ok(())
2736
}
2837

38+
// Invoke the rust compiler without running the resulting binary
2939
fn compile_only(exercise: &Exercise) -> Result<bool, ()> {
3040
let progress_bar = ProgressBar::new_spinner();
3141
progress_bar.set_message(format!("Compiling {}...", exercise).as_str());
@@ -38,6 +48,7 @@ fn compile_only(exercise: &Exercise) -> Result<bool, ()> {
3848
Ok(prompt_for_completion(&exercise, None))
3949
}
4050

51+
// Compile the given Exercise and run the resulting binary in an interactive mode
4152
fn compile_and_run_interactively(exercise: &Exercise) -> Result<bool, ()> {
4253
let progress_bar = ProgressBar::new_spinner();
4354
progress_bar.set_message(format!("Compiling {}...", exercise).as_str());
@@ -63,7 +74,11 @@ fn compile_and_run_interactively(exercise: &Exercise) -> Result<bool, ()> {
6374
Ok(prompt_for_completion(&exercise, Some(output.stdout)))
6475
}
6576

66-
fn compile_and_test(exercise: &Exercise, run_mode: RunMode) -> Result<bool, ()> {
77+
// Compile the given Exercise as a test harness and display
78+
// the output if verbose is set to true
79+
fn compile_and_test(
80+
exercise: &Exercise, run_mode: RunMode, verbose: bool
81+
) -> Result<bool, ()> {
6782
let progress_bar = ProgressBar::new_spinner();
6883
progress_bar.set_message(format!("Testing {}...", exercise).as_str());
6984
progress_bar.enable_steady_tick(100);
@@ -73,7 +88,10 @@ fn compile_and_test(exercise: &Exercise, run_mode: RunMode) -> Result<bool, ()>
7388
progress_bar.finish_and_clear();
7489

7590
match result {
76-
Ok(_) => {
91+
Ok(output) => {
92+
if verbose {
93+
println!("{}", output.stdout);
94+
}
7795
success!("Successfully tested {}", &exercise);
7896
if let RunMode::Interactive = run_mode {
7997
Ok(prompt_for_completion(&exercise, None))
@@ -92,6 +110,8 @@ fn compile_and_test(exercise: &Exercise, run_mode: RunMode) -> Result<bool, ()>
92110
}
93111
}
94112

113+
// Compile the given Exercise and return an object with information
114+
// about the state of the compilation
95115
fn compile<'a, 'b>(
96116
exercise: &'a Exercise,
97117
progress_bar: &'b ProgressBar,

tests/fixture/success/testSuccess.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#[test]
22
fn passing() {
3+
println!("THIS TEST TOO SHALL PASS");
34
assert!(true);
45
}

tests/integration_tests.rs

+22
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,25 @@ fn run_test_exercise_does_not_prompt() {
159159
.code(0)
160160
.stdout(predicates::str::contains("I AM NOT DONE").not());
161161
}
162+
163+
#[test]
164+
fn run_single_test_success_with_output() {
165+
Command::cargo_bin("rustlings")
166+
.unwrap()
167+
.args(&["--nocapture", "r", "testSuccess"])
168+
.current_dir("tests/fixture/success/")
169+
.assert()
170+
.code(0)
171+
.stdout(predicates::str::contains("THIS TEST TOO SHALL PAS"));
172+
}
173+
174+
#[test]
175+
fn run_single_test_success_without_output() {
176+
Command::cargo_bin("rustlings")
177+
.unwrap()
178+
.args(&["r", "testSuccess"])
179+
.current_dir("tests/fixture/success/")
180+
.assert()
181+
.code(0)
182+
.stdout(predicates::str::contains("THIS TEST TOO SHALL PAS").not());
183+
}

0 commit comments

Comments
 (0)