mocksmith/
lib.rs

1mod clangwrap;
2mod generate;
3mod headerpath;
4mod log;
5mod model;
6pub mod naming;
7
8use clangwrap::ClangWrap;
9use headerpath::header_include_path;
10use std::path::{Path, PathBuf};
11
12#[derive(thiserror::Error, Debug, PartialEq)]
13pub enum MocksmithError {
14    #[error("Another thread is already using Mocksmith")]
15    Busy,
16    #[error("Another thread using Mocksmith panicked")]
17    Poisoned,
18    #[error("Could not access Clang: {0}")]
19    ClangError(String),
20    #[error("Invalid sed style replacement string: {0}")]
21    InvalidSedReplacement(String),
22    #[error("Invalid regex string: {0}")]
23    InvalidRegex(String),
24    #[error("Input header file does not exist or is not a file: {0}")]
25    InputFileError(PathBuf),
26    #[error("Parse error {}at line {}, column {}: {}",
27            if file.is_none() {
28                String::new()
29            }
30            else {
31                format!("in file {} ", file.as_ref().unwrap().display())
32            },
33            line, column, message)]
34    ParseError {
35        message: String,
36        file: Option<PathBuf>,
37        line: u32,
38        column: u32,
39    },
40    #[error("No appropriate class to mock was found in the file")]
41    NothingToMock,
42}
43
44pub type Result<T> = std::result::Result<T, MocksmithError>;
45
46/// Enum to control which methods to mock in a class.
47#[derive(Clone, Copy, Debug)]
48pub enum MethodsToMockStrategy {
49    /// Mock all methods, including non-virtual ones.
50    All,
51    /// Mock only virtual methods, including pure virtual ones.
52    AllVirtual,
53    /// Mock only pure virtual methods.
54    OnlyPureVirtual,
55}
56
57/// Representation of a mock produced by Mocksmith.
58#[derive(Debug, PartialEq)]
59pub struct Mock {
60    /// Path to the header file of the mocked class
61    pub source_file: Option<PathBuf>,
62    /// Name of the mocked class
63    pub parent_name: String,
64    /// Name of the mock
65    pub name: String,
66    /// Code for the mock
67    pub code: String,
68}
69
70/// Representation of a mock header produced by Mocksmith.
71#[derive(Debug, PartialEq)]
72pub struct MockHeader {
73    /// The mocks within the header
74    pub mocks: Vec<Mock>,
75    /// Code for the complete mock header
76    pub code: String,
77}
78
79impl crate::MockHeader {
80    fn new() -> Self {
81        Self {
82            mocks: Vec::new(),
83            code: String::new(),
84        }
85    }
86}
87
88/// Mocksmith is a struct for generating Google Mock mocks for C++ classes.
89pub struct Mocksmith {
90    clangwrap: ClangWrap,
91    generator: generate::Generator,
92
93    include_paths: Vec<PathBuf>,
94    methods_to_mock: MethodsToMockStrategy,
95    filter_class: Box<dyn Fn(&str) -> bool>,
96    name_mock: Box<dyn Fn(&str) -> String>,
97}
98
99impl Mocksmith {
100    /// Creates a new Mocksmith instance.
101    ///
102    /// The function fails if another thread already holds an instance, since Clang can
103    /// only be used from one thread.
104    pub fn new(log_write: Option<Box<dyn std::io::Write>>, verbose: bool) -> Result<Self> {
105        let log = log_write.map(|write| log::Logger::new(write, verbose));
106        Self::create(ClangWrap::new(log)?)
107    }
108
109    /// Creates a new Mocksmith instance.
110    ///
111    /// The function waits for any other thread holding an instance to release its
112    /// instance before returning since Clang can only be used from one thread. If a
113    /// thread using Mocksmith panics, the poisoning is cleared.
114    pub fn new_when_available() -> Result<Self> {
115        let mut clangwrap = ClangWrap::blocking_new();
116        while let Err(MocksmithError::Poisoned) = clangwrap {
117            ClangWrap::clear_poison();
118            clangwrap = ClangWrap::blocking_new();
119        }
120        Self::create(clangwrap?)
121    }
122
123    fn create(clangwrap: clangwrap::ClangWrap) -> Result<Self> {
124        let methods_to_mock = MethodsToMockStrategy::AllVirtual;
125        let mocksmith = Self {
126            clangwrap,
127            generator: generate::Generator::new(methods_to_mock),
128            include_paths: Vec::new(),
129            methods_to_mock,
130            filter_class: Box::new(|_| true),
131            name_mock: Box::new(naming::default_name_mock),
132        };
133        Ok(mocksmith)
134    }
135
136    /// Adds an include path to the list of paths to search for headers. If no include
137    /// paths are set, the current directory is used.
138    pub fn include_path<P>(mut self, include_path: P) -> Self
139    where
140        P: AsRef<Path>,
141    {
142        self.include_paths.push(include_path.as_ref().to_path_buf());
143        self
144    }
145
146    /// Adds include paths to the list of paths to search for headers. If no include
147    /// paths are set, the current directory is used.
148    pub fn include_paths(mut self, include_paths: &[PathBuf]) -> Self {
149        self.include_paths.extend(include_paths.iter().cloned());
150        self
151    }
152
153    /// Sets which methods to mock in the classes. Default is `AllVirtual`, which mocks
154    /// all virtual methods.
155    pub fn methods_to_mock(mut self, methods: MethodsToMockStrategy) -> Self {
156        self.methods_to_mock = methods;
157        self.generator.methods_to_mock(methods);
158        self
159    }
160
161    /// Sets a function to filter which classes to mock. The function takes the name of
162    /// a class and should return `true` if the class should be mocked.
163    pub fn class_filter_fun(mut self, filter: impl Fn(&str) -> bool + 'static) -> Self {
164        self.filter_class = Box::new(filter);
165        self
166    }
167
168    /// Errors detected by Clang during parsing normally causes mock generation to fail.
169    /// Setting this option disables which may be useful, e.g., when not able to provide
170    /// all the include paths. Beware that this may lead to unknown types in arguments
171    /// being referred to as `int` in generated mocks, and entire methods and classes
172    /// being ignored (when return value of method is unknown).
173    pub fn ignore_errors(mut self, value: bool) -> Self {
174        self.clangwrap.set_ignore_errors(value);
175        self
176    }
177
178    /// Sets the C++ standard to use when parsing the source header files. Default is
179    /// "c++17".
180    pub fn cpp_standard(mut self, standard: Option<String>) -> Self {
181        self.clangwrap.set_cpp_standard(standard);
182        self
183    }
184
185    /// Sets additional arguments to the clang C++ parser.
186    pub fn additional_clang_args(mut self, args: Vec<String>) -> Self {
187        self.clangwrap.set_additional_clang_args(args);
188        self
189    }
190
191    /// For easy testability of parser warnings.
192    pub fn parse_function_bodies(mut self, value: bool) -> Self {
193        self.clangwrap.set_parse_function_bodies(value);
194        self
195    }
196
197    /// Sets whether to add MSVC pragma to allow overriding methods marked as deprecated.
198    /// If it is not added mocked methods marked as deprecated will cause compilation
199    /// warnings. The pragma is only added when generating headers. Default is false.
200    pub fn msvc_allow_overriding_deprecated_methods(mut self, value: bool) -> Self {
201        self.generator.add_deprecation_pragma(value);
202        self
203    }
204
205    /// Controls whether to use C++17 style nested namespace declarations with colon
206    /// separation or older style. Default is true.
207    pub fn simplified_nested_namespaces(mut self, value: bool) -> Self {
208        self.generator.simplified_nested_namespaces(value);
209        self
210    }
211
212    /// Sets the string to use for indentation for the generated code. Default is 2 spaces.
213    pub fn indent_str(mut self, indent: String) -> Self {
214        self.generator.indent_str(indent);
215        self
216    }
217
218    /// Sets a custom function to generate mock names based on class names.
219    pub fn mock_name_fun(mut self, name_mock: impl Fn(&str) -> String + 'static) -> Self {
220        self.name_mock = Box::new(name_mock);
221        self
222    }
223
224    /// Generates mocks for classes in the given file. If no appropriate classes to mock
225    /// are found, an empty vector is returned.
226    pub fn create_mocks_for_file<P>(&self, file: P) -> Result<Vec<Mock>>
227    where
228        P: AsRef<Path>,
229    {
230        if !file.as_ref().is_file() {
231            return Err(MocksmithError::InputFileError(file.as_ref().to_path_buf()));
232        }
233        self.clangwrap
234            .with_tu_from_file(&self.include_paths, file.as_ref(), |tu| {
235                let mut mocks = self.create_mocks(tu)?;
236                mocks.iter_mut().for_each(|m| {
237                    m.source_file = Some(file.as_ref().to_path_buf());
238                });
239                Ok(mocks)
240            })
241    }
242
243    /// Generates mocks for classes in the given string. If no appropriate classes to mock
244    /// are found, an empty vector is returned.
245    pub fn create_mocks_from_string(&self, content: &str) -> Result<Vec<Mock>> {
246        self.clangwrap
247            .with_tu_from_string(&self.include_paths, content, |tu| self.create_mocks(tu))
248    }
249
250    /// Generate the contents for a header file with mocks for classes in the give file.
251    /// If no appropriate classes to mock are found, an error is returned.
252    pub fn create_mock_header_for_files<P>(&self, files: &[P]) -> Result<MockHeader>
253    where
254        P: AsRef<Path>,
255    {
256        let source_file_include_paths: Vec<String> = files
257            .iter()
258            .map(|f| self.header_include_path(f.as_ref()))
259            .collect();
260
261        let mut header = MockHeader::new();
262        for file in files {
263            let mocks = self.create_mocks_for_file(file.as_ref())?;
264            header.mocks.extend(mocks);
265        }
266
267        header.code = self
268            .generator
269            .header(&source_file_include_paths, &header.mocks);
270
271        Ok(header)
272    }
273
274    fn header_include_path(&self, header_file: &Path) -> String {
275        if self.include_paths.is_empty() {
276            header_include_path(header_file, &[PathBuf::from(".")])
277        } else {
278            header_include_path(header_file, &self.include_paths)
279        }
280    }
281
282    fn create_mocks(&self, tu: &clang::TranslationUnit) -> Result<Vec<Mock>> {
283        let classes = model::classes_in_translation_unit(tu, self.methods_to_mock);
284        Ok(classes
285            .iter()
286            .filter(|class| (self.filter_class)(class.name.as_str()))
287            .map(|class| self.generator.mock(class, &self.mock_name(class)))
288            .collect())
289    }
290
291    fn mock_name(&self, class: &model::ClassToMock) -> String {
292        (self.name_mock)(&class.name)
293    }
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn test_new_with_threads() {
302        let mocksmith = Mocksmith::new(None, false).unwrap();
303
304        let handle = std::thread::spawn(|| {
305            assert!(matches!(
306                Mocksmith::new(None, false),
307                Err(MocksmithError::Busy)
308            ));
309        });
310        handle.join().unwrap();
311
312        let handle = std::thread::spawn(|| {
313            let _mocksmith = Mocksmith::new_when_available().unwrap();
314        });
315        std::thread::sleep(std::time::Duration::from_millis(25));
316        std::mem::drop(mocksmith);
317        handle.join().unwrap();
318    }
319}