Skip to content

Commit 933246d

Browse files
pfultz2Your Name
andauthored
Add unit tests for cppcheckdata.py (#8707)
This adds unit tests for cppcheckdata.py which runs with pytest. It will also run these tests with cmake automatically if python is available, so this should run on the CI. The unit tests did surface a couple of problems, but I didnt fix it this PR to keep the scope smaller. Instead I just marked the test with `pytest.mark.xfail` for now. I can do a follow up PR to fix these issues: 1. `Token.isBoolean` is dead code. `Tokenizer::dump` (lib/tokenize.cpp:6179) checks `tok->isName()` before `isBoolean()`, and `eBoolean` tokens count as names (lib/token.h:397), so true/false are always dumped as `type="name"` and the `type="boolean"` branch at tokenize.cpp:6199 is unreachable for them(I see there is `TODO: "true"/"false" aren't really a name...`) 2. Line suppressions match every line. The final "other suppression" fallback in `Suppression.isMatch()` (cppcheckdata.py:1007) doesn't check that `lineNumber` is unset, so a suppression for line 5 also matches line 6. The fix would be a one-line `self.lineNumber is None` guard. 3. In C++ `Token::Match`, `!!x` also matches when there is no token at all (null), but Python `match()` fails when the token list ends: `match(last_brace, '} !!x')` returns false. --------- Co-authored-by: Your Name <you@example.com>
1 parent 1a96ce8 commit 933246d

6 files changed

Lines changed: 1161 additions & 1 deletion

File tree

test/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ if (BUILD_TESTING)
4747
if (REGISTER_TESTS)
4848
# CMAKE_MATCH_<n> usage for if (MATCHES) requires CMake 3.9
4949

50+
add_subdirectory(addon)
51+
5052
find_package(Threads REQUIRED)
5153
include(ProcessorCount)
5254
ProcessorCount(N)

test/addon/CMakeLists.txt

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
if (NOT Python_Interpreter_FOUND)
2+
message(WARNING "Python interpreter not found - skipping addon tests.")
3+
return()
4+
endif()
5+
6+
# creating a virtual environment needs the venv and ensurepip modules - some
7+
# distributions ship them separately from the interpreter (e.g. the Debian/Ubuntu
8+
# python3-venv package)
9+
execute_process(COMMAND ${Python_EXECUTABLE} -c "import venv, ensurepip"
10+
RESULT_VARIABLE PYTHON_VENV_RESULT
11+
OUTPUT_QUIET
12+
ERROR_QUIET)
13+
if (NOT PYTHON_VENV_RESULT EQUAL 0)
14+
message(WARNING "Python venv module not available (e.g. install the python3-venv package) - skipping addon tests.")
15+
return()
16+
endif()
17+
18+
set(VENV_DIR ${CMAKE_CURRENT_BINARY_DIR}/venv)
19+
if (WIN32)
20+
set(VENV_PYTHON ${VENV_DIR}/Scripts/python.exe)
21+
else()
22+
set(VENV_PYTHON ${VENV_DIR}/bin/python)
23+
endif()
24+
25+
# fixture: create a virtual environment and install the python dependencies into it
26+
add_test(NAME addon-venv-create
27+
COMMAND ${Python_EXECUTABLE} -m venv ${VENV_DIR})
28+
set_tests_properties(addon-venv-create PROPERTIES
29+
FIXTURES_SETUP addon-venv-dir)
30+
31+
add_test(NAME addon-venv-install
32+
COMMAND ${VENV_PYTHON} -m pip install -r ${CMAKE_CURRENT_SOURCE_DIR}/requirements.txt)
33+
set_tests_properties(addon-venv-install PROPERTIES
34+
FIXTURES_REQUIRED addon-venv-dir
35+
FIXTURES_SETUP addon-venv
36+
TIMEOUT 300)
37+
38+
add_test(NAME addon-cppcheckdata
39+
COMMAND ${VENV_PYTHON} -m pytest --cppcheck-binary=$<TARGET_FILE:cppcheck> ${CMAKE_CURRENT_SOURCE_DIR})
40+
set_tests_properties(addon-cppcheckdata PROPERTIES
41+
FIXTURES_REQUIRED addon-venv)

test/addon/conftest.py

Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
"""pytest configuration for the addon tests.
2+
3+
The tests exercise addons/cppcheckdata.py against dump files that are
4+
generated on the fly with the cppcheck binary given by --cppcheck-binary.
5+
"""
6+
import os
7+
import shutil
8+
import subprocess
9+
import sys
10+
11+
import pytest
12+
13+
# Make 'import cppcheckdata' resolve to <repo>/addons/cppcheckdata.py
14+
_ADDONS_DIR = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'addons'))
15+
if _ADDONS_DIR not in sys.path:
16+
sys.path.insert(0, _ADDONS_DIR)
17+
18+
19+
def pytest_addoption(parser):
20+
parser.addoption('--cppcheck-binary',
21+
default='cppcheck',
22+
help='path to the cppcheck binary used to generate dump files '
23+
'(default: cppcheck found in PATH)')
24+
25+
26+
@pytest.fixture(scope='session')
27+
def cppcheck_binary(request):
28+
binary = request.config.getoption('--cppcheck-binary')
29+
resolved = shutil.which(binary)
30+
if resolved is None:
31+
pytest.fail("cppcheck binary '%s' not found - point --cppcheck-binary at a cppcheck executable" % binary)
32+
return os.path.abspath(resolved)
33+
34+
35+
class DumpFactory:
36+
"""Runs 'cppcheck --dump' on a source snippet and parses the result."""
37+
38+
def __init__(self, binary, tmp_path_factory):
39+
self.binary = binary
40+
self.tmp_path_factory = tmp_path_factory
41+
42+
def create(self, code, filename='test.c', extra_args=()):
43+
"""Write the code to a file, dump it and return the dump file path."""
44+
directory = self.tmp_path_factory.mktemp('cppcheckdata')
45+
path = directory / filename
46+
path.write_text(code)
47+
cmd = [self.binary, '--dump', '--quiet', str(path)] + list(extra_args)
48+
proc = subprocess.run(cmd,
49+
stdout=subprocess.PIPE,
50+
stderr=subprocess.PIPE,
51+
check=True,
52+
universal_newlines=True)
53+
assert proc.returncode == 0, \
54+
'cppcheck failed with exit code %d:\n%s\n%s' % (proc.returncode, proc.stdout, proc.stderr)
55+
return str(path) + '.dump'
56+
57+
def parse(self, code, filename='test.c', extra_args=()):
58+
"""Write the code to a file, dump it and return the parsed CppcheckData."""
59+
import cppcheckdata
60+
return cppcheckdata.parsedump(self.create(code, filename, extra_args))
61+
62+
63+
@pytest.fixture(scope='session')
64+
def dump_factory(cppcheck_binary, tmp_path_factory):
65+
return DumpFactory(cppcheck_binary, tmp_path_factory)
66+
67+
68+
SAMPLE_C = """#define ANSWER 42
69+
static int add(int a, int b)
70+
{
71+
return a + b;
72+
}
73+
74+
double half(double d)
75+
{
76+
return d / 2.0;
77+
}
78+
79+
int main(void)
80+
{
81+
int x = ANSWER;
82+
int arr[10];
83+
arr[0] = add(x, 1);
84+
int neg = -x;
85+
return arr[0] + neg;
86+
}
87+
"""
88+
89+
SAMPLE_CPP = """namespace ns {
90+
int twice(int v) { return 2 * v; }
91+
}
92+
93+
class Shape {
94+
public:
95+
virtual ~Shape() {}
96+
virtual double area() const = 0;
97+
protected:
98+
double mScale;
99+
};
100+
101+
enum Color { RED, GREEN };
102+
103+
const int limit = 5;
104+
bool flag = true;
105+
const char *msg = "hello";
106+
char ch = 'x';
107+
108+
int run()
109+
{
110+
Color color = RED;
111+
if (flag && limit > 1) {
112+
return ns::twice(21);
113+
}
114+
return static_cast<int>(color);
115+
}
116+
"""
117+
118+
MULTI_CFG_C = """typedef int myint;
119+
typedef float myfloat;
120+
#if defined(FOO) && FOO > 1
121+
int foo(void) { return 1; }
122+
#endif
123+
myint bar(void) { myint y = 3; return y; }
124+
"""
125+
126+
MATCH_C = """struct Point {
127+
int x;
128+
int y;
129+
};
130+
131+
int calc(int a, int b)
132+
{
133+
int bit_or = a | b;
134+
int log_or = a || b;
135+
int mod = a % b;
136+
int mul = a * b;
137+
int not_a = !a;
138+
int neq = a != b;
139+
int arr[3];
140+
arr[0] = a;
141+
a += 1;
142+
if (a > b) {
143+
return a;
144+
}
145+
return calc(a, b);
146+
}
147+
"""
148+
149+
MATCH_CPP = """class Widget {};
150+
151+
bool use(int i, int j)
152+
{
153+
std::vector<int> v;
154+
bool less = i < j;
155+
return less && v.empty();
156+
}
157+
"""
158+
159+
160+
@pytest.fixture(scope='session')
161+
def sample_data(dump_factory):
162+
"""Parsed dump of the canonical C sample."""
163+
return dump_factory.parse(SAMPLE_C, filename='sample.c')
164+
165+
166+
@pytest.fixture(scope='session')
167+
def sample_cfg(sample_data):
168+
cfgs = sample_data.configurations
169+
assert len(cfgs) == 1
170+
return cfgs[0]
171+
172+
173+
@pytest.fixture(scope='session')
174+
def sample_cpp_data(dump_factory):
175+
"""Parsed dump of the canonical C++ sample."""
176+
return dump_factory.parse(SAMPLE_CPP, filename='sample.cpp')
177+
178+
179+
@pytest.fixture(scope='session')
180+
def sample_cpp_cfg(sample_cpp_data):
181+
cfgs = sample_cpp_data.configurations
182+
assert len(cfgs) == 1
183+
return cfgs[0]
184+
185+
186+
@pytest.fixture(scope='session')
187+
def multi_cfg_data(dump_factory):
188+
"""Parsed dump of a file with two preprocessor configurations."""
189+
return dump_factory.parse(MULTI_CFG_C, filename='multi.c')
190+
191+
192+
@pytest.fixture(scope='session')
193+
def match_cfg(dump_factory):
194+
"""Configuration of a C sample covering the match() pattern syntax."""
195+
return dump_factory.parse(MATCH_C, filename='match.c').configurations[0]
196+
197+
198+
@pytest.fixture(scope='session')
199+
def match_cpp_cfg(dump_factory):
200+
"""Configuration of a C++ sample with linked '<' tokens for match()."""
201+
return dump_factory.parse(MATCH_CPP, filename='match.cpp').configurations[0]

test/addon/requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pytest

0 commit comments

Comments
 (0)