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.
@@ -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"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
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,2 @@
1
+ [pytest11]
2
+ cppcheck = pytest_cppcheck.plugin
@@ -0,0 +1,2 @@
1
+ pytest>=7.0
2
+ cppcheck>=1.4.0
@@ -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)