diff --git a/Cargo.toml b/Cargo.toml index 2ad1562..51a4a7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,12 +26,12 @@ thiserror = "2.0" # instructing clang-sys where libclang is located. For some reason, clang-sys does a # better job at finding libclang this way. [target.'cfg(not(target_os = "windows"))'.dependencies] -clang = { version="2.0", features=["runtime", "clang_5_0"] } +clang = { version="2.0", features=["runtime", "clang_6_0"] } # There is an issue with clang 19 and 20 on Windows when using the runtime feature. # Unloading of DLL causes segfault when the clang::Clang object is droppped. [target.'cfg(target_os = "windows")'.dependencies] -clang = { version="2.0", features=["clang_5_0"] } +clang = { version="2.0", features=["clang_6_0"] } [dev-dependencies] diff --git a/src/clangwrap.rs b/src/clangwrap.rs index 9112aa5..f38a099 100644 --- a/src/clangwrap.rs +++ b/src/clangwrap.rs @@ -3,6 +3,7 @@ use crate::{log, verbose}; use capitalize::Capitalize; use std::{ path::{Path, PathBuf}, + rc::Rc, sync::{Mutex, MutexGuard, TryLockError}, }; @@ -16,7 +17,7 @@ static DUMMY_FILE: &str = "mocksmith_dummy_input_file.h"; // Struct to wrap the Clang library and a mutex guard to ensure only one thread can use it // at a time, at least via this library. pub(crate) struct ClangWrap { - log: Option, + log: Rc>, clang: clang::Clang, // After clang::Clang to ensure releasing lock after Clang is dropped _clang_lock: MutexGuard<'static, ()>, @@ -31,7 +32,7 @@ impl ClangWrap { CLANG_MUTEX.clear_poison(); } - pub(crate) fn new(log: Option) -> crate::Result { + pub(crate) fn new(log: Rc>) -> crate::Result { let clang_lock = CLANG_MUTEX.try_lock().map_err(|error| match error { TryLockError::WouldBlock => crate::MocksmithError::Busy, TryLockError::Poisoned(_) => MocksmithError::Poisoned, @@ -41,12 +42,12 @@ impl ClangWrap { pub(crate) fn blocking_new() -> crate::Result { let clang_lock = CLANG_MUTEX.lock().map_err(|_| MocksmithError::Poisoned)?; - Self::create(clang_lock, None) + Self::create(clang_lock, Rc::new(None)) } fn create( clang_lock: MutexGuard<'static, ()>, - log: Option, + log: Rc>, ) -> crate::Result { let clang = clang::Clang::new().map_err(MocksmithError::ClangError)?; // Create clang object before getting version to ensure libclang is loaded @@ -133,11 +134,11 @@ impl ClangWrap { .filter(|diagnostic| { diagnostic.get_severity() >= clang::diagnostic::Severity::Error }) - .for_each(|diagnostic| log!(&self.log, "{}", diagnostic)); + .for_each(|diagnostic| log!(self.log, "{}", diagnostic)); } else { diagnostics .iter() - .for_each(|diagnostic| verbose!(&self.log, "{}", diagnostic)); + .for_each(|diagnostic| verbose!(self.log, "{}", diagnostic)); } if !self.ignore_errors { diff --git a/src/lib.rs b/src/lib.rs index f85c156..27ab4a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,10 @@ pub mod naming; use clangwrap::ClangWrap; use headerpath::header_include_path; -use std::path::{Path, PathBuf}; +use std::{ + path::{Path, PathBuf}, + rc::Rc, +}; #[derive(thiserror::Error, Debug, PartialEq)] pub enum MocksmithError { @@ -87,6 +90,7 @@ impl crate::MockHeader { /// Mocksmith is a struct for generating Google Mock mocks for C++ classes. pub struct Mocksmith { + log: Rc>, clangwrap: ClangWrap, generator: generate::Generator, @@ -102,8 +106,8 @@ impl Mocksmith { /// The function fails if another thread already holds an instance, since Clang can /// only be used from one thread. pub fn new(log_write: Option>, verbose: bool) -> Result { - let log = log_write.map(|write| log::Logger::new(write, verbose)); - Self::create(ClangWrap::new(log)?) + let log = Rc::new(log_write.map(|write| log::Logger::new(write, verbose))); + Self::create(Rc::clone(&log), ClangWrap::new(log)?) } /// Creates a new Mocksmith instance. @@ -117,12 +121,13 @@ impl Mocksmith { ClangWrap::clear_poison(); clangwrap = ClangWrap::blocking_new(); } - Self::create(clangwrap?) + Self::create(Rc::new(None), clangwrap?) } - fn create(clangwrap: clangwrap::ClangWrap) -> Result { + fn create(log: Rc>, clangwrap: clangwrap::ClangWrap) -> Result { let methods_to_mock = MethodsToMockStrategy::AllVirtual; let mocksmith = Self { + log, clangwrap, generator: generate::Generator::new(methods_to_mock), include_paths: Vec::new(), @@ -280,7 +285,8 @@ impl Mocksmith { } fn create_mocks(&self, tu: &clang::TranslationUnit) -> Result> { - let classes = model::classes_in_translation_unit(tu, self.methods_to_mock); + let classes = + model::classes_in_translation_unit(Rc::clone(&self.log), tu, self.methods_to_mock); Ok(classes .iter() .filter(|class| (self.filter_class)(class.name.as_str())) @@ -299,7 +305,8 @@ mod tests { #[test] fn test_new_with_threads() { - let mocksmith = Mocksmith::new(None, false).unwrap(); + // new_when_available to wait for other tests using ClangWrap + let mocksmith = Mocksmith::new_when_available().unwrap(); let handle = std::thread::spawn(|| { assert!(matches!( diff --git a/src/log.rs b/src/log.rs index ea462f1..349c725 100644 --- a/src/log.rs +++ b/src/log.rs @@ -3,7 +3,7 @@ use std::{cell::RefCell, io::Write}; #[macro_export] macro_rules! log { ($logger:expr, $($arg:tt)*) => { - if let Some(logger) = &$logger { + if let Some(logger) = &*$logger { logger.log(&format!($($arg)*)); } }; @@ -12,7 +12,7 @@ macro_rules! log { #[macro_export] macro_rules! verbose { ($logger:expr, $($arg:tt)*) => { - if let Some(logger) = &$logger { + if let Some(logger) = &*$logger { if logger.verbose { logger.log(&format!($($arg)*)); } @@ -42,6 +42,7 @@ impl Logger { #[cfg(test)] mod tests { use super::*; + use std::rc::Rc; #[test] fn macro_doesnt_evaluate_args_if_verbose_disabled() { @@ -52,7 +53,7 @@ mod tests { }; let write = Box::new(Vec::::new()); - let log = Some(Logger::new(write, false)); + let log = Rc::new(Some(Logger::new(write, false))); verbose!(log, "{}", fun()); assert_eq!(calls, 0); } @@ -66,7 +67,7 @@ mod tests { }; let write = Box::new(Vec::::new()); - let log = Some(Logger::new(write, true)); + let log = Rc::new(Some(Logger::new(write, true))); verbose!(log, "{}", fun()); assert_eq!(calls, 1); } diff --git a/src/model.rs b/src/model.rs index 342c8ef..d6caf05 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,3 +1,8 @@ +use crate::log; +use std::rc::Rc; + +mod factory; + // Represents a class that shall be mocked #[derive(Debug)] pub(crate) struct ClassToMock { @@ -6,18 +11,22 @@ pub(crate) struct ClassToMock { pub(crate) methods: Vec, } +// Represents a class method that shall be mocked #[derive(Debug)] pub(crate) struct MethodToMock { pub(crate) name: String, pub(crate) result_type: String, pub(crate) arguments: Vec, + is_static: bool, pub(crate) is_const: bool, pub(crate) is_virtual: bool, + is_pure_virtual: bool, pub(crate) is_noexcept: bool, pub(crate) ref_qualifier: Option, } -#[derive(Debug)] +// Represents a method argument +#[derive(Debug, PartialEq)] pub(crate) struct Argument { pub(crate) type_name: String, pub(crate) name: Option, @@ -25,71 +34,17 @@ pub(crate) struct Argument { // Finds classes to mock in the main file of a translation unit pub(crate) fn classes_in_translation_unit( + log: Rc>, root: &clang::TranslationUnit, methods_to_mock: crate::MethodsToMockStrategy, ) -> Vec { - AstTraverser::new(root, methods_to_mock).traverse() -} - -impl ClassToMock { - fn from_entity( - class: &clang::Entity, - namespaces: &Vec, - methods_to_mock: crate::MethodsToMockStrategy, - ) -> Self { - Self { - name: class.get_name().expect("Class should have a name"), - namespaces: namespaces - .iter() - .map(|ns| ns.get_name().expect("Namespace should have a name")) - .collect::>(), - methods: class - .get_children() - .iter() - .filter(|child| child.get_kind() == clang::EntityKind::Method) - .filter(|method| methods_to_mock.should_mock(method)) - .map(|method| MethodToMock::from_entity(method)) - .collect(), - } - } -} - -impl MethodToMock { - fn from_entity(method: &clang::Entity) -> Self { - Self { - name: method.get_name().expect("Method should have a name"), - result_type: method - .get_result_type() - .expect("Method should have a return type") - .get_display_name(), - arguments: method - .get_arguments() - .expect("Method should have arguments") - .iter() - .map(|arg| Argument { - type_name: arg - .get_type() - .expect("Argument should have a type") - .get_display_name(), - name: arg.get_name(), - }) - .collect(), - is_const: method.is_const_method(), - is_virtual: method.is_virtual_method(), - is_noexcept: (method.get_exception_specification() - == Some(clang::ExceptionSpecification::BasicNoexcept)), - ref_qualifier: method.get_type().and_then(|t| t.get_ref_qualifier()).map( - |rq| match rq { - clang::RefQualifier::LValue => "&".to_string(), - clang::RefQualifier::RValue => "&&".to_string(), - }, - ), - } - } + AstTraverser::new(log, root, methods_to_mock).traverse() } +// Traverses the AST to find classes to mock struct AstTraverser<'a> { root: clang::Entity<'a>, + factory: factory::ModelFactory, methods_to_mock: crate::MethodsToMockStrategy, classes: Vec, @@ -98,11 +53,13 @@ struct AstTraverser<'a> { impl<'a> AstTraverser<'a> { pub fn new( + log: Rc>, root: &'a clang::TranslationUnit<'a>, methods_to_mock: crate::MethodsToMockStrategy, ) -> Self { Self { root: root.get_entity(), + factory: factory::ModelFactory::new(log), methods_to_mock, classes: Vec::new(), namespace_stack: Vec::new(), @@ -118,7 +75,7 @@ impl<'a> AstTraverser<'a> { match entity.get_kind() { clang::EntityKind::ClassDecl => { if entity.is_definition() && self.should_mock_class(&entity) { - self.classes.push(ClassToMock::from_entity( + self.classes.push(self.factory.class_from_entity( &entity, &self.namespace_stack, self.methods_to_mock, @@ -152,11 +109,168 @@ impl<'a> AstTraverser<'a> { } impl crate::MethodsToMockStrategy { - fn should_mock(self, method: &clang::Entity) -> bool { + // TODO: Must look at MethodToMock rather than clang::Entity! + fn should_mock(self, method: &MethodToMock) -> bool { match self { - crate::MethodsToMockStrategy::All => !method.is_static_method(), - crate::MethodsToMockStrategy::AllVirtual => method.is_virtual_method(), - crate::MethodsToMockStrategy::OnlyPureVirtual => method.is_pure_virtual_method(), + crate::MethodsToMockStrategy::All => !method.is_static, + crate::MethodsToMockStrategy::AllVirtual => method.is_virtual, + crate::MethodsToMockStrategy::OnlyPureVirtual => method.is_pure_virtual, } } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::clangwrap::ClangWrap; + + #[test] + fn class_with_methods_with_recognized_types() { + let code = r#" + class MyClass { + public: + virtual void foo() const noexcept; + int bar(int x); + virtual int baz() = 0; + virtual auto bizz() const noexcept -> int = 0; + static void staticMethod(); + }; + "#; + + let clang = ClangWrap::blocking_new().unwrap(); + let _ = clang.with_tu_from_string(&[], code, |tu| { + let classes = + classes_in_translation_unit(Rc::new(None), &tu, crate::MethodsToMockStrategy::All); + + assert_eq!(classes.len(), 1); + let class = &classes[0]; + assert_eq!(class.name, "MyClass"); + // staticMethod should be excluded + assert_eq!(class.methods.len(), 4); + + assert!(matches!( + &class.methods[0], + &MethodToMock { + name: ref n, + result_type: ref rt, + arguments: ref args, + is_const: true, + is_virtual: true, + is_noexcept: true, + ref_qualifier: None, + } + if n == "foo" && rt == "void" && args.is_empty() + )); + + assert!(matches!( + &class.methods[1], + &MethodToMock { + name: ref n, + result_type: ref rt, + arguments: ref args, + is_const: false, + is_virtual: false, + is_noexcept: false, + ref_qualifier: None, + } if n == "bar" + && rt == "int" + && args == &vec![Argument{ type_name: "int".to_string(), name: Some("x".to_string()) }] + )); + + assert!(matches!( + &class.methods[2], + &MethodToMock { + name: ref n, + result_type: ref rt, + arguments: ref args, + is_const: false, + is_virtual: true, + is_noexcept: false, + ref_qualifier: None, + } if n == "baz" && rt == "int" && args.is_empty() + )); + + assert!(matches!( + &class.methods[3], + &MethodToMock { + name: ref n, + result_type: ref rt, + arguments: ref args, + is_const: true, + is_virtual: true, + is_noexcept: true, + ref_qualifier: None, + } if n == "bizz" && rt == "int" && args.is_empty() + )); + + Ok(()) + }).unwrap(); + } + + #[test] + fn unknown_argument_types_can_be_handled() { + let code = r#" + class MyClass { + public: + virtual void foo(Unknown x) const noexcept; + void bar(Unknown); + void bizz(Unknown1, Unknown2 x, Unknown3); + static void staticMethods(Unknown); + }; + "#; + + let mut clang = ClangWrap::blocking_new().unwrap(); + clang.set_ignore_errors(true); + let _ = clang.with_tu_from_string(&[], code, |tu| { + let classes = + classes_in_translation_unit(Rc::new(None), &tu, crate::MethodsToMockStrategy::All); + + assert_eq!(classes.len(), 1); + let class = &classes[0]; + // staticMethod should be excluded + assert_eq!(class.methods.len(), 3); + + assert!(matches!( + &class.methods[0], + &MethodToMock { + name: ref n, + arguments: ref args, + is_const: true, + is_virtual: true, + is_noexcept: true, + .. + } + if n == "foo" && args == &vec![Argument { type_name: "Unknown".to_string(), name: Some("x".to_string()) }] + )); + + assert!(matches!( + &class.methods[1], + &MethodToMock { + name: ref n, + arguments: ref args, + is_const: false, + is_virtual: false, + is_noexcept: false, + .. + } + if n == "bar" && args == &vec![Argument { type_name: "Unknown".to_string(), name: None }] + )); + + assert!(matches!( + &class.methods[2], + &MethodToMock { + name: ref n, + arguments: ref args, + .. + } + if n == "bizz" && args == &vec![ + Argument { type_name: "Unknown1".to_string(), name: None }, + Argument { type_name: "Unknown2".to_string(), name: Some("x".to_string()) }, + Argument { type_name: "Unknown3".to_string(), name: None } + ] + )); + + Ok(()) + }).unwrap(); + } +} diff --git a/src/model/factory.rs b/src/model/factory.rs new file mode 100644 index 0000000..3b1fe8a --- /dev/null +++ b/src/model/factory.rs @@ -0,0 +1,223 @@ +use super::{Argument, ClassToMock, MethodToMock}; +use crate::log; +use std::rc::Rc; + +// Factory for creating model objects from clang entities +pub(crate) struct ModelFactory { + log: Rc>, + file_contents: Option, +} + +// Represents signature details parsed from source code method declaration +struct MethodSignature { + is_virtual: bool, + is_pure_virtual: bool, + is_static: bool, +} + +impl ModelFactory { + pub(crate) fn new(log: Rc>) -> Self { + Self { + log, + file_contents: None, + } + } + + pub(crate) fn class_from_entity( + &mut self, + class: &clang::Entity, + namespaces: &Vec, + methods_to_mock: crate::MethodsToMockStrategy, + ) -> ClassToMock { + self.cache_file_contents(class); + ClassToMock { + name: class.get_name().expect("Class should have a name"), + namespaces: namespaces + .iter() + .map(|ns| ns.get_name().expect("Namespace should have a name")) + .collect::>(), + methods: class + .get_children() + .iter() + .filter(|child| child.get_kind() == clang::EntityKind::Method) + .filter(|method| methods_to_mock.should_mock(method)) + .map(|method| self.method_from_entity(method)) + .collect(), + } + } + + fn method_from_entity(&mut self, method: &clang::Entity) -> MethodToMock { + println!("Processing method: {:?}", method); + let signature = self + .extract_method_declaration_from_source(method) + .map_or(None, |d| MethodSignature::parse_declaration(&d)); + + MethodToMock { + name: method.get_name().expect("Method should have a name"), + result_type: method + .get_result_type() + .expect("Method should have a return type") + .get_display_name(), + arguments: method + .get_arguments() + .expect("Method should have arguments") + .iter() + .map(|arg| Argument { + type_name: self.get_argument_type(arg), + name: arg.get_name(), + }) + .collect(), + is_static: method.is_static_method() + || signature.as_ref().map_or(false, |s| s.is_static), + is_const: method.is_const_method(), + is_virtual: method.is_virtual_method() + || signature.as_ref().map_or(false, |s| s.is_virtual), + is_pure_virtual: method.is_pure_virtual_method() + || signature.as_ref().map_or(false, |s| s.is_pure_virtual), + is_noexcept: (method.get_exception_specification() + == Some(clang::ExceptionSpecification::BasicNoexcept)), + ref_qualifier: method.get_type().and_then(|t| t.get_ref_qualifier()).map( + |rq| match rq { + clang::RefQualifier::LValue => "&".to_string(), + clang::RefQualifier::RValue => "&&".to_string(), + }, + ), + } + } + + fn get_argument_type(&mut self, arg_entity: &clang::Entity) -> String { + self.extract_argument_type_from_source(arg_entity) + .unwrap_or_else(|| { + arg_entity + .get_type() + .expect("Entity should have a type") + .get_display_name() + }) + } + + fn get_method_declaration_range( + &self, + method_entity: &clang::Entity, + ) -> Option<(usize, usize)> { + method_entity.get_range().map(|r| { + let start = r.get_start().get_file_location().offset as usize; + let end = r.get_end().get_file_location().offset as usize; + (start, end) + }) + } + + fn get_arg_range(&self, arg_entity: &clang::Entity) -> Option<(usize, usize)> { + // entity.get_range() only seems to work when argument has a name, but + // get_location() seems to work. We use it to find the start and then scan the + // source to find the end + if arg_entity.get_name().is_some() { + arg_entity.get_range().map(|r| { + let start = r.get_start().get_file_location().offset as usize; + let end = r.get_end().get_file_location().offset as usize; + (start, end) + }) + } else if let Some(file_contents) = &self.file_contents + && let Some(location) = arg_entity.get_location() + { + // Location is now _after_ the unknown argument type, so we need to scan + // backwards to find the start + let end = location.get_file_location().offset as usize; + let bytes = file_contents.as_bytes(); + let mut start = 0; + + for i in (0..end).rev() { + let c = bytes[i] as char; + if c == ',' || c == '(' { + start = i + 1; + break; + } + } + Some((start, end)) + } else { + None + } + } + + fn extract_method_declaration_from_source(&mut self, method: &clang::Entity) -> Option { + if let Some((start, end)) = self.get_method_declaration_range(method) + && let Some(file_contents) = &self.file_contents + { + return Some(file_contents[start..end].trim().to_string()); + } + None + } + + fn extract_argument_type_from_source(&mut self, arg_entity: &clang::Entity) -> Option { + if let Some((start, mut end)) = self.get_arg_range(arg_entity) + && let Some(file_contents) = &self.file_contents + { + if let Some(name) = arg_entity.get_name() { + end -= name.len(); + } + + if start >= end || end > file_contents.len() { + log!( + self.log, + "Falling back to clang type extraction for entity {:?} \ + due to illegal file position", + arg_entity + ); + return None; + } + return Some(file_contents[start..end].trim().to_string()); + } + log!( + self.log, + "Falling back to clang type extraction for entity {:?} \ + due to missing range or file contents", + arg_entity + ); + None + } + + fn cache_file_contents(&mut self, entity: &clang::Entity) { + if self.file_contents.is_none() + && let Some(location) = entity.get_location() + && let Some(file) = location.get_file_location().file + { + self.file_contents = file.get_contents(); + } + } +} + +impl MethodSignature { + fn parse_declaration(decl: &str) -> Option { + // Remove part not part of signature, e.g., function body + let signature = decl.split(";").next().unwrap().split("{").next().unwrap(); + println!("\nSignature: {:#?}", signature); + + let pre_parts = signature + .split("(") + .next() + .unwrap() + .split_ascii_whitespace() + .collect::>(); + let Some(post_parts) = signature + .split(")") + .skip(1) + .next() + .map(|s| s.split_ascii_whitespace().collect::>()) + else { + return None; + }; + //println!("Post parts: {:#?}", post_parts); + + let is_virtual = pre_parts.iter().any(|s| *s == "virtual") + || post_parts.iter().any(|s| *s == "override"); + let is_pure_virtual = is_virtual + && (post_parts.iter().any(|s| *s == "=0") + || post_parts.windows(2).any(|w| w[0] == "=" && w[1] == "0")); + let is_static = pre_parts.iter().any(|s| *s == "static"); + + Some(MethodSignature { + is_virtual, + is_pure_virtual, + is_static, + }) + } +} diff --git a/tests/assertions/mod.rs b/tests/assertions/mod.rs index 8ae27f3..fe36098 100644 --- a/tests/assertions/mod.rs +++ b/tests/assertions/mod.rs @@ -45,6 +45,14 @@ macro_rules! assert_mocks { err ) }); + + assert_eq!(actual_mocks.len(), + expected_mocks.len(), + "Number of generated mocks ({}) does not match expected ({})", + actual_mocks.len(), + expected_mocks.len() + ); + actual_mocks.iter() .zip(expected_mocks.iter()) .for_each(|(actual, expected)| { diff --git a/tests/lib_integration_tests.rs b/tests/lib_integration_tests.rs index c7b84ac..312f99b 100644 --- a/tests/lib_integration_tests.rs +++ b/tests/lib_integration_tests.rs @@ -55,7 +55,7 @@ fn various_return_types_and_argument_types_can_be_mocked() { "class MockFoo : public Foo", "{", "public:", - " MOCK_METHOD(std::string, bar, (const std::string & arg1, const char * arg2), (override));", + " MOCK_METHOD(std::string, bar, (const std::string& arg1, const char* arg2), (override));", " MOCK_METHOD(uint32_t, fizz, (uint32_t arg1, uint64_t arg2, int32_t arg3, int64_t arg4), (override));", "};" ) @@ -128,7 +128,7 @@ fn types_with_commas_are_wrapped_with_parenthesis() { "class MockFoo : public Foo", "{", "public:", - " MOCK_METHOD((std::map), bar, ((const std::map & arg)), (override));", + " MOCK_METHOD((std::map), bar, ((const std::map& arg)), (override));", "};" ) ); @@ -199,6 +199,45 @@ fn unknown_argument_type_is_treated_as_error() { ); } +#[test] +fn unknown_argument_can_be_handled_if_ignoring_errors() { + let mocksmith = Mocksmith::new_when_available().unwrap().ignore_errors(true); + let cpp_class = " + class Foo { + public: + virtual ~Foo() = default; + virtual void bar(const Unknown& arg) = 0; + };"; + assert_mocks!( + mocksmith.create_mocks_from_string(cpp_class), + lines!( + "class MockFoo : public Foo", + "{", + "public:", + " MOCK_METHOD(void, bar, (const Unknown& arg), (override));", + "};" + ) + ); + + let cpp_class = " + class Foo { + public: + virtual ~Foo() = default; + // Include of is missing + virtual void fizz(const std::string& arg) = 0; + };"; + assert_mocks!( + mocksmith.create_mocks_from_string(cpp_class), + lines!( + "class MockFoo : public Foo", + "{", + "public:", + " MOCK_METHOD(void, fizz, (const std::string& arg), (override));", + "};" + ) + ); +} + #[test] fn unknown_return_type_is_treated_as_error() { let mocksmith = Mocksmith::new_when_available().unwrap(); @@ -221,6 +260,50 @@ fn unknown_return_type_is_treated_as_error() { ); } +#[test] +fn unknown_return_type_can_be_handled_if_ignoring_errors() { + let mocksmith = Mocksmith::new_when_available().unwrap().ignore_errors(true); + let cpp_class = " + class Foo { + public: + virtual ~Foo() = default; + virtual SomeReturnType bar() = 0; + };"; + assert_mocks!( + mocksmith.create_mocks_from_string(cpp_class), + lines!( + "class MockFoo : public Foo", + "{", + "public:", + " MOCK_METHOD(Unknown, bar, (), (override));", + "};" + ) + ); +} + +#[test] +fn unknown_types_in_file_can_be_handled_if_ignoring_errors() { + let file = temp_file_from( + " + class Foo { + public: + virtual ~Foo() = default; + virtual SomeReturnType bar(const SomeArgType& arg) = 0; + };", + ); + let mocksmith = Mocksmith::new_when_available().unwrap().ignore_errors(true); + assert_mocks!( + mocksmith.create_mocks_for_file(file.path()), + lines!( + "class MockFoo : public Foo", + "{", + "public:", + " MOCK_METHOD(SomeReturnType, bar, (const SomeArgType& arg), (override));", + "};" + ) + ); +} + #[test] fn error_in_included_file_is_reported_in_correct_file() { let dir = temp_dir();