Enhancing POI testing approach through the use of additional information

08/23/2018 ∙ by Sergio Pérez, et al. ∙ 0

Recently, a new approach to perform regression testing has been defined: the point of interest (POI) testing. A POI, in this context, is any expression of a program. The approach receives as input a set of relations between POIs from a version of a program and POIs from another version, and also a sequence of input functions, i.e. test cases. Then, a program instrumentation, an input test case generation and different comparison functions are used to obtain the final report which indicates whether the alternative version of the program behaves as expected, e.g. it produces the same values or it uses less CPU/memory. In this paper, we explain how we can improve the POI testing approach through the use of common stack traces and a more sophisticated tracing for calls. These enhancements of the approach allow users to identify errors earlier and easier. Additionally, they enable new comparison modes and new categories of reported unexpected behaviours.

READ FULL TEXT VIEW PDF
POST COMMENT

Comments

There are no comments yet.

Authors

page 1

page 2

page 3

page 4

This week in AI

Get the week's most popular data science and artificial intelligence research sent straight to your inbox every Saturday.

1 Introduction

During its useful lifetime, a program might evolve many times. Each evolution is often composed of several changes that produce a new release of the software. There are multiple ways to control that these changes do not modify the behaviour of any part of the program that was already correct. Most of the companies rely on regression testing [21] to assure that a desired behaviour of the original program is kept in the new version, but there exist other alternatives such as the static inference of the impact of changes [13].

Even when a program is perfectly working and it fulfils all its functional requirements, sometimes we still need to improve parts of it. There are several reasons why a released program needs to be modified. For instance, improving the maintainability or efficiency; or for other reasons such as obfuscation, security improvement, parallelization, distribution, platform changes, and hardware changes, among others. Although regression testing should be ideally done after each change, in real projects the methodology is less "real". As reported in [6], only 10% of the companies do regression testing daily. This means that, when an unexpected behaviour (UB) is detected, it can be hidden after a large number of subsequent changes. The authors also claim that this long-term regression testing is mainly due to the lack of time and resources.

Programmers that want to check whether the semantics of the original program remains unchanged in the new version usually create a test suite. There are several tools that can help in all this process. For instance, Travis CI can be easily integrated in a GitHub repository so that each time a pull request is performed, the test suite is launched. Point of interest (POI) testing [9, 11, 12] (briefly described in section 2) is an alternative and complementary approach that creates an automatic test suite to do regression testing: (i) An alternative approach because it can work as a standalone way without the need of using other techniques. Therefore, POI testing can check the evolution of the code even if no test suite has been defined. (ii) A complementary approach because it can also be used to complement other techniques, e.g. unit testing, providing a major reliability in the assurance of behaviour preservation.

In the context of debugging, programmers often use breakpoints to observe the values of an expression during an execution. POI testing makes this feature available in testing, allowing users to easily focus the test cases on one or more specific points without modifying the source code (as it happens when using asserts) or adding more code (as it happens in unit testing). POI testing introduces the ability to specify kind-of breakpoints in the context of testing. A POI can be any expression in the code (e.g., a function call) meaning that we want to check the behaviour of that expression, e.g. the POI {module.erl, 5, {var, ’A’}, 2}} refers to the second occurrence111When this argument is omitted, it is assumed to be the first occurrence of the expression. of variable A in the fifth line of file module.erl. Although they handle similar concepts, POIs are not exactly like breakpoints, since their purpose is different. Breakpoints are used to indicate where the computation should stop, so users can inspect variable values or control statements. In contrast, a POI defines an expression whose sequence of evaluations to values must be recorded, so that users can check the expected behaviour (by value comparison) after the execution.

Although POI testing is a complete approach that allows users to control how, when and what should be compared, there are still some improvements which can increase its usability. In this paper, we present two enhancements of the approach that enrich it with new information, new features and new comparison modes. These enhancements share a common framework, described in Section 3, which can be used to define new and more complex improvements of the technique. In particular, the enhancements presented in this paper improve the POI testing approach by the use of better call traces, described in Section 4, and stack traces, described in Section 5. The goal of both enhancements is to provide users with more information and to ease the discovery of UB sources. In this paper, we provide concrete examples by using Erlang as the running language.

On the one hand, the call trace enhancement provides more information about calls in such a way that, when an UB is detected in a call expression, users can automatically compare the call arguments that produced this UB. Therefore, users can know earlier if the cause of the UB is a discrepancy between these arguments, or a behaviour change in the called function. On the other hand, we define a enhancement which makes use of the common stack traces, i.e. the ones showed in error report. Stack trace are a common element used while debugging buggy code. Therefore, it seems natural to incorporate them into the POI testing approach. Stack traces can be compared to find earlier whether an UB comes from some differences in the followed execution paths. Using this discrepancy it is easier to point to the code which causes the UB and start a debugging process from there.

2 POI testing

In POI testing, (i) the programmer identifies a POI and a set of input functions whose invocations should evaluate the POI. Then, by using some automatic test case generation technique, (ii) the approach automatically generates a test suite that tries to cover all possible paths that reach the POI (trying also to produce execution paths that evaluate the POI several times). Therefore, in POI testing, the input of a test case (ITC) is defined as a call to an input function with some specific arguments, and the output is the sequence of those values the POIs are evaluated to during the execution of the ITC. For the sake of disambiguation, in the rest of the paper we use the term traces to refer to these sequences of values. Next, (iii) the test suite is used to automatically check whether the behaviour of the program remains unchanged across new versions 222Steps (ii) and (iii) could also be executed in parallel. Here, for the sake of simplicity, we only consider the sequential execution of these steps.. Finally, (iv) the user is provided with a report about the success or failure of these test cases.

Note that the technique allows the definition of multiple POIs [10]. With this feature, users can trace several (and maybe unrelated) functionalities in a single run. Additionally, users can strengthen the quality of their test suite by checking behaviour preservation in more than one point. Finally, this feature is needed in those cases where a POI in one version is associated with more than one POI in another version (e.g., when a POI in the final source code is associated with two or more POIs in the initial source code due to a refactoring or a removal of duplicated code).

An example of a POI tester is the tool named SecEr (Software Evolution Control for Erlang), which is publicly available at: https://github.com/mistupv/secer. All the analyses performed by SecEr are transparent to the user. The only task in SecEr that requires user intervention is identifying suitable POIs in both the old and the new versions of the program. SecEr allows to define test configuration files to ease all this process and also to make it reusable. The interested readers are referred to [10] where they can found an extensive discussion about the similarities with similar tools and how to deal with concurrency.

In the following, we introduce all the concrete details of the approach needed to understand the enhancements presented in this paper. It has been divided in three items, the inputs that the approach needs, a summarized description of its internals, and the outputs it produces.

  • Inputs: POI testing approach needs two parameters at least to be able to operate. Additionally, the POI testing approach can also be run with some specific comparison and report functions. The comparison and report functions are explained within the internals and the outputs of the approach, respectively.

    • POI relation. It relates POIs from two different versions of the same program. In Erlang, it is represented by a list of tuples. Each one of these tuples contain two POIs, one of each version of the program. For instance, the POI relation [{{old.erl, 14, {var, ’X’}}, {new.erl, 14, {var, ’BetterName’}, 1}}, {{old.erl, 130, case}, {new.erl, 145, if}}] defines a relation between two POIs. The first one is indicating that the variable X at line 14 has been renamed to BetterName. The second one indicates that a case expression has been changed by an if expression and their lines has also changed from 130 to 145.

    • Input functions. They are the entry points of the test cases. In Erlang, it is a list of function names. For examples, the list [main/2, test_1/0] defines two entry points for the approach, i.e. all the ITCs generated for the approach are calls to one of these functions. In concrete, function main/2 requires the generation of concrete arguments which are not provided by the user. This is further discussed in the internals of the approach. On the other hand, function test_1/0 has no arguments, so the approach simply run it once, and no more ITCs can be generated for it. Functions like this one are usually unit tests. Therefore, unit tests cannot be used in the POI testing approach to perform regression testing, instead they can only be used to debug failing test cases.

  • Internals: There are three main stages of the approach. First, how the traces are built, then how they are compared and finally how new ITCs can be generated.

    • Trace building. The basis of the POI testing approach is the tracing of some POIs during the evaluation of a concrete ITC. For this reason, it is needed a way to both, create and collect these traces. The first one is usually done by a program instrumentation, like the one defined in [10] for Erlang. This program instrumentation builds tuples of the form 333Note that is not necessarily the value the expression is evaluated to. It can also be other type of values, e.g. the time needed to evaluate it. which are produced during the evaluation of certain ITC. However, it is not enough with this, and we need a way to collect and sort these traces. In Erlang, a trace server is used in the following way: all the tuples produced by the instrumentation are sent to a server which collects and sorts them building the final trace of a concrete ITC evaluation.

    • Trace comparison. Once the traces are generated and stored, the next step is to compare them in order to infer if there is any UB444The observed UBs are represented and identified using literals, e.g. the atom slower could be used to represent an UB that occurs when an expression took more time to evaluate in the new version than in the old one. The UB representations are defined during the comparison process as it is then when the UBs are found.. In case some UB is found, its UB type together with an UB report is generated. These are several ways of comparing the traces. The most relevant techniques to compare multi-POI traces are described in [10]. The main distinction between them is whether the traces are compared as a whole or independently for each POI. For example, consider two versions of a program, with two POIs each one, i.e. first version (POI_1 and POI_2) and second version (POI_1’ and POI_2’). These POIs are related in the following way: POI_1 is related with POI_1’ and POI_2 is related with POI_2’. After the execution of a concrete ITC in bot version we obtain the following traces: [{POI_1, 3}, {POI_2, 4}] and [{POI_2’, 4}, {POI_1’, 2}]. If they are compared as a whole, the UB reported is that a trace from POI POI_1 was expected but a trace from POI POI_2 was generated. On the other hand, when they are compared independently, independent traces are generated for each POI. Therefore, for this example, there is not any UB when comparing the traces of POI_2, but there is an UB in POI_1’s traces, i.e. value 3 was expected but value 2 was generated. There is an optional input parameter that allows users to define their own comparison functions. A user-define comparison function should receive two traces as input, i.e. the traces to be compared, and should return either or a tuple of the form , where is the type of the observed UB and is a string to be printed when the UB is reported to the user.

    • ITC generation. The POI testing approach starts by generating an ITC for each input function. However, in order to reinforce the obtained UB report, each time an ITC is compared new ITCs for the input functions should be generated according to the comparison result. These new ITCs are usually based on the results of the previously compared ITCs. In this way, when an UB is found for a concrete ITC, we can generate new ITCs based on this ITC so they are more propitious to generate the same or other UBs. In [10], an ITC generation based on the mutation of the arguments of the ITC is described. Alternative generators can be used, but they should ideally take into account the result of the trace comparison in order to obtain better results for the POI testing approach.

  • Outputs: The POI testing approach produces as output a collection of ITCs together with the result of the trace comparison. When none of the ITC evaluated have generated an UB, users are informed of the successful result by adding also some additional information like the number of ITCs evaluated. Conversely, when one or more UBs have been observed, users get a report that can be configured in several ways. For example, the UB report can show only an ITC sample for each observed UB type in two ways: without distinguish between POIs or for each individual POI. Another alternative is to show all the ITCs where an UB has been observed. Additionally, users can define report functions to choose the reported information. A report function receives the comparison result and defines what information should appear in the UB report. Finally, some or all the failing ITCs can be stored somewhere so they can be reused after UB debugging to check whether the observed UBs have been mended.

3 Enhancing POI testing with additional information

This section introduces a general overview of the enhancements that we have defined for the POI testing approach. Therefore, Sections 4 and 5 are special cases of the general methodology explained here.

3.1 Obtaining and merging additional information with current traces

POI testing uses POI traces to check whether some UBs exist across several program versions. We represent each element of this trace as a tuple: In this paper, we propose an extension where some additional information is attached to each trace element (TE). Therefore, we need to extend this representation in order to be able to refer to this additional information. Moreover, we want to do this extension in a way that future trace enhancements follow the same scheme. For this reason, we have chosen a mapping function, represented as , e.g. will return the stored information about stack traces. Thus, in the enhanced approach a TE is a triplet .

Therefore, a concrete implementation of an enhanced POI testing approach should be able to produce traces which TEs are not tuples but these triplets. As mentioned in Section 2, the TEs, i.e. the tuples, are sent when the instrumented code is executed. Then, these TEs are collected and stored by the tracer which produces the final trace. Thus, the task of each concrete enhancement of the POI testing approach is to build these triplets storing in all the particular information needed for the posterior trace comparison, e.g. stack traces. In other words, for each enhancement we need to build a specific code instrumentation and a tracer.

3.2 Using enhanced traces to check program behaviour

1cf_general(TO, TN, VEF, TECF, UBRM) ->
2  cf_general(TO, TN, VEF, TECF, UBRM, []).
3
4cf_general([], [], _, _, _, _) ->
5  true;
6cf_general([TOE | TO], [TNE | TN], VEF, TECF, UBRM, His) ->
7  case TECF(VEF, TOE, TNE) of
8    true ->
9      cf_general(TO, TN, VEF, TECF, UBRM, [{TOE ,TNE} | His]);
10    UBT ->
11      {
12        UBT,
13        (dict:fetch(UBT, UBRM))(TOE, TNE, lists:reverse(His))
14      }
15  end.
Figure 1: General comparison function

POI testing allows using any comparison function. This feature gives users a complete freedom to configure the testing and/or debugging process in the best way according to their needs. However, it is common that users always rely on the same type of comparison functions. Therefore, an implementation of the POI testing approach usually provides some default comparison functions. Then, in order to generically deal with all possible enhancements of the POI testing approach, we define a default comparison function.

The general comparison function for our running language Erlang is depicted in Figure 1. The first two parameters are the usual ones, i.e. the whole traces of two program version, TO and TN. The rest of parameters of this function are introduced below.

  • Value-extractor function (VEF): This function extracts the concrete value that will be used to compare each TE. It takes a TE as input an returns the comparison values. For example, function fun(TE) -> TE end uses the whole TE to check UBs, i.e. its POI, its value and all its additional information. On the other hand, function fun({POI, V, AI}) -> {POI, V, dict:fetch(st, AI)} end555Function dict:fetch(Key, Dict) returns the value associated with Key in dictionary Dict. uses only the stack trace information and ignores the rest of additional information. The default value for VEF is fun({_, V, AI}) -> {V, AI} end666We assume that the POI comparison is previously performed by a POI-relation check function..

  • Trace-element comparison function (TECF): In order to allow users to check UBs in different ways and not only a plain equality function, i.e. operator ==, we add a comparison function for each pair of TEs. This function takes as input the VEF and the TEs of both versions of the program. Internally, it applies VEF to each TE and decides whether the observed behaviour is the expected or not. In case of an UB is found, its type is returned. For example, fun(VEF, TOE, TNE) -> case compare(VEF(TOE), VEF(TNE)) of gt -> true; eq -> same; lt -> downgrade end end, is a simple example where function compare/2 is used to check whether a reduction in some performance indicator is obtained. Then, when it is not obtained, either a same or a downgrade UB type is returned. The default value for TECF is fun(VEF, TOE, TNE) -> VEF(TOE) == VEF(TNE) end.

  • Unexpected-behaviour report mapping (UBRM): When an UB is detected, a specific report should be generated, i.e. a message should be provided to users. Therefore, this parameter allows users to specify how the POI tester should react to a particular UB. In this case, we use a mapping, because it allows easy redefinitions and additions of UBs reports. The mapping returns a function for a given unexpected behaviour type UBT. The returned function builds a string using the TEs of both versions of the program and the previous compared TEs, i.e. His. For example, expression dict:fetch(diff_values, UBRM) in line 1 of Figure 1, can return a function like fun({P, V1, _}, {P, V2, _}, His) -> "Value for P differ. V1 vs V2. Previous trace elements: His" end777This is not a valid Erlang string. We do not show here the actual implementation for the sake of the presentation simplicity.. Thus, in this way, users can define the message that should be shown when an UB of type diff_values is found. The default value for UBRM is a mapping that, for any UB type, returns a function that prints all the available information, i.e. the current and the previous TEs.

There are several modes of using the additional information stored in the TEs and all these modes are defined by the arguments given to the general comparison function (Figure 1). Although, given the freedom of the POI testing approach, more specific and user-defined modes are possible, we list the three modes which surely will be more frequently needed by users.

  • Additional information is not used during comparison (NUAI). In this mode, the traced values are the only data used when comparing the TEs. This is the mode that should be used when the additional information is expected to vary due to the differences between program versions or simply due to the type of data it contains. Additionally, this mode can also be used to lighten the comparison process or to reduce the relevance of the additional information. This mode will use a value-extractor function like fun({POI, V, _}) -> {POI, V} end or a particular variant, where the additional information is simply ignored. According on how the additional information is used, we have identified three submodes.

    • Additional information is only used to define UB types (NUAI-T). The additional information is only used to define new types of UBs, but it will not appear in the UB report. This mode is really convenient in such cases where the additional information is too complex or too big, so it will not give a significative feedback to the user. However, when a UB is found it is interesting to consider it to build a specific type of UB, e.g. diff_value_same_stack_trace can characterize those UBs where a difference in the POI values has been found but their stack traces were the same. The trace-element comparison function is the one that should be defined to use this mode. For example, Listing 1 shows a trace-element comparison function which distinguishes between those unexpected values where the stack trace is the same and those where the stack trace is different888Function ai is defined as ai({_,_,AI}) -> AI..

      1fun(VEF, TOE, TNE) ->
      2    case VEF(TOE) == VEF(TNE) of
      3        true -> true;
      4        false -> 
      5            case dict:fetch(st, ai(TOE)) == dict:fetch(st, ai(TNE)) of  
      6                true -> diff_value_same_stack_trace; 
      7                false -> diff_value_diff_stack_trace 
      8            end
      9    end
      10end
      Listing 1: Trace-element comparison function which returns different UB types.

      Categorizing different types of UBs has several benefits in the POI testing approach. First of all, these types can be considered in the ITC generation as a criteria to decide whether an ITC should be mutated or not. In the example above, if we do not distinguish between diff_value_same_stack_trace and diff_value_diff_stack_trace, once a diff_value type has been mutated it can have less chances of being selected to be mutated again. However, with the distinction, each one is treated separately, so they are mutated as separated entities. Additionally, if the final report is enriched with several UB types, users have more feedback that can help while finding the source of the UB.

    • Additional information is only used in the UB report (NUAI-R). In case we consider that the additional information is not representative enough to categorize new type of UBs, we can use its data only in the reports. This is the less intrusive way of using the additional information, but still a useful way to obtain richer feedback in the final report of each UB. We should add to the UB report mapping a new function associated to each UB type that could benefit from the stored additional information. For example, we can store in the UB report mapping a function fun({P1, V1, AI1}, {P2, V2, AI2}, _) -> "Value for P1 (V1) and for P2 (V2) differ.n Their stacks were:n dict:fetch(st, AI1)n dict:fetch(st, AI2)" end associated with key diff_value. Therefore, each time there is a difference in the compared values, we also get a feedback on their stack traces although internally it will be treated as a simple diff_value UB type.

    • Additional information is used to categorize and report UBs (NUAI-TR). This submode takes the advantages of both previous submodes. It also involves specific trace-element comparison function and additions in the UB report mapping. As this submode is a conjunction of both previous submodes, it is not further discussed in the particular enhancements presented in Sections 4 and 5.

  • Additional information is used during comparison (UAI). This mode is the one that gives a major relevance to the additional information. By using this mode, the value and the additional information is compared as a whole. This means that, for instance, even if the compared values are the same, when any pair of elements of the additional information differs, the ITC is reported to be generating an UB. This mode is very convenient to early uncover some UBs. It can also be used for performance checking, e.g. the values of the TEs are equal but a performance indicator included in the additional information is revealing some downgrade. This mode uses a value-extractor function and a trace-element comparison function which takes into account all or some parts of the additional information. The amount of information that is finally used to build the UB reports is left to user’s choice.

  • The additional information is not attached to the TE, instead considered as an independent trace element (AIT). Finally, in this completely different mode, the additional information is considered as a separated entity and constitutes a single TE as the ones that are generated for the POIs. This mode can be similar to the original POI testing approach, however it still needs special instrumentation (to send the new TEs), tracing (to receive and store the new TEs) and maybe some special comparison functions (to take into account their particularities). This mode is very convenient in such cases where the additional information can be directly used to uncover an UB, avoiding in this way the comparison of several subcomputations. For instance, if a user places a POI in a call, and the call parameters are compared before comparing the call result, all intermediate TEs are not compared. This mode can reuse the program instrumentation of modes UAI and NUAI in most of the cases. However, the tracer should be redefined in order to build the new traces accordingly. Finally, this mode can be combined in such a way that other additional information is attached to these special TEs forming a hybrid mode which can help the user in some specific scenarios.

4 Enhancement by using improved call tracing

In this section we explain both, how we have improved the call tracing and how we can incorporate the enhanced call traces to the POI testing approach.

4.1 Motivation

As it is usual in testing and specially in debugging, when an UB is found we still have to find its source to fix it. Unfortunately, the POI testing approach is not an exception. For example, consider a call that is used as POI and the values computed by different version of a program are different. We know that the UB is due to the call, however what we do not know is whether the problem is in the arguments of the call or inside the function called. With the previous versions of POI testing several iterations 999An iteration includes creating and moving POIs and comparing the new traces which need to be recomputed. would be needed in order to find an answer to this question. With the enhancement that we propose here, we can save some time to users by avoiding these intermediate steps. In the new approach, the POIs that are calls will be treated in a special way. This special treatment allows us to directly know where is the source of the UB, i.e. in the argument or in the called function. This is just one of the benefits of using an enhanced tracing for calls, but there are more explained in the rest of the section.

4.2 Improvements in the call tracing

When we place a POI in a call, we are saying that we are interested in comparing the result of this call, so the standard behaviour of a POI tester is to trace only these values. In this work, we want to create an enhanced trace where not only the result of the call, but also its arguments are traced. Therefore, this enhancement adds to the additional information mapping a new element whose key is ca and whose value is a list that contains the call arguments.

In order to obtain the improved call traces, we have to define a way for sending, receiving and merging the call traces. The main idea is to send the arguments traces before actually performing the call and its result just after. Thus, we should define how the code instrumentation is extended to create this enhanced TEs, and also we should define how the tracer deals with them.

(a) Instrumentation rule for call tracing
1tracer({Stack, Trace}) -> 2    receive 3        {add_i, POI, Ref, V} -> 4            tracer({[{Ref, V} | Stack], Trace}); 5        {add, POI, Ref, V} -> 6            {CalleeArgs, NStack} = 7                remove_same_ref(Ref, Stack), 8            tracer({NStack, 9                [{POI, V, store(ca, CalleeArgs)} 10                 | Trace]}); 11        {add, POI, V} ->  12            tracer({Stack, [{POI, V} | Trace]})  13    end.
(b) Simplified tracing server
Figure 2: Example of the elements needed to obtain an enhanced call tracer in Erlang

The sending process is done thanks to a program instrumentation that enables this double tracing for the call in two steps, i.e. arguments before performing the call and the result just after the call. We show in Figure 1(a) how this instrumentation can be done in our running language, i.e. Erlang. When the code instrumentation process finds a call, i.e. , the expression is the replaced by the block expression (begin-end) on the right-hand side. The first expression of this block creates a unique reference (with function make_ref/0) which serves to identify all the traces belonging to the same concrete execution of a call. The result is stored in a free variable, i.e. 101010All free variables used in the rule are represented as . Each one of these free variables is unique in the instrumented module and different to all the original variables of the module.. The second expression evaluates the callee an its arguments111111It could happen that some of these expressions are already POIs or that some of these expressions are calls, e.g. f(2, g(1)). The POI testing approach is ready to handle all these scenarios [10].

and store its result separately making use of pattern matching facilities, i.e.

. Then, the next two expressions are sending to the tracer the value of the callee, and each one of its argument. The third expression in the block is performing the actual call by using the value of the callee, i.e. , and the values for the arguments, i.e. . Then, the result is stored in the fresh variable fv. The fourth expression in the block is sending to the tracer the result of the evaluation, i.e. fv, the POI identifier, i.e. POI, and also the reference that uniquely identifies the call, i.e. . Note that this reference is the same one that it has been used inside the list comprehension when the values of the callee and the arguments have been sent. However, the atom at the first element of the tuple sent to the tracer is different in each case, i.e. one uses add_i and the other uses add. The reason for this difference is explained latter. Finally, the actual value of the call, i.e. fv, is placed as the last expression to make the whole block evaluate to the expected result.

All the information sent while running the instrumented code is received and merged by the tracer. In Erlang, the tracer is a server which is continuously receiving TEs until the end of the execution or until a timeout is raised. Figure 1(b) shows a simplification of the Erlang function tracer/1 which is in charge of this tracing process. The server’s state is a tuple containing: 1) a stack, where the callee and arguments are stored in the order they are received, and 2) the trace generated so far. Its body is a receive expression with three clauses: the first one is for the information sent by function calls’ callees and arguments, the second one is for the result of the function call, and the third one is for the rest of TEs, i.e. those that do not come from a function call. When a callee or an argument value is received, it is simply stacked. When the call result is received, all its arguments, which are at the top of the stack, are unstacked by using function remove_same_ref/2, and stored121212Function store/2 is defined as store(Key, Value) -> dict:store(Key, Value, dict:new()). in the additional information of the call’s TE. Finally, the rest of TEs are simply added to the current trace with an empty additional information mapping.

4.3 Using the enhanced call tracing to compare traces

Once we have these new traces for the function call, we have to define how we can use them to determine whether the behaviour across different program versions is the expected. In Section 3.2, the most useful comparison modes are introduced. Here, we describe the particular requirements needed to implement this enhancement.

  • NUAI mode: The enhanced call trace is compared by only using the call result, i.e. as the non-enhanced POI testing approach does. However, when an UB is found, the callee and the arguments can be used in three ways.

    • NUAI-T mode: In this mode, users can create new UB categories related to the call traces. The trace-element comparison function in Lisinting 1 can be used in this enhancement by doing some small changes. First of all, only the POIs which are calls have the call-arguments additional information. Therefore, when the UB is found (line 1), we should first check that this additional information is available. Then, the access to the mappings in line 1 should be change to dict:fetch(ca, ai(TOE | TNE)), and the UB types returned in lines 1 and 1 should be changed to something like diff_value_same_call_args and diff_value_diff_call_args resdpectively.

    • NUAI-R mode: This mode allows users to save the intermediate step mentioned in Section 4.1. For instance, instead of just reporting UBs like "The values of the last trace elements differ", it can now produce reports like "The values of the last trace elements differ and their calls are the same" and "The values of the last trace elements differ and their calls are different".

  • UAI mode: The enhanced call trace is compared using the call result as well as the callee and the arguments. This means that even when the call results behave as expected, if something in the call arguments is unexpected, an UB is reported.

  • AIT mode: Once the callee and the arguments values differ, they are directly reported as an UB, without having to compare the rest of the TEs generated between them and the call result. For instance, this can save time and resources when analyzing recursive functions. It is even useful when it has been performed some function/module renaming or some change in the parameter order. For example, by using the value-extractor function fun({POI, {callee_args, [_|Args]}, _}) -> {POI, Args} end, we are ignoring the callee, so just the arguments are used during comparison. Note that here we are assuming the existence of callee_args traces, while they are not introduced in Section 4.2. In order to obtain them we should modify Figures 1(a) and 1(b). On Figure 1(a), the call result is sent without including the unique reference, i.e. it is replaced by . On the other hand, Figure 1(b) needs more modifications. The alternative trace server is depicted in Figure 3. The server’s state is the same but the stack is used here to temporally store all the elements of the call until they are all sent. The first receive clause (lines 3-3) treats all the information coming from callees and arguments of calls. It distinguishes 2 cases: the top of the stack is an element with the same reference as the received TE, and the rest of cases. Thus, the first one is in charge of storing in the current trace an already-complete call trace, while the second clause is storing a part of a call trace in the stack. The same idea is followed by the other clause (lines 3-3).

 

1tracer({Stack, Trace}) ->
2  receive
3    {add_i, POI, Ref, V} ->
4      case Stack of
5        [{PrevRef, _} | _] when PrevRef /= Ref ->
6          {CAs, [PrevPOI]} =
7            remove_same_ref(PrevRef, Stack),
8          tracer(
9            {[{Ref, V}, POI],
10             [{PrevPOI, {callee_args, CAs}}
11              | Trace]});
12        _ ->
13          tracer(
14            {[{Ref, POI, V} | Stack],
15             Trace})
16      end;
17    {add, POI, V} ->
18      case Stack of
19        [{Ref, _} | _] ->
20          {CAs, [PrevPOI]} =
21            remove_same_ref(Ref, Stack),
22            tracer(
23              {[],
24               [{POI, V},
25                {PrevPOI, {callee_args, CAs}}
26                | Trace]});
27        [] ->
28          tracer({[], [{POI, V} | Trace]})
29      end
30  end.

 

Figure 3: Alternative server that enables independent callee and arguments comparison

5 Enhancement by using stack traces

In this section we explain how the POI testing approach is improved by the insertion of stack traces in the POI traces.

5.1 Motivation

Stack traces are common elements in error reports due to the valuable information they provide to help find the source of a failure. In our context, a failure is not a common bug but an UB. Nevertheless, stack traces can be also really handy in this context. Suppose we are performing POI testing on two programs using a single POI inside a common function that is called from different parts of the program. When the POI tester is run, we can get some reports informing of some UB, for instance, that the POI values are different in a particular execution point. Then, users should start placing POIs in previous stages of the evaluation in order to find the source of the discrepancy. However, it is not clear how to proceed in this debugging process, as there is not enough information about the discrepancy’s source. This discrepancy could happen for several reasons. As in Section 4, one of this reason can be that the calls are not executed with the same arguments. However, if we are using as POI an expression which is not a call, e.g. a case expression, we cannot benefit from the enhanced call trace information. Therefore, we need to explore alternative ways to provide users with some evaluation context when an UB is found. By providing UB reports with stack traces, users can check whether both versions have perform the same calls or not, i.e. they have followed parallel paths. Even when the top of the stack trace is a call with the same arguments, the produced value can differ simply because some of the elements of the rest of the stack trace differ. The discrepancy in the followed paths can be, e.g. due to impure features of the language, like, for instance, the process dictionary in Erlang. Thus, it is not a simple task to identify these discrepancies. With the enhancement proposed here, users can use the stack trace to go directly to the function that start creating the bifurcation on the paths and start a debugging process there. By using special comparison functions, this enhancement can be useful even when some renaming or refactoring process has been performed.

5.2 Adding stack traces to the POI traces

In a similar way as in Section 4.2, we need to modify the standard POI testing approach in order to add stack traces to the POI testing approach. This again involves modifying sending and reception of the TEs. However, in this case, the modifications needed are much simpler. First, the rules used by the instrumentation process need to augment each message send to the tracer with the stack trace, e.g. in Figure 1(a). In Erlang, this is done by using erlang:get_stacktrace/0131313Calls to this function are only allowed in the handler of a try-catch expression. We do not considered this particularity for the sake of simplicity of the presentation.. On the other hand, the reception should be adapted accordingly to process these new TEs. This involves, modifying the receive’s clauses. In concrete, we can use a single clause like the one in lines 1(b)-1(b) of Figure 1(b), e.g. {add, POI, V, ST} -> tracer([{POI, V, store(st, ST)} | Trace]}).

(a) Calls (b) Function definitions
(c) Stack trace template
Figure 4: Transformation rules to obtain the stack trace
1stack_tracer(Stack) ->
2  receive
3    {begin, Ref, Call} ->
4      stack_tracer([{Ref, Call} | Stack]);
5    {end, Ref} ->
6      case Stack of
7        [{Ref, _} | T] ->
8          stack_tracer(T);
9        [_ | T] ->
10          NStack =
11            unstack_till(T, Ref),
12          stack_tracer(NStack)
13      end
14  end.
Figure 5: Stack tracer

However, the most challenging part here is not modifying the POI testing approach, but getting useful stack traces. Some programming language, like Erlang, perform last call optimization (LCO) which solve important performance issues. However, LCO comes with an important drawback: the stack traces that are reported in errors can be incomplete. This can be very confusing and annoying for users when they use reported stack traces that could be used during the debugging of a buggy code. Nevertheless, there are ways to inhibit LCO. Of course, this should be done very carefully, as its impact in the performance can be disastrous. For instance, a program transformation that changes the code in a way that the last expression of function bodies is never a call. In this way, we can get full stack traces by using the standard methods provided by the language. An alternative is to forget these standard methods and manually build the stack trace during the POI tracing instrumentation. With this approach, the stack trace is dynamically built and each time a POI trace is sent, a snapshot of the current stack (which is part of the server’s state) is stored in the additional information mapping. Finally, there is another alternative which does not include the stack trace during the testing process. Instead, before UBs are reported, their correspondent ITCs are rerun sending the stack trace for only a specific execution of a POI, e.g. the fourth time it is executed.

Both alternatives, LCO inhibition and manual stack trace building, can be useful tools to get the full stack trace, especially in those cases where the expected size of the stack traces are not huge. In the case of Erlang, by inhibiting LCO we get the standard stack trace provided by Erlang, which does not include all the call arguments, but just the callee. We can improve this in the manual version. Figure 4 shows two transformation rules that can be used to manually obtain stack traces. This transformation should be done after the POI instrumentation in order to be correct. Both rules use a template (represented by ) to create stack traces. The idea behind this template is to send a begin trace just before starting the call and an end trace just after. This can be done in the calls (Figure 3(a)) or/and in the function definitions (Figure 3(b)141414The rule shows how a clause of a function definition is transformed. A whole function definition is transformed by applying this rule for each of its clauses. ). By transforming only the calls we can stack all the calls performed in user-defined code, even calls to external libraries. However, calls to user function performed from a non-user-defined code, e.g. from a lists:map/2

, would not be stacked. By transforming only function definitions this benefit and drawback are reversed. Using both at the same time is probably the best choice. However, this configuration produces duplicated

begin-end traces when a user-defined function is called from user-defined code. This is solved at the tracer side. Figure 5 shows a simplification of a stack tracer whose state is the current stack, i.e. Stack. The most interesting part is how the end traces are processed. Clause of lines 5-5 represents the case where the top of the stack coincides with the end trace, i.e. a successfully finished call. On the other hand, clause of lines 5-5, represents calls where some error has been raised during their evaluation. Function unstack_till/2 unstack elements of the stack until the expected reference, i.e. Ref, is found. Errors raised during a call evaluation are the reason why the call is put inside a try-catch expression in the transformation rule show in Figure 3(a). The handler of this try-catch expression sends an end trace to inform the tracer that some error has occurred.

5.3 Using stack traces during POI traces comparison

The inclusion of the stack trace in the comparison process unveils new challenges to the comparison modes identified in Section 3.2.

  • NUAI mode: Stack traces are only used when a discrepancy is found. This is the only mode that does not need to include the stack traces in the stacks, i.e. they can be calculated afterwards.

    • NUAI-T mode: A new types of UB types can be generated related to stack traces. This involves the study of the stack trace. The most common way to identyfy UB types is to indicate whether the stack traces are equal till certain point or not (see Listing 1). However, there are other UB types like that the stack traces are equal except some elements, e.g. when alternative or renamed functions are used during the calculation.

    • NUAI-R mode: Using this mode, users can know whether the stack traces are equal or not for each UB detected. When they are not equal, some additional information about the discrepancy in the stack traces can be really helpful. However, we cannot always show the entire stack traces, as they are usually impractical. In order to make this information more user-friendly, a redesign of this output is needed, e.g. showing only the function calls that created a bifurcation in the stack traces.

  • UAI mode: This mode requires to have the stack trace information during the comparison process, i.e. it cannot be calculated afterwards. Stack trace discrepancies are usually an important reason for an UB. Therefore, stopping comparison process as soon as one of this discrepancies is found is a good strategy to find and fix the source of their discrepancy.

  • AIT mode: This mode requieres a special instrumentation, like the one which manually builds stack traces in Section 5.2. This mode can be used even without defining any POI, since the idea is to have a TE each time we call/enter to a function, independently of where the POIs are placed. Therefore, this is a really interesting mode that only requires from users to define some input functions. This information is enough to check that the stack traces are equivalent in the provided tests. Of course, it could be also mixed with POI checking, merging, in this way, different checks in a one-step comparison process.

6 Related work

The orchestrated survey of methodologies for automated software test case generation [1] identifies five techniques to automatically generate test cases. POI testing approach could be included in the class of adaptive random technique as a variant of random testing. Inside this class, the authors identify five approaches. POI testing’s mutation approach of the test input shares some similarities with various of these approaches like selection of best candidate as next test case or exclusion. According to a survey on test amplification [5]

, which identifies four categories that classify all the work done in the field, our work could be included in the category named

amplification by synthesizing (new tests with respect to changes). Inside this category, our technique falls on the "other approaches" subcategory.

The value spectra presented in [20] is a program spectra [14] that shares several similarities with our call trace enhancement. In particular, value trace spectra record the sequence of the user-function executions traversed as a program executes. After the spectra recording, spectra comparison techniques are used to find value spectra differences that expose internal behavioral deviations inside the black box. However, the spectrum is generated for all the user-defined functions while in our approach users are who decide which functions should be compared. Additionally, POI testing approach allows a more flexible use of these call traces. Finally, the motivation and also some techniques of the enhanced call traces are similar to the ones of algorithmic debugging [16]. In fact, this approach has been successfully applied in Erlang [3].

The benefits of using stack traces in error reports are well-known. For instance, [15] examined stack traces in bug reports and found that bugs are fixed faster when their reports contain at least one stack trace. Comparison of stack traces is also common in the literature. One example is [2] where the authors propose a technique that can identify similar bugs based on a comparison of stack traces.

Most of the efforts in regression testing research have been put in the regression testing minimization, selection, and prioritization [21], although among practitioners it does not seem to be the most important issue [6]. In fact, in the particular case of the Erlang language, most of the works in the area are focused on this specific task [17, 19]. We can find other works in Erlang that share similar goals but more focused on checking whether applying a refactoring rule will yield to a semantics-preserving new code [13].

With respect to tracing, there are multiple approximations similar to the POI testing’s. In Erlang’s standard libraries, there are implemented two tracing modules. Both are able to trace the function calls and the process related events (spawn, send, receive, etc.). One of these modules is oriented to trace the processes of a single Erlang node [7], allowing for the definition of filters to function calls, e.g., with names of the function to be traced. The second module is oriented to distributed system tracing [8] and the output trace of all the nodes can be formatted in many different ways. Cronqvist [4] presented a tool named redbug where a call stack trace is added to the function call tracing, making possible to trace both the result and the call stack. Till [18] implemented erlyberly, a debugging tool with a Java GUI able to trace the previously defined features (calls, messages, etc.) but also giving the possibility to add breakpoints and trace other features such as exceptions thrown or incomplete calls. All these tools are accurate to trace specific features of the program, but none of them is able to trace the value of an arbitrary point of it. In our approach, we can trace both the already defined features and also a point of the program regardless of its position.

7 Conclusions

We have presented a common framework to enhance POI testing with the addition of new information. This new information enriches the approach, allowing users to get better UB reports and to define new UB types. These new UB types benefit some of the internal processes of the approach, e.g. the ITC generation. These additions needs new ways to send and store this additional information and also new comparison modes. In this paper, two enhancements have been proposed by adding stack traces and augmenting call traces. Both enhancements have their particularities, but both share the same common framework presented in Section 3.

This work opens a via to extensions of the POI testing approach. Following the same common framework described in this paper, we can easily include different additional information. This additional data can be other functional data, i.e. similar data to richer call traces or stack traces. One interesting enhancement is to store a snapshot of the current environment for each POI, so more contextual information is available to find the UB source. We could also store the followed conditional paths, so it could be used to improve the coverage during the ITC generation. At the same time, we are studying the use of some special additional information that enables the mocking of values. The idea is to rerun an ITC that leads to an UB, but when the value that uncovers the UB is found, replace it by the value computed by a correct version of the program. This will allow the technique to find further errors using the same ITC. The same idea can be applied when an internal call is a previously run ITC in order to avoid its recomputation. Finally, we plan to define extensions of the approach that studies non-functional data, e.g. CPU or memory usage. After some preliminary work, we have concluded that the common framework presented in this paper represents a very natural way to operate with such kind of data.

References

  • [1] S. Anand, E. K. Burke, T. Y. Chen, J. A. Clark, M. B. Cohen, W. Grieskamp, M. Harman, M. J. Harrold, and P. McMinn. An orchestrated survey of methodologies for automated software test case generation. Journal of Systems and Software, 86(8):1978–2001, 2013.
  • [2] M. Brodie, S. Ma, L. Rachevsky, and J. Champlin. Automated problem determination using call-stack matching. Journal of Network and Systems Management, 13(2):219–237, 2005.
  • [3] R. Caballero, E. Martin-Martin, A. Riesco, and S. Tamarit. EDD: A declarative debugger for sequential Erlang programs. In E. Ábrahám and K. Havelund, editors, 20th International Conference Tools and Algorithms for the Construction and Analysis of Systems (TACAS 2014), volume 8413 of Lecture Notes in Computer Science (LNCS), pages 581–586. Springer, April 2014.
  • [4] M. Cronqvist. redbug. Available at: https://github.com/massemanet/redbug, 2017.
  • [5] B. Danglot, O. Vera-Perez, Z. Yu, M. Monperrus, and B. Baudry. The Emerging Field of Test Amplification: A Survey. CoRR, abs/1705.10692, 2017.
  • [6] E. Engström and P. Runeson. A Qualitative Survey of Regression Testing Practices. In M. A. Babar, M. Vierimaa, and M. Oivo, editors, Product-Focused Software Process Improvement, 11th International Conference, PROFES 2010, Limerick, Ireland, June 21-23, 2010. Proceedings, volume 6156 of Lecture Notes in Business Information Processing, pages 3–16. Springer, 2010.
  • [7] Ericsson AB. dbg. Available at: http://erlang.org/doc/man/dbg.html, 2017.
  • [8] Ericsson AB. Trace tool builder. Available at: http://erlang.org/doc/apps/observer/ttb_ug.html, 2017.
  • [9] D. Insa, S. Pérez, J. Silva, and S. Tamarit. Erlang code evolution control. Pre-proceedings of the 27th International Symposium on Logic-Based Program Synthesis and Transformation (LOPSTR 2017), abs/1709.05291, 2017.
  • [10] D. Insa, S. Pérez, J. Silva, and S. Tamarit. Behaviour Preservation across Code Versions in Erlang. Scientific Programming, 2018, 2018.
  • [11] D. Insa, S. Pérez, J. Silva, and S. Tamarit. Erlang code evolution control. Logic-Based Program Synthesis and Transformation (LOPSTR 2017) Lecture Notes in Computer Science (To appear), 2018.
  • [12] D. Insa, S. Pérez, J. Silva, and S. Tamarit. Erlang code evolution control (use cases). CoRR, abs/1802.03998, 2018.
  • [13] E. Jumpertz. Using QuickCheck and semantic analysis to verify correctness of Erlang refactoring transformations; Master’s thesis, Radboud University Nijmegen, 2010.
  • [14] T. W. Reps, T. Ball, M. Das, and J. R. Larus. The use of program profiling for software maintenance with applications to the year 2000 problem. In M. Jazayeri and H. Schauer, editors, Software Engineering - ESEC/FSE ’97, 6th European Software Engineering Conference Held Jointly with the 5th ACM SIGSOFT Symposium on Foundations of Software Engineering, Zurich, Switzerland, September 22-25, 1997, Proceedings, volume 1301 of Lecture Notes in Computer Science, pages 432–449. Springer, 1997.
  • [15] A. Schröter, N. Bettenburg, and R. Premraj. Do stack traces help developers fix bugs? In J. Whitehead and T. Zimmermann, editors, Proceedings of the 7th International Working Conference on Mining Software Repositories, MSR 2010 (Co-located with ICSE), Cape Town, South Africa, May 2-3, 2010, Proceedings, pages 118–121. IEEE Computer Society, 2010.
  • [16] E. Y. Shapiro. Algorithmic program debugging. MIT Press, April 1982.
  • [17] R. Taylor, M. Hall, K. Bogdanov, and J. Derrick. Using behaviour inference to optimise regression test sets. In IFIP International Conference on Testing Software and Systems, pages 184–199. Springer, 2012.
  • [18] A. Till. erlyberly. Available at: https://github.com/andytill/erlyberly, 2017.
  • [19] I. B. M. Tóth and Z. Horváth. Reduction of regression tests for Erlang based on impact analysis. 2013.
  • [20] T. Xie and D. Notkin. Checking inside the black box: Regression testing by comparing value spectra. IEEE Trans. Software Eng., 31(10):869–883, 2005.
  • [21] S. Yoo and M. Harman. Regression testing minimization, selection and prioritization: A survey. Softw. Test. Verif. Reliab., 22(2):67–120, 2012.

Appendix 0.A Use case 1: Align Columns

This use case illustrates how the enhanced call tracing can be used to find out the source of a discrepancy. In concrete, we compare two versions of an Erlang program that aligns columns of a string with multiple lines. The code of both versions is shown in Listing 2. For the sake of ease the presentation, there is only one difference between both code versions. While align_columns_ok.erl version code is implemented with line 2 of Listing 2, align_columns.erl version replace that line of code with line 2. Both program versions are part of the benchmarks used in EDD (Erlang Declarative Debugger) [3].

1-module (align_columns_ok). / -module (align_columns).
2-export([align_left/0, align_right/0, align_center/0]).
3
4align_left()-> align_columns(left). 
5align_right()-> align_columns(right).
6align_center()-> align_columns(centre).
7align_columns(Alignment) -> 
8    Lines =
9         ["Given$a$text$file$of$many$lines$where$fields$within$a$line$",
10          "are$delineated$by$a$single$’dollar’$character,$write$a$program",
11          "that$aligns$each$column$of$fields"],
12    Words = [ string:tokens(Line, "$") || Line <- Lines ],
13    Words_length  = lists:foldl( fun max_length/2, [], Words),
14    [prepare_line(Words_line, Words_length, Alignment) 
15     || Words_line <- Words].
16
17max_length(Words_of_a_line, Acc_maxlength) ->
18    Line_lengths = [length(W) || W <- Words_of_a_line ],
19    Max_nb_of_length = lists:max([length(Acc_maxlength), length(Line_lengths)]),
20    Line_lengths_prepared = adjust_list(Line_lengths, Max_nb_of_length, 0),
21    Acc_maxlength_prepared = adjust_list(Acc_maxlength, Max_nb_of_length, 0),
22    Two_lengths =lists:zip(Line_lengths_prepared, Acc_maxlength_prepared),
23    [ lists:max([A, B]) || {A, B} <- Two_lengths].
24
25adjust_list(L, Desired_length, Elem) ->
26    L++lists:duplicate(Desired_length - length(L), Elem).
27
28prepare_line(Words_line, Words_length, Alignment) -> 
29    All_words = adjust_list(Words_line, length(Words_length), ""),
30    Zipped = lists:zip(All_words, Words_length),
31    [ apply(string, Alignment, [Word, Length + 1, $\s]) %align_columns_ok 
32    [ apply(string, Alignment, [Word, Length - 1, $\s]) %align_columns 
33      || {Word, Length} <- Zipped ].
Listing 2: Align columns program versions.

They can be found at: https://github.com/tamarit/edd/tree/master/examples/align_columns. These programs export only three functions with zero parameters. Thus, they can be considered unit cases. We could use any or all of these functions as starting point for the behaviour comparison process, but we will focus in just one of these three functions for simplicity. The SecEr’s configuration file (Listing 3), defines that the function selected as input function is align_left/0 (Listing 3, line 3).

In order to increase the usability of the tool, a set of functions has been defined to easily define the SecEr configuration file, e.g. function call secer_api:nuai_tr_config/2 in line 3 of Listing 3 is used to define NUAI-TR comparison mode. There are also other functions used to define the VEF (Listing 3, line 3) or the UBRM (Listing 3, line 3).

1-module (test_align).
2-compile(export_all).
3poi1Old() -> {’align_columns_ok.erl’, 2, call}.    poi1New() -> {’align_columns.erl’, 2, call}. 
4poi2Old() -> {’align_columns_ok.erl’, 2, call}.   poi2New() -> {’align_columns.erl’, 2, call}. 
5poi3Old() -> {’align_columns_ok.erl’, 2, call}.   poi3New() -> {’align_columns.erl’, 2, call}.
6
7rel1() -> [{poi1Old(),poi1New()}]. 
8rel2() -> [{poi2Old(),poi2New()}]. 
9rel3() -> [{poi3Old(),poi3New()}].
10funs() -> "[align_left/0]".
11
12config() -> secer_api:nuai_tr_config(mytecf(),ubrm()). 
13mytecf() ->
14  fun(TO,TN) -> VEF = secer_api:vef_value_only(), 
15                case VEF(TO) == VEF(TN) of
16                     true -> true;
17                  false ->
18                    case secer_api:get_te_args(TO) == secer_api:get_te_args(TN) of
19                      true -> different_value_same_args;
20                      false -> different_value_different_args
21                    end
22                end
23  end.
24ubrm() -> [{different_value_same_args,[val,ca]},{different_value_different_args,[val,ca]}]. 
Listing 3: Align columns configuration file.

In order to test the behaviour preservation between both versions with SecEr, it is a common practice to start by selecting as POI the last expression of each input function. Therefore, we select the POIs defined in line 3 which are paired by the relation defined in function rel1() in line 3 of Listing 3. The execution of SecEr shown in Listing 4 reveals an UB found in the execution of align_left/0.151515Note that the command secer is defined by a set of flags: -pois inputs the relation between POIs of both versions, -funs inputs the set of input functions, -to inputs the timeout given to SecEr (in seconds), and -config (unused in this example) inputs the configuration mode given to the tool. When there is no configuration defined, NUAI mode is used instead.

1$ ./secer -pois "test_align:rel1()" -funs "test_align:funs()" -to 5
2
3Function: align_left/0
4----------------------------
5Generated test cases: 1
6Mismatching test cases: 1 (100.0%)
7  Error Types:
8    + different_value => 1 Errors
9        Example call: align_left()
10
11------ Detected Error ------
12Call: align_left()
13Error Type: different_value
14- - - - - - - - - - - - - -
15POI: { ’align_columns_ok.erl’,2,call,1}
16  Trace:
17    [[["Given ","a          ","text ","file   ","of     ","many     ",
18       "lines      ","where ","fields ","within  ","a ","line "],
19      ["are   ","delineated ","by   ","a      ","single "," ’dollar’ ",
20       "character, ","write ","a      ","program ","  ","     "],
21      ["that  ","aligns     ","each ","column ","of     ","fields   ",
22       "           ","      ","       ","        ","  ","     "]]]
23
24POI: { ’align_columns.erl’,2,call,1}
25  Trace:
26    [[["Give","a        ","tex","file ","of   ","many   ","lines    ","wher",
27       "field","within",[],"lin"],
28      ["are ","delineate","by ","a    ","singl"," ’dollar","character","writ",
29       "a    ","progra",[],"   "],
30      ["that","aligns   ","eac","colum","of   ","fields ","         ","    ",
31       "     ","      ",[],"   "]]]
32----------------------------’
Listing 4: SecEr reports UB from list comprehension in line 2 as POI.

The current POI is a static call, therefore the UB source cannot be at its argument. Then, we should look for the UB source inside the function align_columns/1 (line 2, Listing 2). In this step, one of the new features presented in this work, the enhanced call tracing, can be really helpful. Therefore, in order to use this additional information, we use the configuration defined by function config/0 (line 3, Listing 3). This function uses the enhanced call information to classify and report new types of errors. The UB is reported again for the call POI. However, as we can observe in Listing 5, this result has been extended with specific call information. The reported UB is different_value_same_args. Thus, it indicates that both versions performed exactly the same call as it is confirmed in the additional information provided. Therefore, according to this report, the UB source must be inside the function prepare_line/3 (line 2, Listing 2).

1$ ./secer -pois "test_align:rel2()" -funs "test_align:funs()" -to 5 -config "test_align:config()"
2Function: align_left/0
3----------------------------
4Generated test cases: 1
5Mismatching test cases: 1 (100.0%)
6  Error Types:
7    + different_value_same_args => 1 Errors
8        Example call: align_left()
9------ Detected Error ------
10Call: align_left()
11Error Type: different_value_same_args
12- - - - - - - - - - - - - -
13POI: { ’align_columns_ok.erl’,2,call,1}
14  Trace:
15    [["Given ","a          ","text ","file   ","of     ","many     ",
16      "lines      ","where ","fields ","within  ","a ","line "]]
17  Call POI Info:
18    Callee: prepare_line
19    Args: [["Given","a","text","file","of","many","lines","where","fields","within","a","line"],
20           [5,10,4,6,6,8,10,5,6,7,1,4],left]
21POI: { ’align_columns.erl’,2,call,1}
22  Trace:
23    [["Give","a        ","tex","file ","of   ","many   ","lines    ","wher",
24      "field","within",[],"lin"]]
25  Call POI Info:
26    Callee: prepare_line
27    Args: [["Given","a","text","file","of","many","lines","where","fields","within","a","line"],
28           [5,10,4,6,6,8,10,5,6,7,1,4],left]
29----------------------------
Listing 5: SecEr reports UB from call to prepare_line in line 2 as POI.

Then, we define a new POI inside prepare_line implementation. Instead of selecting as POI its last expression, i.e. the list comprehension, which we already know that behaves different, we select the expression inside this list comprehension, i.e. the call to function apply/3 (line 2/2, Listing 2). Being this expression a function call, we can reuse the previous configuration. Listing 6 shows the report provided by SecEr with this configuration. Note that the reported UB is different_value_different_args now. This means that there is a discrepancy in one of the arguments between versions. By looking the UB example provided by SecEr, we can easily find out what argument is the UB source.

1$ ./secer -pois "test_align:rel2()" -funs "test_align:funs()" -to 5 -config "test_align:config2()"
2
3Function: align_left/0
4----------------------------
5Generated test cases: 1
6Mismatching test cases: 1 (100.0%)
7  Error Types:
8    + different_value_different_args => 1 Errors
9        Example call: align_left()
10
11------ Detected Error ------
12Call: align_left()
13Error Type: different_value_different_args
14- - - - - - - - - - - - - -
15POI: { ’align_columns_ok.erl’,2,call,1}
16  Trace:
17    ["Given      "]
18  Call POI Info:
19    Callee: apply
20    Args: [string,left,["Given",11,32]]
21
22POI: { ’align_columns.erl’,2,call,1}
23  Trace:
24    ["Given    "]
25  Call POI Info:
26    Callee: apply
27    Args: [string,left,["Given",9,32]]
28----------------------------
Listing 6: SecEr reports UB from call to apply in lines 2/2 as POI.

Appendix 0.B Use case 2: Mergesort

This use case demonstrates how the stack trace can help us when looking for an UB source. Suppose that we are comparing the behaviour of two different versions of an Erlang program that implement the mergesort algorithm. The code of both versions is shown in Listing 7. For the sake of ease the presentation, there is just one difference between both code versions. While merge_ok.erl version code is implemented with line 7 of Listing 7, merge.erl version replace this line of code with line 7. Both program versions are part of the benchmarks used in EDD (Erlang Declarative Debugger) [3]. They can be found at: https://github.com/tamarit/edd/tree/master/examples/mergesort. Both programs export the function mergesortcomp/1 which has only one parameter, i.e. a list of integers. Therefore, this function becomes our input function as it is defined in line 8 of Listing 8.

1-module (merge_ok). / -module (merge).
2-export([mergesortcomp/1]).
3
4-spec mergesortcomp([integer()]) -> any().
5mergesortcomp(List) ->
6    mergesort(List, fun comp/2).
7
8mergesort([], _Comp) -> [];
9mergesort([X], _Comp) -> [X];
10mergesort(L, Comp) ->
11    Half = length(L) div 2,
12    L1 = take(Half, L),
13    L2 = last(length(L) - Half, L),
14    LOrd1 = mergesort(L1, Comp),
15    LOrd2 = mergesort(L2, Comp),
16    merge(LOrd1, LOrd2, Comp). 
17
18merge([], [], _Comp) -> [];   
19merge([], S2, _Comp) -> S2;
20merge(S1, [], _Comp) -> S1;
21merge([H1 | T1], [H2 | T2], Comp)  ->   
22        case Comp(H1,H2) of   
23            false -> [H2 | merge([H1 | T1], T2, Comp)]; % merge_ok.erl 
24            false -> [H2 | merge(T1 ++ [H1], T2, Comp)]; % merge.erl 
25            true ->  [H1 | merge(T1, [H2 | T2], Comp)]  
26        end.
27
28comp(X,Y) -> X < Y.
29
30take(0,_) -> [];
31take(1,[H|_])-> [H];
32take(_,[])-> [];
33take(N,[H|T])-> [H | take(N-1, T)].
34
35last(N, List) -> lists:reverse(take(N, lists:reverse(List))).
Listing 7: Mergesort program versions.

In this case, we cannot define a custom TECF as we did in the previous use case. Instead, we are just adding stack trace information to the final report of the detected UBs. For this purpose, we take profit of the defined function secer_api:nuai_r_config/1 (line 8, Listing 8), which makes SecEr run in NUAI-R mode.

1-module (test_mergesort).
2poi1Old() -> {’merge_ok.erl’, 7, call, 1}.      poi1New() -> {’merge.erl’,7, call, 1}.
3poi2Old() -> {’merge_ok.erl’, 7, ’case’, 1}.      poi2New() -> {’merge.erl’, 7, ’case’, 1}.
4
5rel1() -> [{poi1Old(),poi1New()}].
6rel2() -> [{poi2Old(),poi2New()}].
7funs() -> "[mergesortcomp/1]". 
8
9config() -> secer_api:nuai_r_config([{different_value,[val,st]}]). 
Listing 8: Mergesort configuration file.

The difference between these two versions in in the recursive clause of the function merge/3. Therefore, it makes sense to select as POI the call to this function in line 7 of the Listing 7. With the command used in Listing 9, SecEr provides the report shown in the same Listing. This report indicates that there are some UBs. Using the UB report, we can notice that the forth evaluation of the POI differs between both versions, while their stack is the same.

1$ ./secer -pois "test_mergesort:rel1()" -funs "test_mergesort:funs()" -to 5
2          -config "test_mergesort:config()"
3
4Function: mergesortcomp/1
5----------------------------
6Generated test cases: 5692
7Mismatching test cases: 3369 (59.18%)
8  Error Types:
9    + different_value => 3369 Errors
10        Example call: mergesortcomp([0,-1,1,2,-3])
11
12------ Detected Error ------
13Call: mergesortcomp([0,-1,1,2,-3])
14Error Type: different_value
15- - - - - - - - - - - - - -
16POI: { ’merge_ok.erl’,7,call,1}
17  Trace:
18    [[-1,0],[-3,2],[-3,1,2],[-3,-1,0,1,2]]
19  Stack
20    {merge_ok,mergesort,2,{line,7}}
21
22POI: { ’merge.erl’,7,call,1}
23  Trace:
24    [[-1,0],[-3,2],[-3,1,2],[-3,0,-1,1,2]]
25  Stack
26    {merge,mergesort,2,{line,7}}
27----------------------------
Listing 9: SecEr reports UB from call to merge in line 7 as POI.

Therefore, in order to know whether the error is in the arguments of the call or in the called function, we run SecEr with a configuration such as the one used in the previous use case. We omit the details of this step here. The UB report indicates that the problem is inside the called function.

Then, the next step is to place a POI inside the function merge/3 (line 7, Listing 7). We choose the clause that contains the recursive calls (line 7, Listing 7) because it is the most visited clause during the evaluation. In concrete, we place the POI in the case expression in line 7 of the Listing 7. When we rerun SecEr, we obtain the report shown in Listing 10. This report provides some interesting information about both POIs. The behaviour discrepancy has been detected in the values computed in the fifth evaluation of the POI. Additionally, both stack traces differ. In the stack trace produced by the old version there are two stacked calls to function merge/2 while in the stack trace of the new one there is only one. This means that the old version is performing an extra recursive call before reaching the base case. Then, the final step is to place a POI in each recursive call observing the values of their arguments as in the previous case. With the report provided by SecEr when using this configuration, users can easily spot the UB source.

1$ ./secer -pois "test_mergesort:rel2()" -funs "test_mergesort:funs()" -to 5
2          -config "test_mergesort:config()"
3
4Function: mergesortcomp/1
5----------------------------
6Generated test cases: 4878
7Mismatching test cases: 2885 (59.14%)
8  Error Types:
9    + different_value => 2885 Errors
10        Example call: mergesortcomp([5,-6,-6,2,3])
11
12------ Detected Error ------
13Call: mergesortcomp([5,-6,-6,2,3])
14Error Type: different_value
15- - - - - - - - - - - - - -
16POI: { ’tests/mergesort/merge_ok.erl’,7, ’case’,1}
17  Trace:
18    [[-6,5],[2,3],[-6,2,3],[3,5],[2,3,5]]
19  Stack
20    {merge_ok,merge,3,{line,7}}
21    {merge_ok,merge,3,{line,7}}
22
23POI: { ’tests/mergesort/merge.erl’,7, ’case’,1}
24  Trace:
25    [[-6,5],[2,3],[-6,2,3],[3,5],[-6,3,5]]
26  Stack
27    {merge,merge,3,{line,7}}
28----------------------------
Listing 10: SecEr reports UB from case expression in line 7 as POI.