added a script to bisect regressions (#4286)
This commit is contained in:
parent
132ae52428
commit
41d2d1b0b5
|
@ -0,0 +1,91 @@
|
|||
# Bisecting
|
||||
|
||||
NOTE: THIS IS WORK IN PROGRESS
|
||||
|
||||
`bisect.sh` is a script to bisect issues.
|
||||
|
||||
## Command
|
||||
|
||||
```
|
||||
./bisect.sh <hash-good> <hash-bad> "<cppcheck-options>"
|
||||
```
|
||||
|
||||
`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)
|
||||
`hash-bad` - the known bad commit hash - in case of daca the one from the `head-info:` line
|
||||
`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
|
||||
|
||||
If possible use `main` as the function to test stuff with since it won't emit an `unusedFunction` warning.
|
||||
|
||||
## Bisecting scan time regressions
|
||||
|
||||
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.
|
||||
|
||||
You need to download the archive as specified by the second line in the output and extract it.
|
||||
|
||||
If the overall scan time regressed you need to specify the whole folder.
|
||||
|
||||
If a timeout (potential hang) was introduced you can simply specify the file from `error: Internal error: Child process crashed with signal 15 [cppcheckError]`.
|
||||
|
||||
|
||||
## Bisecting result regressions
|
||||
|
||||
Results regressions are being bisected based on the `--error-exitcode=` result.
|
||||
If nothing is found the result will be `0` and it is treated as a _good_ commit.
|
||||
If a finding occurs the result will be `1` which is treated as a _bad_ commit.
|
||||
|
||||
### False positive
|
||||
|
||||
Provide a code sample which will trigger the false postive.
|
||||
|
||||
```cpp
|
||||
// cppcheck-suppress unusedFunction
|
||||
static void f()
|
||||
{
|
||||
<code triggering FP>
|
||||
}
|
||||
```
|
||||
|
||||
### False negative
|
||||
|
||||
Provide a code sample which will trigger a `unmatchedSuppression`.
|
||||
|
||||
```cpp
|
||||
// cppcheck-suppress unusedFunction
|
||||
static void f()
|
||||
{
|
||||
// cppcheck-suppress unreadVariable
|
||||
int i;
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
### Compilation issues:
|
||||
|
||||
- 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`.
|
||||
- 1.88 and 1.89 cannot be compiled:
|
||||
```
|
||||
make: python: No such file or directory
|
||||
```
|
||||
- 1.39 to 1.49 (possibly more versions - 1.54 and up work) cannot be compiled:
|
||||
```
|
||||
lib/mathlib.cpp:70:42: error: invalid conversion from ‘char’ to ‘char**’ [-fpermissive]
|
||||
70 | return std::strtoul(str.c_str(), '\0', 16);
|
||||
| ^~~~
|
||||
| |
|
||||
| char
|
||||
```
|
||||
- some commits between 2.0 and 2.2 cannot be compiled:
|
||||
```
|
||||
cli/cppcheckexecutor.cpp:333:22: error: size of array ‘mytstack’ is not an integral constant-expression
|
||||
333 | static char mytstack[MYSTACKSIZE]= {0}; // alternative stack for signal handler
|
||||
| ^~~~~~~~~~~
|
||||
```
|
||||
RESOLVED: a hot-patch is applied before compilation.
|
||||
- some commits between 1.54 and 1.55 cannot be compiled:
|
||||
```
|
||||
lib/preprocessor.cpp:2103:5: error: ‘errorLogger’ was not declared in this scope; did you mean ‘_errorLogger’?
|
||||
2103 | errorLogger->reportInfo(errmsg);
|
||||
| ^~~~~~~~~~~
|
||||
| _errorLogger
|
||||
```
|
|
@ -0,0 +1,96 @@
|
|||
#!/bin/sh
|
||||
|
||||
# TODO: set -e
|
||||
set -x
|
||||
|
||||
# TODO: check parameters
|
||||
hash_good=$1
|
||||
hash_bad=$2
|
||||
options=$3
|
||||
|
||||
# TODO: verify "good" commit happened before "bad" commit
|
||||
|
||||
hang=0
|
||||
expected=""
|
||||
|
||||
script_dir="$(dirname "$(realpath "$0")")"
|
||||
|
||||
# TODO: make configurable
|
||||
bisect_dir=~/.bisect
|
||||
|
||||
mkdir -p "$bisect_dir" || exit 1
|
||||
|
||||
cd "$bisect_dir" || exit 1
|
||||
|
||||
if [ ! -d 'cppcheck' ]; then
|
||||
git clone https://github.com/danmar/cppcheck.git || exit 1
|
||||
fi
|
||||
|
||||
bisect_repo_dir="$bisect_dir/cppcheck"
|
||||
|
||||
cd $bisect_repo_dir || exit 1
|
||||
|
||||
git fetch --all --tags || exit 1
|
||||
|
||||
# clean up in case we previously exited prematurely
|
||||
git restore . || exit 1
|
||||
git clean -df || exit 1
|
||||
|
||||
# reset potentially unfinished bisect - also reverts to 'main' branch
|
||||
git bisect reset || exit 1
|
||||
|
||||
# update `main` branch
|
||||
git pull || exit 1
|
||||
|
||||
# TODO: filter addons, cfg and platforms based on the options
|
||||
# limit to paths which actually affect the analysis
|
||||
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
|
||||
|
||||
git checkout "$hash_good" || exit 1
|
||||
|
||||
if [ $hang -eq 1 ]; then
|
||||
# TODO: exitcode overflow on 255
|
||||
# get expected time from good commit
|
||||
python3 "$script_dir/bisect_hang.py" "$bisect_dir" "$options"
|
||||
elapsed_time=$?
|
||||
else
|
||||
# verify the given commit is actually "good"
|
||||
python3 "$script_dir/bisect_res.py" "$bisect_dir" "$options" "$expected"
|
||||
# shellcheck disable=SC2181
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "given good commit is not actually good"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# mark commit as "good"
|
||||
git bisect good || exit 1
|
||||
|
||||
git checkout "$hash_bad" || exit 1
|
||||
|
||||
# verify the given commit is actually "bad"
|
||||
if [ $hang -eq 1 ]; then
|
||||
python3 "$script_dir/bisect_hang.py" "$bisect_dir" "$options" $elapsed_time
|
||||
else
|
||||
python3 "$script_dir/bisect_res.py" "$bisect_dir" "$options" "$expected"
|
||||
fi
|
||||
|
||||
if [ $? -ne 1 ]; then
|
||||
echo "given bad commit is not actually bad"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# mark commit as "bad"
|
||||
git bisect bad || exit 1
|
||||
|
||||
# perform the actual bisect
|
||||
if [ $hang -eq 1 ]; then
|
||||
git bisect run python3 "$script_dir/bisect_hang.py" "$bisect_dir" "$options" $elapsed_time || exit 1
|
||||
else
|
||||
git bisect run python3 "$script_dir/bisect_res.py" "$bisect_dir" "$options" "$expected" || exit 1
|
||||
fi
|
||||
|
||||
# show the bisect log
|
||||
git bisect log || exit 1
|
||||
|
||||
git bisect reset || exit 1
|
|
@ -0,0 +1,46 @@
|
|||
#!/usr/bin/env python
|
||||
import subprocess
|
||||
import os
|
||||
import shutil
|
||||
|
||||
EC_GOOD = 0 # tells bisect that the commit is "good"
|
||||
EC_BAD = 1 # tells bisect that the commit is "bad"
|
||||
EC_SKIP = 125 # tells bisect to skip this commit since it cannot be tested
|
||||
|
||||
def build_cppcheck(bisect_path):
|
||||
commit_hash = subprocess.check_output(['git', 'rev-parse', '--short', 'HEAD']).decode('ascii').strip()
|
||||
install_path = os.path.join(bisect_path, commit_hash)
|
||||
cppcheck_path = os.path.join(install_path, 'cppcheck')
|
||||
if os.path.exists(install_path):
|
||||
print('binary for {} already exists'.format(commit_hash))
|
||||
return cppcheck_path
|
||||
|
||||
bisect_repo_dir = os.path.join(bisect_path, 'cppcheck')
|
||||
|
||||
if os.path.exists(os.path.join(bisect_repo_dir, 'cppcheck')):
|
||||
os.remove(os.path.join(bisect_repo_dir, 'cppcheck'))
|
||||
|
||||
print('patching cli/cppcheckexecutor.cpp')
|
||||
subprocess.check_call(['sed', '-i', 's/SIGSTKSZ/32768/g', os.path.join(bisect_repo_dir, 'cli', 'cppcheckexecutor.cpp')])
|
||||
|
||||
# TODO: older versions do not build because of include changes in libstdc++ - check compiler version and try to use an earlier one
|
||||
# TODO: make jobs configurable
|
||||
# TODO: use "make install"?
|
||||
# TODO: allow CXXFLAGS overrides to workaround compiling issues in older versions
|
||||
print('building {}'.format(commit_hash))
|
||||
subprocess.check_call(['make', '-C', bisect_repo_dir, '-j6', 'MATCHCOMPILER=yes', 'CXXFLAGS=-O2 -w -pipe', '-s'])
|
||||
|
||||
# TODO: remove folder if installation failed
|
||||
print('installing {}'.format(commit_hash))
|
||||
os.mkdir(install_path)
|
||||
if os.path.exists(os.path.join(bisect_repo_dir, 'cfg')):
|
||||
shutil.copytree(os.path.join(bisect_repo_dir, 'cfg'), os.path.join(install_path, 'cfg'))
|
||||
if os.path.exists(os.path.join(bisect_repo_dir, 'platforms')):
|
||||
shutil.copytree(os.path.join(bisect_repo_dir, 'platforms'), os.path.join(install_path, 'platforms'))
|
||||
shutil.copy(os.path.join(bisect_repo_dir, 'cppcheck'), cppcheck_path)
|
||||
|
||||
# reset the patches so the subsequent checkout works
|
||||
print('resetting repo')
|
||||
subprocess.check_call(['git', 'reset', '--hard'])
|
||||
|
||||
return cppcheck_path
|
|
@ -0,0 +1,75 @@
|
|||
#!/usr/bin/env python
|
||||
import time
|
||||
import sys
|
||||
|
||||
from bisect_common import *
|
||||
|
||||
# TODO: detect missing file
|
||||
def run(cppcheck_path, options, elapsed_time=None):
|
||||
timeout = None
|
||||
if elapsed_time:
|
||||
timeout = elapsed_time * 2
|
||||
cmd = options.split()
|
||||
cmd.insert(0, cppcheck_path)
|
||||
print('running {}'.format(cppcheck_path))
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
||||
try:
|
||||
p.communicate(timeout=timeout)
|
||||
if p.returncode != 0:
|
||||
print('error')
|
||||
return None
|
||||
print('done')
|
||||
except subprocess.TimeoutExpired:
|
||||
print('timeout')
|
||||
p.kill()
|
||||
p.communicate()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# TODO: check arguments
|
||||
bisect_path = sys.argv[1]
|
||||
options = sys.argv[2]
|
||||
if '--error-exitcode=0' not in options:
|
||||
options += ' --error-exitcode=0'
|
||||
if len(sys.argv) == 4:
|
||||
elapsed_time = float(sys.argv[3])
|
||||
else:
|
||||
elapsed_time = None
|
||||
|
||||
try:
|
||||
cppcheck_path = build_cppcheck(bisect_path)
|
||||
except Exception as e:
|
||||
# TODO: how to persist this so we don't keep compiling these
|
||||
print(e)
|
||||
sys.exit(EC_SKIP)
|
||||
|
||||
if not elapsed_time:
|
||||
t = time.perf_counter()
|
||||
# TODO: handle error result
|
||||
run(cppcheck_path, options)
|
||||
elapsed_time = time.perf_counter() - t
|
||||
print('elapsed_time: {}'.format(elapsed_time))
|
||||
# TODO: write to stdout and redirect all all printing to stderr
|
||||
sys.exit(round(elapsed_time + .5)) # return the time
|
||||
|
||||
t = time.perf_counter()
|
||||
run_res = run(cppcheck_path, options, elapsed_time)
|
||||
run_time = time.perf_counter() - t
|
||||
|
||||
if not elapsed_time:
|
||||
# TODO: handle error result
|
||||
print('elapsed_time: {}'.format(run_time))
|
||||
# TODO: write to stdout and redirect all printing to stderr
|
||||
sys.exit(round(run_time + .5)) # return the time
|
||||
|
||||
if run_res is None:
|
||||
sys.exit(EC_SKIP) # error occured
|
||||
|
||||
if not run_res:
|
||||
sys.exit(EC_BAD) # timeout occured
|
||||
|
||||
print('run_time: {}'.format(run_time))
|
||||
|
||||
sys.exit(EC_GOOD) # no timeout
|
|
@ -0,0 +1,65 @@
|
|||
#!/usr/bin/env python
|
||||
import sys
|
||||
|
||||
from bisect_common import *
|
||||
|
||||
# TODO: detect missing file
|
||||
def run(cppcheck_path, options):
|
||||
cmd = options.split()
|
||||
cmd.insert(0, cppcheck_path)
|
||||
print('running {}'.format(cppcheck_path))
|
||||
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
|
||||
stdout, stderr = p.communicate()
|
||||
# only 0 and 1 are well-defined in this case
|
||||
if p.returncode > 1:
|
||||
print('error')
|
||||
return None, None, None
|
||||
# signals are report as negative exitcode (e.g. SIGSEGV -> -11)
|
||||
if p.returncode < 0:
|
||||
print('crash')
|
||||
return None, None, None
|
||||
print('done')
|
||||
return p.returncode, stderr, stdout
|
||||
|
||||
|
||||
# TODO: check arguments
|
||||
bisect_path = sys.argv[1]
|
||||
options = sys.argv[2]
|
||||
expected = None
|
||||
if len(sys.argv) == 4:
|
||||
expected = sys.argv[3]
|
||||
if len(expected) == 0:
|
||||
expected = None
|
||||
if expected is None:
|
||||
if '--error-exitcode=1' not in options:
|
||||
options += ' --error-exitcode=1'
|
||||
else:
|
||||
if '--error-exitcode=0' not in options:
|
||||
options += ' --error-exitcode=0'
|
||||
|
||||
try:
|
||||
cppcheck_path = build_cppcheck(bisect_path)
|
||||
except Exception as e:
|
||||
# TODO: how to persist this so we don't keep compiling these
|
||||
print(e)
|
||||
sys.exit(EC_SKIP)
|
||||
|
||||
run_ec, run_stderr, run_stdout = run(cppcheck_path, options)
|
||||
|
||||
if expected is None:
|
||||
print(run_ec)
|
||||
print(run_stdout)
|
||||
print(run_stderr)
|
||||
|
||||
# if no ec is set we encountered an unexpected error
|
||||
if run_ec is None:
|
||||
sys.exit(EC_SKIP) # error occured
|
||||
|
||||
# check output for expected string
|
||||
if expected is not None:
|
||||
if (expected not in run_stderr) and (expected not in run_stdout):
|
||||
sys.exit(EC_BAD) # output not found occured
|
||||
|
||||
sys.exit(EC_GOOD) # output found
|
||||
|
||||
sys.exit(run_ec) # return the elapsed time - not a result for bisect
|
Loading…
Reference in New Issue