3
\$\begingroup\$

Parts

Removing redundant MOCK_SYS usage

After writing a bunch of unit test we see a pattern emerge. The lambda's we are using don't actually do much and most of the time simply return the same value. We only need to specialize them when testing error cases (most of the time they return the same thing).

So part two is about building an object that provides a default implementation for all the mocks.

Building: test/MockHeaderInclude.h

We build the following as part of the build files.

test/MockHeaderInclude.h: .FORCE
    buildMockHeaderInclude

Then the script that does the work:

#!/bin/bash

IFS='%' read -r -d '' -a fileTemplate <<PREFIX
#ifndef THORSANVIl_THORS_SOCKET_MOCK_HEADER_INCLUDE
#define THORSANVIl_THORS_SOCKET_MOCK_HEADER_INCLUDE

#include <functional>

// Please add includes for all mocked libraries here.
// PART-1-Start
%
// PART-1-End
namespace ThorsAnvil::BuildTools::Mock
{

// Please define all FuncType_<XXX> here
// There should be one for each MOCK_TFUNC you use in the code.
// The make files will provide the declaration but these need to be filled in by
// the developer and committed to source control
// PART-2-Start
%
// PART-2-End
// This default implementation of overridden functions
// Please provide a lambda for the implementation
// When you add/remove a MOCK_FUNC or MOCK_TFUNC to the source
// This list will be updated.

}

#include "coverage/MockHeaders.h"

namespace ThorsAnvil::BuildTools::Mock
{
class MockAllDefaultFunctions
{
    int version;
// PART-3-Start
%
// PART-3-End

    public:
        MockAllDefaultFunctions()
            : version(2)
// PART-4-Start
%
// PART-4-End
        {}
};


}

#endif
PREFIX

function copyPart {

    local fileName=$1
    local section=$2

    cat ${fileName} | awk 'BEGIN {InSection=0;} /'PART-${section}'-Start/ {InSection=1;next;} /PART-'${section}'-End/ {InSection=0} {if (InSection == 1){print}}'
}

function getFunctions {
    perl -ne '/MOCK_(T?)FUNC\([ \t]*([^\) \t]*)/ and print "$2 $1\n"' * | sort | uniq
}

function buildFuncType {
    local fileName=$1

    while read line; do
        split=(${line})
        name=${split[0]}
        type=${split[1]}

        if [[ "${type}" == "T" ]]; then
            find=$(grep "using FuncType_${name}[ \t]*=" "${fileName}")
            if [[ "${find}" == "" ]]; then
                echo "using FuncType_${name}        = /* Add function type info here */;"
            fi
        fi
    done < <(getFunctions)
}

function buildMEMBER {
    local fileName=$1

    while read line; do
        split=(${line})
        name=${split[0]}
        type=${split[1]}

        find=$(grep "MOCK_${type}MEMBER(${name});" "${fileName}")
        if [[ "${find}" == "" ]]; then
            echo "    MOCK_${type}MEMBER(${name});"
        fi
    done < <(getFunctions)
}

function buildPARAM {
    local fileName=$1

    while read line; do
        split=(${line})
        name=${split[0]}

        find=$(grep " MOCK_PARAM(${name}," "${fileName}")
        if [[ "${find}" == "" ]]; then
            echo "            , MOCK_PARAM(${name},     []( Add expected parameters here ){return Add default value here;}),"
        fi
    done < <(getFunctions)
}

function createFile {
    local fileName=$1

    echo "${fileTemplate[0]}"
    copyPart "${fileName}" 1
    echo "${fileTemplate[1]}"
    copyPart "${fileName}" 2
    buildFuncType ${fileName}
    echo "${fileTemplate[2]}"
    copyPart "${fileName}" 3
    buildMEMBER ${fileName}
    echo "${fileTemplate[3]}"
    copyPart "${fileName}" 4
    buildPARAM ${fileName}
    echo "${fileTemplate[4]}"
}

function buildFile {

    local fileName=$1

    if [[ -e test ]]; then
        createFile ${fileName} > ${fileName}.tmp
        if [[ -e ${fileName} ]]; then
            diff ${fileName}.tmp ${fileName}
            if [[ $? == 1 ]]; then
                echo "ReBuilt: ${fileName}"
                mv ${fileName}.tmp ${fileName}
            else                                                                                        \
                rm ${fileName}.tmp
            fi
        else                                                                                            \
            echo "Built: ${fileName}"
            mv ${fileName}.tmp ${fileName}
        fi
    fi
}

buildFile test/MockHeaderInclude.h

This then generates the following file:

#ifndef THORSANVIl_THORS_SOCKET_MOCK_HEADER_INCLUDE
#define THORSANVIl_THORS_SOCKET_MOCK_HEADER_INCLUDE

#include <functional>

// Please add includes for all mocked libraries here.
// PART-1-Start
// PART-1-End
namespace ThorsAnvil::BuildTools::Mock
{

// Please define all FuncType_<XXX> here
// There should be one for each MOCK_TFUNC you use in the code.
// The make files will provide the declaration but these need to be filled in by
// the developer and committed to source control
// PART-2-Start
// PART-2-End
// This default implementation of overridden functions
// Please provide a lambda for the implementation
// When you add/remove a MOCK_FUNC or MOCK_TFUNC to the source
// This list will be updated.

}

#include "coverage/MockHeaders.h"

namespace ThorsAnvil::BuildTools::Mock
{
class MockAllDefaultFunctions
{
    int version;
// PART-3-Start
    MOCK_MEMBER(close);
    MOCK_MEMBER(connect);
    MOCK_TMEMBER(fcntl);
    MOCK_MEMBER(gethostbyname);
    MOCK_TMEMBER(open);
    MOCK_MEMBER(pipe);
    MOCK_MEMBER(read);
    MOCK_MEMBER(shutdown);
    MOCK_MEMBER(socket);
    MOCK_MEMBER(write);
// PART-3-End

    public:
        MockAllDefaultFunctions()
            : version(2)
// PART-4-Start
            , MOCK_PARAM(close,     []( Add expected parameters here ){return Add default value here;}),
            , MOCK_PARAM(connect,     []( Add expected parameters here ){return Add default value here;}),
            , MOCK_PARAM(fcntl,     []( Add expected parameters here ){return Add default value here;}),
            , MOCK_PARAM(gethostbyname,     []( Add expected parameters here ){return Add default value here;}),
            , MOCK_PARAM(open,     []( Add expected parameters here ){return Add default value here;}),
            , MOCK_PARAM(pipe,     []( Add expected parameters here ){return Add default value here;}),
            , MOCK_PARAM(read,     []( Add expected parameters here ){return Add default value here;}),
            , MOCK_PARAM(shutdown,     []( Add expected parameters here ){return Add default value here;}),
            , MOCK_PARAM(socket,     []( Add expected parameters here ){return Add default value here;}),
            , MOCK_PARAM(write,     []( Add expected parameters here ){return Add default value here;}),
// PART-4-End
        {}
};


}

#endif

Which I then defined like this:

#ifndef THORSANVIl_THORS_SOCKET_MOCK_HEADER_INCLUDE
#define THORSANVIl_THORS_SOCKET_MOCK_HEADER_INCLUDE

#include <functional>

// Please add includes for all mocked libraries here.
// PART-1-Start
#include <fcntl.h>
#include <netdb.h>
#include "ThorsSocketConfig.h"

// PART-1-End
namespace ThorsAnvil::BuildTools::Mock
{

// Please define all FuncType_<XXX> here
// There should be one for each MOCK_TFUNC you use in the code.
// The make files will provide the declaration but these need to be filled in by
// the developer and committed to source control
// PART-2-Start
using FuncType_open     = int(const char*, int, unsigned short);
using FuncType_fcntl    = int(int, int, int);

// PART-2-End
// This default implementation of overridden functions
// Please provide a lambda for the implementation
// When you add/remove a MOCK_FUNC or MOCK_TFUNC to the source
// This list will be updated.

}

#include "coverage/MockHeaders.h"

namespace ThorsAnvil::BuildTools::Mock
{
class MockAllDefaultFunctions
{
    int version;
// PART-3-Start
    std::function<hostent*(const char*)> getHostByNameMock =[]  (char const*) {
        static char* addrList[] = {""};
        static hostent result {.h_length=1, .h_addr_list=addrList};
        return &result;
    };

    MOCK_MEMBER(read);
    MOCK_MEMBER(write);
    MOCK_TMEMBER(open);
    MOCK_MEMBER(close);
    MOCK_TMEMBER(fcntl);
    MOCK_MEMBER(pipe);
    MOCK_MEMBER(socket);
    MOCK_MEMBER(gethostbyname);
    MOCK_MEMBER(connect);
    MOCK_MEMBER(shutdown);

// PART-3-End

    public:
        MockAllDefaultFunctions()
            : version(2)
// PART-4-Start
            , MOCK_PARAM(read,                                  [ ](int, void*, ssize_t size)           {return size;})
            , MOCK_PARAM(write,                                 [ ](int, void const*, ssize_t size)     {return size;})
            , MOCK_PARAM(open,                                  [ ](char const*, int, int)              {return 12;})
            , MOCK_PARAM(close,                                 [ ](int)                                {return 0;})
            , MOCK_PARAM(fcntl,                                 [ ](int, int, int)                      {return 0;})
            , MOCK_PARAM(pipe,                                  [ ](int* p)                             {p[0] = 12; p[1] =13;return 0;})
            , MOCK_PARAM(socket,                                [ ](int, int, int)                      {return 12;})
            , MOCK_PARAM(gethostbyname,                         std::move(getHostByNameMock))
            , MOCK_PARAM(connect,                               [ ](int, sockaddr const*, unsigned int) {return 0;})
            , MOCK_PARAM(shutdown,                              [ ](int, int)                           {return 0;})
// PART-4-End
        {}
};


}

#endif

Now subsequent calls never change this file (anything I have previously defined is left unchanged). So this file is checked into source control.

If I had more MOCK_FUNC() macros to my source then these will of course be added to this file.

Helper Macros

#define MOCK_MEMBER_EXPAND(type, func)                      ThorsAnvil::BuildTools::Mock::MockOutFunction<type> mockOutFunction_ ## func
#define MOCK_TMEMBER(func)                                  MOCK_MEMBER_EXPAND(ThorsAnvil::BuildTools::Mock::FuncType_ ## func, func)
#define MOCK_MEMBER(func)                                   MOCK_MEMBER_EXPAND(decltype(::func), func)
#define MOCK_PARAM_EXPAND(func, name, lambda)               mockOutFunction_ ## func(name, lambda)
#define MOCK_PARAM(func, lambda)                            MOCK_PARAM_EXPAND(func, MOCK_BUILD_MOCK_NAME(func), lambda)

So basically each member declaration expands to (using socket as example):

// MOCK_MEMBER(socket)
ThorsAnvil::BuildTools::Mock::MockOutFunction<decltype(::socket)> mockOutFunction_socket;

With the constructor initializing like this:

// MOCK_PARAM(socket, [](int, int, int){return 12;})
, mockOutFunction_socket("socket", [](int, int, int){return 12;})

MockOutFunction

This was previously defined in C++ Mock Library: Part 1

Usage in Tests

TEST(UniTestBloc, MyUnitTest)
{
    MockAllDefaultFunctions   defaultMocks;

    auto action = [](){
        ThorsSocket::Socket socket("www.google.com", 80);
        // Do tests
    };
    ASSERT_NO_THROW(action());
}
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

Write it in C++

The script is something I would cobble together as well when I would start working out this problem. However, it is nice for rapid prototyping, but it's actually terrible. It's not just a script, it's specifically bash, and other shells might not be able to run that script. Furthermore, it relies on lots of external tools, most notably: awk, diff, grep, perl, sort, and uniq, some of which have to be called with tiny scripts in their own language. And useless use of cat for good measure.

It's perfectly possible to write this in a platform-independent way in C++, especially using std::filesystem, std::format() and std::regex. Or alternatively, if you are depending on a heavy-handed tool like Perl anyway, rewrite it as a Perl script.

Performance

Apart from probably getting a big performance boost by rewriting it in C++ (probably even despite its poor regex implementation), one issue I noticed is that the script makes it look like it doesn't rebuild anything if nothing has changed, but it actually always rebuilds the output file, but then just diffs it against the old file to check if it was different. Consider instead checking if any of the input files have a newer timestamp than the output file. If not, then you don't have to do anything. Of course, that is something that make could already do for you. So consider changing the Makefile so that all the input files are part of the rule's dependency, and also pass those in as command line parameters.

\$\endgroup\$
4
  • \$\begingroup\$ Consider instead checking if any of the input files have a newer timestamp than the output file. Normally yes. But in this case they also have a dependency on the Build Tools themselves. If the build tools are updated I need to force a rebuild. \$\endgroup\$ Sep 16, 2023 at 20:59
  • \$\begingroup\$ That can in principle also be expressed in a make rule. \$\endgroup\$
    – G. Sliepen
    Sep 16, 2023 at 21:15
  • \$\begingroup\$ Just tried doing checking of timestamps. The problem here is that the output files depend on all the source (and header files). So any changes will force a rebuild of the MockHeaderInclude.h which will then force a rebuild of everything. Since we are always updating something before each build this results in rebuilding everything every time. \$\endgroup\$ Sep 17, 2023 at 20:28
  • \$\begingroup\$ Ah, I see. So ideally you want to separate tracking whether the script ran from whether MockHeaderInclude.h was updated. This could be done by creating a dummy "stamp file" that is touched every time your script runs, and use that to compare against the timestamps of all the input files, instead of the timestamp of the script itself. That way you can still eliminate calls to your script if really nothing was changed. \$\endgroup\$
    – G. Sliepen
    Sep 18, 2023 at 6:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.