byte-physics / igortest

Igor Pro Universal Testing Framework
https://docs.byte-physics.de/igor-unit-testing-framework/
BSD 3-Clause "New" or "Revised" License
7 stars 2 forks source link

Add support for calculating the cyclomatic complexity for cobertura output #424

Closed t-b closed 1 year ago

t-b commented 1 year ago

See https://en.wikipedia.org/wiki/Cyclomatic_complexity.

According to 1 this is "just"

It will begin by adding 1 point for the function declaration, after that it will add 1 point for every if, while, for and case.

Garados007 commented 1 year ago

It will begin by adding 1 point for the function declaration, after that it will add 1 point for every if, while, for and case.

I don't want to just accept this solution (it seems more like a hack), so I will try to prove it. According to your Wikipedia link the cyclomatic complexity is calculated like this:

M=E-N+2P

where,

The directed graph contains all single instructions including function definition as single nodes and the possible control flow is represented as directed edges between these nodes. An empty function has only a single node and one connected component: M=0-1+2*1=1. Fine.

All of our instructions does only change the number of nodes and edges and do not add more connected components. In the result we will only have one connected component so we can fix the number P at 1.

All normal instructions that have no influence to the control flow introduces one node and one edge into the graph which results in no difference to M. We can ignore them.

The atomic if instruction introduces three nodes (if-statement, true-case, false-case) and four edges (from if to true/false, from true/false to next node). This will change M by 4-3+2*0=1.

An atomic while instruction introduces two nodes (while, body) and three edges (from while to body, from body to while, from while to next node). This will change M by 3-2+2*0=1

An atomic do-while instruction introduces three nodes (do-statement, body, while-check) and four edges (do-statement to body, body to while-check, while-check to do-statement, while-check to next node). This will change M by 4-3+2*0=1.

An atomic for instruction introduces two nodes (for, body) and three edges (from for to body, from body to for, from for to next node). This will change M by 3-2+2*0=1.

A switch instruction is like a list of n if-instructions. n is the same number of the case instructions. So, we only need to count the number of case and add one for each.

A normal if, while, do-while and for statement can contain more than one conditions combined with && or ||. Each condition has to be counted separately as they can be rewritten as nested if-statements. The number of conditions is the same number of atomic if-statements.

From Wikipedia: However, this is true only for decision points counted at the lowest, machine-level instructions.[4] Decisions involving compound predicates like those found in high-level languages like IF cond1 AND cond2 THEN ... should be counted in terms of predicate variables involved, i.e. in this example one should count two decision points, because at machine level it is equivalent to IF cond1 THEN IF cond2 THEN ...

In the result. I mostly agree with your simplification but we need to count the number of conditions inside if, while and for. As long as there is no better solution we can hack this by counting the number of && and || and add one to it.

Garados007 commented 1 year ago

We also need to count the ternary operator cond ? a : b as they can be rewritten to an if-statement. Doing this we also have to count SelectString(cond, b, a) as this is the ternary operator variant for strings.

t-b commented 1 year ago

Very nice derivation! There is also SelectNumber btw.

Garados007 commented 1 year ago

Thinking about this, this can be done with a set of regular expressions. There is no need for an Igor parser & lexer. To prevent wrong matches with strings we can add a preprocess to remove the content of strings:

// original line
string op = SelectString(condition, "||", "&&")
// preprocessed line
string op = SelectString(condition, "", "")

We are only interested in the number of (?<!\w)if\s*\(, (?<!\w)while\s*\(, (?<!\w)for\s*\(, (?<!\w)case\s*:, &&, \|\|, \?, (?<!\w)SelectString\s*\(, (?<!\w)SelectNumber\s*\( matches and therefore is the content of strings irrelevant.

t-b commented 1 year ago

I would think that try/catch is also a condition or?