diff --git a/src/mlx/warnings/regex_checker.py b/src/mlx/warnings/regex_checker.py index 8b7f1c16..19fbbe77 100644 --- a/src/mlx/warnings/regex_checker.py +++ b/src/mlx/warnings/regex_checker.py @@ -78,7 +78,13 @@ class CoverityChecker(RegexChecker): def __init__(self, verbose=False): super().__init__(verbose) self._cq_description_template = Template('Coverity: $checker') - self.checkers = {} + self.checkers = { + "unclassified": CoverityClassificationChecker("unclassified", verbose=self.verbose), + "pending": CoverityClassificationChecker("pending", verbose=self.verbose), + "bug": CoverityClassificationChecker("bug", verbose=self.verbose), + "intentional": CoverityClassificationChecker("intentional", verbose=self.verbose), + "false positive": CoverityClassificationChecker("false positive", verbose=self.verbose), + } @property def counted_warnings(self): @@ -140,15 +146,14 @@ def check(self, content): matches = re.finditer(self.pattern, content) for match in matches: if (classification := match.group("classification").lower()) in self.checkers: - self.checkers[classification].check(match) - else: - checker = CoverityClassificationChecker(classification=classification, verbose=self.verbose) - self.checkers[classification] = checker + checker = self.checkers[classification] checker.cq_enabled = self.cq_enabled checker.exclude_patterns = self.exclude_patterns checker.cq_description_template = self.cq_description_template checker.cq_default_path = self.cq_default_path checker.check(match) + else: + print(f"WARNING: Unrecognized classification {match.group('classification')!r}") def parse_config(self, config): """Process configuration @@ -165,22 +170,11 @@ def parse_config(self, config): self.add_patterns(value, self.exclude_patterns) for classification, checker_config in config.items(): classification_key = classification.lower().replace("_", " ") - if classification_key in CoverityClassificationChecker.SEVERITY_MAP: - checker = CoverityClassificationChecker(classification=classification_key, verbose=self.verbose) - if maximum := checker_config.get("max", 0): - checker.maximum = int(maximum) - if minimum := checker_config.get("min", 0): - checker.minimum = int(minimum) - self.checkers[classification_key] = checker + if classification_key in self.checkers: + self.checkers[classification_key].parse_config(checker_config) else: print(f"WARNING: Unrecognized classification {classification!r}") - for checker in self.checkers.values(): - checker.cq_enabled = self.cq_enabled - checker.exclude_patterns = self.exclude_patterns - checker.cq_description_template = self.cq_description_template - checker.cq_default_path = self.cq_default_path - class CoverityClassificationChecker(WarningsChecker): SEVERITY_MAP = { diff --git a/src/mlx/warnings/warnings.py b/src/mlx/warnings/warnings.py index 1ed6677e..4357b56e 100644 --- a/src/mlx/warnings/warnings.py +++ b/src/mlx/warnings/warnings.py @@ -118,6 +118,8 @@ def check_logfile(self, file): if not self.activated_checkers: print("No checkers activated. Please use activate_checker function") elif "polyspace" in self.activated_checkers: + if len(self.activated_checkers) > 1: + raise WarningsConfigError("Polyspace checker cannot be combined with other warnings checkers") self.activated_checkers["polyspace"].check(file) else: content = file.read() diff --git a/tests/test_coverity.py b/tests/test_coverity.py index de8063d3..f43c4af3 100644 --- a/tests/test_coverity.py +++ b/tests/test_coverity.py @@ -1,8 +1,8 @@ +import filecmp +import os from io import StringIO -from unittest import TestCase from pathlib import Path -import filecmp - +from unittest import TestCase, mock from unittest.mock import patch from mlx.warnings import WarningsPlugin, warnings_wrapper, Finding @@ -11,6 +11,16 @@ TEST_OUT_DIR = Path(__file__).parent / 'test_out' +def ordered(obj): + if isinstance(obj, dict): + return sorted((k, ordered(v)) for k, v in obj.items()) + if isinstance(obj, list): + return sorted(ordered(x) for x in obj) + else: + return obj + + +@mock.patch.dict(os.environ, {"MIN_COV_WARNINGS": "1", "MAX_COV_WARNINGS": "2"}) class TestCoverityWarnings(TestCase): def setUp(self): Finding.fingerprints = {} diff --git a/tests/test_in/code_quality_format.json b/tests/test_in/code_quality_format.json index 634076ae..ad48c5ad 100644 --- a/tests/test_in/code_quality_format.json +++ b/tests/test_in/code_quality_format.json @@ -39,7 +39,7 @@ } } }, - "fingerprint": "8c59fc98f57e808819b5f04054aa5de3" + "fingerprint": "39030b49f58180c8172732f84d9d5fff" }, { "severity": "critical", @@ -53,7 +53,7 @@ } } }, - "fingerprint": "77c88c2e8519e4416776e8b96284e144" + "fingerprint": "e24313e32a3969502b2bc7c9ad9b1527" }, { "severity": "major", @@ -67,7 +67,7 @@ } } }, - "fingerprint": "a48d5550fd854f154597077dc51d1f8a" + "fingerprint": "bf26b89fc5898e957279a23a9540640f" }, { "severity": "critical", @@ -81,7 +81,7 @@ } } }, - "fingerprint": "22d31cc45bf8d96df2f1174df53d1adc" + "fingerprint": "6ca9fab3cdb5050ad01b2b02ec348180" }, { "severity": "critical", @@ -95,7 +95,7 @@ } } }, - "fingerprint": "041265549f9d63651d00019265a3ad90" + "fingerprint": "322c085b4098450c371da8c99046476b" }, { "severity": "major", @@ -109,7 +109,7 @@ } } }, - "fingerprint": "75e6473e0ee239e0f0b7094b12404afc" + "fingerprint": "bec264a0725556cfe0514b3aa168715c" }, { "severity": "major", @@ -123,6 +123,6 @@ } } }, - "fingerprint": "3c02f687278c969c497d8974994e8f76" + "fingerprint": "37dd4fe77518650ac6d416ea8e2e617f" } ] diff --git a/tests/test_in/config_example_coverity.yml b/tests/test_in/config_example_coverity.yml index 52f1eba5..da3b4f0d 100644 --- a/tests/test_in/config_example_coverity.yml +++ b/tests/test_in/config_example_coverity.yml @@ -1,14 +1,19 @@ coverity: enabled: true + unclassified: + min: $MIN_COV_WARNINGS + max: '$MAX_COV_WARNINGS' intentional: min: 0 max: -1 bug: + min: 0 max: 0 pending: min: 0 max: 0 false_positive: + min: 0 max: -1 sphinx: enabled: false diff --git a/tests/test_in/config_example_polyspace_error.yml b/tests/test_in/config_example_polyspace_error.yml new file mode 100644 index 00000000..db7e1ca9 --- /dev/null +++ b/tests/test_in/config_example_polyspace_error.yml @@ -0,0 +1,43 @@ +sphinx: + enabled: true + min: 0 + max: 0 + exclude: + - RemovedInSphinx\d+Warning + - 'WARNING: toctree' +doxygen: + enabled: false +junit: + enabled: false +xmlrunner: + enabled: false +coverity: + enabled: false +robot: + enabled: false +polyspace: + enabled: true + run-time check: + - color: red + min: 0 + max: 0 + - color: orange + min: 0 + max: 10 + global variable: + - color: red + min: 0 + max: 0 + - color: orange + min: 0 + max: 10 + defect: + - information: 'impact: high' + min: 0 + max: 0 + - information: 'impact: medium' + min: 0 + max: 10 + - information: 'impact: low' + min: 0 + max: 30 diff --git a/tests/test_in/coverity_cq.json b/tests/test_in/coverity_cq.json index b83ffae6..f96a95dd 100644 --- a/tests/test_in/coverity_cq.json +++ b/tests/test_in/coverity_cq.json @@ -1,4 +1,46 @@ [ + { + "severity": "major", + "description": "Coverity: MISRA C-2012 Declarations and Definitions (MISRA C-2012 Rule 8.5, Required)", + "location": { + "path": "some/path/dummy_uncl.h", + "positions": { + "begin": { + "line": 194, + "column": 14 + } + } + }, + "fingerprint": "990f3714acdb07ca108f645285bacdf8" + }, + { + "severity": "major", + "description": "Coverity: MISRA C-2012 Declarations and Definitions (MISRA C-2012 Rule 8.6, Required)", + "location": { + "path": "some/path/dummy_uncl.c", + "positions": { + "begin": { + "line": 1404, + "column": 14 + } + } + }, + "fingerprint": "c24f5c885dd07424839f347e7fb0cfd9" + }, + { + "severity": "major", + "description": "Coverity: MISRA C-2012 Identifiers (MISRA C-2012 Rule 5.8, Required)", + "location": { + "path": "some/path/dummy_uncl.c", + "positions": { + "begin": { + "line": 923, + "column": 13 + } + } + }, + "fingerprint": "1f2f1b28535924cd9ab0dc2635aa70c7" + }, { "severity": "info", "description": "Coverity: MISRA C-2012 Standard C Environment (MISRA C-2012 Rule 1.2, Advisory)", @@ -68,47 +110,5 @@ } }, "fingerprint": "5dbd93275d026e6b77330c48eda8d964" - }, - { - "severity": "major", - "description": "Coverity: MISRA C-2012 Declarations and Definitions (MISRA C-2012 Rule 8.5, Required)", - "location": { - "path": "some/path/dummy_uncl.h", - "positions": { - "begin": { - "line": 194, - "column": 14 - } - } - }, - "fingerprint": "990f3714acdb07ca108f645285bacdf8" - }, - { - "severity": "major", - "description": "Coverity: MISRA C-2012 Declarations and Definitions (MISRA C-2012 Rule 8.6, Required)", - "location": { - "path": "some/path/dummy_uncl.c", - "positions": { - "begin": { - "line": 1404, - "column": 14 - } - } - }, - "fingerprint": "c24f5c885dd07424839f347e7fb0cfd9" - }, - { - "severity": "major", - "description": "Coverity: MISRA C-2012 Identifiers (MISRA C-2012 Rule 5.8, Required)", - "location": { - "path": "some/path/dummy_uncl.c", - "positions": { - "begin": { - "line": 923, - "column": 13 - } - } - }, - "fingerprint": "1f2f1b28535924cd9ab0dc2635aa70c7" } ] diff --git a/tests/test_integration.py b/tests/test_integration.py index 926400bf..f840e208 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -6,7 +6,7 @@ from unittest.mock import patch -from mlx.warnings import warnings_wrapper, WarningsConfigError +from mlx.warnings import exceptions, warnings_wrapper, WarningsConfigError, Finding TEST_IN_DIR = Path(__file__).parent / 'test_in' TEST_OUT_DIR = Path(__file__).parent / 'test_out' @@ -14,6 +14,7 @@ class TestIntegration(TestCase): def setUp(self): + Finding.fingerprints = {} if not TEST_OUT_DIR.exists(): TEST_OUT_DIR.mkdir() @@ -368,10 +369,25 @@ def test_cq_description_format(self, path_cwd_mock): filename = 'code_quality_format.json' out_file = str(TEST_OUT_DIR / filename) ref_file = str(TEST_IN_DIR / filename) - retval = warnings_wrapper([ - '--code-quality', out_file, - '--config', 'tests/test_in/config_cq_description_format.json', - 'tests/test_in/mixed_warnings.txt', - ]) + with patch('sys.stdout', new=StringIO()) as fake_output: + retval = warnings_wrapper([ + '--code-quality', out_file, + '--config', 'tests/test_in/config_cq_description_format.json', + 'tests/test_in/mixed_warnings.txt', + ]) + output = fake_output.getvalue().splitlines(keepends=False) + self.assertIn("WARNING: Unrecognized classification 'max'", output) + self.assertIn("WARNING: Unrecognized classification 'min'", output) self.assertEqual(2, retval) self.assertTrue(filecmp.cmp(out_file, ref_file), '{} differs from {}'.format(out_file, ref_file)) + + @patch('pathlib.Path.cwd') + def test_polyspace_error(self, path_cwd_mock): + config_file = str(TEST_IN_DIR / 'config_example_polyspace_error.yml') + with self.assertRaises(exceptions.WarningsConfigError) as context: + warnings_wrapper([ + '--config', config_file, + 'tests/test_in/mixed_warnings.txt', + ]) + self.assertEqual(str(context.exception), 'Polyspace checker cannot be combined with other warnings checkers') + diff --git a/tests/test_polyspace.py b/tests/test_polyspace.py index e45ad3fd..9779ba51 100644 --- a/tests/test_polyspace.py +++ b/tests/test_polyspace.py @@ -14,6 +14,7 @@ class TestCodeProverWarnings(unittest.TestCase): def setUp(self): + Finding.fingerprints = {} self.warnings = WarningsPlugin(verbose=True) self.dut = self.warnings.activate_checker_name('polyspace') self.dut.checkers = [