blob: 619c79c82677f5dedbb8a3e2db0ee3e9f998ee10 [file] [log] [blame]
// Copyright 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.
use std::{
fs::File,
io::{BufRead, BufReader},
path::PathBuf,
};
use itertools::{Either, Itertools};
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::Item;
use autocxx_integration_tests::{CodeChecker, CodeCheckerFns, TestError};
/// Generates a closure which can be used to ensure that the given symbol
/// is mentioned in the output and has documentation attached.
/// The idea is that this is what we do in cases where we can't generate code properly.
pub(crate) fn make_error_finder(error_symbol: &'static str) -> CodeChecker {
Box::new(ErrorFinder(error_symbol))
}
struct ErrorFinder(&'static str);
impl CodeCheckerFns for ErrorFinder {
fn check_rust(&self, rs: syn::File) -> Result<(), TestError> {
let ffi_items = find_ffi_items(rs)?;
// Ensure there's some kind of struct entry for this symbol
let error_item = ffi_items
.into_iter()
.filter_map(|i| match i {
Item::Struct(its) if its.ident == self.0 => Some(its),
_ => None,
})
.next()
.ok_or_else(|| TestError::RsCodeExaminationFail("Couldn't find item".into()))?;
// Ensure doc attribute
error_item
.attrs
.into_iter()
.find(|a| a.path.get_ident().filter(|p| *p == "doc").is_some())
.ok_or_else(|| TestError::RsCodeExaminationFail("Item had no docs".into()))?;
Ok(())
}
}
fn find_ffi_items(f: syn::File) -> Result<Vec<Item>, TestError> {
let md = f
.items
.into_iter()
.filter_map(|i| match i {
Item::Mod(itm) => Some(itm),
_ => None,
})
.next()
.ok_or_else(|| TestError::RsCodeExaminationFail("No mods in file".into()))?;
let mut items = Vec::new();
find_all_non_mod_items(md, &mut items);
Ok(items)
}
fn find_all_non_mod_items(md: syn::ItemMod, items: &mut Vec<Item>) {
let (more_mods, mut these_items): (Vec<_>, Vec<_>) = md
.content
.into_iter()
.flat_map(|(_, more_items)| more_items.into_iter())
.partition_map(|i| match i {
Item::Mod(itm) => Either::Left(itm),
_ => Either::Right(i),
});
items.append(&mut these_items);
for md in more_mods.into_iter() {
find_all_non_mod_items(md, items);
}
}
struct StringFinder(Vec<String>);
impl CodeCheckerFns for StringFinder {
fn check_rust(&self, rs: syn::File) -> Result<(), TestError> {
let toks = rs.to_token_stream().to_string();
for msg in &self.0 {
if !toks.contains(msg) {
return Err(TestError::RsCodeExaminationFail(format!(
"Couldn't find string '{}'",
msg
)));
};
}
Ok(())
}
}
/// Returns a code checker which simply hunts for a given string in the results
pub(crate) fn make_string_finder(error_texts: Vec<String>) -> CodeChecker {
Box::new(StringFinder(error_texts))
}
struct RustCodeFinder(Vec<TokenStream>);
impl CodeCheckerFns for RustCodeFinder {
fn check_rust(&self, rs: syn::File) -> Result<(), TestError> {
let haystack = rs.to_token_stream().to_string();
for msg in &self.0 {
let needle = msg.to_string();
if !haystack.contains(&needle) {
return Err(TestError::RsCodeExaminationFail(format!(
"Couldn't find tokens '{}'",
needle
)));
};
}
Ok(())
}
}
/// Returns a code checker which hunts for the given Rust tokens in the output
pub(crate) fn make_rust_code_finder(code: Vec<TokenStream>) -> CodeChecker {
Box::new(RustCodeFinder(code))
}
/// Searches generated C++ for strings we want to find, or want _not_ to find,
/// or both.
pub(crate) struct CppMatcher<'a> {
positive_matches: &'a [&'a str],
negative_matches: &'a [&'a str],
}
impl<'a> CppMatcher<'a> {
pub(crate) fn new(positive_matches: &'a [&'a str], negative_matches: &'a [&'a str]) -> Self {
Self {
positive_matches,
negative_matches,
}
}
}
impl<'a> CodeCheckerFns for CppMatcher<'a> {
fn check_cpp(&self, cpp: &[PathBuf]) -> Result<(), TestError> {
let mut positives_needed = self.positive_matches.to_vec();
for filename in cpp {
let file = File::open(filename).unwrap();
let lines = BufReader::new(file).lines();
for l in lines.filter_map(|l| l.ok()) {
if self.negative_matches.iter().any(|neg| l.contains(neg)) {
return Err(TestError::CppCodeExaminationFail);
}
positives_needed.retain(|pos| !l.contains(pos));
}
}
if positives_needed.is_empty() {
Ok(())
} else {
Err(TestError::CppCodeExaminationFail)
}
}
}
pub(crate) struct NoSystemHeadersChecker;
impl CodeCheckerFns for NoSystemHeadersChecker {
fn check_cpp(&self, cpp: &[PathBuf]) -> Result<(), TestError> {
for filename in cpp {
let file = File::open(filename).unwrap();
if BufReader::new(file)
.lines()
.any(|l| l.as_ref().unwrap().starts_with("#include <"))
{
return Err(TestError::CppCodeExaminationFail);
}
}
Ok(())
}
fn skip_build(&self) -> bool {
true
}
}