Skip to content

Commit f4a7bd4

Browse files
committed
test(macro): add test infrastructure for macro crate
Fixes: #530
1 parent 9c1285b commit f4a7bd4

File tree

10 files changed

+194
-45
lines changed

10 files changed

+194
-45
lines changed

.github/actions/embed/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,6 @@ ENV RUSTUP_HOME=/rust
1010
ENV CARGO_HOME=/cargo
1111
ENV PATH=/cargo/bin:/rust/bin:$PATH
1212

13-
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path
13+
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path && cargo install cargo-expand
1414

1515
ENTRYPOINT [ "/cargo/bin/cargo", "test", "--all", "--release", "--all-features", "--no-fail-fast" ]

.github/actions/zts/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ ENV PATH=/cargo/bin:/rust/bin:$PATH
1212

1313
RUN (curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain nightly --no-modify-path) && rustup default nightly
1414

15-
ENTRYPOINT [ "/cargo/bin/cargo", "build", "--all", "--release" ]
15+
ENTRYPOINT [ "/cargo/bin/cargo", "build", "--all", "--release" ]

.github/workflows/build.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ jobs:
126126
with:
127127
# increment this manually to force cache eviction
128128
prefix-key: ${{ env.RUST_CACHE_PREFIX }}
129+
- name: Install Cargo expand
130+
run: cargo install cargo-expand --locked
131+
if: "!contains(matrix.os, 'ubuntu')"
132+
- name: Install Cargo expand
133+
uses: dtolnay/install@cargo-expand
134+
if: "contains(matrix.os, 'ubuntu')"
129135
# LLVM & Clang
130136
- name: Cache LLVM and Clang
131137
id: cache-llvm

.github/workflows/coverage.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ jobs:
5555
echo "LIBCLANG_PATH=${{ runner.temp }}/llvm-${{ env.clang }}/lib" >> $GITHUB_ENV
5656
echo "LLVM_VERSION=${{ steps.clang.outputs.version }}" >> $GITHUB_ENV
5757
echo "LLVM_CONFIG_PATH=${{ runner.temp }}/llvm-${{ env.clang }}/bin/llvm-config" >> $GITHUB_ENV
58+
- name: Install Cargo expand
59+
uses: dtolnay/install@cargo-expand
5860
- name: Install tarpaulin
5961
run: |
6062
cargo install cargo-tarpaulin --locked

.lefthook.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ pre-commit:
44
- name: fmt
55
run: rustfmt --edition 2021 {staged_files}
66
glob: "*.rs"
7+
exclude:
8+
- "crates/macros/tests/expand/*.expanded.rs"
79
stage_fixed: true
810
- name: clippy
911
run: cargo clippy --workspace --all-targets --all-features -- -W clippy::pedantic -D warnings

CONTRIBUTING.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ We have both unit and integration tests. When contributing, please ensure that y
3737
covered by an integration test. If possible, add unit tests as well. This might not always be possible
3838
due to the need of a running PHP interpreter.
3939

40+
### Testing macros
41+
42+
To test macro expansion, we use [`runtime-macros`](https://github.com/jeremydavis519/runtime-macros) in conjunction
43+
with the [`macro-test`](https://github.com/eupn/macrotest) crate.
44+
45+
To add new tests add a file inside the `tests/expand` directory. After running the tests, a new `<name>.expanded.rs`
46+
file will be created in the same directory. This file contains the expanded macro code. Verify that the
47+
expanded code is correct and that it matches the expected output. Commit the expanded file as well.
48+
49+
If creating a new macro it needs to be added to the test contained at the bottom of the `crates/macros/src/lib.rs` file.
50+
4051
### State of unit tests
4152
There are still large parts of the library that are not covered by unit tests. We strive to cover
4253
as much as possible, but this is a work in progress. If you make changes to untested code, we would

crates/macros/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,8 @@ convert_case = "0.8.0"
2222

2323
[lints.rust]
2424
missing_docs = "warn"
25+
26+
[dev-dependencies]
27+
glob = "0.3.2"
28+
macrotest = "1.1.0"
29+
runtime-macros = "1.1.1"

crates/macros/src/lib.rs

Lines changed: 147 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,8 @@ mod syn_ext;
1212
mod zval;
1313

1414
use proc_macro::TokenStream;
15-
use syn::{
16-
parse_macro_input, DeriveInput, ItemConst, ItemFn, ItemForeignMod, ItemImpl, ItemStruct,
17-
};
15+
use proc_macro2::TokenStream as TokenStream2;
16+
use syn::{DeriveInput, ItemConst, ItemFn, ItemForeignMod, ItemImpl, ItemStruct};
1817

1918
extern crate proc_macro;
2019

@@ -203,14 +202,17 @@ extern crate proc_macro;
203202
// END DOCS FROM classes.md
204203
#[proc_macro_attribute]
205204
pub fn php_class(args: TokenStream, input: TokenStream) -> TokenStream {
206-
let input = parse_macro_input!(input as ItemStruct);
205+
php_class_internal(args.into(), input.into()).into()
206+
}
207+
208+
#[allow(clippy::needless_pass_by_value)]
209+
fn php_class_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 {
210+
let input = parse_macro_input2!(input as ItemStruct);
207211
if !args.is_empty() {
208-
return err!(input => "`#[php_class(<args>)]` args are no longer supported. Please use `#[php(<args>)]` instead.").to_compile_error().into();
212+
return err!(input => "`#[php_class(<args>)]` args are no longer supported. Please use `#[php(<args>)]` instead.").to_compile_error();
209213
}
210214

211-
class::parser(input)
212-
.unwrap_or_else(|e| e.to_compile_error())
213-
.into()
215+
class::parser(input).unwrap_or_else(|e| e.to_compile_error())
214216
}
215217

216218
// BEGIN DOCS FROM function.md
@@ -371,14 +373,17 @@ pub fn php_class(args: TokenStream, input: TokenStream) -> TokenStream {
371373
// END DOCS FROM function.md
372374
#[proc_macro_attribute]
373375
pub fn php_function(args: TokenStream, input: TokenStream) -> TokenStream {
374-
let input = parse_macro_input!(input as ItemFn);
376+
php_function_internal(args.into(), input.into()).into()
377+
}
378+
379+
#[allow(clippy::needless_pass_by_value)]
380+
fn php_function_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 {
381+
let input = parse_macro_input2!(input as ItemFn);
375382
if !args.is_empty() {
376-
return err!(input => "`#[php_function(<args>)]` args are no longer supported. Please use `#[php(<args>)]` instead.").to_compile_error().into();
383+
return err!(input => "`#[php_function(<args>)]` args are no longer supported. Please use `#[php(<args>)]` instead.").to_compile_error();
377384
}
378385

379-
function::parser(input)
380-
.unwrap_or_else(|e| e.to_compile_error())
381-
.into()
386+
function::parser(input).unwrap_or_else(|e| e.to_compile_error())
382387
}
383388

384389
// BEGIN DOCS FROM constant.md
@@ -436,14 +441,17 @@ pub fn php_function(args: TokenStream, input: TokenStream) -> TokenStream {
436441
// END DOCS FROM constant.md
437442
#[proc_macro_attribute]
438443
pub fn php_const(args: TokenStream, input: TokenStream) -> TokenStream {
439-
let input = parse_macro_input!(input as ItemConst);
444+
php_const_internal(args.into(), input.into()).into()
445+
}
446+
447+
#[allow(clippy::needless_pass_by_value)]
448+
fn php_const_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 {
449+
let input = parse_macro_input2!(input as ItemConst);
440450
if !args.is_empty() {
441-
return err!(input => "`#[php_const(<args>)]` args are no longer supported. Please use `#[php(<args>)]` instead.").to_compile_error().into();
451+
return err!(input => "`#[php_const(<args>)]` args are no longer supported. Please use `#[php(<args>)]` instead.").to_compile_error();
442452
}
443453

444-
constant::parser(input)
445-
.unwrap_or_else(|e| e.to_compile_error())
446-
.into()
454+
constant::parser(input).unwrap_or_else(|e| e.to_compile_error())
447455
}
448456

449457
// BEGIN DOCS FROM module.md
@@ -516,14 +524,17 @@ pub fn php_const(args: TokenStream, input: TokenStream) -> TokenStream {
516524
// END DOCS FROM module.md
517525
#[proc_macro_attribute]
518526
pub fn php_module(args: TokenStream, input: TokenStream) -> TokenStream {
519-
let input = parse_macro_input!(input as ItemFn);
527+
php_module_internal(args.into(), input.into()).into()
528+
}
529+
530+
#[allow(clippy::needless_pass_by_value)]
531+
fn php_module_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 {
532+
let input = parse_macro_input2!(input as ItemFn);
520533
if !args.is_empty() {
521-
return err!(input => "`#[php_module(<args>)]` args are no longer supported. Please use `#[php(<args>)]` instead.").to_compile_error().into();
534+
return err!(input => "`#[php_module(<args>)]` args are no longer supported. Please use `#[php(<args>)]` instead.").to_compile_error();
522535
}
523536

524-
module::parser(input)
525-
.unwrap_or_else(|e| e.to_compile_error())
526-
.into()
537+
module::parser(input).unwrap_or_else(|e| e.to_compile_error())
527538
}
528539

529540
// BEGIN DOCS FROM impl.md
@@ -713,14 +724,17 @@ pub fn php_module(args: TokenStream, input: TokenStream) -> TokenStream {
713724
// END DOCS FROM impl.md
714725
#[proc_macro_attribute]
715726
pub fn php_impl(args: TokenStream, input: TokenStream) -> TokenStream {
716-
let input = parse_macro_input!(input as ItemImpl);
727+
php_impl_internal(args.into(), input.into()).into()
728+
}
729+
730+
#[allow(clippy::needless_pass_by_value)]
731+
fn php_impl_internal(args: TokenStream2, input: TokenStream2) -> TokenStream2 {
732+
let input = parse_macro_input2!(input as ItemImpl);
717733
if !args.is_empty() {
718-
return err!(input => "`#[php_impl(<args>)]` args are no longer supported. Please use `#[php(<args>)]` instead.").to_compile_error().into();
734+
return err!(input => "`#[php_impl(<args>)]` args are no longer supported. Please use `#[php(<args>)]` instead.").to_compile_error();
719735
}
720736

721-
impl_::parser(input)
722-
.unwrap_or_else(|e| e.to_compile_error())
723-
.into()
737+
impl_::parser(input).unwrap_or_else(|e| e.to_compile_error())
724738
}
725739

726740
// BEGIN DOCS FROM extern.md
@@ -789,12 +803,15 @@ pub fn php_impl(args: TokenStream, input: TokenStream) -> TokenStream {
789803
/// [`Zval`]: crate::types::Zval
790804
// END DOCS FROM extern.md
791805
#[proc_macro_attribute]
792-
pub fn php_extern(_: TokenStream, input: TokenStream) -> TokenStream {
793-
let input = parse_macro_input!(input as ItemForeignMod);
806+
pub fn php_extern(args: TokenStream, input: TokenStream) -> TokenStream {
807+
php_extern_internal(args.into(), input.into()).into()
808+
}
809+
810+
#[allow(clippy::needless_pass_by_value)]
811+
fn php_extern_internal(_: TokenStream2, input: TokenStream2) -> TokenStream2 {
812+
let input = parse_macro_input2!(input as ItemForeignMod);
794813

795-
extern_::parser(input)
796-
.unwrap_or_else(|e| e.to_compile_error())
797-
.into()
814+
extern_::parser(input).unwrap_or_else(|e| e.to_compile_error())
798815
}
799816

800817
// BEGIN DOCS FROM zval_convert.md
@@ -953,11 +970,13 @@ pub fn php_extern(_: TokenStream, input: TokenStream) -> TokenStream {
953970
// END DOCS FROM zval_convert.md
954971
#[proc_macro_derive(ZvalConvert)]
955972
pub fn zval_convert_derive(input: TokenStream) -> TokenStream {
956-
let input = parse_macro_input!(input as DeriveInput);
973+
zval_convert_derive_internal(input.into()).into()
974+
}
957975

958-
zval::parser(input)
959-
.unwrap_or_else(|e| e.to_compile_error())
960-
.into()
976+
fn zval_convert_derive_internal(input: TokenStream2) -> TokenStream2 {
977+
let input = parse_macro_input2!(input as DeriveInput);
978+
979+
zval::parser(input).unwrap_or_else(|e| e.to_compile_error())
961980
}
962981

963982
/// Defines an `extern` function with the Zend fastcall convention based on
@@ -992,35 +1011,61 @@ pub fn zval_convert_derive(input: TokenStream) -> TokenStream {
9921011
/// Rust and the `abi_vectorcall` feature enabled.
9931012
#[proc_macro]
9941013
pub fn zend_fastcall(input: TokenStream) -> TokenStream {
995-
let input = parse_macro_input!(input as ItemFn);
1014+
zend_fastcall_internal(input.into()).into()
1015+
}
9961016

997-
fastcall::parser(input).into()
1017+
fn zend_fastcall_internal(input: TokenStream2) -> TokenStream2 {
1018+
let input = parse_macro_input2!(input as ItemFn);
1019+
1020+
fastcall::parser(input)
9981021
}
9991022

10001023
/// Wraps a function to be used in the [`Module::function`] method.
10011024
#[proc_macro]
10021025
pub fn wrap_function(input: TokenStream) -> TokenStream {
1003-
let input = parse_macro_input!(input as syn::Path);
1026+
wrap_function_internal(input.into()).into()
1027+
}
1028+
1029+
fn wrap_function_internal(input: TokenStream2) -> TokenStream2 {
1030+
let input = parse_macro_input2!(input as syn::Path);
10041031

10051032
match function::wrap(&input) {
10061033
Ok(parsed) => parsed,
10071034
Err(e) => e.to_compile_error(),
10081035
}
1009-
.into()
10101036
}
10111037

10121038
/// Wraps a constant to be used in the [`ModuleBuilder::constant`] method.
10131039
#[proc_macro]
10141040
pub fn wrap_constant(input: TokenStream) -> TokenStream {
1015-
let input = parse_macro_input!(input as syn::Path);
1041+
wrap_constant_internal(input.into()).into()
1042+
}
1043+
1044+
fn wrap_constant_internal(input: TokenStream2) -> TokenStream2 {
1045+
let input = parse_macro_input2!(input as syn::Path);
10161046

10171047
match constant::wrap(&input) {
10181048
Ok(parsed) => parsed,
10191049
Err(e) => e.to_compile_error(),
10201050
}
1021-
.into()
10221051
}
10231052

1053+
macro_rules! parse_macro_input2 {
1054+
($tokenstream:ident as $ty:ty) => {
1055+
match syn::parse2::<$ty>($tokenstream) {
1056+
Ok(data) => data,
1057+
Err(err) => {
1058+
return proc_macro2::TokenStream::from(err.to_compile_error());
1059+
}
1060+
}
1061+
};
1062+
($tokenstream:ident) => {
1063+
$crate::parse_macro_input!($tokenstream as _)
1064+
};
1065+
}
1066+
1067+
pub(crate) use parse_macro_input2;
1068+
10241069
macro_rules! err {
10251070
($span:expr => $($msg:tt)*) => {
10261071
::syn::Error::new(::syn::spanned::Spanned::span(&$span), format!($($msg)*))
@@ -1061,3 +1106,62 @@ pub(crate) mod prelude {
10611106
pub(crate) use crate::{bail, err};
10621107
pub(crate) type Result<T> = std::result::Result<T, syn::Error>;
10631108
}
1109+
1110+
#[cfg(test)]
1111+
mod tests {
1112+
use super::*;
1113+
use std::path::PathBuf;
1114+
1115+
type AttributeFn =
1116+
fn(proc_macro2::TokenStream, proc_macro2::TokenStream) -> proc_macro2::TokenStream;
1117+
type FunctionLikeFn = fn(proc_macro2::TokenStream) -> proc_macro2::TokenStream;
1118+
1119+
#[test]
1120+
pub fn test_expand() {
1121+
macrotest::expand("tests/expand/*.rs");
1122+
for entry in glob::glob("tests/expand/*.rs").expect("Failed to read expand tests glob") {
1123+
let entry = entry.expect("Failed to read expand test file");
1124+
runtime_expand_attr(&entry);
1125+
runtime_expand_func(&entry);
1126+
runtime_expand_derive(&entry);
1127+
}
1128+
}
1129+
1130+
fn runtime_expand_attr(path: &PathBuf) {
1131+
let file = std::fs::File::open(path).expect("Failed to open expand test file");
1132+
runtime_macros::emulate_attributelike_macro_expansion(
1133+
file,
1134+
&[
1135+
("php_class", php_class_internal as AttributeFn),
1136+
("php_const", php_const_internal as AttributeFn),
1137+
("php_extern", php_extern_internal as AttributeFn),
1138+
("php_function", php_function_internal as AttributeFn),
1139+
("php_impl", php_impl_internal as AttributeFn),
1140+
("php_module", php_module_internal as AttributeFn),
1141+
],
1142+
)
1143+
.expect("Failed to expand attribute macros in test file");
1144+
}
1145+
1146+
fn runtime_expand_func(path: &PathBuf) {
1147+
let file = std::fs::File::open(path).expect("Failed to open expand test file");
1148+
runtime_macros::emulate_functionlike_macro_expansion(
1149+
file,
1150+
&[
1151+
("zend_fastcall", zend_fastcall_internal as FunctionLikeFn),
1152+
("wrap_function", wrap_function_internal as FunctionLikeFn),
1153+
("wrap_constant", wrap_constant_internal as FunctionLikeFn),
1154+
],
1155+
)
1156+
.expect("Failed to expand function-like macros in test file");
1157+
}
1158+
1159+
fn runtime_expand_derive(path: &PathBuf) {
1160+
let file = std::fs::File::open(path).expect("Failed to open expand test file");
1161+
runtime_macros::emulate_derive_macro_expansion(
1162+
file,
1163+
&[("ZvalConvert", zval_convert_derive_internal)],
1164+
)
1165+
.expect("Failed to expand derive macros in test file");
1166+
}
1167+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#[macro_use]
2+
extern crate ext_php_rs_derive;
3+
const MY_CONST: &str = "Hello, world!";
4+
#[allow(non_upper_case_globals)]
5+
const _internal_const_docs_MY_CONST: &[&str] = &[];
6+
#[allow(non_upper_case_globals)]
7+
const _internal_const_name_MY_CONST: &str = "MY_CONST";
8+
fn main() {
9+
(_internal_const_name_MY_CONST, MY_CONST, _internal_const_docs_MY_CONST);
10+
}

crates/macros/tests/expand/const.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#[macro_use]
2+
extern crate ext_php_rs_derive;
3+
4+
#[php_const]
5+
const MY_CONST: &str = "Hello, world!";
6+
7+
fn main() {
8+
wrap_constant!(MY_CONST);
9+
}

0 commit comments

Comments
 (0)