Module eqc_mocking_c

This module provides functionality for mocking C modules/function.

Copyright © Quviq AB, 2013-2023

Version: 1.46.3

Behaviours: gen_server.

Authors: Hans Svensson (hans.svensson@quviq.com).

Description

This module provides functionality for mocking C modules/function. It is designed to work together with eqc_component (+ eqc_c and eqc_mocking), so that callouts can be mocked conveniently using the functionality in these modules. It is also possible to use this module for traditional mocking. We first show how mocking can be used standalone, while the later part of the documentation focus on the eqc_component use-case.

Mocking C functionality and Erlang functionality uses the same underlying mocking language, as described in eqc_mocking, and thus mocked modules are described in very much the same way. However, since Erlang and C are fundamentally different languages there are, of course, things that are different when mocking. Starting in the API specification; to mock a C function, we need to know its type signature, as well as the direction (in, out, or in-out) of its arguments. If we use the same example as in eqc_mocking, a stack; where its API consists of three functions new/0, push/2, and pop/1. The C version of the API specification looks like:
  api_spec_c() -> #api_spec{
    language = c,
    mocking = eqc_mocking_c,
    modules  = [ #api_module{
      name = stack,
      functions = [
        #api_fun_c{ name = new,  ret = 'Stack', args = []},
        #api_fun_c{ name = push, ret = void,
                    args = [#api_arg_c{ type = 'Stack', name = s, dir = in },
                            #api_arg_c{ type = int, name = val, dir = in }]},
        #api_fun_c{ name = pop,  ret = int,
                    args = [#api_arg_c{ type = 'Stack', name = s, dir = in }] }]
    }]}.
  
Mocking a C module is sligtly more involved than mocking an Erlang module, but only slightly. In addition to the mocking specification we need a C header file defining all data types used in mocking. In this simple example only the Stack type is needed; and since we could consider the Stack type as an abstract data type from the outside perspective it is enough to put the following line in a file (stack.h):
  typedef int Stack;
  
i.e. we use an integer as an abstract identifier of a stack. In the example test case we expect the stack to be called four times; (1) creating a new stack, (2) pushing the integer 4 onto the stack, (3) pushing 6 onto the stack, and (4) popping one element from the stack expecting 6 to be returned. This could be expressed as:
  -define(STACK, 123456). %% Abstract stack reference
  lang() -> ?SEQ([?EVENT(stack, new,  [],          ?STACK),
                  ?EVENT(stack, push, [?STACK, 4], ok),
                  ?EVENT(stack, push, [?STACK, 6], ok),
                  ?EVENT(stack, pop,  [?STACK],    6)
                 ]).
  
Where we use 123456 as an abstract stack reference during the execution. Also note that the Erlang atom 'ok' corresponds to void. Given this, the stack can now be 'used' as follows (assuming that the functions defined above reside in the module stack_mock_c):
  2> eqc_mocking_c:start_mocking(c_stack, stack_mock_c:api_spec(), ["stack.h"], [], [{cppflags, "-I ."}]).
  ok
  3> eqc_mocking_c:init_lang(stack_mock_c:lang(), stack_mock_c:api_spec()).
  ok
  4> S = c_stack:new(), c_stack:push(S, 4), c_stack:push(S, 6), c_stack:pop(S).
  6
  5> eqc_mocking_c:check_callouts(stack_mock_c:lang()).
  true
  6> eqc_mocking_c:(stack_mock_c:lang(), stack_mock_c:api_spec()).
  ok
  7> S = stack_c:new(), stack_c:push(S, 4), stack_c:push(S, 6).
  ok
  8> eqc_mocking_c:check_callouts(stack_mock_c:lang()).
  {expected,{call,stack,pop,[123456]}}
  9> eqc_mocking_c:init_lang(stack_mock_c:lang(), stack_mock_c:api_spec()).
  ok
  10> S = stack_c:new(), stack_c:pop(S).
  ** exception exit: 'Unexpected call to pop!'
       in function  eqc_c:call_external/4 (../src/eqc_c.erl, line 1401)
       in call from stack_c:pop/1 (/tmp/__eqc_tmp1400506259837699_stack_c_wrap.erl, line 80)
       in call from stack_mock_c:run_lang3/0 (stack_mock_c.erl, line 66)
  
Noteable in the usage above is that we can call init_lang/2 many times without restarting the mocking framework in between. Also note that the check check_callouts/1 checks both that all expected calls where made, and that the calls were made with the expected arguments. If a call is made out of order a failure is reported immediately. Finally, we should note that the arguments are normally not checked until check_callouts is called (this is useful not to abort tests prematurely):
  19> eqc_mocking_c:init_lang(stack_mock_c:lang(), stack_mock_c:api_spec()).
  ok
  20> S = stack_c:new(), stack_c:push(S, 7), stack_c:push(S, -3), stack_c:pop(S).
  6
  21> eqc_mocking_c:check_callouts(stack_mock_c:lang()).
  {unexpected,{call,stack,push,[123456,7]},
   expected,{call,stack,push,[123456,4]}}
  

Advanced usage - matching arguments

As we saw in the example above, argument checking is deferred until post-execution. This default behaviour is not always the wanted one, instead it is possible in the API specifiction to define that arguments should be checked for equality:
  ... #api_arg_c{ type = int, name = val, dir = in, matched = true }]},
  

where we are going to match on the argument val during execution. It is currently not possible to check for equality of anything that is not a simple type (i.e. pointers, composed structures, etc.)

Advanced usage - out arguments

An important difference to Erlang mocking is the usage of out-arguments in C, where a pointer is passed to a function and the function is expected to fill it with content. In principle this corresponds to the function possibly returning several things. As an example consider a function get_curr_speed. This API function should return the current speed if successful, and indicate an error otherwise. In Erlang we would simply write a function like:
    get_curr_speed() ->
      ...
        {ok, Speed}
      ...
        [error, Reason}
  
However, in C it would be awkward to return such a structure, instead the normal way to do this in C is to give get_curr_speed the following type signature:
    bool get_curr_speed(int *speed);
  
where the returned value indicates whether the speed is provided (following the pointer) or not. To mock such a function we first need to declare the argument as having direction out:
    #api_fun_c{
      ret = bool, name = get_curr_speed,
      args = [#api_arg_c{ type = 'int *', name = speed, dir = out }] }
  
Thereafter, whenever we expect a call of this function we need to provide not one, but two, return values in our ?EVENT like:
    ?EVENT(sensor, get_curr_speed, [], {42, true})
  

where the returned things are tupled --- first the out-arguments in order of appearance in the API specification and lastly the actual return value.

Advanced usage - stored type

Sometimes a function takes a complex type as parameter, but there is only a single field in the complex type that is intereresting from a mocking perspective. To handle this case it is possible to declare a stored type for a given argument. Suppose ComplexType is declared as:
    typedef struct _ComplexType{
      ...
      int field;
      ...
    } ComplexType;
  
now the API specification could look like:
    #api_fun_c{
      ret = void, name = f,
      args = [#api_arg_c{ type = 'ComplexType *', name = arg,
                          stored_type = int, code = "_stored_->arg = arg->field;" }] }
  
Here type is the actual type of the argument, while the stored_type is the type that is visible from a mocking perspective. I.e. the code that calls the mocked function will provide a (pointer to a) ComplexType, but in the expected behavior (and in the subsequent check) only the simple type (int) is visible. Note it is impossible for the framework to guess what part of the complex type that is interesting, thus the user has to provide a snippet of C code that extracts the necessary data. This code snippet has access to all actual arguments passed to the function (here: arg), and is supposed to fill the correspondingly named field in the struct (containing all input-arguments) named _stored_. Thereafter, whenever we expect a call of this function we only specify the value of the integer field:
    ?EVENT(module, f, [23], ok)
  

Note that stored_type = void is a valid construction, meaning that the argument is not used at all (and should therefore not be in the ?EVENT).

Advanced usage - arrays

Another rather common situation is when an argument represents an array. There is custom support to handle this case. Imagine that we are mocking the C function
    bool get_last_speeds(int n, int *speeds);
  
This function is supposed to return the last N registered speeds. The function should return true if it successfully copies N items to the provided (out)-array. To mock it we need the following API specification:
  #api_fun_c{ ret = bool, name = get_last_speeds,
              args = [#api_arg_c{ type = int, name = n},
                      #api_arg_c{ type = 'int *', name = speeds, dir = out,
                                  buffer = {true, "n"} }] }
  

The "n" is a again a snippet of C code, namely code that calculates the length of the array. If there is need for a custom copy function it is possible to use buffer = true, code = "..." instead.

A correseponding ?EVENT could be:
  ?EVENT(sensor, get_last_speeds, [4], {[42,44,47,49], true})
  

Caveat: for out arguments the "pointer to single value" is the default, and it is necessary to say buffer = true to consider the other case.

Advanced usage - phantom in argument

This is related to the stored type example above, but consider if there is a small number (but larger than 1) of fields that are interesting for a complex data type:

    typedef struct _ComplexType{
      ...
      int field;
      ...
      int field2;
      ...
    } ComplexType;
  
now the API specification could look like:
    #api_fun_c{
      ret = void, name = f,
      args = [#api_arg_c{ type = 'ComplexType *', name = arg,
                          stored_type = int, code = "_stored_->arg = arg->field;" },
              #api_arg_c{ type = 'int', name = arg2, phantom = true,
                          code = "_stored_->arg2 = arg->field2;" }] }
  
Phantom here means an argument that is only visible on the mocking specification side, when giving the expected behavior and checking the mocked calls. I.e. calls to the mocked function takes one argument, but the in the mocking specification it takes two arguments, like:
    ?EVENT(module, f, [23, 42], ok)
  

Advanced usage - phantom out argument

To handle in/out parameters it is sometimes necessary to resort to a phantom out argument. Consider the same function as before for getting the N last speeds, but with a slightly different API:
  typedef struct SpeedsType {
    int n;
    int *speeds;
  } SpeedsType;
 
  bool get_last_speeds2(SpeedsType *speeds);
  
This functions is used by providing a (pointer to) a SpeedsType where n is set to the number of elements to fetch, and speeds has room for these values. The corresponding API specification would use a phantom out argument to hold the actual data:
  #api_fun_c{ ret = bool, name = get_last_speeds2,
              args = [#api_arg_c{ type = 'SpeedsType *', name = speeds,
                                  stored_type = int,
                                  code = "_stored_->speeds = speeds->n;"},
                      #api_arg_c{ type = 'int *', name = the_speeds, buffer = true,
                                  dir = out, phantom = true,
                                  code = "memcpy(speeds->speeds, _return_->the_speeds,"
                                         " speeds->n * sizeof(int));"}] }
  
A correseponding ?EVENT could be:
  ?EVENT(sensor, get_last_speeds2, [4], {[42,44,47,49], true})
  

Advanced usage - silent functions

We have also introduced something that we call silent functions. Consider for example a simplified verision of the get_curr_speed function:
  int get_curr_speed2();
  
Suppose that it is called very often throughout the test and that the speed changes in-frequently. In this case it is possible to define it silent like:
  #api_fun_c{
       name = get_curr_speed2, ret = int,
       args = [], silent = {true, "17"} }.
  
This specifies that the function is always callable, and that calls to this function is not checked against an expected behavior. The C snippet (here "17") computes the starting value of the function. Whenever the speed should change, we call (via eqc_c):
  set_get_curr_speed2__return(N);
  
where _return specifies that it is the return value that we change. (Had it had out parameters they are changed by a set function suffixed by the name of the out parameter respectively.)

Data Types

api_arg_c()

api_arg_c() = #api_arg_c{type = atom() | string(), stored_type = atom() | string(), name = atom() | string() | {atom(), string()}, dir = in | out, buffer = false | true | {true, non_neg_integer()} | {true, non_neg_integer(), string()}, phantom = boolean(), matched = boolean(), default_val = no | string(), code = no | string()}

api_fun_c()

api_fun_c() = #api_fun_c{name = atom(), classify = any(), ret = atom() | api_arg_c(), args = [api_arg_c()], silent = false | {true, any()}}

api_fun_erl()

api_fun_erl() = #api_fun{name = atom(), classify = any(), arity = non_neg_integer(), fallback = boolean(), matched = [non_neg_integer()] | fun((any(), any()) -> boolean()) | all}

api_module()

api_module() = #api_module{name = atom(), fallback = atom(), functions = [api_fun_erl()] | [api_fun_c()]}

api_spec()

api_spec() = #api_spec{language = erlang | c, mocking = atom(), config = any(), modules = [api_module()]}

path()

path() = string()

Function Index

c_code/1Equivalent to c_code([], API).
c_code/2Generates C-code for the mocked API.
check_callouts/1Checks that the correct callouts have been made.
gen_headers/1Generates C function prototypes for the given API specification.
init_lang/2Initialize the mocking language.
merge_spec_modules/1Safely merge multiple specifications of the same module.
start_mocking/3Equivalent to start_mocking(c_code, APISpec, HdrFiles, ObjFiles, [], []).
start_mocking/4Equivalent to start_mocking(CMod, APISpec, HdrFiles, ObjFiles, [], []).
start_mocking/5Equivalent to start_mocking(CMod, APISpec, HdrFiles, ObjFiles, COpts, []).
start_mocking/6Initialize mocking, mocked modules are created as specified in the supplied callout specification.
stop_mocking/0Gracefully stop mocking.

Function Details

c_code/1

c_code(APISpec::#api_spec{language = c, mocking = atom(), config = any(), modules = [api_module()]}) -> string()

Equivalent to c_code([], API).

c_code/2

c_code(Headers, APISpec) -> string()

Generates C-code for the mocked API. If additional definitions are needed, for example type definitions, a list of include files may be given as an argument to this function.

check_callouts/1

check_callouts(MockLang) -> true | Error

Checks that the correct callouts have been made. Given a language, checks that the mocked functions called so far matches the language (the argument checking during execution is normaly relaxed, so errors could be undiscovered until here). Also checks that the language is in an "accepting state", i.e. a state where it is ok to stop.

gen_headers/1

gen_headers(API) -> string()

Generates C function prototypes for the given API specification. These can then be used while building the system under test.

init_lang/2

init_lang(MockLang, APISpec) -> ok | no_return()

Initialize the mocking language. This sets the language describing how the mocked modules should behave. Calling this function also resets the collected trace. Some sanity checks are made, throws an error if the language and the callout specification are inconsistent or if the c-code is not available.

merge_spec_modules/1

merge_spec_modules(ModSpecs::[api_module()]) -> [api_module()] | no_return()

Safely merge multiple specifications of the same module.

start_mocking/3

start_mocking(APISpec, HeaderFiles, ObjectFiles) -> Result

Equivalent to start_mocking(c_code, APISpec, HdrFiles, ObjFiles, [], []).

start_mocking/4

start_mocking(Module, APISpec, HeaderFiles, ObjectFiles) -> Result

Equivalent to start_mocking(CMod, APISpec, HdrFiles, ObjFiles, [], []).

start_mocking/5

start_mocking(Module, APISpec, HeaderFiles, ObjectFiles, COptions) -> Result

Equivalent to start_mocking(CMod, APISpec, HdrFiles, ObjFiles, COpts, []).

start_mocking/6

start_mocking(Module, APISpec, HeaderFiles, ObjectFiles, COptions, Components) -> Result

Initialize mocking, mocked modules are created as specified in the supplied callout specification. The C-Module wrapper is loaded as CMod. HdrFiles is a list of C header files containing type definitions necessary for generate mocking stubs. ObjFiles is the compiled C object files, which constitute the SUT. It is possible to provide options for eqc_c:start/2 that is called by start_mocking (for example cppflags, verbose, keep_files, etc. Finally if we are mocking for a cluster, the cluster components should be given in Components so that internal calls can be filtered away properly.

stop_mocking/0

stop_mocking() -> ok

Gracefully stop mocking


Generated by EDoc