blob: 619c79c82677f5dedbb8a3e2db0ee3e9f998ee10 [file] [log] [blame]
Brian Silverman4e662aa2022-05-11 23:10:19 -07001// Copyright 2021 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
4// https://www.apache.org/licenses/LICENSE-2.0> or the MIT license
5// <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your
6// option. This file may not be copied, modified, or distributed
7// except according to those terms.
8
9use std::{
10 fs::File,
11 io::{BufRead, BufReader},
12 path::PathBuf,
13};
14
15use itertools::{Either, Itertools};
16use proc_macro2::TokenStream;
17use quote::ToTokens;
18use syn::Item;
19
20use autocxx_integration_tests::{CodeChecker, CodeCheckerFns, TestError};
21
22/// Generates a closure which can be used to ensure that the given symbol
23/// is mentioned in the output and has documentation attached.
24/// The idea is that this is what we do in cases where we can't generate code properly.
25pub(crate) fn make_error_finder(error_symbol: &'static str) -> CodeChecker {
26 Box::new(ErrorFinder(error_symbol))
27}
28struct ErrorFinder(&'static str);
29
30impl CodeCheckerFns for ErrorFinder {
31 fn check_rust(&self, rs: syn::File) -> Result<(), TestError> {
32 let ffi_items = find_ffi_items(rs)?;
33 // Ensure there's some kind of struct entry for this symbol
34 let error_item = ffi_items
35 .into_iter()
36 .filter_map(|i| match i {
37 Item::Struct(its) if its.ident == self.0 => Some(its),
38 _ => None,
39 })
40 .next()
41 .ok_or_else(|| TestError::RsCodeExaminationFail("Couldn't find item".into()))?;
42 // Ensure doc attribute
43 error_item
44 .attrs
45 .into_iter()
46 .find(|a| a.path.get_ident().filter(|p| *p == "doc").is_some())
47 .ok_or_else(|| TestError::RsCodeExaminationFail("Item had no docs".into()))?;
48 Ok(())
49 }
50}
51
52fn find_ffi_items(f: syn::File) -> Result<Vec<Item>, TestError> {
53 let md = f
54 .items
55 .into_iter()
56 .filter_map(|i| match i {
57 Item::Mod(itm) => Some(itm),
58 _ => None,
59 })
60 .next()
61 .ok_or_else(|| TestError::RsCodeExaminationFail("No mods in file".into()))?;
62 let mut items = Vec::new();
63 find_all_non_mod_items(md, &mut items);
64 Ok(items)
65}
66
67fn find_all_non_mod_items(md: syn::ItemMod, items: &mut Vec<Item>) {
68 let (more_mods, mut these_items): (Vec<_>, Vec<_>) = md
69 .content
70 .into_iter()
71 .flat_map(|(_, more_items)| more_items.into_iter())
72 .partition_map(|i| match i {
73 Item::Mod(itm) => Either::Left(itm),
74 _ => Either::Right(i),
75 });
76 items.append(&mut these_items);
77 for md in more_mods.into_iter() {
78 find_all_non_mod_items(md, items);
79 }
80}
81
82struct StringFinder(Vec<String>);
83
84impl CodeCheckerFns for StringFinder {
85 fn check_rust(&self, rs: syn::File) -> Result<(), TestError> {
86 let toks = rs.to_token_stream().to_string();
87 for msg in &self.0 {
88 if !toks.contains(msg) {
89 return Err(TestError::RsCodeExaminationFail(format!(
90 "Couldn't find string '{}'",
91 msg
92 )));
93 };
94 }
95 Ok(())
96 }
97}
98
99/// Returns a code checker which simply hunts for a given string in the results
100pub(crate) fn make_string_finder(error_texts: Vec<String>) -> CodeChecker {
101 Box::new(StringFinder(error_texts))
102}
103
104struct RustCodeFinder(Vec<TokenStream>);
105
106impl CodeCheckerFns for RustCodeFinder {
107 fn check_rust(&self, rs: syn::File) -> Result<(), TestError> {
108 let haystack = rs.to_token_stream().to_string();
109 for msg in &self.0 {
110 let needle = msg.to_string();
111 if !haystack.contains(&needle) {
112 return Err(TestError::RsCodeExaminationFail(format!(
113 "Couldn't find tokens '{}'",
114 needle
115 )));
116 };
117 }
118 Ok(())
119 }
120}
121
122/// Returns a code checker which hunts for the given Rust tokens in the output
123pub(crate) fn make_rust_code_finder(code: Vec<TokenStream>) -> CodeChecker {
124 Box::new(RustCodeFinder(code))
125}
126
127/// Searches generated C++ for strings we want to find, or want _not_ to find,
128/// or both.
129pub(crate) struct CppMatcher<'a> {
130 positive_matches: &'a [&'a str],
131 negative_matches: &'a [&'a str],
132}
133
134impl<'a> CppMatcher<'a> {
135 pub(crate) fn new(positive_matches: &'a [&'a str], negative_matches: &'a [&'a str]) -> Self {
136 Self {
137 positive_matches,
138 negative_matches,
139 }
140 }
141}
142
143impl<'a> CodeCheckerFns for CppMatcher<'a> {
144 fn check_cpp(&self, cpp: &[PathBuf]) -> Result<(), TestError> {
145 let mut positives_needed = self.positive_matches.to_vec();
146 for filename in cpp {
147 let file = File::open(filename).unwrap();
148 let lines = BufReader::new(file).lines();
149 for l in lines.filter_map(|l| l.ok()) {
150 if self.negative_matches.iter().any(|neg| l.contains(neg)) {
151 return Err(TestError::CppCodeExaminationFail);
152 }
153 positives_needed.retain(|pos| !l.contains(pos));
154 }
155 }
156 if positives_needed.is_empty() {
157 Ok(())
158 } else {
159 Err(TestError::CppCodeExaminationFail)
160 }
161 }
162}
163
164pub(crate) struct NoSystemHeadersChecker;
165
166impl CodeCheckerFns for NoSystemHeadersChecker {
167 fn check_cpp(&self, cpp: &[PathBuf]) -> Result<(), TestError> {
168 for filename in cpp {
169 let file = File::open(filename).unwrap();
170 if BufReader::new(file)
171 .lines()
172 .any(|l| l.as_ref().unwrap().starts_with("#include <"))
173 {
174 return Err(TestError::CppCodeExaminationFail);
175 }
176 }
177 Ok(())
178 }
179 fn skip_build(&self) -> bool {
180 true
181 }
182}