blob: f0cd6165f62c0caeee71b41666eb1eae8473ca96 [file] [log] [blame]
// Part of the Crubit project, under the Apache License v2.0 with LLVM
// Exceptions. See /LICENSE for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
/// Like `token_stream_matchers::assert_cc_matches!`, but expects `IR` instance
/// as input. The macro converts the instance to its corresponding struct
/// expression and matches the pattern against that. See the documentation of
/// `token_stream_matchers` for more information.
///
/// Example:
/// ```rust
/// let ir = ir_from_cc(..., "struct SomeStruct {};').unwrap();
/// assert_ir_matches!(
/// ir,
/// quote! {
/// Record {
/// rs_name: "SomeStruct" ...
/// }
/// }
/// );
/// ```
#[macro_export]
macro_rules! assert_ir_matches {
($ir:expr, $pattern:expr $(,)*) => {
$crate::internal::match_ir(&$ir, &$pattern)
.expect("input unexpectedly didn't match the pattern");
};
}
/// Like `assert_ir_matches`, but asserts that the pattern does not match the
/// `IR`.
#[macro_export]
macro_rules! assert_ir_not_matches {
($ir:expr, $pattern:expr $(,)*) => {
$crate::internal::mismatch_ir(&$ir, &$pattern).unwrap();
};
}
/// Like `assert_ir_matches!`, but expects a list of Items and a list of
/// TokenStreams as input. The macro converts each Item to its struct expression
/// and matches the corresponding pattern against that.
/// The use case for this macro is to compare a list of Items to expected
/// patterns, e.g when we want to confirm that the children items of an item
/// appear in a certain order.
#[macro_export]
macro_rules! assert_items_match {
($items:expr, $patterns:expr $(,)*) => {
assert_eq!($items.len(), $patterns.len());
for (idx, (item, pattern)) in $items.into_iter().zip($patterns).enumerate() {
$crate::internal::match_item(&item, &pattern).expect(&format!(
"input at position {} unexpectedly didn't match the pattern",
&idx
));
}
};
}
/// Only used to make stuff needed by exported macros available
pub mod internal {
use anyhow::Result;
use ir::{Item, IR};
use itertools::Itertools;
pub use proc_macro2::TokenStream;
use quote::quote;
pub use token_stream_printer::{
rs_tokens_to_formatted_string, rs_tokens_to_formatted_string_for_tests,
};
pub fn match_ir(ir: &IR, pattern: &TokenStream) -> Result<()> {
token_stream_matchers::internal::match_tokens(
&ir_to_token_stream(ir)?,
pattern,
&ir_to_string,
)
}
pub fn mismatch_ir(ir: &IR, pattern: &TokenStream) -> Result<()> {
token_stream_matchers::internal::mismatch_tokens(
&ir_to_token_stream(ir)?,
pattern,
&ir_to_string,
)
}
fn ir_to_token_stream(ir: &IR) -> Result<TokenStream> {
// derived debug impl doesn't emit commas after the last element of a group,
// (for example `[a, b, c]`), but rustfmt automatically adds it (`[a, b,
// c,]`). We use rustfmt to format the failure messages. So to make the
// input token stream consistent with failure messages we format the
// input token stream with rustfmt as well.
Ok(ir_to_string(ir.flat_ir_debug_print().parse().unwrap())?.parse().unwrap())
}
fn ir_to_string(input: TokenStream) -> Result<String> {
// Rustfmt refuses to format some kinds of invalid Rust code. Let's put our IR
// struct expression into the body of a function, format it, and then remove
// the function.
let input_stream = quote! { fn make_rustfmt_happy() { #input } };
let formatted = rs_tokens_to_formatted_string_for_tests(input_stream)?;
let snippet = formatted
.strip_prefix("fn make_rustfmt_happy() {\n")
.unwrap()
.strip_suffix("}\n")
.unwrap()
.lines()
.map(|line| &line[4..])
.join("\n");
Ok(snippet)
}
pub fn match_item(item: &Item, pattern: &TokenStream) -> Result<()> {
token_stream_matchers::internal::match_tokens(
&item_to_token_stream(item)?,
pattern,
&ir_to_string,
)
}
fn item_to_token_stream(item: &Item) -> Result<TokenStream> {
// derived debug impl doesn't emit commas after the last element of a group,
// (for example `[a, b, c]`), but rustfmt automatically adds it (`[a, b,
// c,]`). We use rustfmt to format the failure messages. So to make the
// input token stream consistent with failure messages we format the
// input token stream with rustfmt as well.
Ok(ir_to_string(format! {"{:?}", item}.parse().unwrap())?.parse().unwrap())
}
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
/// We aren't testing platform-specific details, just the matchers.
fn ir_from_cc(header: &str) -> arc_anyhow::Result<ir::IR> {
ir_testing::ir_from_cc(multiplatform_testing::Platform::X86Linux, header)
}
#[test]
fn test_optional_trailing_comma() {
assert_ir_matches!(ir_from_cc("").unwrap(), quote! { FlatIR { ... }});
assert_ir_matches!(ir_from_cc("").unwrap(), quote! { FlatIR { ... }},);
assert_ir_not_matches!(ir_from_cc("").unwrap(), quote! {this pattern is not in the ir});
assert_ir_not_matches!(ir_from_cc("").unwrap(), quote! {this pattern is not in the ir},);
}
#[test]
fn test_assert_ir_matches_assumes_trailing_commas_in_groups() {
assert_ir_matches!(ir_from_cc("").unwrap(), quote! {{... , }});
}
#[test]
fn test_assert_not_matches_accepts_not_matching_pattern() {
assert_ir_not_matches!(ir_from_cc("").unwrap(), quote! {this pattern is not in the ir});
}
#[test]
#[should_panic(expected = "input:\n\n```\nFlatIR {")]
fn test_assert_ir_not_matches_panics_on_match() {
assert_ir_not_matches!(ir_from_cc("").unwrap(), quote! {items});
}
#[test]
#[should_panic]
fn test_assert_ir_matches_panics_on_mismatch() {
assert_ir_matches!(ir_from_cc("").unwrap(), quote! {this pattern is not in the ir});
}
}