Skip to content

Commit cfb8fa1

Browse files
committed
feat: Add support for discovering quickcheck tests
While quichcheck tests expand into a regular #[test] with the original property test function stored within a new wrapper function declaration, the call within the generated wrapper to the quickcheck harness casts the original function item into a function pointer. This results in the information necessary to construct a call relation between the wrapper function and the original property test function to be lost. To solve this, we introduce a very thin layer of special handling for quickcheck test definitions. The same idea could be reused for other test wrappers that work in a similar way to quickcheck.
1 parent 58e667d commit cfb8fa1

8 files changed

Lines changed: 157 additions & 15 deletions

File tree

mutest-driver/src/passes/analysis.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ pub fn run(config: &mut Config) -> CompilerResult<Option<AnalysisPassResult>> {
191191
let t_test_discovery_start = Instant::now();
192192
let tests = opts.crate_kind.provides_tests()
193193
.then(|| match opts.embedded {
194-
false => mutest_emit::analysis::tests::collect_tests(&generated_crate_ast, &def_res),
194+
false => mutest_emit::analysis::tests::collect_tests(tcx, &generated_crate_ast, &def_res),
195195
true => mutest_emit::analysis::tests::collect_and_mark_embedded_tests(sess, &mut generated_crate_ast, &def_res),
196196
})
197197
.unwrap_or_default();

mutest-driver/src/print.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,7 +463,8 @@ pub fn print_mutations<'tcx>(tcx: TyCtxt<'tcx>, mutations: &[Mut], mutation_batc
463463
);
464464

465465
if let TargetReachability::DirectEntry = mutation.target.reachability {
466-
let entry_point = LocalEntryPoint { local_def_id: mutation.target.def_id().expect_local() };
466+
// NOTE: The entry point's original body override, if any, can be discarded here, as it is no longer used.
467+
let entry_point = LocalEntryPoint { local_def_id: mutation.target.def_id().expect_local(), body_local_def_id: None };
467468

468469
if nested { print!(" "); }
469470
println!(" <- {entry_point} (direct entry point)",

mutest-emit/src/analysis/call_graph.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use smallvec::{SmallVec, smallvec};
1212
use crate::analysis::ast_lowering;
1313
use crate::analysis::hir;
1414
use crate::analysis::res;
15-
use crate::analysis::tests::{self, Test};
15+
use crate::analysis::tests::{self, Test, TestKind};
1616
use crate::analysis::ty::{self, TyCtxt};
1717
use crate::codegen::ast;
1818
use crate::codegen::ast::visit::Visitor;
@@ -131,9 +131,14 @@ pub struct EntryPointAssoc {
131131
#[derive(Copy, Clone, Debug)]
132132
pub struct LocalEntryPoint {
133133
pub local_def_id: hir::LocalDefId,
134+
pub body_local_def_id: Option<hir::LocalDefId>,
134135
}
135136

136137
impl LocalEntryPoint {
138+
pub fn body_local_def_id(&self) -> hir::LocalDefId {
139+
self.body_local_def_id.unwrap_or(self.local_def_id)
140+
}
141+
137142
pub fn path_str<'tcx>(&self, tcx: TyCtxt<'tcx>) -> String {
138143
res::def_id_path(tcx, self.local_def_id.to_def_id()).iter()
139144
.skip(1) // Skip crate name in entry point path strings.
@@ -346,7 +351,10 @@ impl<'a> EntryPoints<'a> {
346351
EntryPoints::Tests(tests) => {
347352
let iter = tests.iter()
348353
.filter(|test| !test.ignore)
349-
.map(|test| LocalEntryPoint { local_def_id: test.def_id });
354+
.map(|test| match &test.kind {
355+
&TestKind::Test => LocalEntryPoint { local_def_id: test.def_id, body_local_def_id: None },
356+
&TestKind::QuickCheck { original_def_id } => LocalEntryPoint { local_def_id: test.def_id, body_local_def_id: Some(original_def_id) },
357+
});
350358
Box::new(iter)
351359
},
352360
EntryPoints::External => Box::new(iter::empty()),
@@ -559,7 +567,7 @@ pub fn reachable_fns<'ast, 'tcx, 'ent>(
559567
let mut previously_found_callees: FxHashSet<Callee<'tcx>> = Default::default();
560568

561569
for entry_point in entry_points.iter() {
562-
let body_mir = tcx.instance_mir(ty::InstanceKind::Item(entry_point.local_def_id.to_def_id()));
570+
let body_mir = tcx.instance_mir(ty::InstanceKind::Item(entry_point.body_local_def_id().to_def_id()));
563571

564572
// NOTE: We expect entry points to be non-polymorphic (i.e. no type or const generic) functions.
565573
// This is because we cannot build a complete call graph with uninstantiated type and const parameters.
@@ -915,7 +923,8 @@ pub fn reachable_fns<'ast, 'tcx, 'ent>(
915923
_ => None,
916924
};
917925
let mut call_trace = CallTrace { root: entry_point, nested_calls: smallvec![call.callee] };
918-
let entry_point = LocalEntryPoint { local_def_id: entry_point };
926+
// HACK: We can discard any def body overrides for entry points, as we have already collected all call information from them.
927+
let entry_point = LocalEntryPoint { local_def_id: entry_point, body_local_def_id: None };
919928
record_nested_targets(tcx, def_res, krate, &test_def_ids, &callee_lookup_cache, entry_point, targeting, unsafety, &mut call_trace, &mut targets, trace_length_limit);
920929
}
921930
}

mutest-emit/src/analysis/tests.rs

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,19 @@ use crate::analysis::ast_lowering;
1010
use crate::analysis::hir;
1111
use crate::codegen::ast;
1212
use crate::codegen::ast::visit::Visitor;
13-
use crate::codegen::symbols::{DUMMY_SP, FileNameDisplayPreference, Ident, Symbol, path, sym};
13+
use crate::codegen::symbols::{DUMMY_SP, ExpnKind, FileNameDisplayPreference, Ident, MacroKind, Symbol, path, sym};
14+
15+
pub enum TestKind {
16+
Test,
17+
QuickCheck { original_def_id: hir::LocalDefId },
18+
}
1419

1520
pub struct Test {
1621
pub path: Vec<Ident>,
1722
pub descriptor: Box<ast::Item>,
1823
pub item: Box<ast::Item>,
1924
pub def_id: hir::LocalDefId,
25+
pub kind: TestKind,
2026
pub ignore: bool,
2127
}
2228

@@ -58,7 +64,7 @@ fn is_test_case(item: &ast::Item) -> bool {
5864
item.attrs.iter().any(|attr| attr.has_name(sym::rustc_test_marker))
5965
}
6066

61-
fn extract_expanded_tests(def_res: &ast_lowering::DefResolutions, path: &[Ident], items: &[Box<ast::Item>]) -> Vec<Test> {
67+
fn extract_expanded_tests<'tcx>(tcx: TyCtxt<'tcx>, def_res: &ast_lowering::DefResolutions, path: &[Ident], items: &[Box<ast::Item>]) -> Vec<Test> {
6268
let mut tests = vec![];
6369

6470
let mut item_iterator = items.iter();
@@ -74,29 +80,54 @@ fn extract_expanded_tests(def_res: &ast_lowering::DefResolutions, path: &[Ident]
7480

7581
let Some(ident) = test_case.kind.ident() else { panic!("encountered test case without ident"); };
7682

83+
let mut kind = TestKind::Test;
84+
let expn_data = test_item.span.ctxt().outer_expn_data();
85+
let expn_macro_crate_name = expn_data.macro_def_id.map(|def_id| tcx.crate_name(def_id.krate).as_str().to_owned());
86+
match (expn_macro_crate_name.as_deref(), expn_data.kind) {
87+
(Some("quickcheck"), ExpnKind::Macro(MacroKind::Bang, _)) => {
88+
if let ast::ItemKind::Fn(fn_item) = &test_item.kind && let Some(body) = &fn_item.body {
89+
if let [original_fn_item_stmt, _] = &body.stmts[..] && let ast::StmtKind::Item(original_fn_item) = &original_fn_item_stmt.kind {
90+
let Some(original_def_id) = def_res.node_id_to_def_id.get(&original_fn_item.id).copied() else { unreachable!(); };
91+
kind = TestKind::QuickCheck { original_def_id };
92+
}
93+
}
94+
}
95+
(Some("quickcheck_macros"), ExpnKind::Macro(MacroKind::Attr, _)) => {
96+
if let ast::ItemKind::Fn(fn_item) = &test_item.kind && let Some(body) = &fn_item.body {
97+
if let [original_fn_item_stmt, _] = &body.stmts[..] && let ast::StmtKind::Item(original_fn_item) = &original_fn_item_stmt.kind {
98+
let Some(original_def_id) = def_res.node_id_to_def_id.get(&original_fn_item.id).copied() else { unreachable!(); };
99+
kind = TestKind::QuickCheck { original_def_id };
100+
}
101+
}
102+
}
103+
_ => {}
104+
}
105+
77106
let ignore = test_item.attrs.iter().any(|attr| attr.has_name(sym::ignore));
78107

79108
tests.push(Test {
80109
path: path.iter().copied().chain(iter::once(ident)).collect(),
81110
descriptor: test_case.to_owned(),
82111
item: test_item.to_owned(),
83112
def_id,
113+
kind,
84114
ignore,
85115
});
86116
}
87117

88118
tests
89119
}
90120

91-
struct TestCollector<'op> {
121+
struct TestCollector<'tcx, 'op> {
122+
tcx: TyCtxt<'tcx>,
123+
def_res: &'op ast_lowering::DefResolutions,
92124
current_path: Vec<Ident>,
93125
tests: Vec<Test>,
94-
def_res: &'op ast_lowering::DefResolutions,
95126
}
96127

97-
impl<'ast, 'op> ast::visit::Visitor<'ast> for TestCollector<'op> {
128+
impl<'tcx, 'ast, 'op> ast::visit::Visitor<'ast> for TestCollector<'tcx, 'op> {
98129
fn visit_crate(&mut self, krate: &'ast ast::Crate) {
99-
let mut tests = extract_expanded_tests(self.def_res, &self.current_path, &krate.items);
130+
let mut tests = extract_expanded_tests(self.tcx, self.def_res, &self.current_path, &krate.items);
100131
self.tests.append(&mut tests);
101132

102133
ast::visit::walk_crate(self, krate);
@@ -108,7 +139,7 @@ impl<'ast, 'op> ast::visit::Visitor<'ast> for TestCollector<'op> {
108139

109140
if let Some(ident) = ident { self.current_path.push(ident); }
110141

111-
let mut tests = extract_expanded_tests(self.def_res, &self.current_path, &items);
142+
let mut tests = extract_expanded_tests(self.tcx, self.def_res, &self.current_path, &items);
112143
self.tests.append(&mut tests);
113144

114145
ast::visit::walk_item(self, item);
@@ -118,8 +149,8 @@ impl<'ast, 'op> ast::visit::Visitor<'ast> for TestCollector<'op> {
118149
}
119150
}
120151

121-
pub fn collect_tests(krate: &ast::Crate, def_res: &ast_lowering::DefResolutions) -> Vec<Test> {
122-
let mut collector = TestCollector { current_path: vec![], tests: vec![], def_res };
152+
pub fn collect_tests<'tcx>(tcx: TyCtxt<'tcx>, krate: &ast::Crate, def_res: &ast_lowering::DefResolutions) -> Vec<Test> {
153+
let mut collector = TestCollector { tcx, def_res, current_path: vec![], tests: vec![] };
123154
collector.visit_crate(krate);
124155

125156
collector.tests
@@ -237,6 +268,7 @@ fn extract_and_mark_expanded_embedded_tests(sess: &Session, def_res: &ast_loweri
237268
descriptor,
238269
item: test_item,
239270
def_id,
271+
kind: TestKind::Test,
240272
ignore,
241273
});
242274

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#![crate_type = "lib"]
2+
3+
#[macro_export]
4+
macro_rules! quickcheck {
5+
{
6+
$(#[$m:meta])*
7+
fn $fn_name:ident($($arg_name:ident : $arg_ty:ty),*) -> $ret:ty {
8+
$($code:tt)*
9+
}
10+
} => (
11+
#[test]
12+
$(#[$m])*
13+
fn $fn_name() {
14+
fn prop($($arg_name: $arg_ty),*) -> $ret {
15+
$($code)*
16+
}
17+
$crate::quickcheck(prop as fn($($arg_ty),*) -> $ret);
18+
}
19+
)
20+
}
21+
22+
// Dummy implementation of the underlying quickcheck tester.
23+
pub fn quickcheck(f: fn(u32, u32) -> bool) {
24+
assert!(f(0, 0));
25+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
//@ edition: 2024
2+
3+
#![crate_type = "proc-macro"]
4+
5+
#![feature(proc_macro_quote)]
6+
7+
extern crate proc_macro;
8+
9+
use proc_macro::{Ident, Span, TokenStream, TokenTree, quote};
10+
11+
#[proc_macro_attribute]
12+
pub fn quickcheck(_args: TokenStream, input: TokenStream) -> TokenStream {
13+
let mut original_ident = None;
14+
15+
let prop_fn = input.into_iter()
16+
.map(|token_tree| {
17+
// HACK: Find first ident, ignoring the `fn` keyword.
18+
if let TokenTree::Ident(ident) = &token_tree && ident.to_string() != "fn" && let None = &original_ident {
19+
original_ident = Some(ident.clone());
20+
}
21+
token_tree
22+
})
23+
.collect::<TokenStream>();
24+
25+
let Some(ident) = original_ident else { panic!("cannot find prop fn ident"); };
26+
27+
quote! {
28+
#[test]
29+
fn $ident() {
30+
$prop_fn
31+
::quickcheck::quickcheck($ident as fn(u32, u32) -> bool);
32+
}
33+
}
34+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//@ print-tests
2+
//@ print-targets
3+
//@ stdout
4+
//@ stderr: empty
5+
//@ aux-build: quickcheck.rs
6+
//@ aux-build: quickcheck_macros.rs
7+
8+
extern crate quickcheck;
9+
extern crate quickcheck_macros;
10+
11+
use quickcheck_macros::quickcheck;
12+
13+
fn mutable_fn(a: u32, b: u32) -> u32 {
14+
a + b
15+
}
16+
17+
quickcheck::quickcheck! {
18+
fn quickcheck_decl_macro_test(a: u32, b: u32) -> bool {
19+
a + b == mutable_fn(a, b)
20+
}
21+
}
22+
23+
#[quickcheck]
24+
fn quickcheck_attr_macro_test(a: u32, b: u32) -> bool {
25+
a + b == mutable_fn(a, b)
26+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
2+
@@@ tests @@@
3+
4+
test quickcheck_attr_macro_test
5+
test quickcheck_decl_macro_test
6+
7+
tests: 2 total; 0 ignored
8+
9+
@@@ targets @@@
10+
11+
tests -(0)-> mutable_fn at tests/ui/test_discovery/quickcheck.rs:13:1: 13:37 (#0)
12+
(0) quickcheck_attr_macro_test
13+
(0) quickcheck_decl_macro_test
14+
15+
targets: 1 total; 1 safe; 0 unsafe (0 tainted)

0 commit comments

Comments
 (0)