rollbear / trompeloeil

Header only C++14 mocking framework
Boost Software License 1.0
811 stars 86 forks source link

Unable to create expectation for raw pointer parameter that matches nullptr #249

Closed IvanRibakov closed 3 years ago

IvanRibakov commented 3 years ago

Hi, I have a follow piece of code:

class ISipDialog {
    ...
  protected:
    virtual void freeDialog(dlg_t *p) const = 0;
    ...
}

class SipDialog : public ISipDialog {
    ...
  protected:
    void freeDialog(dlg_t *p) const override;
    ...
}

class SipDialogMock : public ISipDialog {
    MAKE_CONST_MOCK1(freeDialog, void(dlg_t *), final);
    ...
};

I've tried setting an expectation in the following ways:

ALLOW_CALL(*sipDialogMock, freeDialog(ANY(dlg_t *)));
ALLOW_CALL(*sipDialogMock, freeDialog(_));
ALLOW_CALL(*sipDialogMock, freeDialog(nullptr));
ALLOW_CALL(*sipDialogMock, freeDialog(eq(nullptr)));

but all of them result in the following runtime failure:

include/catch2/trompeloeil.hpp:43: FAILED:
explicitly with message:
  No match for call of freeDialog with signature void(dlg_t *) with.
    param  _1 == nullptr

terminate called after throwing an instance of 'Catch::TestFailureException'

Am I missing something here?

rollbear commented 3 years ago

Hmm. Can you post a short complete small example program that exhibits the problem?

Here's one that works as expected: https://godbolt.org/z/e5Ys83hhx

The message you get indicates that there is no expectation at all for the object at the place of the call, otherwise it would look something like:

No match for call of func with signature void(int*) with.
  param  _1 == nullptr

Tried s.func(&i) at /app/example.cpp:18
  Expected  _1 == 0x7ffcc34710fc

Where it lists expectations that are there, but do not match the call.

IvanRibakov commented 3 years ago

I've been trying to prepare a minimal example that reproduces the issue but it isn't looking that minimal after all. In the process I got slightly better understanding of what is actually happening in the code under test and the test itself so let me update issue description with some additional details.

Code under test involves a singleton that has a collection of shared pointers to instances of class A. class A internally has a field of type class B. class B destructor calls mocked method from the ISipDialog interface shown earlier.

I'm not 100% sure but what could be happening is that class B destructor is actually being called after mock object was destroyed.

On the other hand, I'm seeing following coredumps that may (or may not?) suggest that some trompeloeil objects still exist:

Stack trace of thread 41080:
#0  0x00000000004bd4e5 _ZNKSt15__uniq_ptr_implINSt7__cxx1119basic_ostringstreamIcSt11char_traitsIcESaIcEEESt14default_deleteIS5_EE6_M_ptrEv
#1  0x000000000049f1dc _ZNKSt10unique_ptrINSt7__cxx1119basic_ostringstreamIcSt11char_traitsIcESaIcEEESt14default_deleteIS5_EE3getEv
#2  0x0000000000436a5d _ZN5Catch20ReusableStringStreamC2Ev
#3  0x0000000000430e7e _ZN5Catch16getResultCaptureEv
#4  0x000000000041015d _ZN5Catch16AssertionHandlerC2ERKNS_9StringRefERKNS_14SourceLineInfoES1_NS_17ResultDisposition5FlagsE
#5  0x000000000057f701 _ZN11trompeloeil8reporterINS_11specializedEE4sendENS_8severityEPKcmS5_
#6  0x0000000000583aed _ZN11trompeloeil11send_reportINS_11specializedEEEvNS_8severityENS_8locationERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE
#7  0x000000000058c2f9 _ZN11trompeloeil15report_mismatchIFvP3dlgEEEvRNS_17call_matcher_listIT_EES7_RKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEERKNS_16call_params_typeIS5_E4typeE
#8  0x000000000058697d _ZN11trompeloeil9mock_funcILb0EFvP3dlgEJRS2_EEENS_9return_ofIT0_E4typeESt17integral_constantIbLb1EERNS_12expectationsIXT_ES6_EEPKcSF_DpOT1_
#9  0x0000000000582c04 _ZNK13SipDialogMock10freeDialogEP3dlg
...
rollbear commented 3 years ago

It does not really provide any new information. Running the backtrace through c++filt gives:

#1  0x000000000049f1dc std::unique_ptr<std::__cxx11::basic_ostringstream<char, std::char_traits<char>, std::allocator<char> >, std::default_delete<std::__cxx11::basic_ostringstream<char, std::char_traits<char>, std::allocator<char> > > >::get() const
#2  0x0000000000436a5d Catch::ReusableStringStream::ReusableStringStream()
#3  0x0000000000430e7e Catch::getResultCapture()
#4  0x000000000041015d Catch::AssertionHandler::AssertionHandler(Catch::StringRef const&, Catch::SourceLineInfo const&, Catch::StringRef, Catch::ResultDisposition::Flags)
#5  0x000000000057f701 trompeloeil::reporter<trompeloeil::specialized>::send(trompeloeil::severity, char const*, unsigned long, char const*)
#6  0x0000000000583aed void trompeloeil::send_report<trompeloeil::specialized>(trompeloeil::severity, trompeloeil::location, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&)
#7  0x000000000058c2f9 void trompeloeil::report_mismatch<void (dlg*)>(trompeloeil::call_matcher_list<void (dlg*)>&, trompeloeil::call_matcher_list<void (dlg*)>&, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, trompeloeil::call_params_type<void (dlg*)>::type const&)
#8  0x000000000058697d trompeloeil::return_of<void (dlg*)>::type trompeloeil::mock_func<false, void (dlg*), dlg*&>(std::integral_constant<bool, true>, trompeloeil::expectations<false, void (dlg*)>&, char const*, char const*, dlg*&)

The last 2 lines shows that a call has been made to a function with signature void(dlg*), and no match has been found, so it is reported as a violation. We already knew that. The interesting bit would be the code that sets up the expectation and where the call is made.

I don't think the problem lies with the mock object at all, but the expectation. Where is ALLOW_CALL(...) in relation to the piece of test code that causes the call to be made? ALLOW_CALL(...) creates an expectation object in that location, which is alive until the end of that scope (it creates a local variable, holding the expectation, and when its destructor runs, the expectation is no more.)

IvanRibakov commented 3 years ago

I don't think the problem lies with the mock object at all, but the expectation

I think you are spot on now that I better understand the order in which things are happening.

I was able to reproduce the original error message: https://godbolt.org/z/e84rsKMdT

rollbear commented 3 years ago

Ouch, that's an evil case. A very minor modification to your example program shows that the violation is reported after leaving main(), when the global destructors are run.

https://godbolt.org/z/843oxx1MK

You really want to find a way around that, to write your tests a bit differently. This means that state is kept between test cases, which you almost certainly do not want. If you can't fix it on the program design level (which I guess is hard, because singletons tend to spread in nasty ways) you can maybe create some way to clean up the global state at the end of each test case.

IvanRibakov commented 3 years ago

@rollbear thanks a lot for your help and patience! I think I have everything I need to dig myself out of this situation now.

rollbear commented 3 years ago

Glad to be of help.