blob: ae12e3c528bb7da68cfa21601df50081651349d5 [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!(
Austin Schuh6ea9bfa2023-08-06 19:05:10 -070090 "Couldn't find string '{msg}'"
Brian Silverman4e662aa2022-05-11 23:10:19 -070091 )));
92 };
93 }
94 Ok(())
95 }
96}
97
98/// Returns a code checker which simply hunts for a given string in the results
99pub(crate) fn make_string_finder(error_texts: Vec<String>) -> CodeChecker {
100 Box::new(StringFinder(error_texts))
101}
102
103struct RustCodeFinder(Vec<TokenStream>);
104
105impl CodeCheckerFns for RustCodeFinder {
106 fn check_rust(&self, rs: syn::File) -> Result<(), TestError> {
107 let haystack = rs.to_token_stream().to_string();
108 for msg in &self.0 {
109 let needle = msg.to_string();
110 if !haystack.contains(&needle) {
111 return Err(TestError::RsCodeExaminationFail(format!(
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700112 "Couldn't find tokens '{needle}'"
Brian Silverman4e662aa2022-05-11 23:10:19 -0700113 )));
114 };
115 }
116 Ok(())
117 }
118}
119
120/// Returns a code checker which hunts for the given Rust tokens in the output
121pub(crate) fn make_rust_code_finder(code: Vec<TokenStream>) -> CodeChecker {
122 Box::new(RustCodeFinder(code))
123}
124
125/// Searches generated C++ for strings we want to find, or want _not_ to find,
126/// or both.
127pub(crate) struct CppMatcher<'a> {
128 positive_matches: &'a [&'a str],
129 negative_matches: &'a [&'a str],
130}
131
132impl<'a> CppMatcher<'a> {
133 pub(crate) fn new(positive_matches: &'a [&'a str], negative_matches: &'a [&'a str]) -> Self {
134 Self {
135 positive_matches,
136 negative_matches,
137 }
138 }
139}
140
141impl<'a> CodeCheckerFns for CppMatcher<'a> {
142 fn check_cpp(&self, cpp: &[PathBuf]) -> Result<(), TestError> {
143 let mut positives_needed = self.positive_matches.to_vec();
144 for filename in cpp {
145 let file = File::open(filename).unwrap();
146 let lines = BufReader::new(file).lines();
Austin Schuh6ea9bfa2023-08-06 19:05:10 -0700147 for l in lines.map_while(Result::ok) {
Brian Silverman4e662aa2022-05-11 23:10:19 -0700148 if self.negative_matches.iter().any(|neg| l.contains(neg)) {
149 return Err(TestError::CppCodeExaminationFail);
150 }
151 positives_needed.retain(|pos| !l.contains(pos));
152 }
153 }
154 if positives_needed.is_empty() {
155 Ok(())
156 } else {
157 Err(TestError::CppCodeExaminationFail)
158 }
159 }
160}
161
162pub(crate) struct NoSystemHeadersChecker;
163
164impl CodeCheckerFns for NoSystemHeadersChecker {
165 fn check_cpp(&self, cpp: &[PathBuf]) -> Result<(), TestError> {
166 for filename in cpp {
167 let file = File::open(filename).unwrap();
168 if BufReader::new(file)
169 .lines()
170 .any(|l| l.as_ref().unwrap().starts_with("#include <"))
171 {
172 return Err(TestError::CppCodeExaminationFail);
173 }
174 }
175 Ok(())
176 }
177 fn skip_build(&self) -> bool {
178 true
179 }
180}