SDART
SDART
This paper improves the breadth-¯rst search strategy in directed automated random testing
(DART) to generate a fewer number of test data while gaining higher branch coverage, namely
Static DART or SDART for short. In addition, the paper extends the test data compilation
mechanism in DART, which currently only supports the projects written in C, to generate test
data for C++ projects. The main idea of SDART is when it is less likely to increase code
coverage with the current path selection strategies, the static test data generation will be
applied with the expectation that more branches are covered earlier. Furthermore, in order to
extend the test data compilation of DART for C++ context, the paper suggests a general test
driver technique for C++ which supports various types of parameters including basic types,
arrays, pointers, and derived types. Currently, an experimental tool has been implemented
based on the proposal in order to demonstrate its e±cacy in practice. The results have shown
that SDART achieves higher branch coverage with a fewer number of test data in comparison
with that of DART in practice.
Keywords: Directed automated random testing; concolic testing; test data compilation; test
data generation; control °ow graph; C++; SMT-Solver.
1. Introduction
Unit testing has been considered an important phase to ensure the high quality of
software, especially for the system software written in C++ due to the painstaking
requirements of quality. Two well-known approaches for unit testing are black-box
testing and white-box testing [22]. Black-box testing only focuses on the correctness
of input and output without considerations about its source code. In contrast,
§ Corresponding author.
1279
1280 D.-A. Nguyen et al.
white-box testing tries to inspect the quality of source code by analyzing it. This
approach allows detecting potential errors in software that cannot be found by black-
box testing. However, the cost for evaluating the quality of software is quite ex-
by CHALMERS UNIVERSITY OF TECHNOLOGY on 04/18/20. Re-use and distribution is strictly not permitted, except for Open Access articles.
pensive, especially in large-scale software. The need for automated unit testing is
becoming more and more urgent and unavoidable to reduce the budget of the testing
phase. Up to the present, there are two major directions of automated test data
generation known as static testing and dynamic symbolic execution (DSE) [3]. The
idea of the former is to generate test data automatically by applying source code
analysis techniques. Although this direction seems to be e®ective, it faces several
Int. J. Soft. Eng. Knowl. Eng. 2019.29:1279-1312. Downloaded from www.worldscientific.com
issues in practice. The main reason is that the source code analysis requires a large
amount of e®ort to deal with various syntaxes such as API usage, lambda, etc. The
latter can be divided into two major methods including execution generated testing
(EGT) and concolic testing. EGT aims to detect possible potential bugs in C++
projects by executing it symbolically in up to thousands of separate forks [5, 6, 14]. In
contrast, concolic testing, which is ¯rst implemented in directed automated random
testing (DART) [10], aims to produce a series of test data maximizing a speci¯c code
coverage criterion with the least number of test data.
In DART, given a function under test, initial test data are generated randomly
based on the type of passing variables. These initial test data are then passed into
an instrumented program to execute it on the runtime environment. During this
test data execution, DART collects path constraints, denoted by PC, at each de-
cision until the testing program raises an error or terminates. In the ¯rst case, a bug
is reported and DART starts generating another test data randomly again. Oth-
erwise, DART negates the current path constraints PC in the way that the solution
of the negated path constraints tends to visit the unvisited branches when exe-
cuting it on the testing function. Although DART demonstrates its e®ectiveness in
practice, this method still remains an issue related to the number of test data.
In addition, DART currently provides a fast test data compilation mechanism
to reduce the computational cost of test data generation, but only supports
C projects.
Regarding the number of test data in concolic testing, it should be minimized to
facilitate the testing management process with the smaller number of iterations. In
order to achieve this objective, DART tries to lower the number of iterations as
many as possible. Generally, the process of the next test data generation in DART
includes four main steps: (i) generating path constraints from the current test path,
(ii) negating these path constraints PC to get :PC, (iii) generating the next test data
by solving :PC, and (iv) executing the next test data to get the next test path. The
solution to reduce the number of iterations depends mainly on step ii, where a
constraint in PC is negated to generate the next test data so as to go through
unvisited branches. However, in fact, the negated path constraints :PC could
not ensure completely that its solution will pass through unvisited branches due
to several reasons. One main reason is that there might exist many candidate
constraints to negate based on the selected path selection strategy [9–11, 13]
Improvements of DART in Test Data Generation 1281
(e.g. breadth-¯rst search (BFS), depth-¯rst search (DFS), etc.) and how to choose
the best one is still a challenging problem. In the worst case, it might take a large
number of iterations to achieve high code coverage due to the selected strategies.
by CHALMERS UNIVERSITY OF TECHNOLOGY on 04/18/20. Re-use and distribution is strictly not permitted, except for Open Access articles.
not go through the new visited branches as expected. As a result, the computational
cost of test data generation might increase signi¯cantly.
Currently, the test data compilation suggested in DART only applies to basic
types, pointer, struct, and array in C projects. Therefore, when generating test data
for C++ projects, this compilation mechanism needs to be improved to reduce the
computational cost of test data generation. According to DART, when new test
data are discovered, then they are executed on runtime environment to collect
visited instructions by a general test driver. The main idea of the test data com-
pilation in DART is to store the generated test data in a unique external ¯le, and
then a test driver analyzes this ¯le to load the values of test data when executing.
One big advantage of this strategy is that the general test driver only needs to
compile once to create an executable ¯le. Because of one-time compilation, the total
computational cost of test data compilation, in particular, and test data generation
in general are reduced signi¯cantly, especially in the projects having a large number
of test data.
Therefore, this paper proposes two techniques to deal with the mentioned lim-
itations. First, in order to reduce the number of test data, the paper improves the
BFS strategy proposed in DART by combining this strategy with a static test data
generation strategy, namely Static DART, or SDART for short. Speci¯cally, when it
is less likely to increase code coverage with the current path selection strategy, the
static test data generation strategy will be selected instead. In this static analysis
strategy, SDART will generate a list of partial test paths which go through unvisited
branches, then try to generate test data traversing these partial test paths. In other
words, by combining the existing path selection strategies with the static test data
generation strategy, SDART expects that more newly visited branches will be
detected earlier than keeping the current strategies. Second, the paper extends the
idea of the test data compilation for C projects to deal with C++ projects by using a
C++ general test driver. In essence, the idea of the general C++ test driver is similar
to the test driver used in the test data compilation proposed in DART. However, the
C++ general test driver is presented in a more general representation by using
templates. By using templates, the C++ general test driver is more °exible and
expandable to support various data types.
The structure of the paper is organized as follows. Several outstanding
related works are discussed in detail in Sec. 2 to provide the overview of the test
1282 D.-A. Nguyen et al.
data generation. Section 3 presents the background of DART. After that, Sec. 4
provides the overview of the proposed method and the description of source code
preprocessing phase. Next, the details of the second phase called test data generation
by CHALMERS UNIVERSITY OF TECHNOLOGY on 04/18/20. Re-use and distribution is strictly not permitted, except for Open Access articles.
2. Related Works
Int. J. Soft. Eng. Knowl. Eng. 2019.29:1279-1312. Downloaded from www.worldscientific.com
Many works have been proposed for enhancing test data generation phase by several
research groups. Focus on the most outstanding works only, there are seven e®ective
improvements of test data generation for C/C++ projects including test data
compilation [10, 13], compositional testing [21], symbolic execution [8, 11, 17–19],
constraints optimization [6, 8, 11, 14, 15], parallel SMT-Solvers [16], path selection
strategies [8–11, 13], and initial test data generation [23].
Godefroid et al. extended DART for the purpose of compositional testing by in-
troducing an algorithm, namely SMART (Systematic Modular Automated Random
Testing) [21]. SMART tests functions in isolation, then encoding test results as
function summaries expressed using input preconditions and output post-conditions.
After that, these summaries are used for testing higher-level functions. Currently,
our method does not focus on testing compositional functions.
The computational cost of test data generation can be reduced signi¯cantly in the
test data compilation step. Both DART [10] and CREST [13] applied the same
technique to accelerate the test data compilation step. CREST is known as an open-
source prototype test generation tool for C. The main idea is that all of the generated
test data are stored in an external ¯le, and a unique test driver reads this ¯le to
collect the values during execution. It means that the compilation process of general
test driver takes place once for all of the produced test data. However, CREST
proposal is only applied for basic types rather than for derived types which are
used widely on C++ projects. In addition, the method proposed in DART limits on
C projects. Therefore, our proposed method is developed based on the original idea
of CREST and DART to deal with not only basic types but also derived types
(i.e. class, struct) on C++ projects.
Because the constraints generated from a test path may be complicated and
lengthy, SMT-Solvers may take a long time to solve these constraints. To reduce the
cost of solving these constraints, these constraints will be optimized before passing
into SMT-Solvers. There are three main types of constraints optimization. The ¯rst
optimization named incremental solving technique is used in CUTE [11], EXE [14],
KLEE [6], and CAUT [8]. The main idea is that only the constraints related to
the last negated condition are solved rather than all of the original constraints.
The second optimization, which is called cache-based unsatis¯ability check, is
implemented in EXE [14] and KLEE [6]. In this optimization, all of the previous
constraints are cached for the next solving constraints. A subset of constraints
Improvements of DART in Test Data Generation 1283
having no solution means that the current constraints are unsatis¯able. Otherwise,
the current constraints may have a solution if there exists a subset of constraints
having a solution. Third, the constraint optimization in CUTE [11] and CAUT
by CHALMERS UNIVERSITY OF TECHNOLOGY on 04/18/20. Re-use and distribution is strictly not permitted, except for Open Access articles.
[8, 12] tries to check whether the last condition is a negation of any previous
constraints. If it is, SMT-solvers do not solve these constraints because it is always
impossible. Another constraint optimization removes evident subconstraints from
the original constraints so as to reduce the complexity of these constraints [6, 11].
Recently, in [15], Cadar et al. proposed a new constraint optimization for array case.
Their paper introduced three transformations called index-based transformation,
Int. J. Soft. Eng. Knowl. Eng. 2019.29:1279-1312. Downloaded from www.worldscientific.com
where var presents a variable; symbolic value is the symbolic value of the variable
var. The symbolic value of var is a concrete value (e.g. a constant, a string) or an
expression. When var is updated, its symbolic value to a new value named
new symbolic value, S is updated by using operator [ as follows:
S ¼ S [ fðvar; new symbolic valueÞg:
update the state of memory model M. In essence, memory model M takes responsi-
bility for storing the current address of variables. A variable is a parameter or a local
variable. After executing a statement or a condition changing the address of vari-
ables, the state of memory model M is updated.
De¯nition 4 (Memory model). Given a program execution path of a function
fn, its memory model M is a mapping from memory addresses to variables, and
de¯ned as follows:
M ¼ fðaddr; varÞ þ g;
where addr is the address of a variable; var is the name of the variable having the
address addr in memory model M. When var is updated to a new address named
new addr, M is updated by using operator [ as follows:
M ¼ M [ fðnew addr; varÞg:
In order to distinguish the di®erence between symbolic map S and memory model
M, let see via the following example. Assume that DART visits the statement
z ¼ x þ y þ 2, where both x, z are local variables; and y is a parameter. The value of
x and z is set to 1 and 0 previously, respectively. Before analyzing the assignment of
z, the state of memory model M and symbolic map S are as follows:
where addrðxÞ, addrðyÞ, addrðzÞ, addrðpÞ are the address of the variable x, y, z, and p,
respectively; Y is the initial symbolic value of the parameter y; p is a one-level pointer
of size 2.
After analyzing this statement, the state of memory model M and symbolic map S
are updated as follows:
fn, its corresponding path constraints are de¯ned as a logic expression, de¯ned as
follows:
PC ¼ pc0 ^ pc2 ^ ^ pcn1 ;
where n is the number of conditions on TP ; pci is a constraint (0 <¼ i <¼ n 1); pc0
and pcn1 are the path constraints corresponding to the ¯rst condition and the last
condition in TP , respectively.
After glancing at the fundamental de¯nitions used in DART, we move to the idea
of DART presented in Algorithm 1. Given a function named fn, DART aims to
generate a series of test data satisfying branch coverage, e.g. all branches in the
function fn are visited. However, the test data generation will perform on the
instrumented function of fn 0 other than the original function fn. The main reason is
that fn 0 has the same behavior as fn. The only di®erence is that fn 0 is added marked
statements to record the visited statements and visited branches when executing a
test data. The process of function instrumentation will be discussed in detail in
Sec. 4.2. The parameter DEPTH is used to specify the number of times the top-level
function is to be called iteratively in a single run. For each value of depth, the initial
test data is generated at random.
Algorithm 2 illustrates how DART can generate the next input vector from the
current input vector. The input includes the instrumented function fn 0 and the
current input vector Ii . The objective of each iteration presented in this algorithm
by CHALMERS UNIVERSITY OF TECHNOLOGY on 04/18/20. Re-use and distribution is strictly not permitted, except for Open Access articles.
is to ¯nd the next input vector Iiþ1 , or a bug. Initially, memory model M is
initialized from the input vector Ii (line 1). Along with step, symbolic map S is also
initialized to store the symbolic value of parameters (line 2). The current state-
ment is detected by using the function statement at(counter, M), where counter is
the index of this statement in the instrumented function fn 0 (line 4). The value of
counter is changed when DART moves to a new statement. At this step, the
Int. J. Soft. Eng. Knowl. Eng. 2019.29:1279-1312. Downloaded from www.worldscientific.com
statement s is executed to get its state of execution (i.e. abort, or halt). In the
case there is no error, based on the type of statement s, there are two cases as
follows.
In the ¯rst case, if the statement s is an assignment, the value of variable m will be
updated in memory model M. In addition, the symbolic value of variable m is
updated in symbolic map S; and the value of variable counter increases by one
by CHALMERS UNIVERSITY OF TECHNOLOGY on 04/18/20. Re-use and distribution is strictly not permitted, except for Open Access articles.
(lines 6–9).
In the second case, if the statement s is corresponding to a condition C, DART
will evaluate the value of C by replacing the variables used in C with its corre-
sponding values (line 11). Simultaneously, a new constraint pc is created through the
process of evaluating symbolically this constraint (line 12). If evaluate concrete
returns true, it means that DART will visit the true branch under the vector Ii .
Int. J. Soft. Eng. Knowl. Eng. 2019.29:1279-1312. Downloaded from www.worldscientific.com
Therefore, pc is added to PC and the visited branches set is updated (lines 13–16).
Otherwise, PC will add the negation of the condition C, then the value of visited
branches set and of counter are updated at the same time.
Next, the next statement is obtained and this process proceeds repeatably until
the execution returns an error (abort) or a success signal (halt) (lines 20–23). If a
signal halt returns, DART tries to negate the current path constraints PC by ap-
plying BFS, DFS, or another strategy. After that, DART calls an SMT-Solver, e.g.
lp solve, to get a new input vector Iiþ1 .
In this paper, the term test data is used rather than input vector for consistency.
Similarly, instead of using program execution path, we refer to an equivalent term,
namely test path.
These functions are instrumented in such a way that visited statements and visited
branches will be printed to an external test data ¯le when executing the testing
function. Later, the content of this external test data ¯le is analyzed to get the
by CHALMERS UNIVERSITY OF TECHNOLOGY on 04/18/20. Re-use and distribution is strictly not permitted, except for Open Access articles.
next step, a series of test data are produced, and then unit test ¯les are represented in
the form of Google Test and MS. Excel ¯les are created automatically to facilitate
testing management steps.
In the second phase, when a number of continuous test data do not increase
coverage by using the BFS strategy, the static test data generation will be performed
instead. In this situation, all possible partial test paths traversing through unvisited
branches will be generated automatically. After that, a series of test data are pro-
duced from these partial test paths with the expectation that newly uncovered
branches will be traversed as soon as possible.
Also in the second phase, SDART extends the test data compilation mechanism
proposed in DART to deal with C++ projects by using a C++ general test driver.
This test driver provides the ability to deal with various data types. An external data
¯le, which takes responsibility for storing test data, is unique during test data gen-
eration. Whenever new test data are created, the content of the external test data ¯le
is updated. The executable test driver will load the value of test data storing in the
external test data ¯le to initialize variables dynamically.
where
. X is the subcomponents of the node nd (e.g. ¯les in a folder),
. np presents the parent of the node nd (e.g. a folder containing a list of ¯les), and
. D represents the logic dependencies of the node nd with the other nodes in the
structure tree S. The type of the node nd is folder, ¯le (e.g. source code ¯le, header
¯le); or logic element such as derived type, method, attribute, etc.
1290 D.-A. Nguyen et al.
where
. k is the number of nodes in the structure tree,
. Vk ¼ fnd0 ; nd1 ; . . . ; ndk1 g is a list of nodes, and
. E ¼ fðndi ; ndj Þ g Vk Vk presents a list of edges (0 <¼ i; j <¼ k 1; ndi 2
Vk ; ndj 2 Vk ). An edge ðndi ; ndj Þ means that node ndj is a subcomponent of
Int. J. Soft. Eng. Knowl. Eng. 2019.29:1279-1312. Downloaded from www.worldscientific.com
node ndi .
Consider a structure tree S ¼ ðVk ; EÞ, among two nodes nd and nd 0 , where
ðnd; nd 0 Þ 62 E, there may exist several logic dependencies starting at nd and ¯nishing
at nd 0 . Each logic dependency represents a type of relationship between two nodes nd
and nd 0 . For example, Table 1 illustrates several typical types of logic dependency in
a structure tree. Speci¯cally, the ¯rst dependency describes a relationship between a
method and an attribute. In this logic dependency, the method is considered as a
getter of the mentioned attribute in a class. In the case if a class extends another
class, there is a logic dependency starting at derived class and ¯nishing at base clas.
a https://clang.llvm.org/.
b https://gcc.gnu.org/.
1292 D.-A. Nguyen et al.
role of the function mark(str) is to print out the string str to a speci¯c ¯le.
return, g, f
while(hconditioni)...dof...g while (mark(\hconditioni") && hconditioni)dof...g
dof...gwhile(hconditioni) dofg while (mark(\hconditioni") && hconditioni)
if (hcondition1i)f...gelse if
(hcondition2i)f...gelsef...g if (mark(\hcondition1i") && hcondition1i)f...gelseif
(mark(\hcondition2i") && hcondition2i)f...gelsef...g
for(init, condition, increment)f...g for(mark(\hiniti") && init, mark(\hconditioni") &&
condition, mark(\hincrementi") &&
increment)f...g
tryf...gcatch(hexception1i)f...g mark(\try");tryf...g
catch(hexception2i)f...g catch(hexception1i)fmark(\hexception1i");...g
catch(hexception2i) fmark(\hexception2i");...g
5. An Improvement of DART
Static DART, or SDART for short, extends DART to reduce the number of test data
by CHALMERS UNIVERSITY OF TECHNOLOGY on 04/18/20. Re-use and distribution is strictly not permitted, except for Open Access articles.
with the smaller number of iterations by improving the BFS strategy introduced in
DART [10]. SDART is presented in Algorithm 3, where the notation ðÞ at the end of a
statement implies that this statement exists in the original algorithm proposed in DART.
Algorithm 3. SDART
Input: f n instrumented function of a function f n (*), DEP T H: depth of test
data generation (*), T HRESHOLD: the threshold to switch to the static test
Int. J. Soft. Eng. Knowl. Eng. 2019.29:1279-1312. Downloaded from www.worldscientific.com
data generation
Output: a series of test data
1: P = get arguments and external variables in f n (*)
2: Cpp testdriver = Cpp-general-test-driver-generation(I)
3: for int depth = 0; depth <DEPTH ; DEPTH ++ (*) do
4: I0 = random initialization(P ) (*)
5: notIncreasingCoverageCount = 0;
6: while compute coverage(fn’ ) <100% (*) do
7: if the coverage of f n does not change then
8: notIncreasingCoverageCount++;
9: else
10: notIncreasingCoverageCount = 0;
11: end if
12: if notIncreasingCoverageCount == T HRESHOLD then
13: break
14: else
15: Ii+1 = instrumented program(f n , Ii ) (*)
16: if Ii+1 does not exist (*) then
17: Ii+1 = random initialization(P ) (*)
18: end if
19: end if
20: end while
21: possibleT estpaths = generate partial test paths containing unvisited branches
22: for testpath : possibleT estpaths do
23: P C = symbolic-execution(testpath)
24: if P C does not exist before then
25: SM T − LIB = Transform P C into SMT-Lib format
26: I = SMT-Solver(SM T − LIB)
27: testpath = Execute Cpp testdriver
28: compute coverage(fn’ )
29: end if
30: end for
31: end for
1294 D.-A. Nguyen et al.
Generally, when there occur signals showing that they are less likely to increase
code coverage, the static test data generation will be chosen immediately. Speci¯cally,
the value THRESHOLD is initialized by users. When there exists THRESHOLD
by CHALMERS UNIVERSITY OF TECHNOLOGY on 04/18/20. Re-use and distribution is strictly not permitted, except for Open Access articles.
continuous test data which do not increase code coverage (lines 7–13), SDART will
generate partial test paths traversing unvisited branches (line 21). After that, each test
path performs symbolic execution so as to construct corresponding path constraints
(line 23). These path constraints PC are checked whether they are generated before or
not (line 24). If it is not, PC is converted into input of SMT-Solvers, then solved by
using an SMT-Solver such as Z3 to obtain new test data I (lines 25–26). Next, the
Int. J. Soft. Eng. Knowl. Eng. 2019.29:1279-1312. Downloaded from www.worldscientific.com
newly generated test data I are passed to a general test driver to execute to get its
corresponding test path (line 27). After that, from the collect test path, the coverage of
fn is then updated (line 28).
8: return P C
9: else
10: P C.add(stm)
11: end if
12: else if stm ≡ declaration of variable v then
13: M = M ∪ {(addr(v), v)}
14: if type of (v) ∈ {number, character} then
15: S = S ∪ {(v, 0)}
16: else if type of (v) ∈ {pointer} then
17: S = S ∪ {(v, N U LL)}
18: else if type of (v) ∈ {string} then
19: S = S ∪ {(v, “”)}
20: end if
21: v.setScope(scope)
22: else if stm ≡ µ(e1 , e2 , =) then
23: if e1 ≡ a pointer then
24: if e2 ≡ an allocation statement (size = n) then
25: M = M ∪ {(addr(e1 ), e1 )}
26: S = S ∪ n−1
i=0 {(ei , 0)}
27: P C.add(n >= 0)
28: else if e2 ≡ N U LL then
29: M = M ∪ {(N U LL, e1 )}
30: S = S ∪ {(e1 , N U LL)}
31: else if e2 ≡ a pointer p +/- k then
32: M = M ∪ {(M (p)k , e1 )}
sizeof (p)−k−1
33: S = S ∪ i=0 {(e1 (i), pi+k )}
34: end if
35: else if type of (e1 ) ∈ {number, character} then
36: S = S ∪ {(e1 , e2 )}
37: end if
38: else if stm ≡ scope then
39: if stm ≡ ‘{’ then
40: scope + +;
41: else if stm ≡ ‘}’ then
42: M.removeV ariablesAtScope(scope)
43: S.removeV ariablesAtScope(scope)
44: scope − −;
45: end if
46: end if
47: end for
48: return P C
1296 D.-A. Nguyen et al.
is not initialized before (line 2). For example, the initial value of uninitialized pa-
rameter x is X, where X is the initial symbolic value of the parameter x.
Next, all statements of this test path will be analyzed in sequential order from the
¯rst statement (line 3). Each statement, denoted by stm in the test path, is rewritten
into a simpler statement which makes symbolic execution become easier (line 4). This
step is called simpli¯cation process, which is based on the inspiration of CIL [1]. In
Int. J. Soft. Eng. Knowl. Eng. 2019.29:1279-1312. Downloaded from www.worldscientific.com
this process, the content of stm is modi¯ed based on memory model M and symbolic
map S. The variables, which are used in stm, are replaced with its corresponding values
or its corresponding addresses. Algorithm 5 will delve into the simpli¯cation process.
After the statement stm is rewritten, based on the type of statement stm, an
appropriate analysis will be performed. There are four basic cases including assign-
ment, declaration, condition, and scope as follows:
— Case 1: Condition (lines 5–11): Note that the condition is normalized in step 4 in
which the used variables are replaced with its corresponding values or addresses.
If the condition is false, it means that PC has no solution. In this case, the
symbolic execution is terminated. Otherwise, the normalized condition will be
added to path constraints PC.
— Case 2: Declaration (lines 12–21). For simplicity, our assumption is that the
declaration does not has any assignment. In this case, the declared variable v will
be added to memory model M and symbolic map S. The default value of a
numerical variable, a pointer, and a string are 0, NULL, and \", respectively. The
default value of a pointer is NULL because the pointer is not initialized in the
declaration. The scope of the declared variable v is stored.
— Case 3: Assignment (lines 22–37). There are two main kinds of assignment
depending on the type of the assigned variable (i.e. pointer, primitive variable).
The value of the assigned variable e1 in memory model M and symbolic map S
will be updated. In the ¯rst case, the assigned variable e1 is a pointer, if there
exists an allocation of a pointer e1 with size n (line 24), the assigned variable e1 is
updated in M at the given address (line 25). In addition, the initial value of
pointer elements in e1 are initialized to 0 (line 26). The constraint about the size
of the pointer e1 must be added to PC (i.e. n >¼ 0) (line 27). In the second case,
the variable e1 is assigned to NULL (line 28). Memory model M will update the
new address of e1 (line 29). All of the pointer elements of e1 are removed from S
(line 30). In the last case, the pointer e1 is assigned to another pointer, namely e2
from a speci¯ed location of the block where e2 points to (lines 31–33). The
starting location on this block is denoted by k (k 2 Z).
— Case 4: Scope (lines 28–35). The algorithm uses a variable named scope to store
the scope of variables. Here, scope is used to store the level of locality of variables.
The level of locality of a variable is used to locate where a variable is created in
Improvements of DART in Test Data Generation 1297
the program. The level of locality of a parameter is always set to 0. For a local
variable, its level of locality is always equal to 1. The value of scope will be
decreased or increased when the statement stm is a closing scope or an opening
by CHALMERS UNIVERSITY OF TECHNOLOGY on 04/18/20. Re-use and distribution is strictly not permitted, except for Open Access articles.
scope, respectively. When the statement stm is a closing scope of a control block
f...g, it means that all of the variables created in this block should be removed
because these variables will not be used in the remaining part of the test path. In
this case, memory model M and symbolic map S will remove these local variables.
In order to detect which variables will be removed, Algorithm 4 will check the
level of locality of each variable and will remove the redundant variables having
Int. J. Soft. Eng. Knowl. Eng. 2019.29:1279-1312. Downloaded from www.worldscientific.com
— Case 1: Assignment (lines 1–10). The right-hand side of the statement stm,
denoted by e2 , will be rewritten. Initially, the right-hand side is converted to its
corresponding AST by using the procedure AST ðÞ. Next, the replacement of
variables is performed on this AST (line 2). Finally, the newly modi¯ed AST of
the right-hand side will be converted into an expression (line 3). In the case which
the left-hand side of the statement stm, denoted by e1 , is an array item, the
indexes of e1 are rewritten similarly to the right-hand side (lines 4–8). The ¯nal
rewritten statement rewritten stm is created by merging the two rewritten
expressions e 01 and e 02 (line 10).
— Case 2: Condition type 1 (lines 11–20). This is the comparison between the two
expressions not related to NULL. Similar to the assignment case, the ASTs of the
two sides of stm are constructed. After that, from the state of the variables stored
in memory model M and symbolic map S, these two ASTs will be modi¯ed by
1298 D.-A. Nguyen et al.
replacing the variables used in these trees with its corresponding values/addresses
(lines 12 and 13). This step is done by using the procedure replace variablesðÞ.
Next, each of these two modi¯ed ASTs is exported to an expression, denoted by e 01
by CHALMERS UNIVERSITY OF TECHNOLOGY on 04/18/20. Re-use and distribution is strictly not permitted, except for Open Access articles.
and e 02 (lines 14 and 15). The rewritten condition, denoted by rewritten stm,
evaluates its boolean value (lines 16–19). If the written condition rewritten stm
cannot be evaluated whether true or false, it means that this condition will still has
variables inside and will added to the path constraints PC later.
— Case 3: Condition type 2 (lines 21–22). This is the comparison between the two
sides in which one side is NULL and the other is a pointer. The pointer e1 used in
Int. J. Soft. Eng. Knowl. Eng. 2019.29:1279-1312. Downloaded from www.worldscientific.com
this statement is replaced with NULL if this pointer is not allocated or not
NULL if this pointer is allocated.
For example, given a statement stm0 a½1 þ a½x > a½3, or ða½1 þ a½x; a½3; >Þ
in which the state of memory model M and symbolic map S before parsing this
statement is as follows:
S ¼ fðx; 2Þ; ða½2; 0Þ; ða½1; 4Þ; ða½3; 10Þg;
Int. J. Soft. Eng. Knowl. Eng. 2019.29:1279-1312. Downloaded from www.worldscientific.com
The second step in this procedure is to replace the used variables in the two
ASTs, which are stored in memory model M and symbolic map S, with its corre-
sponding symbolic values and its addresses, respectively. Here, we see that the
variable x should be used in the ¯rst replacement step rather than array item of
a. After the ¯rst replacement of x on the two ASTs, the corresponding condition will
become
stm1 a½1 þ a½2 > a½3:
In the two modi¯ed ASTs, both variables a½2 and a½3 can be replaced in the next
step. After the replacement of a½2 and a½3 on the two ASTs, the corresponding
condition is rewritten as follows:
Next, after the replacement of a½1 on the AST of the left-hand side, the corre-
sponding AST tree of the expression is as follows:
by CHALMERS UNIVERSITY OF TECHNOLOGY on 04/18/20. Re-use and distribution is strictly not permitted, except for Open Access articles.
5.1.3. Discussion
There are two main problems in symbolic execution including name resolution and
the implicit usage of variables. Our analyzer solves these two main problems partially
as follows:
— Name resolution: Our analyzer needs to detect the semantics of tokens in stm
and its related information including declaration, de¯nition, and references. For
example, given the statement int x = test(classA.getX()), this statement has four
primary tokens including int, x, test, and classA:getX. The symbolic execution
engine needs to detect which ones in four tokens are variables, attributes, func-
tions, etc. as well as its de¯nitions/references. Our analyzer solves this problem
by converting the testing function fn into the corresponding AST. This AST
containing information about name resolution can be collected. The AST gen-
eration from the testing function fn could be performed by using GCC, Clang, or
CDT plugin.c After that, the AST of the testing function fn will be traversed to
collect necessary information related to name resolution.
— Implicit usage of variables: The used variables in stm are used implicitly that
make the simpli¯cation process take more cost compared to the explicit usage.
For example, considering the statement int x ¼ a½a½y, we assume that a½1 ¼ 2,
a½2 ¼ 0, and y ¼ 1. In order to specify the value of variable x, our analyzer
performs a simpli¯cation process as described in line 4 of Algorithm 4. In this
process, the right-hand side is repeatably rewritten under a number of iterations.
In an iteration, the name of variables is replaced with its values stored in sym-
bolic map S or its address stored in memory model M. In this example, three
iterations are good enough to make stm become simplest (i.e. no need for
simpli¯cation any more). After the ¯rst iteration, stm becomes int x ¼ a½a½1;
c https://www.eclipse.org/cdt/.
Improvements of DART in Test Data Generation 1301
after the second one, it is int x ¼ a½2; and it becomes int x ¼ 0 after the third
iteration.
by CHALMERS UNIVERSITY OF TECHNOLOGY on 04/18/20. Re-use and distribution is strictly not permitted, except for Open Access articles.
modern SMT-Solvers have accepted the inputs satisfying SMT-Lib format. In other
words, the format of path constraints, which is called logic expression, is incom-
patible with the input format of SMT-Solvers. Therefore, this paper presents the
process of SMT-Lib generation from logic expressions in Fig. 3. First, each con-
straint, which is called a logic expression, in the given path constraints is transformed
into a corresponding post¯x expression. Next, the post¯x expression is analyzed to
obtain a corresponding expression tree. Finally, the expression tree is traversed to
generate a corresponding SMT-LIB expression.
this tree. After that, this SMT-Lib expression is passed into an SMT-Solver to seek
an appropriate solution.
by CHALMERS UNIVERSITY OF TECHNOLOGY on 04/18/20. Re-use and distribution is strictly not permitted, except for Open Access articles.
reading content from ¯le, etc. In later steps, this basic test driver will be added extra
code for analyzing the structure of test data stored in an external ¯le for the purpose
of test data initialization.
For primitive variables, the corresponding source code for loading these variables
is created. In terms of derived types, the source code for each of these variables is
produced based on the con¯guration parameters presented in Listing 4. In this list,
the role of the parameter DEPTH LINKED LIST aims at specifying the maximum
depth of a linked list to avoid the in¯nite construction problem. Parameter MAX
RECURSIVE stipulates the maximum of recursive iteration. Parameters DEFAU
LT VALUE FOR NUMBER and DEFAULT VALUE FOR CHARACTER pres-
ent the default value of numbers and that of characters during initialization, re-
spectively. The output of the proposed technique is a general test driver which can
treat all of the values of primitive/derived types.
The unique external ¯le storing a test data is structured in the format of con-
tinuous lines in which each line represents a portion of test data. A portion of a test
Improvements of DART in Test Data Generation 1303
where
given function. In the case where a series of test data traverses all branches of the
decisions in the testing function (i.e. true branch, false branch), this test data set
satis¯es branch coverage criterion. The MC/DC coverage criterion is similar
by CHALMERS UNIVERSITY OF TECHNOLOGY on 04/18/20. Re-use and distribution is strictly not permitted, except for Open Access articles.
1: Stack<CfgNode>recursiveP oints = {}
2: CfgNode currentN ode = CF G.beginN ode
3: for Statement stm: tp do
4: if currentN ode is EndN ode then
5: if recursiveP oints.size >= 1 then
6: currentN ode = recursiveP oints.pop().nextN ode
Int. J. Soft. Eng. Knowl. Eng. 2019.29:1279-1312. Downloaded from www.worldscientific.com
7: end if
8: else if currentN ode is a recursive call then
9: recursiveP oints.push(currentN ode)
10: currentN ode = CF G.beginN ode
11: else if currentN ode is a condition then
12: if nextStm is currentN ode.trueN ode then
13: setVisitedBranch(currentN ode, currentN ode.trueN ode)
14: currentN ode = currentN ode.trueN ode
15: else
16: setVisitedBranch(currentN ode, currentN ode.f alseN ode)
17: currentN ode = currentN ode.f alseN ode
18: end if
19: else
20: setVisitedStatement(currentN ode)
21: currentN ode = currentN ode.nextN ode
22: end if
23: end for
might be the start of another call (namely C2 ) because of recursive call; or the
program terminates completely. When the ¯rst case happens, currentNode will point
to the next executed statement where C1 happens (lines 4–7).
In the second case statement that stm contains a recursive call, currentNode is
pointed to the beginning of CFG. currentNode is set to the beginning of CFG (lines
8–10). The main reason is that, when a recursive call is performed, the testing
function fn will be executed in another stack.
In other cases that stm is a condition or a simple statement (e.g. assignment,
declaration, return), the state of nodes corresponding to visited statements/bran-
ches are updated (lines 11–21).
For example, considering the test path \f=>n == 0=>n == 1=> return
(Fibonacci(n 1) + Fibonacci(n2)); (*)=> f=> n == 0=>n == 1=> return
1;=>f=> n == 0=>return 0;" generated from executing function Fibonacci in
Listing 1. This function contains two recursive calls Fibonacciðn 1Þ and
Fibonacciðn 2Þ. The bold statements are corresponding to the executed statements
Improvements of DART in Test Data Generation 1307
of the ¯rst call while the italic ones belong to the results of the second call. At the
statement denoted by (*), because there are two recursive calls, two recursive points
are created in recursivePoints that recursivePoints = f(*).nextNode, (*).nextNodeg.
by CHALMERS UNIVERSITY OF TECHNOLOGY on 04/18/20. Re-use and distribution is strictly not permitted, except for Open Access articles.
On this example, (*).nextNode is equivalent to the end node of CFG. When the ¯rst
recursive call is performed, the testing function is executed in another stack.
Therefore, currentNode will be pointed to the starting node of CFG. After executing
bold statements, the current call terminates, then currentNode continues moving to
the beginning point of the second call Fibonacciðn 2Þ.
Int. J. Soft. Eng. Knowl. Eng. 2019.29:1279-1312. Downloaded from www.worldscientific.com
6. Experiments
6.1. The implemented tool
Our proposed SDART has been implemented in a tool named ICFT4Cppd using
Java to demonstrate its e®ectiveness. Our objective is to demonstrate the e±cacy of
SDART in comparison with DART with the same core implementation (e.g. sym-
bolic execution engine). There are several di®erences between the implementation of
DART and SDART. However, these di®erences do not a®ect the accuracy of the
experiment. Speci¯cally, rather than applying DSE, we try to record executed
statements/branches during test data execution and then stored it in an external ¯le.
When the testing function raises an exception or successes, the test path in this ¯le is
loaded to perform symbolic execution.
Another di®erence is that ICFT4Cpp does not evaluate code coverage during test
data execution. Instead, only when a test path is recorded successfully, code coverage
is computed immediately. This code coverage computation di®ers from that of
DART, which evaluates during test data execution. In DART, the process of eval-
uating the boolean value of condition depends completely upon the function eva-
luate concrete which might be incorrect due to bad implementation. In contrast, we
simply use execution results to check whether the value of a condition true or false.
Therefore, there is no mistake in this step.
Figure 7 presents the architecture of ICFTCpp. In brief, the architecture of
ICFT4Cpp is similar to CFT4Cpp, which is proposed in [23]. There are two main
layers including presentation layer and logic layer. The logic layer takes responsibility
for interacting with Z3 SMT-Solver [4] and MingW32 compiler. The implemented tool
uses plug-in Eclipse CDT to assist in syntax analysis [20] and mcpp library for re-
moving preprocessor.e Eclipse CDT supports name resolution directly in its AST so it
is easy to resolve the properties of a variable, e.g. its de¯nition or its references.
f https://www.bloodshed.net/devcpp.html.
Improvements of DART in Test Data Generation 1309
not increase code coverage, the static test data generation mode will be used
instead.
by CHALMERS UNIVERSITY OF TECHNOLOGY on 04/18/20. Re-use and distribution is strictly not permitted, except for Open Access articles.
With the given con¯guration, it could be seen that in the worst case, both DART
and SDART will take at most 51 functions 30 iterations ¼ 1530 iterations. Also,
in the best case, both strategies will perform at least 51 iterations in total. It also
means that the ¯rst random test data will traverse all branches.
In Fig. 8, it can be seen that SDART tends to achieve the higher number of visited
branches compared to DART. Speci¯cally, although SDART does not show its ef-
fectiveness in about 120 beginning iterations, SDART gradually surpasses DART in
Int. J. Soft. Eng. Knowl. Eng. 2019.29:1279-1312. Downloaded from www.worldscientific.com
the remaining ones. While DART takes around 410 iterations in total, SDART only
executes these testing functions with approximately 370 iterations. The main reason
is when SDART detects the low possibility of increasing code coverage, it switches to
the static test data generation. With this strategy, SDART expects that more
branches will be found earlier than keeping the current path selection strategy.
Fig. 8. The comparison between DART (BFS) and SDART (threshold = 4) in terms of visited branches.
DART-BFS and SDART (threshold = 4) are represented in a dashed line and a solid line, respectively.
More details about the comparison between DART and SDART are presented in
Table 3. First, the total number of solver calls in DART is considerably greater than
that of SDART, with 1279 solver calls and 413 solver calls, respectively. The main
reason is that DART tends to generate the next test data by negating the conditions
which are seem to be useless in terms of increasing code coverage. Second, SDART
generates a larger number of meaningful test data with a smaller number of iterations
in comparison with DART. Here, test data are considered as a meaningful test data
when the test data increase code coverage. Speci¯cally, the meaningful test data
1310 D.-A. Nguyen et al.
generated in SDART accounts for 101/366 * 100% = 27.6%, where 101 and 366 are
the numbers of meaningful test data and the number of iterations, respectively. This
percentage in DART is just 102/409 * 100% = 24.94%. Whenever SDART detects, it
is less likely to increase code coverage under a number of iterations, SDART will
switch to the static test data generation. SDART expects that this static test data
generation will generate a series of newly meaningful test data.
7. Conclusion
This paper presented two improvements for tackling the problems related to the
number of test data and the computational cost of test data compilation on C++
projects. The ¯rst improvement named SDART aims to increase code coverage as
fast as possible by combining the BFS strategy of DART with the static test data
generation approach. Speci¯cally, whenever it is less likely to generate a new test
data increasing coverage, the static test data generation will be chosen instead. A list
of partial test paths covering unvisited branches will be selected by analyzing
CFG. The cost for analyzing these partial test paths in symbolic execution engine is
reduced because these paths are made up of a smaller number of statements. Second,
concerning the computational cost of test data compilation, the proposal tries to
generalize a C++ test driver to deal with various data types to reduce the compu-
tational cost of test data compilation on C++ projects. In order to do that, the
proposed method extends the DART to deal with C++ projects. All of the test data
are stored in an external ¯le and a C++ general test driver takes responsibility for
reading these values to initialize the parameters of a testing function dynamically.
The experiment has shown that the proposed SDART only takes a smaller
number of iterations with the smaller number of iterations while gaining higher
branch coverage in comparison with the BFS strategy of DART. Currently, the
proposed method has been implemented in the tool named ICFT4Cpp. This tool is
currently used in Toshiba Software Development Vietnam company and received
many positive feedbacks. Based on these suggestions, the proposal will be improved
to deal with more C++ features used widely in industrial projects. First, generating
test data for templates and polymorphism is still considered as a big challenge. The
main reason is that it is di±cult to detect exactly the functions which are called
Improvements of DART in Test Data Generation 1311
during execution. Second, the proposal will extend to generate test data for the
functions containing exceptions. Speci¯cally, the research will extend to produce a
series of test data which cause runtime errors. Third, the C++ general test driver will
by CHALMERS UNIVERSITY OF TECHNOLOGY on 04/18/20. Re-use and distribution is strictly not permitted, except for Open Access articles.
be improved to support more various types in C++ such as vector, list, etc. Finally,
the symbolic execution needs to be improved to analyze the statements using over-
loading mechanism. Speci¯cally, both memory model and symbolic map should be
enhanced to deal with overloading, e.g. the subtraction of two class instances.
Acknowledgments
Int. J. Soft. Eng. Knowl. Eng. 2019.29:1279-1312. Downloaded from www.worldscientific.com
This work is supported by the research project No. QG.16.31 granted by Vietnam
National University, Hanoi (VNU).
References
1. G. C. Necula, S. McPeak, S. P. Rahul and W. Weimer, CIL: Intermediate language and
tools for analysis and transformation of C programs, in Proc. 11th Int. Conf. Compiler
Construction, 2002, pp. 213–228.
2. J. C. King, Symbolic execution and program testing, Commun. ACM 19 (1976) 385–394.
3. C. Cadar and K. Sen, Symbolic execution for software testing: Three decades later,
Commun. ACM 56 (2013) 82–90.
4. L. De Moura and N. Bjrner, Z3: An e±cient SMT solver, inTools and Algorithms for the
Construction and Analysis of Systems, 2008, pp. 337–340.
5. G. Li, I. Ghosh and S. P. Rajan, KLOVER: A symbolic execution and automatic test
generation tool for C++ programs, in Proc. 23rd Int. Conf. Computer Aided Veri¯cation,
2011, pp. 609–615.
6. C. Cadar, D. Dunbar and D. Engler, KLEE: Unassisted and automatic generation of high-
coverage tests for complex systems programs, in Proc. 8th USENIX Conf. Operating
Systems Design and Implementation, 2008, pp. 209–224.
7. D. Riehle, Composite design patterns, in Proc. 12th ACM SIGPLAN Conf. Object-
Oriented Programming, Systems, Languages, and Applications, 1997, pp. 218–228.
8. Z. Wang, X. Yu, T. Sun, G. Pu, Z. Ding and J. Hu, Test data generation for derived types
in C program, in Third IEEE Int. Symp. Theoretical Aspects of Software Engineering,
2009, pp. 155–162.
9. N. Williams, B. Marre, P. Mouy and M. Roger, PathCrawler: Automatic generation of
path tests by combining static and dynamic analysis, in Proc. 5th European Conf.
Dependable Computing, 2005, pp. 281–292.
10. P. Godefroid, N. Klarlund and K. Sen, DART: Directed automated random testing, in
Proc. ACM SIGPLAN Conf. Programming Language Design and Implementation, 2005,
pp. 213–223.
11. K. Sen, D. Marinov and G. Agha, CUTE: A concolic unit testing engine for C, in Proc.
10th European Software Engineering Conf. held jointly with 13th ACM SIGSOFT Int.
Symp. Foundations of Software Engineering, 2005, pp. 263–272.
12. T. Su et al., Automated coverage-driven test data generation using dynamic symbolic
execution, in Eighth Int. Conf. Software Security and Reliability, 2014, pp. 98–107.
13. J. Burnim and K. Sen, Heuristics for scalable dynamic test generation, in Proc. 23rd
IEEE/ACM Int. Conf. Automated Software Engineering, 2008, pp. 443–446.
1312 D.-A. Nguyen et al.
18. B. Elkarablieh, P. Godefroid and M. Y. Levin, Precise pointer reasoning for dynamic
test generation, in Proc. Eighteenth Int. Symp. Software Testing and Analysis, 2009,
pp. 129–140.
19. T. Sun, Z. Wang, G. Pu, X. Yu, Z. Qiu and B. Gu, Towards scalable compositional test
generation, in Ninth Int. Conf. Quality Software, 2009, pp. 353–358.
20. D. Piatov, A. Janes, A. Sillitti and G. Succi, Using the eclipse C/C++ development
tooling as a robust, fully functional, actively maintained, open source C++ parser, in
Open Source Systems: Long-Term Sustainability, 2012, p. 399.
21. P. Godefroid, Compositional dynamic test generation, in Proc. 34th Annual ACM
SIGPLAN-SIGACT Symp. Principles of Programming Languages, 2007, pp. 47–54.
22. P. C. Jorgensen, Software Testing: A Craftsman's Approach (CRC Press, Boca Raton,
2014), pp. 6–9.
23. D.-A. Nguyen and P. N. Hung, A test data generation method for C/C++ projects,
in Proc. Eighth Int. Symp. Information and Communication Technology, 2017,
pp. 431–438.