Skip to content

Commit 41d2d1b

Browse files
authored
added a script to bisect regressions (#4286)
1 parent 132ae52 commit 41d2d1b

5 files changed

Lines changed: 373 additions & 0 deletions

File tree

tools/bisect/README.md

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
# Bisecting
2+
3+
NOTE: THIS IS WORK IN PROGRESS
4+
5+
`bisect.sh` is a script to bisect issues.
6+
7+
## Command
8+
9+
```
10+
./bisect.sh <hash-good> <hash-bad> "<cppcheck-options>"
11+
```
12+
13+
`hash-good` - the last known good commit hash - in case of daca it is the last tagged minor release (not patch release - i.e. 2.x)
14+
`hash-bad` - the known bad commit hash - in case of daca the one from the `head-info:` line
15+
`cppcheck-options` - the options for the Cppcheck invokation - in case of daca the ones from the `cppcheck-options:` line and the path to the folder/file to scan
16+
17+
If possible use `main` as the function to test stuff with since it won't emit an `unusedFunction` warning.
18+
19+
## Bisecting scan time regressions
20+
21+
We use daca to track differences in scan time. An overview of regressions in scan time can be found at http://cppcheck1.osuosl.org:8000/time_gt.html.
22+
23+
You need to download the archive as specified by the second line in the output and extract it.
24+
25+
If the overall scan time regressed you need to specify the whole folder.
26+
27+
If a timeout (potential hang) was introduced you can simply specify the file from `error: Internal error: Child process crashed with signal 15 [cppcheckError]`.
28+
29+
30+
## Bisecting result regressions
31+
32+
Results regressions are being bisected based on the `--error-exitcode=` result.
33+
If nothing is found the result will be `0` and it is treated as a _good_ commit.
34+
If a finding occurs the result will be `1` which is treated as a _bad_ commit.
35+
36+
### False positive
37+
38+
Provide a code sample which will trigger the false postive.
39+
40+
```cpp
41+
// cppcheck-suppress unusedFunction
42+
static void f()
43+
{
44+
<code triggering FP>
45+
}
46+
```
47+
48+
### False negative
49+
50+
Provide a code sample which will trigger a `unmatchedSuppression`.
51+
52+
```cpp
53+
// cppcheck-suppress unusedFunction
54+
static void f()
55+
{
56+
// cppcheck-suppress unreadVariable
57+
int i;
58+
}
59+
```
60+
61+
## Notes
62+
63+
### Compilation issues:
64+
65+
- 2.5 and before can only be built with GCC<=10 because of missing includes caused by cleanups within the standard headers. You need to specify `CXX=g++-10`.
66+
- 1.88 and 1.89 cannot be compiled:
67+
```
68+
make: python: No such file or directory
69+
```
70+
- 1.39 to 1.49 (possibly more versions - 1.54 and up work) cannot be compiled:
71+
```
72+
lib/mathlib.cpp:70:42: error: invalid conversion from ‘char’ to ‘char**’ [-fpermissive]
73+
70 | return std::strtoul(str.c_str(), '\0', 16);
74+
| ^~~~
75+
| |
76+
| char
77+
```
78+
- some commits between 2.0 and 2.2 cannot be compiled:
79+
```
80+
cli/cppcheckexecutor.cpp:333:22: error: size of array ‘mytstack’ is not an integral constant-expression
81+
333 | static char mytstack[MYSTACKSIZE]= {0}; // alternative stack for signal handler
82+
| ^~~~~~~~~~~
83+
```
84+
RESOLVED: a hot-patch is applied before compilation.
85+
- some commits between 1.54 and 1.55 cannot be compiled:
86+
```
87+
lib/preprocessor.cpp:2103:5: error: ‘errorLogger’ was not declared in this scope; did you mean ‘_errorLogger’?
88+
2103 | errorLogger->reportInfo(errmsg);
89+
| ^~~~~~~~~~~
90+
| _errorLogger
91+
```

tools/bisect/bisect.sh

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#!/bin/sh
2+
3+
# TODO: set -e
4+
set -x
5+
6+
# TODO: check parameters
7+
hash_good=$1
8+
hash_bad=$2
9+
options=$3
10+
11+
# TODO: verify "good" commit happened before "bad" commit
12+
13+
hang=0
14+
expected=""
15+
16+
script_dir="$(dirname "$(realpath "$0")")"
17+
18+
# TODO: make configurable
19+
bisect_dir=~/.bisect
20+
21+
mkdir -p "$bisect_dir" || exit 1
22+
23+
cd "$bisect_dir" || exit 1
24+
25+
if [ ! -d 'cppcheck' ]; then
26+
git clone https://github.com/danmar/cppcheck.git || exit 1
27+
fi
28+
29+
bisect_repo_dir="$bisect_dir/cppcheck"
30+
31+
cd $bisect_repo_dir || exit 1
32+
33+
git fetch --all --tags || exit 1
34+
35+
# clean up in case we previously exited prematurely
36+
git restore . || exit 1
37+
git clean -df || exit 1
38+
39+
# reset potentially unfinished bisect - also reverts to 'main' branch
40+
git bisect reset || exit 1
41+
42+
# update `main` branch
43+
git pull || exit 1
44+
45+
# TODO: filter addons, cfg and platforms based on the options
46+
# limit to paths which actually affect the analysis
47+
git bisect start -- Makefile 'addons/*.py' 'cfg/*.cfg' 'cli/*.cpp' 'cli/*.h' 'externals/**/*.cpp' 'externals/**/*.h' 'lib/*.cpp' 'lib/*.h' platforms tools/matchcompiler.py || exit 1
48+
49+
git checkout "$hash_good" || exit 1
50+
51+
if [ $hang -eq 1 ]; then
52+
# TODO: exitcode overflow on 255
53+
# get expected time from good commit
54+
python3 "$script_dir/bisect_hang.py" "$bisect_dir" "$options"
55+
elapsed_time=$?
56+
else
57+
# verify the given commit is actually "good"
58+
python3 "$script_dir/bisect_res.py" "$bisect_dir" "$options" "$expected"
59+
# shellcheck disable=SC2181
60+
if [ $? -ne 0 ]; then
61+
echo "given good commit is not actually good"
62+
exit 1
63+
fi
64+
fi
65+
66+
# mark commit as "good"
67+
git bisect good || exit 1
68+
69+
git checkout "$hash_bad" || exit 1
70+
71+
# verify the given commit is actually "bad"
72+
if [ $hang -eq 1 ]; then
73+
python3 "$script_dir/bisect_hang.py" "$bisect_dir" "$options" $elapsed_time
74+
else
75+
python3 "$script_dir/bisect_res.py" "$bisect_dir" "$options" "$expected"
76+
fi
77+
78+
if [ $? -ne 1 ]; then
79+
echo "given bad commit is not actually bad"
80+
exit 1
81+
fi
82+
83+
# mark commit as "bad"
84+
git bisect bad || exit 1
85+
86+
# perform the actual bisect
87+
if [ $hang -eq 1 ]; then
88+
git bisect run python3 "$script_dir/bisect_hang.py" "$bisect_dir" "$options" $elapsed_time || exit 1
89+
else
90+
git bisect run python3 "$script_dir/bisect_res.py" "$bisect_dir" "$options" "$expected" || exit 1
91+
fi
92+
93+
# show the bisect log
94+
git bisect log || exit 1
95+
96+
git bisect reset || exit 1

tools/bisect/bisect_common.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
#!/usr/bin/env python
2+
import subprocess
3+
import os
4+
import shutil
5+
6+
EC_GOOD = 0 # tells bisect that the commit is "good"
7+
EC_BAD = 1 # tells bisect that the commit is "bad"
8+
EC_SKIP = 125 # tells bisect to skip this commit since it cannot be tested
9+
10+
def build_cppcheck(bisect_path):
11+
commit_hash = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('ascii').strip()
12+
install_path = os.path.join(bisect_path, commit_hash)
13+
cppcheck_path = os.path.join(install_path, 'cppcheck')
14+
if os.path.exists(install_path):
15+
print('binary for {} already exists'.format(commit_hash))
16+
return cppcheck_path
17+
18+
bisect_repo_dir = os.path.join(bisect_path, 'cppcheck')
19+
20+
if os.path.exists(os.path.join(bisect_repo_dir, 'cppcheck')):
21+
os.remove(os.path.join(bisect_repo_dir, 'cppcheck'))
22+
23+
print('patching cli/cppcheckexecutor.cpp')
24+
subprocess.check_call(['sed', '-i', 's/SIGSTKSZ/32768/g', os.path.join(bisect_repo_dir, 'cli', 'cppcheckexecutor.cpp')])
25+
26+
# TODO: older versions do not build because of include changes in libstdc++ - check compiler version and try to use an earlier one
27+
# TODO: make jobs configurable
28+
# TODO: use "make install"?
29+
# TODO: allow CXXFLAGS overrides to workaround compiling issues in older versions
30+
print('building {}'.format(commit_hash))
31+
subprocess.check_call(['make', '-C', bisect_repo_dir, '-j6', 'MATCHCOMPILER=yes', 'CXXFLAGS=-O2 -w -pipe', '-s'])
32+
33+
# TODO: remove folder if installation failed
34+
print('installing {}'.format(commit_hash))
35+
os.mkdir(install_path)
36+
if os.path.exists(os.path.join(bisect_repo_dir, 'cfg')):
37+
shutil.copytree(os.path.join(bisect_repo_dir, 'cfg'), os.path.join(install_path, 'cfg'))
38+
if os.path.exists(os.path.join(bisect_repo_dir, 'platforms')):
39+
shutil.copytree(os.path.join(bisect_repo_dir, 'platforms'), os.path.join(install_path, 'platforms'))
40+
shutil.copy(os.path.join(bisect_repo_dir, 'cppcheck'), cppcheck_path)
41+
42+
# reset the patches so the subsequent checkout works
43+
print('resetting repo')
44+
subprocess.check_call(['git', 'reset', '--hard'])
45+
46+
return cppcheck_path

tools/bisect/bisect_hang.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
#!/usr/bin/env python
2+
import time
3+
import sys
4+
5+
from bisect_common import *
6+
7+
# TODO: detect missing file
8+
def run(cppcheck_path, options, elapsed_time=None):
9+
timeout = None
10+
if elapsed_time:
11+
timeout = elapsed_time * 2
12+
cmd = options.split()
13+
cmd.insert(0, cppcheck_path)
14+
print('running {}'.format(cppcheck_path))
15+
p = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
16+
try:
17+
p.communicate(timeout=timeout)
18+
if p.returncode != 0:
19+
print('error')
20+
return None
21+
print('done')
22+
except subprocess.TimeoutExpired:
23+
print('timeout')
24+
p.kill()
25+
p.communicate()
26+
return False
27+
28+
return True
29+
30+
31+
# TODO: check arguments
32+
bisect_path = sys.argv[1]
33+
options = sys.argv[2]
34+
if '--error-exitcode=0' not in options:
35+
options += ' --error-exitcode=0'
36+
if len(sys.argv) == 4:
37+
elapsed_time = float(sys.argv[3])
38+
else:
39+
elapsed_time = None
40+
41+
try:
42+
cppcheck_path = build_cppcheck(bisect_path)
43+
except Exception as e:
44+
# TODO: how to persist this so we don't keep compiling these
45+
print(e)
46+
sys.exit(EC_SKIP)
47+
48+
if not elapsed_time:
49+
t = time.perf_counter()
50+
# TODO: handle error result
51+
run(cppcheck_path, options)
52+
elapsed_time = time.perf_counter() - t
53+
print('elapsed_time: {}'.format(elapsed_time))
54+
# TODO: write to stdout and redirect all all printing to stderr
55+
sys.exit(round(elapsed_time + .5)) # return the time
56+
57+
t = time.perf_counter()
58+
run_res = run(cppcheck_path, options, elapsed_time)
59+
run_time = time.perf_counter() - t
60+
61+
if not elapsed_time:
62+
# TODO: handle error result
63+
print('elapsed_time: {}'.format(run_time))
64+
# TODO: write to stdout and redirect all printing to stderr
65+
sys.exit(round(run_time + .5)) # return the time
66+
67+
if run_res is None:
68+
sys.exit(EC_SKIP) # error occured
69+
70+
if not run_res:
71+
sys.exit(EC_BAD) # timeout occured
72+
73+
print('run_time: {}'.format(run_time))
74+
75+
sys.exit(EC_GOOD) # no timeout

tools/bisect/bisect_res.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
#!/usr/bin/env python
2+
import sys
3+
4+
from bisect_common import *
5+
6+
# TODO: detect missing file
7+
def run(cppcheck_path, options):
8+
cmd = options.split()
9+
cmd.insert(0, cppcheck_path)
10+
print('running {}'.format(cppcheck_path))
11+
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
12+
stdout, stderr = p.communicate()
13+
# only 0 and 1 are well-defined in this case
14+
if p.returncode > 1:
15+
print('error')
16+
return None, None, None
17+
# signals are report as negative exitcode (e.g. SIGSEGV -> -11)
18+
if p.returncode < 0:
19+
print('crash')
20+
return None, None, None
21+
print('done')
22+
return p.returncode, stderr, stdout
23+
24+
25+
# TODO: check arguments
26+
bisect_path = sys.argv[1]
27+
options = sys.argv[2]
28+
expected = None
29+
if len(sys.argv) == 4:
30+
expected = sys.argv[3]
31+
if len(expected) == 0:
32+
expected = None
33+
if expected is None:
34+
if '--error-exitcode=1' not in options:
35+
options += ' --error-exitcode=1'
36+
else:
37+
if '--error-exitcode=0' not in options:
38+
options += ' --error-exitcode=0'
39+
40+
try:
41+
cppcheck_path = build_cppcheck(bisect_path)
42+
except Exception as e:
43+
# TODO: how to persist this so we don't keep compiling these
44+
print(e)
45+
sys.exit(EC_SKIP)
46+
47+
run_ec, run_stderr, run_stdout = run(cppcheck_path, options)
48+
49+
if expected is None:
50+
print(run_ec)
51+
print(run_stdout)
52+
print(run_stderr)
53+
54+
# if no ec is set we encountered an unexpected error
55+
if run_ec is None:
56+
sys.exit(EC_SKIP) # error occured
57+
58+
# check output for expected string
59+
if expected is not None:
60+
if (expected not in run_stderr) and (expected not in run_stdout):
61+
sys.exit(EC_BAD) # output not found occured
62+
63+
sys.exit(EC_GOOD) # output found
64+
65+
sys.exit(run_ec) # return the elapsed time - not a result for bisect

0 commit comments

Comments
 (0)