diff --git a/ChangeLog b/ChangeLog index 752dc3ed..c2a78214 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,24 @@ + * Silenced noisy g++-7 -Wuseless-cast warning + + * Fixed a bug where a mocked function would not compile if a parameter + had an operator==(nullptr) returning a type that is not convertible + to bool. + + * Fixed a bug where a mocked function would not compile if a parameter + was a range with iterators to rvalue proxy objects, like vector. + + * Mock objects can be move constructed if they have a static constexpr + bool member named trompeloeil_movable_mock with value = true. + + * ANY() matcher gives a short descriptive compilation error message + when the type is an array type (it would silently decay into a + pointer type, which can be very confusing.) + + * When compiled with preprocessor macro + TROMPELOEIL_USER_DEFINED_COMPILE_TIME_REPORTER, a `extern` + `trompeloeil::reporter` is declared from `trompeloeil.hpp`. + Thanks @rcdailey + * Minor documentation updates. * Update pattern in expectation_with_wrong_type.cpp to match diff --git a/compilation_errors/any_with_array_type.cpp b/compilation_errors/any_with_array_type.cpp new file mode 100644 index 00000000..d3773828 --- /dev/null +++ b/compilation_errors/any_with_array_type.cpp @@ -0,0 +1,37 @@ +/* + * Trompeloeil C++ mocking framework + * + * Copyright Björn Fahller 2018 + * + * Use, modification and distribution is subject to the + * Boost Software License, Version 1.0. (See accompanying + * file LICENSE_1_0.txt or copy at + * http://www.boost.org/LICENSE_1_0.txt) + * + * Project home: https://github.com/rollbear/trompeloeil + */ + +//array parameter type decays to pointer type for ANY + +#include + + +struct MS +{ + MAKE_MOCK1(f, void(int [3])); +}; + +int main() +{ + MS obj; + +#if (TROMPELOEIL_CPLUSPLUS == 201103L) + + REQUIRE_CALL_V(obj, f(ANY(int[3]))); + +#else /* (TROMPELOEIL_CPLUSPLUS == 201103L) */ + + REQUIRE_CALL(obj, f(ANY(int[3]))); + +#endif /* !(TROMPELOEIL_CPLUSPLUS == 201103L) */ +} diff --git a/compilation_errors/illegal_move_mock.cpp b/compilation_errors/illegal_move_mock.cpp new file mode 100644 index 00000000..1bc4ee80 --- /dev/null +++ b/compilation_errors/illegal_move_mock.cpp @@ -0,0 +1,33 @@ +/* + * Trompeloeil C++ mocking framework + * + * Copyright Björn Fahller 2019 + * + * Use, modification and distribution is subject to the + * Boost Software License, Version 1.0. (See accompanying + * file LICENSE_1_0.txt or copy at + * http://www.boost.org/LICENSE_1_0.txt) + * + * Project home: https://github.com/rollbear/trompeloeil + */ + +// make a mock object movable, see: + +#include + + +struct M +{ + MAKE_MOCK1(f, void(int)); +}; + +template +T ident(T t) +{ + return t; +} + +int main() +{ + auto obj = ident(M{}); +} diff --git a/docs/CookBook.md b/docs/CookBook.md index 71627294..224084f8 100644 --- a/docs/CookBook.md +++ b/docs/CookBook.md @@ -82,14 +82,22 @@ namespace trompeloeil } ``` -In all other [translation units]( - http://stackoverflow.com/questions/8342185/ddg#8342233 -), add the following extern declaration in global namespace instead: +If you have multiple translation units, you can define the +`TROMPELOEIL_USER_DEFINED_COMPILE_TIME_REPORTER` preprocessor definition which causes the +`` header to automatically generate the following `extern` statement: ```Cpp extern template struct trompeloeil::reporter; ``` +This preprocessor definition can be added to your unit test code via your build system to make this +more transparent to your code. This definition provides the ability to define a compile-time +reporter without being required to insert code into multiple, existing translation units. + +The old (legacy) requirement was that the `extern` statement above had to be explicitly declared in +every translation unit, which is tedious. However, this is still a functional alternative if you +can't define the preprocessor directive at the build system level for whatever reason. + It is important to understand the first parameter `trompeloeil::severity`. It is an enum with the values `trompeloeil::severity::fatal` and `trompeloeil::severity::nonfatal`. The value diff --git a/docs/FAQ.md b/docs/FAQ.md index d08a9e64..222e2819 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -19,6 +19,7 @@ - Q. [Can I check if an expectation is fulfilled?](#query_expectation) - Q. [What does it mean to mix **`IN_SEQUENCE`** and **`TIMES`**?](#sequence_times) - Q. [How do I use *Trompeloeil* in a CMake project?](#cmake) +- Q. [Why are mock objects not move constructible?](#move_constructible) ## Q. Why a name that can neither be pronounced nor spelled? @@ -571,3 +572,24 @@ Finally, you can add *Trompeloeil* to your project and then either (a) use CMake `include_directories()`; or (b) use `add_subdirectory()` (one or two argument version) to add its path to your project. + +### + +Q. Why are mock objects not move constructible? + +Because a move is potentially dangerous in non-obvious ways. If a mock object is +moved, the actions associated with an expectation +([**`.WITH()`**](reference.md/#WITH), + [**`.SIDE_EFFECT()`**](reference.md/#SIDE_EFFECT), + [**`.RETURN()`**](reference.md/#RETURN), + [**`.THROW()`**](reference.md/#THROW)) and their + `LR_` versions, are *not* moved. If they refer to data members stored in a + moved mock object, they will refer to dead data. This is an accepted const + in normal C++ code, but since the effect is hidden under the macros, + it is better to play safe. + +With that said, you can explicitly make mock objects movable, if you want to. +See: [**`trompeloeil_movable_mock`**](reference.md/#movable_mock). + + + diff --git a/docs/reference.md b/docs/reference.md index 4dcac419..81a6ed25 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -63,6 +63,8 @@ - [`trompeloeil::print(std::ostream&, T const&)`](#print) - [`trompeloeil::set_reporter(...)`](#set_reporter) - [`trompeloeil::sequence::is_completed()`](#is_completed) +- [Constants](#constants) + - [`trompeloeil_movable_mock`](#movable_mock) ## Notions @@ -2122,7 +2124,7 @@ protected: See "[Writing custom tracers](CookBook.md/#custom_tracer)" in the [Cook Book](CookBook.md) for an example. -### `trompeloeil::typed_matcher` +### `trompeloeil::typed_matcher` Convenience class available when writing custom matchers for a specific type. It inherits from [`trompeloeil::matcher`](#matcher_type). @@ -2130,6 +2132,7 @@ type. It inherits from [`trompeloeil::matcher`](#matcher_type). See "[Writing custom matchers](CookBook.md/#custom_matchers)" in the [Cook Book](CookBook.md) for examples. + ## Functions ### `trompeloeil::expectation::is_satisfied() const` @@ -2277,3 +2280,75 @@ void test() assert(seq.is_completed()); // now sequence is completed } ``` + +## Constants + +### `trompeloeil_movable_mock` + +By adding a static constexpr bool member `trompeloeil_movable_mock` with the +value `true` to your mock struct/class, you make it move constructible. Note +that when a mock object is moved, any current expectations will be taken over +by the newly constructed mock object, but note also that if the implicitly +created lambdas associated with +[**`.WITH()`**](reference.md/#WITH), +[**`.SIDE_EFFECT()`**](reference.md/#SIDE_EFFECT), +[**`.RETURN()`**](reference.md/#RETURN) and +[**`.THROW()`**](reference.md/#THROW) and their `**LR_**` counter parts, refers +to member variables in the mock objects, they will continue to refer the old +moved from object. + +Also, keep in mind the lifetime of expectations. If the lifetime of an +expectation is associated with the life of the moved-from object, your test +will likely fail, since the expectation object would then be destroyed before it +has been satisfied. Using +[**`NAMED_REQUIRE_CALL()`**](reference.md/#NAMED_REQUIRE_CALL), +[**`NAMED_ALLOW_CALL()`**](reference.md/#NAMED_ALLOW_CALL) or +[**`NAMED_FORBID_CALL()`**](reference.md/#NAMED_FORBID_CALL) can help, since +they make the expectation life times more visible. + +```Cpp +class immobile +{ +public: + MAKE_MOCK1(func, void(int)); +}; + +class movable +{ +public: + int i = 0; + + static constexpr bool trompeloeil_movable_mock = true; + // allow move construction + + MAKE_MOCK1(func, void(int)); +}; + +template +T transfer(T t) +{ + return t; +} + +test(...) +{ + auto m = transfer(immobile{}); // compilation error + ... +} +test(...) +{ + movable m; + auto e = NAMED_REQUIRE_CALL(m, func(3)); + auto mm = transfer(std::move(m)); + // A call to mm.func() now satisfies e + ... +} +test(...) +{ + movable m{3}; + auto e = NAMED_REQUIRE_CALL(m, func(_)) + .LR_WITH(_1 == m.i); + auto mm = transfer(std::move(m)); // Danger! e still refers to m.i. + ... +} +``` diff --git a/include/trompeloeil.hpp b/include/trompeloeil.hpp index 20b02dad..826b059f 100644 --- a/include/trompeloeil.hpp +++ b/include/trompeloeil.hpp @@ -295,6 +295,8 @@ #endif /* !(TROMPELOEIL_CPLUSPLUS == 201103L) */ +static constexpr bool trompeloeil_movable_mock = false; + namespace trompeloeil { template @@ -1053,7 +1055,9 @@ template }; template - using equality_comparison = decltype(std::declval() == std::declval()); + using equality_comparison = decltype((std::declval() == std::declval()) + ? true + : false); template using is_equal_comparable = is_detected; @@ -2017,17 +2021,33 @@ template return {std::move(pred), std::move(print), std::forward(t)...}; } + template < typename T, typename R = make_matcher_return> inline auto - any_matcher(char const* type_name) + any_matcher_impl(char const* type_name, std::false_type) TROMPELOEIL_TRAILING_RETURN_TYPE(R) { return make_matcher(lambdas::any_predicate(), lambdas::any_printer(type_name)); } + template + wildcard + any_matcher_impl(char const*, std::true_type); + + template + inline + auto + any_matcher(char const* name) + TROMPELOEIL_TRAILING_RETURN_TYPE(decltype(any_matcher_impl(name, std::is_array{}))) + { + static_assert(!std::is_array::value, + "array parameter type decays to pointer type for ANY()" + " matcher. Please rephrase as pointer instead"); + return any_matcher_impl(name, std::is_array{}); + } template < typename T = wildcard, typename V, @@ -3858,25 +3878,46 @@ template } }; - template + template struct expectations { + expectations() = default; + expectations(expectations&&) = default; + ~expectations() { + active.decommission(); + saturated.decommission(); + } + call_matcher_list active{}; + call_matcher_list saturated{}; + }; + + template + struct expectations + { + expectations() = default; + expectations(expectations&&) + { + static_assert(std::is_same::value, + "By default, mock objects are not movable. " + "To make a mock object movable, see: " + "https://github.com/rollbear/trompeloeil/blob/master/docs/reference.md#movable_mock"); + } ~expectations() { active.decommission(); saturated.decommission(); } - call_matcher_list active; - call_matcher_list saturated; + call_matcher_list active{}; + call_matcher_list saturated{}; }; template return_of_t mock_func(std::false_type, P&& ...); - template + template return_of_t mock_func(std::true_type, - expectations& e, + expectations& e, char const *func_name, char const *sig_name, P&& ... p) @@ -3966,6 +4007,9 @@ template using T::T; }; +#if defined(TROMPELOEIL_USER_DEFINED_COMPILE_TIME_REPORTER) + extern template struct reporter; +#endif // TROMPELOEIL_USER_DEFINED_COMPILE_TIME_REPORTER } #define TROMPELOEIL_LINE_ID(name) \ @@ -4190,7 +4234,9 @@ template static_assert(TROMPELOEIL_LINE_ID(cardinality_match)::value, \ "Function signature does not have " #num " parameters"); \ using TROMPELOEIL_LINE_ID(matcher_list_t) = ::trompeloeil::call_matcher_list;\ - using TROMPELOEIL_LINE_ID(expectation_list_t) = ::trompeloeil::expectations; \ + using TROMPELOEIL_LINE_ID(expectation_list_t) = \ + ::trompeloeil::expectations; \ + \ struct TROMPELOEIL_LINE_ID(tag_type_trompeloeil) \ { \ const char* trompeloeil_expectation_file; \ @@ -4247,21 +4293,22 @@ template using T_ ## name = typename std::remove_reference::type; \ \ using pmf_s_t = typename ::trompeloeil::signature_to_member_function< \ - T_ ## name, decltype(*this), sig>::type; \ + T_ ## name, decltype(*this), sig>::type const; \ \ using pmf_e_t = typename ::trompeloeil::signature_to_member_function< \ - T_ ## name, TROMPELOEIL_LINE_ID(tag_type_trompeloeil), sig>::type; \ + T_ ## name, TROMPELOEIL_LINE_ID(tag_type_trompeloeil), sig>::type const; \ \ - auto s_ptr = static_cast(&T_ ## name::trompeloeil_self_ ## name); \ - auto e_ptr = static_cast(&T_ ## name::trompeloeil_tag_ ## name); \ + pmf_s_t const s_ptr = &T_ ## name::trompeloeil_self_ ## name; \ + pmf_e_t const e_ptr = &T_ ## name::trompeloeil_tag_ ## name; \ \ ::trompeloeil::ignore(s_ptr, e_ptr); \ \ - return ::trompeloeil::mock_func(TROMPELOEIL_LINE_ID(cardinality_match){}, \ - TROMPELOEIL_LINE_ID(expectations), \ - #name, \ - #sig \ - TROMPELOEIL_PARAMS(num)); \ + return ::trompeloeil::mock_func( \ + TROMPELOEIL_LINE_ID(cardinality_match){}, \ + TROMPELOEIL_LINE_ID(expectations), \ + #name, \ + #sig \ + TROMPELOEIL_PARAMS(num)); \ } \ \ auto \ @@ -4279,7 +4326,7 @@ template return {nullptr, 0ul, nullptr}; \ } \ \ - mutable TROMPELOEIL_LINE_ID(expectation_list_t) TROMPELOEIL_LINE_ID(expectations) + mutable TROMPELOEIL_LINE_ID(expectation_list_t) TROMPELOEIL_LINE_ID(expectations){} #define TROMPELOEIL_LPAREN ( diff --git a/test/compiling_tests.hpp b/test/compiling_tests.hpp index 47c48759..6d874e7d 100644 --- a/test/compiling_tests.hpp +++ b/test/compiling_tests.hpp @@ -241,11 +241,30 @@ struct uncomparable_string { std::string s; }; +struct null_comparable { + void* p; + bool operator==(std::nullptr_t) const { return !p; } + friend std::ostream& operator<<(std::ostream& os, const null_comparable&) + { + return os << "null_comparable"; + } +}; + +struct pseudo_null_comparable { + void operator==(std::nullptr_t) const {} // looking at you, boost::variant<>! + friend + std::ostream& operator<<(std::ostream& os, const pseudo_null_comparable&) + { + return os << "pseudo_null_comparable"; + } +}; + class C { public: C() {} C(int) {} + C(C&&) = default; virtual ~C() = default; virtual int count() = 0; virtual void func(int, std::string& s) = 0; @@ -274,6 +293,14 @@ class mock_c : public C using C::p_; }; +class movable_mock +{ +public: + movable_mock() = default; + static constexpr bool trompeloeil_movable_mock = true; + MAKE_MOCK1(func, void(int)); +}; + int intfunc(int i); extern int global_n; diff --git a/test/compiling_tests_11.cpp b/test/compiling_tests_11.cpp index 55c8156c..2ddb067e 100644 --- a/test/compiling_tests_11.cpp +++ b/test/compiling_tests_11.cpp @@ -1,7 +1,7 @@ /* * Trompeloeil C++ mocking framework * - * Copyright Björn Fahller 2014-2018 + * Copyright Björn Fahller 2014-2019 * Copyright (C) 2017 Andrew Paxie * * Use, modification and distribution is subject to the @@ -3910,6 +3910,36 @@ TEST_CASE_METHOD( } } +TEST_CASE_METHOD( + Fixture, + "C++11: A null-comparable object is printed as 'nullptr' if eqeual", + "[C++11][C++14][streaming]") +{ + std::ostringstream os; + trompeloeil::print(os, null_comparable{nullptr}); + REQUIRE(os.str() == "nullptr"); +} + +TEST_CASE_METHOD( + Fixture, + "C++11: A null-comparable object is printed as using its ostream insertion if ueqeual", + "[C++11][C++14][streaming]") +{ + std::ostringstream os; + trompeloeil::print(os, null_comparable{&os}); + REQUIRE(os.str() == "null_comparable"); +} + +TEST_CASE_METHOD( + Fixture, + "C++11: An object for which null-compare is non-bool, is printed using its ostream insertion", + "[C++11][C++14][streaming]") +{ + std::ostringstream os; + trompeloeil::print(os, pseudo_null_comparable{}); + REQUIRE(os.str() == "pseudo_null_comparable"); +} + // tests on scoping (lifetime) of expectations TEST_CASE_METHOD( @@ -5249,3 +5279,22 @@ TEST_CASE_METHOD( m.f2(0,1); } +TEST_CASE_METHOD( + Fixture, + "C++11: A named expectation follows a moved mock object", + "[C++11][C++14]" +) +{ + bool called = false; + auto set_expectation = [&called](movable_mock obj) { + auto exp = NAMED_REQUIRE_CALL_V(obj, func(3), + .LR_SIDE_EFFECT(called = true)); + return std::make_pair(std::move(obj), std::move(exp)); + }; + + auto e = set_expectation(movable_mock{}); + e.first.func(3); + REQUIRE(reports.empty()); + REQUIRE(called); +} + diff --git a/test/compiling_tests_14.cpp b/test/compiling_tests_14.cpp index 68e53cf5..dd9f5e48 100644 --- a/test/compiling_tests_14.cpp +++ b/test/compiling_tests_14.cpp @@ -3663,6 +3663,7 @@ TEST_CASE_METHOD( } } + // tests on scoping (lifetime) of expectations TEST_CASE_METHOD(