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!();