pytest-cppcheck 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_cppcheck-0.1.0/PKG-INFO +10 -0
- pytest_cppcheck-0.1.0/README.md +97 -0
- pytest_cppcheck-0.1.0/pyproject.toml +24 -0
- pytest_cppcheck-0.1.0/setup.cfg +4 -0
- pytest_cppcheck-0.1.0/src/pytest_cppcheck/__init__.py +0 -0
- pytest_cppcheck-0.1.0/src/pytest_cppcheck/plugin.py +98 -0
- pytest_cppcheck-0.1.0/src/pytest_cppcheck.egg-info/PKG-INFO +10 -0
- pytest_cppcheck-0.1.0/src/pytest_cppcheck.egg-info/SOURCES.txt +11 -0
- pytest_cppcheck-0.1.0/src/pytest_cppcheck.egg-info/dependency_links.txt +1 -0
- pytest_cppcheck-0.1.0/src/pytest_cppcheck.egg-info/entry_points.txt +2 -0
- pytest_cppcheck-0.1.0/src/pytest_cppcheck.egg-info/requires.txt +2 -0
- pytest_cppcheck-0.1.0/src/pytest_cppcheck.egg-info/top_level.txt +1 -0
- pytest_cppcheck-0.1.0/tests/test_plugin.py +149 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-cppcheck
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A pytest plugin that runs cppcheck static analysis on C/C++ source files
|
|
5
|
+
Classifier: Framework :: Pytest
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Requires-Dist: pytest>=7.0
|
|
10
|
+
Requires-Dist: cppcheck>=1.4.0
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# pytest-cppcheck
|
|
2
|
+
|
|
3
|
+
A pytest plugin that runs [cppcheck](https://cppcheck.sourceforge.io/) static
|
|
4
|
+
analysis on C/C++ source files. Each file is collected as a test item and
|
|
5
|
+
reported as a pass or failure in the normal pytest output.
|
|
6
|
+
|
|
7
|
+
Useful for Python projects with C extension modules where you already run pytest
|
|
8
|
+
and want cppcheck findings surfaced in the same test run.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
pip install pytest-cppcheck
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This pulls in cppcheck automatically via the
|
|
17
|
+
[cppcheck](https://pypi.org/project/cppcheck/) PyPI package. If you prefer a
|
|
18
|
+
specific version, install cppcheck yourself via your system package manager
|
|
19
|
+
(`apt install cppcheck`, `brew install cppcheck`, etc.) — the plugin uses
|
|
20
|
+
whichever `cppcheck` is on PATH.
|
|
21
|
+
|
|
22
|
+
## Usage
|
|
23
|
+
|
|
24
|
+
The plugin does nothing unless explicitly enabled:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
pytest --cppcheck
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
This collects all `.c` and `.cpp` files and runs cppcheck on each one.
|
|
31
|
+
Files with findings fail; clean files pass.
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
PASSED src/clean.c::CPPCHECK
|
|
35
|
+
FAILED src/buggy.c::CPPCHECK
|
|
36
|
+
src/buggy.c:42:8: error: Array 'arr[10]' accessed at index 10, which is
|
|
37
|
+
out of bounds. [arrayIndexOutOfBounds]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
You can combine `--cppcheck` with your normal test run — Python tests and
|
|
41
|
+
cppcheck items appear together in the results.
|
|
42
|
+
|
|
43
|
+
## Configuration
|
|
44
|
+
|
|
45
|
+
All options go in `pyproject.toml`, `pytest.ini`, or `setup.cfg` under `[pytest]`.
|
|
46
|
+
|
|
47
|
+
### `cppcheck_args`
|
|
48
|
+
|
|
49
|
+
Extra arguments forwarded to every cppcheck invocation. This is the main
|
|
50
|
+
configuration surface — use it for `--enable`, `--suppress`, and any other
|
|
51
|
+
cppcheck flags. The plugin always passes `--quiet` and `--error-exitcode=1`
|
|
52
|
+
automatically.
|
|
53
|
+
|
|
54
|
+
With no `cppcheck_args`, cppcheck runs its default checks (mostly
|
|
55
|
+
error-severity). Use `--enable` to broaden coverage. A good starting
|
|
56
|
+
configuration:
|
|
57
|
+
|
|
58
|
+
```ini
|
|
59
|
+
[pytest]
|
|
60
|
+
cppcheck_args =
|
|
61
|
+
--enable=warning,style,performance,portability
|
|
62
|
+
--suppress=missingIncludeSystem
|
|
63
|
+
--suppress=normalCheckLevelMaxBranches
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`missingIncludeSystem` suppresses noise about system headers that aren't
|
|
67
|
+
available to cppcheck. `normalCheckLevelMaxBranches` suppresses an
|
|
68
|
+
informational message that cppcheck emits on complex files and that would
|
|
69
|
+
otherwise be reported as a failure.
|
|
70
|
+
|
|
71
|
+
### `cppcheck_extensions`
|
|
72
|
+
|
|
73
|
+
File extensions to collect. Default: `.c .cpp`.
|
|
74
|
+
|
|
75
|
+
```ini
|
|
76
|
+
[pytest]
|
|
77
|
+
cppcheck_extensions = .c .cpp .h
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Caching
|
|
81
|
+
|
|
82
|
+
Results are cached based on file modification time and `cppcheck_args`. On
|
|
83
|
+
subsequent runs, files that previously passed are skipped. The cache is
|
|
84
|
+
automatically invalidated when a file is modified or `cppcheck_args` changes.
|
|
85
|
+
Caching relies on pytest's built-in cache provider (the `.pytest_cache` directory).
|
|
86
|
+
If the cache provider is disabled (for example with `-p no:cacheprovider`), results
|
|
87
|
+
will not be cached and all files will be re-checked on each run.
|
|
88
|
+
|
|
89
|
+
To force a full re-check:
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
pytest --cppcheck --cache-clear
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pytest-cppcheck"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A pytest plugin that runs cppcheck static analysis on C/C++ source files"
|
|
9
|
+
requires-python = ">=3.8"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"pytest>=7.0",
|
|
12
|
+
"cppcheck>=1.4.0",
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Framework :: Pytest",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.entry-points."pytest11"]
|
|
21
|
+
cppcheck = "pytest_cppcheck.plugin"
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["src"]
|
|
File without changes
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
from cppcheck import get_cppcheck_dir
|
|
5
|
+
|
|
6
|
+
CPPCHECK_BIN = str(get_cppcheck_dir() / "cppcheck")
|
|
7
|
+
CACHE_KEY = "cppcheck/mtimes"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def pytest_addoption(parser):
|
|
11
|
+
group = parser.getgroup("cppcheck", "cppcheck static analysis")
|
|
12
|
+
group.addoption(
|
|
13
|
+
"--cppcheck",
|
|
14
|
+
action="store_true",
|
|
15
|
+
default=False,
|
|
16
|
+
help="run cppcheck on C/C++ source files",
|
|
17
|
+
)
|
|
18
|
+
parser.addini(
|
|
19
|
+
"cppcheck_extensions",
|
|
20
|
+
type="args",
|
|
21
|
+
default=[".c", ".cpp"],
|
|
22
|
+
help="file extensions to collect for cppcheck (default: .c .cpp)",
|
|
23
|
+
)
|
|
24
|
+
parser.addini(
|
|
25
|
+
"cppcheck_args",
|
|
26
|
+
type="args",
|
|
27
|
+
default=[],
|
|
28
|
+
help="extra arguments forwarded to every cppcheck invocation",
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def pytest_configure(config):
|
|
33
|
+
if not config.getoption("cppcheck"):
|
|
34
|
+
return
|
|
35
|
+
cache = getattr(config, "cache", None)
|
|
36
|
+
config._cppcheck_mtimes = cache.get(CACHE_KEY, {}) if cache else {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def pytest_unconfigure(config):
|
|
40
|
+
mtimes = getattr(config, "_cppcheck_mtimes", None)
|
|
41
|
+
if mtimes is None:
|
|
42
|
+
return
|
|
43
|
+
cache = getattr(config, "cache", None)
|
|
44
|
+
if cache:
|
|
45
|
+
cache.set(CACHE_KEY, mtimes)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def pytest_collect_file(parent, file_path):
|
|
49
|
+
if not parent.config.getoption("cppcheck"):
|
|
50
|
+
return None
|
|
51
|
+
extensions = parent.config.getini("cppcheck_extensions")
|
|
52
|
+
if file_path.suffix in extensions:
|
|
53
|
+
return CppcheckFile.from_parent(parent, path=file_path)
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class CppcheckError(Exception):
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class CppcheckFile(pytest.File):
|
|
62
|
+
def collect(self):
|
|
63
|
+
yield CppcheckItem.from_parent(self, name="CPPCHECK")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class CppcheckItem(pytest.Item):
|
|
67
|
+
def setup(self):
|
|
68
|
+
mtimes = getattr(self.config, "_cppcheck_mtimes", {})
|
|
69
|
+
self._mtime = self.path.stat().st_mtime_ns
|
|
70
|
+
args = self.config.getini("cppcheck_args")
|
|
71
|
+
old = mtimes.get(str(self.path))
|
|
72
|
+
if old == [self._mtime, args]:
|
|
73
|
+
pytest.skip("previously passed cppcheck")
|
|
74
|
+
|
|
75
|
+
def runtest(self):
|
|
76
|
+
args = self.config.getini("cppcheck_args")
|
|
77
|
+
cmd = [CPPCHECK_BIN, "--quiet", "--error-exitcode=1"] + args + [str(self.path)]
|
|
78
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
79
|
+
if result.returncode != 0:
|
|
80
|
+
output = result.stderr or result.stdout
|
|
81
|
+
if not output:
|
|
82
|
+
output = f"cppcheck exited with code {result.returncode}"
|
|
83
|
+
raise CppcheckError(output)
|
|
84
|
+
# Cache only on success
|
|
85
|
+
if hasattr(self.config, "_cppcheck_mtimes"):
|
|
86
|
+
self._mtime = getattr(self, "_mtime", self.path.stat().st_mtime_ns)
|
|
87
|
+
self.config._cppcheck_mtimes[str(self.path)] = [
|
|
88
|
+
self._mtime,
|
|
89
|
+
args,
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
def repr_failure(self, excinfo):
|
|
93
|
+
if excinfo.errisinstance(CppcheckError):
|
|
94
|
+
return str(excinfo.value)
|
|
95
|
+
return super().repr_failure(excinfo)
|
|
96
|
+
|
|
97
|
+
def reportinfo(self):
|
|
98
|
+
return self.path, None, f"{self.path}::CPPCHECK"
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-cppcheck
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A pytest plugin that runs cppcheck static analysis on C/C++ source files
|
|
5
|
+
Classifier: Framework :: Pytest
|
|
6
|
+
Classifier: Programming Language :: Python :: 3
|
|
7
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
8
|
+
Requires-Python: >=3.8
|
|
9
|
+
Requires-Dist: pytest>=7.0
|
|
10
|
+
Requires-Dist: cppcheck>=1.4.0
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/pytest_cppcheck/__init__.py
|
|
4
|
+
src/pytest_cppcheck/plugin.py
|
|
5
|
+
src/pytest_cppcheck.egg-info/PKG-INFO
|
|
6
|
+
src/pytest_cppcheck.egg-info/SOURCES.txt
|
|
7
|
+
src/pytest_cppcheck.egg-info/dependency_links.txt
|
|
8
|
+
src/pytest_cppcheck.egg-info/entry_points.txt
|
|
9
|
+
src/pytest_cppcheck.egg-info/requires.txt
|
|
10
|
+
src/pytest_cppcheck.egg-info/top_level.txt
|
|
11
|
+
tests/test_plugin.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pytest_cppcheck
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"""Tests for pytest-cppcheck using pytester."""
|
|
2
|
+
|
|
3
|
+
# C source with an out-of-bounds access that cppcheck detects.
|
|
4
|
+
C_ERROR = """\
|
|
5
|
+
#include <stdlib.h>
|
|
6
|
+
int main() {
|
|
7
|
+
int arr[10];
|
|
8
|
+
arr[10] = 0;
|
|
9
|
+
return 0;
|
|
10
|
+
}
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
# C source with no issues.
|
|
14
|
+
C_CLEAN = """\
|
|
15
|
+
int main() {
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_clean_file_passes(pytester):
|
|
22
|
+
pytester.makefile(".c", clean=C_CLEAN)
|
|
23
|
+
result = pytester.runpytest("--cppcheck", "-v")
|
|
24
|
+
result.stdout.fnmatch_lines(["*PASSED*"])
|
|
25
|
+
result.assert_outcomes(passed=1)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_error_file_fails(pytester):
|
|
29
|
+
pytester.makeini(
|
|
30
|
+
"[pytest]\n"
|
|
31
|
+
"cppcheck_args = --enable=warning\n"
|
|
32
|
+
)
|
|
33
|
+
pytester.makefile(".c", bad=C_ERROR)
|
|
34
|
+
result = pytester.runpytest("--cppcheck")
|
|
35
|
+
result.assert_outcomes(failed=1)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_stderr_in_failure_output(pytester):
|
|
39
|
+
pytester.makeini(
|
|
40
|
+
"[pytest]\n"
|
|
41
|
+
"cppcheck_args = --enable=warning\n"
|
|
42
|
+
)
|
|
43
|
+
pytester.makefile(".c", bad=C_ERROR)
|
|
44
|
+
result = pytester.runpytest("--cppcheck")
|
|
45
|
+
result.stdout.fnmatch_lines(["*arrayIndexOutOfBounds*"])
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_not_collected_without_flag(pytester):
|
|
49
|
+
pytester.makefile(".c", clean=C_CLEAN)
|
|
50
|
+
result = pytester.runpytest()
|
|
51
|
+
result.assert_outcomes()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_custom_extensions(pytester):
|
|
55
|
+
pytester.makeini(
|
|
56
|
+
"[pytest]\n"
|
|
57
|
+
"cppcheck_extensions = .h\n"
|
|
58
|
+
)
|
|
59
|
+
pytester.makefile(".h", header=C_CLEAN)
|
|
60
|
+
pytester.makefile(".c", also_clean=C_CLEAN)
|
|
61
|
+
pytester.makefile(".cpp", also_also_clean=C_CLEAN)
|
|
62
|
+
result = pytester.runpytest("--cppcheck")
|
|
63
|
+
# only .h collected, not .c or .cpp
|
|
64
|
+
result.assert_outcomes(passed=1)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_cpp_collected_by_default(pytester):
|
|
68
|
+
pytester.makefile(".cpp", clean=C_CLEAN)
|
|
69
|
+
result = pytester.runpytest("--cppcheck")
|
|
70
|
+
result.assert_outcomes(passed=1)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_quiet_flag_always_passed(pytester):
|
|
74
|
+
pytester.makefile(".c", clean=C_CLEAN)
|
|
75
|
+
result = pytester.runpytest("--cppcheck", "-v")
|
|
76
|
+
result.assert_outcomes(passed=1)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_cppcheck_args_forwarded(pytester):
|
|
80
|
+
"""Suppress the specific error so the file passes despite the bug."""
|
|
81
|
+
pytester.makeini(
|
|
82
|
+
"[pytest]\n"
|
|
83
|
+
"cppcheck_args = --enable=warning --suppress=arrayIndexOutOfBounds\n"
|
|
84
|
+
)
|
|
85
|
+
pytester.makefile(".c", bad=C_ERROR)
|
|
86
|
+
result = pytester.runpytest("--cppcheck")
|
|
87
|
+
result.assert_outcomes(passed=1)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_cache_skips_on_second_run(pytester):
|
|
91
|
+
pytester.makefile(".c", clean=C_CLEAN)
|
|
92
|
+
# First run: passes and populates cache
|
|
93
|
+
result = pytester.runpytest("--cppcheck", "-p", "cacheprovider")
|
|
94
|
+
result.assert_outcomes(passed=1)
|
|
95
|
+
# Second run: skipped via cache
|
|
96
|
+
result = pytester.runpytest("--cppcheck", "-p", "cacheprovider")
|
|
97
|
+
result.assert_outcomes(skipped=1)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_cache_reruns_after_file_change(pytester):
|
|
101
|
+
import os
|
|
102
|
+
path = pytester.makefile(".c", clean=C_CLEAN)
|
|
103
|
+
# First run
|
|
104
|
+
result = pytester.runpytest("--cppcheck", "-p", "cacheprovider")
|
|
105
|
+
result.assert_outcomes(passed=1)
|
|
106
|
+
# Bump mtime explicitly to avoid filesystem resolution issues
|
|
107
|
+
st = path.stat()
|
|
108
|
+
os.utime(path, ns=(st.st_atime_ns, st.st_mtime_ns + 1_000_000_000))
|
|
109
|
+
# Second run: re-checked because mtime changed
|
|
110
|
+
result = pytester.runpytest("--cppcheck", "-p", "cacheprovider")
|
|
111
|
+
result.assert_outcomes(passed=1)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_cache_reruns_after_args_change(pytester):
|
|
115
|
+
pytester.makefile(".c", clean=C_CLEAN)
|
|
116
|
+
# First run
|
|
117
|
+
result = pytester.runpytest("--cppcheck", "-p", "cacheprovider")
|
|
118
|
+
result.assert_outcomes(passed=1)
|
|
119
|
+
# Second run with different args: re-checked
|
|
120
|
+
pytester.makeini(
|
|
121
|
+
"[pytest]\n"
|
|
122
|
+
"cppcheck_args = --enable=warning\n"
|
|
123
|
+
)
|
|
124
|
+
result = pytester.runpytest("--cppcheck", "-p", "cacheprovider")
|
|
125
|
+
result.assert_outcomes(passed=1)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_cache_does_not_skip_failures(pytester):
|
|
129
|
+
pytester.makeini(
|
|
130
|
+
"[pytest]\n"
|
|
131
|
+
"cppcheck_args = --enable=warning\n"
|
|
132
|
+
)
|
|
133
|
+
pytester.makefile(".c", bad=C_ERROR)
|
|
134
|
+
# First run: fails
|
|
135
|
+
result = pytester.runpytest("--cppcheck", "-p", "cacheprovider")
|
|
136
|
+
result.assert_outcomes(failed=1)
|
|
137
|
+
# Second run: still fails (not cached)
|
|
138
|
+
result = pytester.runpytest("--cppcheck", "-p", "cacheprovider")
|
|
139
|
+
result.assert_outcomes(failed=1)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def test_cache_clear_forces_rerun(pytester):
|
|
143
|
+
pytester.makefile(".c", clean=C_CLEAN)
|
|
144
|
+
# First run: passes and populates cache
|
|
145
|
+
result = pytester.runpytest("--cppcheck", "-p", "cacheprovider")
|
|
146
|
+
result.assert_outcomes(passed=1)
|
|
147
|
+
# Second run with --cache-clear: re-checked
|
|
148
|
+
result = pytester.runpytest("--cppcheck", "--cache-clear", "-p", "cacheprovider")
|
|
149
|
+
result.assert_outcomes(passed=1)
|