rdroidtest: add attribute proc macro am: 4e93ffd4e2
Original change: https://android-review.googlesource.com/c/platform/platform_testing/+/2890086
Change-Id: Ida7f2486cf986d3d44cac1531c72f0389c14c1f5
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
diff --git a/libraries/rdroidtest/Android.bp b/libraries/rdroidtest/Android.bp
index cd0bbec..2842f3e 100644
--- a/libraries/rdroidtest/Android.bp
+++ b/libraries/rdroidtest/Android.bp
@@ -12,7 +12,10 @@
"liblog_rust",
"liblogger",
],
- proc_macros: ["libpaste"],
+ proc_macros: [
+ "libpaste",
+ "librdroidtest_macro",
+ ],
apex_available: [
"//apex_available:platform",
"//apex_available:anyapex",
@@ -20,10 +23,25 @@
vendor_available: true,
}
+rust_proc_macro {
+ name: "librdroidtest_macro",
+ crate_name: "rdroidtest_macro",
+ cargo_env_compat: true,
+ cargo_pkg_version: "0.1.0",
+ srcs: ["macro/lib.rs"],
+ edition: "2021",
+ rustlibs: [
+ "libproc_macro2",
+ "libquote",
+ "libsyn",
+ ],
+}
+
rust_defaults {
name: "rdroidtest.defaults",
test_harness: false,
cfgs: ["test"],
+ proc_macros: ["librdroidtest_macro"],
rustlibs: [
"librdroidtest",
"liblinkme",
@@ -38,7 +56,7 @@
rust_test {
name: "librdroidtest_test",
- srcs: ["tests/raw.rs"],
+ srcs: ["tests/main.rs"],
test_suites: [
"general-tests",
],
diff --git a/libraries/rdroidtest/README.md b/libraries/rdroidtest/README.md
index 199e79d..780967e 100644
--- a/libraries/rdroidtest/README.md
+++ b/libraries/rdroidtest/README.md
@@ -32,24 +32,26 @@
(If you're testing a library or anything else which doesn't have a `main` function, you don't need
to worry about this.)
-Each test case should be marked with the `rdroidtest::test!` macro, rather than the standard
+Each test case should be marked with the `rdroidtest` attribute, rather than the standard
`#[test]` attribute:
```rust
-use rdroidtest::test;
+use rdroidtest::rdroidtest;
-test!(one_plus_one);
+#[rdroidtest]
fn one_plus_one() {
assert_eq!(1 + 1, 2);
}
```
-To ignore a test, you can add an `ignore_if` clause with a boolean expression:
+To ignore a test, you can add an `ignore_if` attribute whose argument is an expression that
+evaluates to a boolean:
```rust
-use rdroidtest::test;
+use rdroidtest::{ignore_if, rdroidtest};
-test!(clap_hands, ignore_if: !feeling_happy());
+#[rdroidtest]
+#[ignore_if(!feeling_happy())]
fn clap_hands() {
assert!(HANDS.clap().is_ok());
}
@@ -67,21 +69,21 @@
## Parameterized Tests
-To run the same test multiple times with different parameter values, use the `rdroidtest::ptest!`
-macro:
+To run the same test multiple times with different parameter values, add an argument to the
+`rdroidtest` attribute:
```rust
-use rdroidtest::ptest;
+use rdroidtest::rdroidtest;
-ptest!(is_even, my_instances());
+#[rdroidtest(my_instances())]
fn is_even(param: u32) {
assert_eq!(param % 2, 0);
}
```
-The second argument to the `ptest!` macro is an expression that is called at runtime to generate
-the set of parameters to invoke the test with. This expression should emit a vector of
-`(String, T)` values:
+The initial argument to the `rdroidtest` attribute is an expression that generates the set of
+parameters to invoke the test with. This expression should evaluate to a vector of `(String, T)`
+values for some type `T`:
```rust
fn my_instances() -> Vec<(String, u32)> {
@@ -96,11 +98,13 @@
The test method will be invoked with each of the parameter values in turn, passed in as the single
argument of type `T`.
-Parameterized tests can also be ignored, using an `ignore_if` clause that accepts the parameter
-value (this time as type `&T`) and returns a boolean:
+Parameterized tests can also be ignored, using an `ignore_if` attribute. For a parameterized test,
+the argument is an expression that emits a boolean when invoked with a single argument, of type
+`&T`:
```rust
-ptest!(is_even_too, my_instances(), ignore_if: |p| feeling_odd(p));
+#[rdroidtest(my_instances())]
+#[ignore_if(feeling_odd)]
fn is_even_too(param: u32) {
assert_eq!(param % 2, 0);
}
@@ -109,3 +113,15 @@
*param % 2 == 1
}
```
+
+## Summary Table
+
+| | Normal | Conditionally Ignore |
+|---------------|----------------------|-----------------------------------------------|
+| Normal | `#[rdroidtest]` | `#[rdroidtest]` <br> `#[ignore_if(<I>)]` |
+| Parameterized | `#[rdroidtest(<G>)]` | `#[rdroidtest(<G>)]` <br> `#[ignore_if(<C>)]` |
+
+Where:
+- `<I>` is an expression that evaluates to a `bool`.
+- `<G>` is an expression that evaluates to a `Vec<String, T>`.
+- `<C>` is an callable expression with signature `fn(&T) -> bool`.
diff --git a/libraries/rdroidtest/macro/lib.rs b/libraries/rdroidtest/macro/lib.rs
new file mode 100644
index 0000000..feeacec
--- /dev/null
+++ b/libraries/rdroidtest/macro/lib.rs
@@ -0,0 +1,65 @@
+//! Attribute proc macro for rdroidtest instances.
+use proc_macro::TokenStream;
+use proc_macro2::TokenStream as TokenStream2;
+use quote::{quote, ToTokens};
+use syn::{parse_macro_input, ItemFn, Meta};
+
+/// Macro to mark an `rdroidtest` test function. Can take one optional argument, an expression that
+/// evaluates to a `Vec` of parameter (name, value) pairs.
+///
+/// Also detects `#[ignore]` and `#[ignore_if(<expr>)]` attributes on the test function.
+#[proc_macro_attribute]
+pub fn rdroidtest(args: TokenStream, item: TokenStream) -> TokenStream {
+ // Only accept code that parses as a function definition.
+ let item = parse_macro_input!(item as ItemFn);
+ let fn_name = &item.sig.ident;
+
+ // If the attribute has any arguments, they are expected to be a parameter generator expression.
+ let param_gen: Option<TokenStream2> = if args.is_empty() { None } else { Some(args.into()) };
+
+ // Look for `#[ignore]` and `#[ignore_if(<expr>)]` attributes on the wrapped item.
+ let mut ignore_if: Option<TokenStream2> = None;
+ let mut ignored = false;
+ for attr in &item.attrs {
+ match &attr.meta {
+ Meta::Path(path) if path.to_token_stream().to_string().as_str() == "ignore" => {
+ // `#[ignore]` attribute.
+ ignored = true;
+ }
+ Meta::List(list) if list.path.to_token_stream().to_string().as_str() == "ignore_if" => {
+ // `#[ignore_if(<expr>)]` attribute.
+ ignore_if = Some(list.tokens.clone());
+ }
+ _ => {}
+ }
+ }
+ if ignored {
+ // `#[ignore]` trumps any specified `#[ignore_if]`.
+ ignore_if = Some(if param_gen.is_some() {
+ // `ignore_if` needs to be something invoked with a single parameter.
+ quote! { |_p| true }.into_iter().collect()
+ } else {
+ quote! { true }.into_iter().collect()
+ });
+ }
+
+ // Build up an invocation of the appropriate `rdroidtest` declarative macro.
+ let invocation = match (param_gen, ignore_if) {
+ (Some(pg), Some(ii)) => quote! { ::rdroidtest::ptest!( #fn_name, #pg, ignore_if: #ii ); },
+ (Some(pg), None) => quote! { ::rdroidtest::ptest!( #fn_name, #pg ); },
+ (None, Some(ii)) => quote! { ::rdroidtest::test!( #fn_name, ignore_if: #ii ); },
+ (None, None) => quote! { ::rdroidtest::test!( #fn_name ); },
+ };
+
+ let mut stream = TokenStream2::new();
+ stream.extend([invocation]);
+ stream.extend(item.into_token_stream());
+ stream.into_token_stream().into()
+}
+
+/// Macro to mark conditions for ignoring an `rdroidtest` test function. Expands to nothing here,
+/// scanned for by the [`rdroidtest`] attribute macro.
+#[proc_macro_attribute]
+pub fn ignore_if(_args: TokenStream, item: TokenStream) -> TokenStream {
+ item
+}
diff --git a/libraries/rdroidtest/src/lib.rs b/libraries/rdroidtest/src/lib.rs
index 94761a3..9ea742e 100644
--- a/libraries/rdroidtest/src/lib.rs
+++ b/libraries/rdroidtest/src/lib.rs
@@ -2,6 +2,9 @@
pub mod runner;
+// Re-export the attribute macros.
+pub use rdroidtest_macro::{ignore_if, rdroidtest};
+
#[doc(hidden)]
pub use libtest_mimic as _libtest_mimic;
#[doc(hidden)]
diff --git a/libraries/rdroidtest/tests/main.rs b/libraries/rdroidtest/tests/main.rs
new file mode 100644
index 0000000..707633d
--- /dev/null
+++ b/libraries/rdroidtest/tests/main.rs
@@ -0,0 +1,112 @@
+//! Test use of `rdroidtest` attribute macro.
+
+use rdroidtest::{ignore_if, rdroidtest};
+
+mod raw;
+
+#[rdroidtest]
+fn one_plus_one() {
+ let result = 1 + 1;
+ assert_eq!(result, 2);
+}
+
+#[rdroidtest]
+#[ignore_if(feeling_happy())]
+fn grumble() {
+ let result = 1 + 1;
+ assert_eq!(result, 2);
+}
+
+#[rdroidtest]
+#[ignore_if(!feeling_happy())]
+fn clap_hands() {
+ let result = 1 + 1;
+ assert_eq!(result, 3);
+}
+
+fn feeling_happy() -> bool {
+ false
+}
+
+#[rdroidtest(my_instances())]
+fn is_less_than_five(param: u32) {
+ assert!(param < 5);
+}
+
+#[rdroidtest(my_instances())]
+#[ignore_if(feeling_odd)]
+fn is_even(param: u32) {
+ assert_eq!(param % 2, 0);
+}
+
+#[rdroidtest(my_instances())]
+#[ignore_if(|p| !feeling_odd(p))]
+fn is_odd(param: u32) {
+ assert_eq!(param % 2, 1);
+}
+
+fn feeling_odd(param: &u32) -> bool {
+ *param % 2 == 1
+}
+
+fn my_instances() -> Vec<(String, u32)> {
+ vec![("one".to_string(), 1), ("two".to_string(), 2), ("three".to_string(), 3)]
+}
+
+#[rdroidtest(wrapped_instances())]
+#[ignore_if(|p| !feeling_odder(p))]
+fn is_odder(param: Param) {
+ assert_eq!(param.0 % 2, 1);
+}
+
+fn feeling_odder(param: &Param) -> bool {
+ param.0 % 2 == 1
+}
+
+struct Param(u32);
+
+fn wrapped_instances() -> Vec<(String, Param)> {
+ vec![
+ ("one".to_string(), Param(1)),
+ ("two".to_string(), Param(2)),
+ ("three".to_string(), Param(3)),
+ ]
+}
+
+#[rdroidtest(more_instances())]
+#[ignore_if(|p| p != "one")]
+fn is_the_one(param: String) {
+ assert_eq!(param, "one");
+}
+
+fn more_instances() -> Vec<(String, String)> {
+ vec![("one".to_string(), "one".to_string()), ("two".to_string(), "two".to_string())]
+}
+
+#[rdroidtest]
+#[ignore]
+fn ignore_me() {
+ panic!("shouldn't run!");
+}
+
+#[rdroidtest]
+#[ignore_if(false)]
+#[ignore]
+fn ignore_me_too() {
+ panic!("shouldn't run either -- attribute trumps ignore_if!");
+}
+
+#[rdroidtest]
+#[ignore]
+#[ignore_if(false)]
+fn ignore_me_as_well() {
+ panic!("shouldn't run either -- attribute trumps ignore_if, regardless of order!");
+}
+
+#[rdroidtest(my_instances())]
+#[ignore]
+fn ignore_all(param: u32) {
+ panic!("parameterized test ({param}) shouldn't run");
+}
+
+rdroidtest::test_main!();
diff --git a/libraries/rdroidtest/tests/raw.rs b/libraries/rdroidtest/tests/raw.rs
index e22305f..748cafd 100644
--- a/libraries/rdroidtest/tests/raw.rs
+++ b/libraries/rdroidtest/tests/raw.rs
@@ -10,6 +10,12 @@
assert_eq!(result, 2);
}
+test!(grumble, ignore_if: feeling_happy());
+fn grumble() {
+ let result = 1 + 1;
+ assert_eq!(result, 2);
+}
+
test!(clap_hands, ignore_if: !feeling_happy());
fn clap_hands() {
let result = 1 + 1;
@@ -70,5 +76,3 @@
fn more_instances() -> Vec<(String, String)> {
vec![("one".to_string(), "one".to_string()), ("two".to_string(), "two".to_string())]
}
-
-rdroidtest::test_main!();