Skip to content
Merged
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
3 changes: 2 additions & 1 deletion exercises/practice/macros/.meta/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"Cargo.toml"
],
"test": [
"tests/macros.rs"
"tests/macros.rs",
"src/compile_fail_tests.rs"
],
"example": [
".meta/example.rs"
Expand Down
5 changes: 5 additions & 0 deletions exercises/practice/macros/.meta/example.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ macro_rules! hashmap {
}
};
}

/// This module contains doctests, which allows writing tests where a code
/// snippet is supposed to fail to compile. These tests also have "ignore"
/// attributes, makes sure to remove them when solving this exercise locally.
pub mod compile_fail_tests;
103 changes: 103 additions & 0 deletions exercises/practice/macros/src/compile_fail_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/// To activate a doctest locally, remove ",ignore" from the code block.
///
/// # Comma separator
///
/// ```compile_fail,ignore
/// use macros::hashmap;
/// use std::collections::HashMap;
///
/// // using only commas is invalid
/// let _hm: HashMap<_, _> = hashmap!('a', 1);
/// ```
///
/// # Double trailing commas
///
/// ```compile_fail,ignore
/// use macros::hashmap;
/// use std::collections::HashMap;
///
/// // a single trailing comma is okay, but two is not
/// let _hm: HashMap<_, _> = hashmap!('a' => 2, ,);
/// ```
///
/// # Only comma
///
/// ```compile_fail,ignore
/// use macros::hashmap;
/// use std::collections::HashMap;
///
/// // a single random comma is not valid
/// let _hm: HashMap<(), ()> = hashmap!(,);
/// ```
///
/// # Single argument
///
/// ```compile_fail,ignore
/// use macros::hashmap;
/// use std::collections::HashMap;
///
/// // a single argument is invalid
/// let _hm: HashMap<_, _> = hashmap!('a');
/// ```
///
/// # Triple arguments
///
/// ```compile_fail,ignore
/// use macros::hashmap;
/// use std::collections::HashMap;
///
/// // three arguments are invalid
/// hashmap!('a' => 1, 'b');
/// ```
///
/// # Only arrow
///
/// ```compile_fail,ignore
/// use macros::hashmap;
/// use std::collections::HashMap;
///
/// // a single random arrow is not valid
/// let _hm: HashMap<(), ()> = hashmap!(=>);
/// ```
///
/// # Trailing arrow
///
/// ```compile_fail,ignore
/// use macros::hashmap;
/// use std::collections::HashMap;
///
/// // a trailing => isn't valid either
/// hashmap!('a' => 2, =>);
/// ```
///
/// # Leading comma
///
/// ```compile_fail,ignore
/// use macros::hashmap;
/// use std::collections::HashMap;
///
/// // leading commas are not valid
/// let _hm: HashMap<_, _> = hashmap!(, 'a' => 2);
/// ```
///
/// # Missing comma
///
/// ```compile_fail,ignore
/// use macros::hashmap;
/// use std::collections::HashMap;
///
/// // Key value pairs must be separated by commas
/// let _hm: HashMap<_, _> = hashmap!('a' => 1 'b' => 2);
/// ```
///
/// # Missing argument
///
/// ```compile_fail,ignore
/// use macros::hashmap;
/// use std::collections::HashMap;
///
/// // an argument should come between each pair of commas
/// let _hm: HashMap<_, _> = hashmap!('a' => 1, , 'b' => 2);
/// ```
///
const _TESTS: () = ();
5 changes: 5 additions & 0 deletions exercises/practice/macros/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,8 @@ macro_rules! hashmap {
todo!()
};
}

/// This module contains doctests, which allows writing tests where a code
/// snippet is supposed to fail to compile. These tests also have "ignore"
/// attributes, makes sure to remove them when solving this exercise locally.
pub mod compile_fail_tests;
55 changes: 0 additions & 55 deletions exercises/practice/macros/tests/invalid/Cargo.toml

This file was deleted.

7 changes: 0 additions & 7 deletions exercises/practice/macros/tests/invalid/comma_sep.rs

This file was deleted.

7 changes: 0 additions & 7 deletions exercises/practice/macros/tests/invalid/double_commas.rs

This file was deleted.

7 changes: 0 additions & 7 deletions exercises/practice/macros/tests/invalid/leading_comma.rs

This file was deleted.

7 changes: 0 additions & 7 deletions exercises/practice/macros/tests/invalid/missing_argument.rs

This file was deleted.

7 changes: 0 additions & 7 deletions exercises/practice/macros/tests/invalid/no_comma.rs

This file was deleted.

7 changes: 0 additions & 7 deletions exercises/practice/macros/tests/invalid/only_arrow.rs

This file was deleted.

7 changes: 0 additions & 7 deletions exercises/practice/macros/tests/invalid/only_comma.rs

This file was deleted.

7 changes: 0 additions & 7 deletions exercises/practice/macros/tests/invalid/single_argument.rs

This file was deleted.

7 changes: 0 additions & 7 deletions exercises/practice/macros/tests/invalid/triple_arguments.rs

This file was deleted.

7 changes: 0 additions & 7 deletions exercises/practice/macros/tests/invalid/two_arrows.rs

This file was deleted.

106 changes: 6 additions & 100 deletions exercises/practice/macros/tests/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,103 +114,9 @@ fn type_override() {
let _with_trailing = hashmap!(1 => 2, 3 => 4,);
}

#[test]
#[ignore]
fn compile_fails_comma_sep() {
simple_trybuild::compile_fail("comma_sep.rs");
}

#[test]
#[ignore]
fn compile_fails_double_commas() {
simple_trybuild::compile_fail("double_commas.rs");
}

#[test]
#[ignore]
fn compile_fails_only_comma() {
simple_trybuild::compile_fail("only_comma.rs");
}

#[test]
#[ignore]
fn compile_fails_single_argument() {
simple_trybuild::compile_fail("single_argument.rs");
}

#[test]
#[ignore]
fn compile_fails_triple_arguments() {
simple_trybuild::compile_fail("triple_arguments.rs");
}

#[test]
#[ignore]
fn compile_fails_only_arrow() {
simple_trybuild::compile_fail("only_arrow.rs");
}

#[test]
#[ignore]
fn compile_fails_two_arrows() {
simple_trybuild::compile_fail("two_arrows.rs");
}

#[test]
#[ignore]
fn compile_fails_leading_comma() {
simple_trybuild::compile_fail("leading_comma.rs");
}

#[test]
#[ignore]
fn compile_fails_no_comma() {
simple_trybuild::compile_fail("no_comma.rs");
}

#[test]
#[ignore]
fn compile_fails_missing_argument() {
simple_trybuild::compile_fail("missing_argument.rs");
}

mod simple_trybuild {
use std::path::PathBuf;
use std::process::Command;

pub fn compile_fail(file_name: &str) {
let invalid_path: PathBuf = ["tests", "invalid"].iter().collect::<PathBuf>();

let mut file_path = invalid_path.clone();
file_path.push(file_name);
assert!(
file_path.exists(),
"{:?} does not exist.",
file_path.into_os_string()
);

let test_name = file_name.strip_suffix(".rs").unwrap();
let macros_dir = ["..", "..", "target", "tests", "macros"]
.iter()
.collect::<PathBuf>();

let result = Command::new("cargo")
.current_dir(invalid_path)
.arg("build")
.arg("--offline")
.arg("--target-dir")
.arg(macros_dir)
.arg("--bin")
.arg(test_name)
.output();

if let Ok(result) = result {
assert!(
!result.status.success(),
"Expected {file_path:?} to fail to compile, but it succeeded."
);
} else {
panic!("Running subprocess failed.");
}
}
}
// Don't forget that there are also tests in `src/compile_fail_tests.rs` !
#[expect(
unused_imports,
reason = "prevent user from deleting the mod declaration"
)]
use macros::compile_fail_tests;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically speaking, what's to prevent a user from exporting a different module as compile_fail_tests in lib.rs?

In general I think the idea is good, but I feel like it can always be cheated somehow...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's true. I feel like it should be ok. It's not like users can win something unfairly by cheating. They're just cheating themselves out of learning.

I think there would be a way to make it completely bullet proof. We could turn lib.rs into a "test" file, meaning students cannot modify it. It includes the doctests and a mod actual_solution or whatever. src/actual_solution.rs would be the file users can modify.

But I don't want to make the exercise structure super confusing for normal users who wouldn't even think of cheating, if the risk is so low anyway.

What if we include a test in tests/macros.rs that reads the solution file and makes sure a normal mod compile_fail_tests; statement is present in the file ? 😅 I guess it's possible to have a string literal with that content... The next step is to parse actual Rust syntax 😄

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it's fine the way it is... 😆

It's not really a big deal, if someone cheats with creating another module then more power to them.

I also agree that we should keep users editing lib.rs, I think cheating is better than having a whole new directory structure.