C/C++test CT supports the following coverage types:
- Line Coverage
- Statement Coverage
- Block Coverage
- Decision (Branch) Coverage
- Modified Condition/Decision Coverage (MC/DC)
- Simple Condition Coverage
- Function Coverage
- Call Coverage
To help understand how these coverage types are handled by C/C++test CT, be sure to read and understand the terms in the following table:
Concept | Description |
---|---|
Basic Block | A sequence of non-branching statements; a linear sequence of code with no control flow route branchings. |
Decision/Branch | Decision/Branch is the possible control flow decision to be taken at the branching point in the code. C/C++test CT considers if-else, for, while, do-while, and switch instructions as the branching points. C/C++test CT does not take into account such dynamic branching points as exception handlers (throw-catch statements). |
Boolean expression | In C++, a boolean expression is simply an expression that has a 'bool' type. In C, C/C++test CT treats the following as boolean expressions:
|
MC/DC Decision | A top-level boolean expression composed of conditions and zero or more boolean operators. C/C++test CT computes MC/DC and SCC on all boolean non-constant expressions in the source code except constructor initializers and function default arguments. |
Condition | An atomic boolean non-constant expression that is a part of the MC/DC decision. A sub-expression of the MC/DC decision is considered to be a condition if it does not contain boolean operators (&&, ||, !). If a given atomic expression appears more than once in a decision, each occurrence is a distinct condition. |
Function Coverage
Indicates how many functions in the source code were reached at least once during execution. Complete, 100% function coverage is obtained if all functions are reached at least once.
Call Coverage
Indicates how many defined function or method calls were executed at program runtime. Complete, 100% call coverage is obtained if all functions or methods calls were executed.
Limitations:
- Constructors calls, both explicit and implicit, are not included in call coverage calculation.
- Implicit destructor calls are not included in call coverage calculation.
- C++ operators, both predefined and overloaded, are not included in call coverage calculation.
Example:
class A { public: A() {} A(const A&) {} A(int val) {} A& foo() { return *this; } ~A() {} }; void funcA(const A& a) {} A& operator+(const A & a, const A & b) {} void defaultConstructorCall() { A objA; // Default constructor call - not included A objArr[10]; // Default constructor calls - not included A objB = objA; // Copy constructor call - not included A objC = 10; // Implicit call of A(int) constructor - not included funcA(10); // Implicit call of A(int) constructor - not included A res = objA + objA; // Operator + call - not included // Copy constructor call - not included A* objD = new A(); // Default constructor call - not included // Operator new call - not included delete objD; // Destructor call - not included // Operator new call - not included A* objE = new A[10]; // Default constructor calls - not included // Operator new call - not included delete[] objE; // Destructor calls - not included // Implicit destructor calls - not included }
Line Coverage
Indicates how many executable lines of source code were reached by the control flow at least once. Complete, 100% line coverage is obtained if all executable lines are reached at least once.
Statement Coverage
Indicates how many executable source code statements were reached by the control flow at least once. Complete, 100% statement coverage is obtained if all executable statements are reached at least once.
Block Coverage
Similar to Line Coverage—except that with Block Coverage, the unit of measured code is a basic block (see the definition of this term in the previous table). This indicates how many basic blocks in the source code were reached by the control flow at least once.
Decision (Branch) Coverage
Indicates how many branches in source code control flow passed through. Complete, 100% coverage is obtained when every decision at all branching points took all possible outcomes at least once.
C/C++test CT considers the following statement types branching points in source code: if-else
, for
, while
, do-while
, and switch
. C/C++test CT does not take into account such dynamic branching points as exception handlers (throw-catch
statements). See also Ignoring Decision/Branching Points When the Decision Outcome is Known at Compile Time.
If there are no decisions in a file, C/C++test CT reports that this metric is not available (using an "N/A" label).
Modified Condition/Decision Coverage (MC/DC)
MC/DC conforms to the international technical standard DO-178B/C (RTCA), which specifies the software certification criteria for mission-critical equipment and systems within the aviation industry. This includes real time embedded systems.
According to the DO-178B/C standard, the following three conditions must be satisfied to obtain complete (100%) MC/DC coverage:
- Every decision has taken all possible outcomes at least once.
- Every condition in a decision has taken all possible outcomes at least once.
- Every condition in a decision has been shown to independently affect that decision's outcome.
Since C/C++test CT considers that every condition and decision can have only two outcomes for MC/DC coverage (true or false), it checks only for the third option (c) listed immediately above—since point (c) implies conditions (a) and (b). A condition is shown to independently affect the outcome of a decision by varying only that particular condition while holding fixed all other possible conditions. To test one given condition, C/C++test CT looks for test cases where:
- The tested condition have both true and false outcomes.
- Other conditions in a decision do not change (or are not evaluated because operators in C/C++ are short-circuit logical).
- The outcome of a decision changes.
Thus, to calculate the MC/DC ratio, C/C++test CT uses the formula
MC/DC[%] = m/n
where m
is the number of boolean conditions proven to independently affect a decision's outcome, and n
is the total number of conditions in a decision.
Note that in order to make a single condition covered, at least two test cases need to be executed: one with the condition evaluated to true
and a second with this condition evaluated to false
.
See also Computing Boolean Expressions in C++ Templates.
MC/DC Example
For example, consider the following code:
if (a && (b || c)) // [...]
There are three simple conditions here: 'a', 'b' and 'c'. As a result, n (in the m/n MC/DC formula) will equal 3.
Now, let's assume that the following test cases were executed:
id | a | b | c | a && (b || c) |
---|---|---|---|---|
1 | true | true | false | true |
2 | false | true | false | false |
3 | true | false | false | false |
To compute MC/DC, C/C++test CT looks for the pairs of test cases where 1) the given decision was evaluated to a different value and 2) the value of only one condition changes (all other conditions remain unchanged).
In our example, there is one pair where independently changing the value of a affects the value of the complete decision:
id | a | b | c | a && (b || c) |
---|---|---|---|---|
1 | true | true | false | true |
2 | false | true | false | false |
Another pair shows that independently changing the value of b affects the value of the complete decision:
id | a | b | c | a && (b || c) |
---|---|---|---|---|
1 | true | true | false | true |
3 | true | false | false | false |
This means that our test cases proved that a and b independently affect the value of the complete decision. In terms of MC/DC coverage, they are covered and the c condition is not covered.
C/C++test CT will report the MC/DC coverage for such an example to be 67% [2/3 conditions covered].
Simple Condition Coverage
Indicates the coverage for the outcomes of all decisions’ conditions. The overall number of outcomes for a decision equals 2 * n, where n is the number of conditions in a decision. Therefore, to obtain 100% coverage, all conditions must take all possible outcomes. However, to obtain non-zero coverage, one condition only needs to take one outcome.
See also Computing Boolean Expressions in C++ Templates.
Ignoring Decision/Branching Points When the Decision Outcome is Known at Compile Time
When the Ignoring decision / branching points when decision outcome is known at compile time option (see -ignore-const-decisions) is enabled, C/C++test will ignore the branching point of a decision when the decision outcome is known at compile time. This setting affects decision (branch) coverage and path coverage.
See the following examples:
constexpr bool constexpr_call(int a) { return a >= 10; } void ingore_decision_known_outcome() { int a = 0; do { ++a; } while (false); // decision / branching point ignored while (true) { // decision / branching point ignored break; } if (false) { // decision / branching point ignored a = 1; } else { a = -1; } if (constexpr_call(1)) { // decision / branching point ignored a = 1; } const int cond = constexpr_call(1); if (cond) { // decision / branching point ignored a = 1; } }
Computing Boolean Expressions in C++ Templates
If the type of a sub-expression inside a decision depends on a C++ template parameter, C/C++test will always assume that such a sub-expression cannot be further decomposed into boolean sub-expressions. This is because the actual type of a template parameter may not be convertible to a boolean type (or the decomposition may be different for different types). This affects the way the whole expression is decomposed into decision(s) and conditions.
See the following examples:
struct D { operator bool() { return false; } }; struct E { bool operator||(const bool& b) { return b; } }; template<typename T> void example(bool a, bool b, D d, E e, T t) { if (a || b) {} // a || b // 1 decision ('a || b'), with 2 conditions ('a' and 'b') if (d || b) {} // d || b // 1 decision ('d || b'), with 2 conditions ('d' and 'b') if (e || b) {} // operator||(e, b) // 1 decision ('operator||(e, b)'), with 1 condition ('operator||(e, b)') // not decomposable ('E' cannot be converted to 'bool') if (t || b) {} // operator||(t, b) // 1 decision ('operator||(t, b)'), with 1 condition ('operator||(t, b)') // assumed not decomposable ('T' may be 'E') if (a || b || t) {} // operator||(a || b, t) // 2 decisions: // * decision 'operator||(a || b, t)', with 1 condition ('operator||(a || b, t)') // assumed outer OR is not decomposable ('T' may be 'E') // * decision 'a || b', with 2 conditions ('a' and 'b') }