diff --git a/docs/design.rst b/docs/design.rst index f43b9e03..fbcfdbd8 100644 --- a/docs/design.rst +++ b/docs/design.rst @@ -10,157 +10,172 @@ Software Design Class Diagram ============= -.. uml generated by `pyreverse mlx.warnings --filter ALL -o plantuml` +.. uml generated by `pyreverse mlx.warnings --filter PUB_ONLY -o plantuml` .. uml:: @startuml classes set namespaceSeparator none class "CoverityChecker" as mlx.warnings.regex_checker.CoverityChecker { - CLASSIFICATION : str - count - name : str - pattern - check(content) + checkers : dict + count : int + cq_default_path + cq_description_template + cq_findings + name : str + pattern + check(content) + parse_config(config) + return_check_limits() + return_count() + } + class "CoverityClassificationChecker" as mlx.warnings.regex_checker.CoverityClassificationChecker { + SEVERITY_MAP : dict + classification + count + cq_description_template + logging_fmt : str + name : str + add_code_quality_finding(match) + check(content) + } + class "DebugOnlyFilter" as mlx.warnings.warnings_checker.DebugOnlyFilter { + filter(record: logging.LogRecord) -> bool } class "DoxyChecker" as mlx.warnings.regex_checker.DoxyChecker { - name : str - pattern + name : str + pattern + } + class "Finding" as mlx.warnings.code_quality.Finding { + column + description + fingerprint + fingerprints : dict + line + path + severity + to_dict() } class "JUnitChecker" as mlx.warnings.junit_checker.JUnitChecker { - count - name : str - _check_testcase(testcase) - check(content) - prepare_tree(root_input) + count + name : str + name_repr + check(content) + prepare_tree(root_input) } class "PolyspaceChecker" as mlx.warnings.polyspace_checker.PolyspaceChecker { - _cq_description_template : Template - checkers : list - count : int - counted_warnings - cq_default_path - cq_description_template - maximum - minimum - name : str - __init__(verbose) - check(content) - parse_config(config) - return_check_limits() - return_count() + checkers : list + count : int + cq_default_path + cq_description_template + cq_findings + maximum + minimum + name : str + check(content) + parse_config(config) + return_check_limits() + return_count() } class "PolyspaceFamilyChecker" as mlx.warnings.polyspace_checker.PolyspaceFamilyChecker { - _cq_description_template - check_value - code_quality_severity : dict - column_name - count - cq_description_template - cq_findings : list - family_value - __init__(family_value, column_name, check_value) - add_code_quality_finding(row) - check(content) - return_count() + check_value + code_quality_severity : dict + column_name + count + cq_description_template + family_value + logging_fmt : str + name : str + add_code_quality_finding(row) + check(content) } class "RegexChecker" as mlx.warnings.regex_checker.RegexChecker { - SEVERITY_MAP : dict - count - name : str - pattern : NoneType - add_code_quality_finding(match) - check(content) + SEVERITY_MAP : dict + count + name : str + pattern : NoneType + add_code_quality_finding(match) + check(content) } class "RobotChecker" as mlx.warnings.robot_checker.RobotChecker { - checkers : list - count : int - counted_warnings - maximum - minimum - name : str - check(content) - parse_config(config) - return_check_limits() - return_count() + checkers : list + count : int + logging_fmt : str + maximum + minimum + name : str + check(content) + parse_config(config) + return_check_limits() + return_count() } class "RobotSuiteChecker" as mlx.warnings.robot_checker.RobotSuiteChecker { - check_suite_name : bool - is_valid_suite_name : bool - name - __init__(name, check_suite_name) - _check_testcase(testcase) - check(content) - return_count() + check_suite_name : bool + is_valid_suite_name : bool + logging_fmt : str + name : str + suite_name + suite_name_repr + check(content) } class "SphinxChecker" as mlx.warnings.regex_checker.SphinxChecker { - name : str - pattern - sphinx_deprecation_regex : str - sphinx_deprecation_regex_in_match : str - include_sphinx_deprecation() + name : str + pattern + sphinx_deprecation_regex : str + sphinx_deprecation_regex_in_match : str + include_sphinx_deprecation() } class "WarningsChecker" as mlx.warnings.warnings_checker.WarningsChecker { - _counted_warnings : list - _cq_description_template : Template - _maximum : int - _minimum : int - count : int - counted_warnings - cq_default_path : str - cq_description_template - cq_enabled : bool - cq_findings : list - exclude_patterns : list - include_patterns : list - maximum - minimum - name : str - verbose : bool - __init__(verbose) - _is_excluded(content) - _return_error_code() - _search_patterns(content, patterns) - add_patterns(regexes, pattern_container) - {abstract}check(content) - parse_config(config) - print_when_verbose(message) - return_check_limits() - return_count() + count : int + cq_default_path : str + cq_description_template + cq_enabled : bool + cq_findings + exclude_patterns : list + include_patterns : list + is_sub_checker + logger : LoggerAdapter, NoneType, RootLogger + logging_args : tuple + logging_fmt : str + maximum + minimum + name : str + name_repr + add_patterns(regexes, pattern_container) + {abstract}check(content) + parse_config(config) + return_check_limits() + return_count() } class "WarningsConfigError" as mlx.warnings.exceptions.WarningsConfigError { } class "WarningsPlugin" as mlx.warnings.warnings.WarningsPlugin { - _maximum : int - _minimum : int - activated_checkers : dict - count : int - cq_enabled : bool - printout : bool - public_checkers : list - verbose : bool - __init__(verbose, config_file, cq_enabled) - activate_checker(checker) - activate_checker_name(name) - check(content) - check_logfile(file) - config_parser(config) - configure_maximum(maximum) - configure_minimum(minimum) - get_checker(name) - return_check_limits(name) - return_count(name) - toggle_printout(printout) - write_code_quality_report(out_file) - write_counted_warnings(out_file) + activated_checkers : dict + count : int + cq_enabled : bool + printout : bool + public_checkers : tuple + activate_checker(checker_type) + activate_checker_name(name) + check(content) + check_logfile(file) + config_parser(config) + configure_maximum(maximum) + configure_minimum(minimum) + get_checker(name) + return_check_limits(name) + return_count(name) + toggle_printout(printout) + write_code_quality_report(out_file) } class "XMLRunnerChecker" as mlx.warnings.regex_checker.XMLRunnerChecker { - name : str - pattern + name : str + pattern } mlx.warnings.junit_checker.JUnitChecker --|> mlx.warnings.warnings_checker.WarningsChecker mlx.warnings.polyspace_checker.PolyspaceChecker --|> mlx.warnings.warnings_checker.WarningsChecker mlx.warnings.polyspace_checker.PolyspaceFamilyChecker --|> mlx.warnings.warnings_checker.WarningsChecker mlx.warnings.regex_checker.CoverityChecker --|> mlx.warnings.regex_checker.RegexChecker + mlx.warnings.regex_checker.CoverityClassificationChecker --|> mlx.warnings.warnings_checker.WarningsChecker mlx.warnings.regex_checker.DoxyChecker --|> mlx.warnings.regex_checker.RegexChecker mlx.warnings.regex_checker.RegexChecker --|> mlx.warnings.warnings_checker.WarningsChecker mlx.warnings.regex_checker.SphinxChecker --|> mlx.warnings.regex_checker.RegexChecker diff --git a/src/mlx/warnings/__init__.py b/src/mlx/warnings/__init__.py index 041b104e..354f8b07 100644 --- a/src/mlx/warnings/__init__.py +++ b/src/mlx/warnings/__init__.py @@ -1,21 +1,21 @@ """ Melexis fork of warnings plugin """ __all__ = [ - 'CoverityChecker', - 'DoxyChecker', - 'Finding', - 'JUnitChecker', - 'PolyspaceChecker', - 'PolyspaceFamilyChecker', - 'RobotChecker', - 'RobotSuiteChecker', - 'SphinxChecker', - 'WarningsChecker', - 'WarningsPlugin', - 'XMLRunnerChecker', - '__version__', - 'warnings_wrapper', - 'WarningsConfigError', + "CoverityChecker", + "DoxyChecker", + "Finding", + "JUnitChecker", + "PolyspaceChecker", + "PolyspaceFamilyChecker", + "RobotChecker", + "RobotSuiteChecker", + "SphinxChecker", + "WarningsChecker", + "WarningsPlugin", + "XMLRunnerChecker", + "__version__", + "warnings_wrapper", + "WarningsConfigError", ] @@ -23,8 +23,8 @@ from .code_quality import Finding from .exceptions import WarningsConfigError from .junit_checker import JUnitChecker +from .polyspace_checker import PolyspaceChecker, PolyspaceFamilyChecker from .regex_checker import CoverityChecker, DoxyChecker, SphinxChecker, XMLRunnerChecker from .robot_checker import RobotChecker, RobotSuiteChecker -from .polyspace_checker import PolyspaceChecker, PolyspaceFamilyChecker from .warnings import WarningsPlugin, warnings_wrapper from .warnings_checker import WarningsChecker diff --git a/src/mlx/warnings/__main__.py b/src/mlx/warnings/__main__.py index 96650370..dfd4a76e 100644 --- a/src/mlx/warnings/__main__.py +++ b/src/mlx/warnings/__main__.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# -*- coding: utf-8 -*- # SPDX-License-Identifier: Apache-2.0 """ @@ -7,6 +6,5 @@ """ from mlx.warnings.warnings import main - -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/src/mlx/warnings/code_quality.py b/src/mlx/warnings/code_quality.py index dd0ef296..b73f9660 100644 --- a/src/mlx/warnings/code_quality.py +++ b/src/mlx/warnings/code_quality.py @@ -1,4 +1,3 @@ - import hashlib from pathlib import Path @@ -32,9 +31,9 @@ def fingerprint(self): 4. Step 3 is repeated until a unique hash is obtained. """ hashable_string = f"{self.severity}{self.path}{self.description}" - new_hash = hashlib.md5(str(hashable_string).encode('utf-8')).hexdigest() + new_hash = hashlib.md5(str(hashable_string).encode("utf-8")).hexdigest() while new_hash in self.fingerprints and self.fingerprints[new_hash] != self: - new_hash = hashlib.md5(f"{hashable_string}{new_hash}".encode('utf-8')).hexdigest() + new_hash = hashlib.md5(f"{hashable_string}{new_hash}".encode()).hexdigest() type(self).fingerprints[new_hash] = self return new_hash @@ -114,6 +113,3 @@ def to_dict(self): }, "fingerprint": self.fingerprint } - - - diff --git a/src/mlx/warnings/junit_checker.py b/src/mlx/warnings/junit_checker.py index fc7664f5..76be3400 100644 --- a/src/mlx/warnings/junit_checker.py +++ b/src/mlx/warnings/junit_checker.py @@ -11,16 +11,16 @@ class JUnitChecker(WarningsChecker): - name = 'junit' + name = "junit" def check(self, content): - ''' Function for counting the number of JUnit failures in a specific text + """Function for counting the number of JUnit failures in a specific text Args: content (str): The content to parse - ''' + """ try: - root_input = etree.fromstring(content.encode('utf-8')) + root_input = etree.fromstring(content.encode("utf-8")) testsuites_root = self.prepare_tree(root_input) suites = JUnitXml.fromelem(testsuites_root) amount_to_exclude = 0 @@ -30,19 +30,23 @@ def check(self, content): suites.update_statistics() self.count += suites.failures + suites.errors - amount_to_exclude except etree.ParseError as err: - print(err) + self.logger.error(err.msg) + + @property + def name_repr(self): + return "JUnit" if self.name == "junit" else super().name_repr @staticmethod def prepare_tree(root_input): - ''' Prepares the tree element by adding a testsuites element as root when missing (to please JUnitXml) + """Prepares the tree element by adding a testsuites element as root when missing (to please JUnitXml) Args: root_input (lxml.etree._Element/xml.etree.ElementTree.Element): Top-level XML element from input file Returns: lxml.etree._Element/xml.etree.ElementTree.Element: Top-level XML element with testsuites tag - ''' - if root_input.tag.startswith('testsuite') and root_input.find('testcase') is None: + """ + if root_input.tag.startswith("testsuite") and root_input.find("testcase") is None: testsuites_root = root_input else: testsuites_root = etree.Element("testsuites") @@ -50,10 +54,11 @@ def prepare_tree(root_input): return testsuites_root def _check_testcase(self, testcase): - """ Handles the check of a test case element by checking if the result is a failure/error. + """Handles the check of a test case element by checking if the result is a failure/error. If it is to be excluded by a configured regex, 1 is returned. - Otherwise, when in verbose mode, the suite name and test case name are printed. + Otherwise, when in verbose/output mode, the suite name and test case name are printed/written + In output mode, the failure/error message is written additionally. Args: testcase (junitparser.TestCase): Test case element to check for failure or error @@ -64,7 +69,6 @@ def _check_testcase(self, testcase): if isinstance(testcase.result, (Failure, Error)): if self._is_excluded(testcase.result.message): return 1 - string = '{classname}.{testname}'.format(classname=testcase.classname, testname=testcase.name) - self.counted_warnings.append('{}: {}'.format(string, testcase.result.message)) - self.print_when_verbose(string) + self.logger.info(f"{testcase.classname}.{testcase.name}") + self.logger.debug(f"{testcase.classname}.{testcase.name} | {testcase.result.message}") return 0 diff --git a/src/mlx/warnings/polyspace_checker.py b/src/mlx/warnings/polyspace_checker.py index afd3ab9d..25024a02 100644 --- a/src/mlx/warnings/polyspace_checker.py +++ b/src/mlx/warnings/polyspace_checker.py @@ -1,8 +1,8 @@ # SPDX-License-Identifier: Apache-2.0 import csv -from io import TextIOWrapper import os +from io import TextIOWrapper from string import Template from .code_quality import Finding @@ -11,32 +11,24 @@ class PolyspaceChecker(WarningsChecker): - name = 'polyspace' + name = "polyspace" checkers = [] - def __init__(self, verbose): + def __init__(self, *logging_args): '''Constructor to set the default code quality description template to "Polyspace: $check"''' - super().__init__(verbose) - self._cq_description_template = Template('Polyspace: $check') + super().__init__(*logging_args) + self._cq_description_template = Template("Polyspace: $check") @property def cq_findings(self): - ''' List[dict]: list of code quality findings''' + """List[dict]: list of code quality findings""" for checker in self.checkers: self._cq_findings.extend(checker.cq_findings) return self._cq_findings - @property - def counted_warnings(self): - '''List[str]: list of counted warnings''' - all_counted_warnings = [] - for checker in self.checkers: - all_counted_warnings.extend(checker.counted_warnings) - return all_counted_warnings - @property def cq_description_template(self): - ''' Template: string.Template instance based on the configured template string ''' + """Template: string.Template instance based on the configured template string""" return self._cq_description_template @cq_description_template.setter @@ -45,11 +37,11 @@ def cq_description_template(self, template_obj): @property def minimum(self): - ''' Gets the lowest minimum amount of warnings + """Gets the lowest minimum amount of warnings Returns: int: the lowest minimum for warnings - ''' + """ if self.checkers: return min(x.minimum for x in self.checkers) return 0 @@ -61,11 +53,11 @@ def minimum(self, minimum): @property def maximum(self): - ''' Gets the highest minimum amount of warnings + """Gets the highest minimum amount of warnings Returns: int: the highest maximum for warnings - ''' + """ if self.checkers: return max(x.maximum for x in self.checkers) return 0 @@ -76,50 +68,48 @@ def maximum(self, maximum): checker.maximum = maximum def check(self, content): - ''' + """ Function for counting the number of failures in a TSV file exported by Polyspace Args: content (_io.TextIOWrapper): The open file to parse - ''' + """ if not isinstance(content, TextIOWrapper): raise TypeError( f"{self.__class__.__name__} can't handle this type; expected {type(TextIOWrapper)}; got {type(content)}" ) - reader = csv.DictReader(content, dialect='excel-tab') + reader = csv.DictReader(content, dialect="excel-tab") # set column names to lowercase reader.fieldnames = [name.lower() for name in reader.fieldnames] for row in reader: for checker in self.checkers: - if row['family'].lower() == checker.family_value: + if row["family"].lower() == checker.family_value: checker.check(row) def return_count(self): - ''' Getter function for the amount of warnings found + """Getter function for the amount of warnings found Returns: int: Number of warnings found - ''' + """ self.count = 0 for checker in self.checkers: self.count += checker.return_count() return self.count def return_check_limits(self): - ''' Function for checking whether the warning count is within the configured limits + """Function for checking whether the warning count is within the configured limits Returns: int: 0 if the amount of warnings is within limits, the count of warnings otherwise (or 1 in case of a count of 0 warnings) - ''' + """ count = 0 for checker in self.checkers: - print( - 'Counted failures for family {!r} \'{}\': \'{}\'' - .format(checker.family_value, checker.column_name, checker.check_value) - ) count += checker.return_check_limits() + if count: + self.logger.warning(f"Returning error code {count}.") return count def parse_config(self, config): @@ -139,10 +129,10 @@ def parse_config(self, config): if family_value == "enabled": continue if family_value == "cq_description_template": - self.cq_description_template = Template(config['cq_description_template']) + self.cq_description_template = Template(config["cq_description_template"]) continue if family_value == "cq_default_path": - self.cq_default_path = config['cq_default_path'] + self.cq_default_path = config["cq_default_path"] continue if family_value == "exclude": self.add_patterns(config.get("exclude"), self.exclude_patterns) @@ -153,7 +143,7 @@ def parse_config(self, config): continue column_name = key.lower() check_value = value.lower() - checker = PolyspaceFamilyChecker(family_value, column_name, check_value, verbose=self.verbose) + checker = PolyspaceFamilyChecker(family_value, column_name, check_value, *self.logging_args) checker.parse_config(check) self.checkers.append(checker) if not (column_name and check_value): @@ -172,6 +162,7 @@ def parse_config(self, config): class PolyspaceFamilyChecker(WarningsChecker): + name = "polyspace_sub" code_quality_severity = { "impact: high": "critical", "impact: medium": "major", @@ -179,8 +170,9 @@ class PolyspaceFamilyChecker(WarningsChecker): "red": "critical", "orange": "major", } + logging_fmt = "{checker.name_repr}: {checker.family_value:15s} : {checker.column_name:11s} : {checker.check_value:14s} | {message}" - def __init__(self, family_value, column_name, check_value, **kwargs): + def __init__(self, family_value, column_name, check_value, *logging_args): """Initialize the PolyspaceFamilyChecker Args: @@ -188,35 +180,26 @@ def __init__(self, family_value, column_name, check_value, **kwargs): column_name (str): The name of the column check_value (str): The value to check in the column """ - super().__init__(**kwargs) + super().__init__(*logging_args) self.family_value = family_value self.column_name = column_name self.check_value = check_value @property def cq_description_template(self): - ''' Template: string.Template instance based on the configured template string ''' + """Template: string.Template instance based on the configured template string""" return self._cq_description_template @cq_description_template.setter def cq_description_template(self, template_obj): self._cq_description_template = template_obj - def return_count(self): - ''' Getter function for the amount of warnings found - - Returns: - int: Number of warnings found - ''' - print("{} warnings found for {!r}: {!r}".format(self.count, self.column_name, self.check_value)) - return self.count - def add_code_quality_finding(self, row): - '''Add code quality finding + """Add code quality finding Args: row (dict): The row of the warning with the corresponding colomn names - ''' + """ try: description = self.cq_description_template.substitute(os.environ, **row) except KeyError as err: @@ -236,24 +219,22 @@ def add_code_quality_finding(self, row): self.cq_findings.append(finding.to_dict()) def check(self, content): - ''' + """ Function for counting the number of failures in a TSV/CSV file exported by Polyspace Args: content (dict): The row of the TSV file - ''' + """ if content[self.column_name].lower() == self.check_value: if content["status"].lower() in ["not a defect", "justified"]: - self.print_when_verbose("Excluded row {!r} because the status is 'Not a defect' or 'Justified'" - .format(content)) + self.logger.info(f"Excluded defect with ID {content.get('id', None)!r} because the status is " + "'Not a defect' or 'Justified'") else: tab_sep_string = "\t".join(content.values()) if not self._is_excluded(tab_sep_string): self.count = self.count + 1 - self.counted_warnings.append('family: {} -> {}: {}'.format( - self.family_value, - self.column_name, - self.check_value - )) + verbose_log_msg = f"ID {content.get('id', None)!r}" + self.logger.info(verbose_log_msg) + self.logger.debug(verbose_log_msg) if self.cq_enabled and content["color"].lower() != "green": self.add_code_quality_finding(content) diff --git a/src/mlx/warnings/regex_checker.py b/src/mlx/warnings/regex_checker.py index da2d478c..0fa4cf22 100644 --- a/src/mlx/warnings/regex_checker.py +++ b/src/mlx/warnings/regex_checker.py @@ -20,42 +20,42 @@ class RegexChecker(WarningsChecker): - name = 'regex' + name = "regex" pattern = None SEVERITY_MAP = { - 'debug': 'info', - 'info': 'info', - 'notice': 'info', - 'warning': 'major', - 'error': 'critical', - 'severe': 'critical', - 'critical': 'critical', - 'failed': 'critical', + "debug": "info", + "info": "info", + "notice": "info", + "warning": "major", + "error": "critical", + "severe": "critical", + "critical": "critical", + "failed": "critical", } def check(self, content): - ''' Function for counting the number of warnings in a specific text + """Function for counting the number of warnings in a specific text Args: content (str): The content to parse - ''' + """ matches = re.finditer(self.pattern, content) for match in matches: match_string = match.group(0).strip() if self._is_excluded(match_string): continue self.count += 1 - self.counted_warnings.append(match_string) - self.print_when_verbose(match_string) + self.logger.info(match_string) + self.logger.debug(match_string) if self.cq_enabled: self.add_code_quality_finding(match) def add_code_quality_finding(self, match): - '''Add code quality finding + """Add code quality finding Args: match (re.Match): The regex match - ''' + """ groups = {name: result for name, result in match.groupdict().items() if result} description = next((result for name, result in groups.items() if name.startswith("description")), None) @@ -72,38 +72,30 @@ def add_code_quality_finding(self, match): class CoverityChecker(RegexChecker): - name = 'coverity' + name = "coverity" pattern = coverity_pattern - def __init__(self, verbose=False): - super().__init__(verbose) - self._cq_description_template = Template('Coverity: CID $cid: $checker') + def __init__(self, *logging_args): + super().__init__(*logging_args) + self._cq_description_template = Template("Coverity: CID $cid: $checker") 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), + "unclassified": CoverityClassificationChecker("unclassified", *logging_args), + "pending": CoverityClassificationChecker("pending", *logging_args), + "bug": CoverityClassificationChecker("bug", *logging_args), + "intentional": CoverityClassificationChecker("intentional", *logging_args), + "false positive": CoverityClassificationChecker("false positive", *logging_args), } - @property - def counted_warnings(self): - ''' List[str]: list of counted warnings''' - all_counted_warnings = [] - for checker in self.checkers.values(): - all_counted_warnings.extend(checker.counted_warnings) - return all_counted_warnings - @property def cq_findings(self): - ''' List[dict]: list of code quality findings''' + """List[dict]: list of code quality findings""" for checker in self.checkers.values(): self._cq_findings.extend(checker.cq_findings) return self._cq_findings @property def cq_description_template(self): - ''' Template: string.Template instance based on the configured template string ''' + """Template: string.Template instance based on the configured template string""" return self._cq_description_template @cq_description_template.setter @@ -111,38 +103,38 @@ def cq_description_template(self, template_obj): self._cq_description_template = template_obj def return_count(self): - ''' Getter function for the amount of warnings found + """Getter function for the amount of warnings found Returns: int: Number of warnings found - ''' + """ self.count = 0 for checker in self.checkers.values(): self.count += checker.return_count() return self.count def return_check_limits(self): - ''' Function for checking whether the warning count is within the configured limits + """Function for checking whether the warning count is within the configured limits Returns: int: 0 if the amount of warnings is within limits, the count of warnings otherwise (or 1 in case of a count of 0 warnings) - ''' + """ count = 0 for checker in self.checkers.values(): - print(f"Counted failures for classification {checker.classification!r}") count += checker.return_check_limits() - print(f"total warnings = {count}") + if count: + self.logger.warning(f"Returning error code {count}.") return count def check(self, content): - ''' + """ Function for counting the number of warnings, but adopted for Coverity output Args: content (str): The content to parse - ''' + """ matches = re.finditer(self.pattern, content) for match in matches: if (classification := match.group("classification").lower()) in self.checkers: @@ -153,7 +145,7 @@ def check(self, content): checker.cq_default_path = self.cq_default_path checker.check(match) else: - print(f"WARNING: Unrecognized classification {match.group('classification')!r}") + self.logger.warning(f"Unrecognized classification {match.group('classification')!r}") def parse_config(self, config): """Process configuration @@ -173,50 +165,44 @@ def parse_config(self, config): if classification_key in self.checkers: self.checkers[classification_key].parse_config(checker_config) else: - print(f"WARNING: Unrecognized classification {classification!r}") + self.logger.warning(f"Unrecognized classification {classification!r}") class CoverityClassificationChecker(WarningsChecker): + name = "coverity_sub" + logging_fmt = "{checker.name_repr}: {checker.classification:<14} | {message}" SEVERITY_MAP = { - 'false positive': 'info', - 'intentional': 'info', - 'bug': 'major', - 'unclassified': 'major', - 'pending': 'critical', + "false positive": "info", + "intentional": "info", + "bug": "major", + "unclassified": "major", + "pending": "critical", } - def __init__(self, classification, **kwargs): + def __init__(self, classification, *args): """Initialize the CoverityClassificationChecker: Args: classification (str): The coverity classification """ - super().__init__(**kwargs) + super().__init__(*args) self.classification = classification @property def cq_description_template(self): - ''' Template: string.Template instance based on the configured template string ''' + """Template: string.Template instance based on the configured template string""" return self._cq_description_template @cq_description_template.setter def cq_description_template(self, template_obj): self._cq_description_template = template_obj - def return_count(self): - ''' Getter function for the amount of warnings found - - Returns: - int: Number of warnings found - ''' - return self.count - def add_code_quality_finding(self, match): - '''Add code quality finding + """Add code quality finding Args: match (re.Match): The regex match - ''' + """ groups = {name: result for name, result in match.groupdict().items() if result} try: description = self.cq_description_template.substitute(os.environ, **groups) @@ -237,41 +223,41 @@ def add_code_quality_finding(self, match): self.cq_findings.append(finding.to_dict()) def check(self, content): - ''' + """ Function for counting the number of warnings, but adopted for Coverity output. Multiple warnings for the same CID are counted as one. Args: content (re.Match): The regex match - ''' + """ match_string = content.group(0).strip() - if not self._is_excluded(match_string) and (content.group('curr') == content.group('max')): + if not self._is_excluded(match_string) and (content.group("curr") == content.group("max")): self.count += 1 - self.counted_warnings.append(match_string) - self.print_when_verbose(match_string) + self.logger.info(match_string) + self.logger.debug(match_string) if self.cq_enabled: self.add_code_quality_finding(content) class DoxyChecker(RegexChecker): - name = 'doxygen' + name = "doxygen" pattern = doxy_pattern class SphinxChecker(RegexChecker): - name = 'sphinx' + name = "sphinx" pattern = sphinx_pattern sphinx_deprecation_regex = r"(?m)^(?:(.+?:(?:\d+|None)?):?\s*)?(DEBUG|INFO|WARNING|ERROR|SEVERE|(?:\w+Sphinx\d+Warning)):\s*(.+)$" sphinx_deprecation_regex_in_match = "RemovedInSphinx\\d+Warning" def include_sphinx_deprecation(self): - ''' + """ Adds the pattern for sphinx_deprecation_regex to the list patterns to include and alters the main pattern - ''' + """ self.pattern = re.compile(self.sphinx_deprecation_regex) self.add_patterns([self.sphinx_deprecation_regex_in_match], self.include_patterns) class XMLRunnerChecker(RegexChecker): - name = 'xmlrunner' + name = "xmlrunner" pattern = xmlrunner_pattern diff --git a/src/mlx/warnings/robot_checker.py b/src/mlx/warnings/robot_checker.py index 85958820..93146fdd 100644 --- a/src/mlx/warnings/robot_checker.py +++ b/src/mlx/warnings/robot_checker.py @@ -9,24 +9,17 @@ class RobotChecker(WarningsChecker): - name = 'robot' + name = "robot" checkers = [] - - @property - def counted_warnings(self): - '''List[str]: list of counted warnings''' - all_counted_warnings = [] - for checker in self.checkers: - all_counted_warnings.extend(checker.counted_warnings) - return all_counted_warnings + logging_fmt = "{checker.name_repr}: {message}" @property def minimum(self): - ''' Gets the lowest minimum amount of warnings + """Gets the lowest minimum amount of warnings Returns: int: the lowest minimum for warnings - ''' + """ if self.checkers: return min(x.minimum for x in self.checkers) return 0 @@ -38,11 +31,11 @@ def minimum(self, minimum): @property def maximum(self): - ''' Gets the highest minimum amount of warnings + """Gets the highest minimum amount of warnings Returns: int: the highest maximum for warnings - ''' + """ if self.checkers: return max(x.maximum for x in self.checkers) return 0 @@ -53,83 +46,76 @@ def maximum(self, maximum): checker.maximum = maximum def check(self, content): - ''' + """ Function for counting the number of failures in a specific Robot Framework test suite Args: content (str): The content to parse - ''' + """ for checker in self.checkers: checker.check(content) def return_count(self): - ''' Getter function for the amount of warnings found + """Getter function for the amount of warnings found Returns: int: Number of warnings found - ''' + """ self.count = 0 for checker in self.checkers: self.count += checker.return_count() return self.count def return_check_limits(self): - ''' Function for checking whether the warning count is within the configured limits + """Function for checking whether the warning count is within the configured limits Returns: int: 0 if the amount of warnings is within limits, the count of warnings otherwise (or 1 in case of a count of 0 warnings) - ''' + """ count = 0 for checker in self.checkers: - if checker.name: - print('Counted failures for test suite {!r}.'.format(checker.name)) - else: - print('Counted failures for all test suites.') count += checker.return_check_limits() + if count: + self.logger.warning(f"Returning error code {count}.") return count def parse_config(self, config): self.checkers = [] - check_suite_name = config.get('check_suite_names', True) - for suite_config in config['suites']: - checker = RobotSuiteChecker(suite_config['name'], check_suite_name=check_suite_name, - verbose=self.verbose) + check_suite_name = config.get("check_suite_names", True) + for suite_config in config["suites"]: + checker = RobotSuiteChecker(suite_config["name"], *self.logging_args, check_suite_name=check_suite_name) checker.parse_config(suite_config) self.checkers.append(checker) class RobotSuiteChecker(JUnitChecker): - def __init__(self, name, check_suite_name=False, **kwargs): - ''' Constructor + name = "robot_sub" + logging_fmt = "{checker.name_repr}: {checker.suite_name_repr:<20} {message}" + + def __init__(self, suite_name, *logging_args, check_suite_name=False): + """Constructor Args: name (str): Name of the test suite to check the results of check_suite_name (bool): Whether to raise an error when no test in suite with given name is found - ''' - super().__init__(**kwargs) - self.name = name + """ + super().__init__(*logging_args) + self.suite_name = suite_name self.check_suite_name = check_suite_name self.is_valid_suite_name = False - def return_count(self): - ''' Getter function for the amount of warnings found - - Returns: - int: Number of warnings found - ''' - msg = "{} warnings found".format(self.count) - if self.name: - msg = "Suite {!r}: {}".format(self.name, msg) - print(msg) - return self.count + @property + def suite_name_repr(self): + return f"suite {self.suite_name!r}" if self.suite_name else "all test suites" def _check_testcase(self, testcase): - """ Handles the check of a test case element by checking if the result is a failure/error. + """Handles the check of a test case element by checking if the result is a failure/error. If it is to be excluded by a configured regex, or the test case does not belong to the suite, 1 is returned. - Otherwise, when in verbose mode, the suite name and test case name are printed. + Otherwise, when in verbose/output mode, the suite name and test case name are printed/written along with the + failure/error message. Args: testcase (junitparser.TestCase): Test case element to check for failure or error @@ -137,13 +123,13 @@ def _check_testcase(self, testcase): Returns: int: 1 if a failure/error is to be subtracted from the final count, 0 otherwise """ - if testcase.classname.endswith(self.name): + if testcase.classname.endswith(self.suite_name): self.is_valid_suite_name = True return super()._check_testcase(testcase) - return int(self.name and isinstance(testcase.result, (Failure, Error))) + return int(self.suite_name and isinstance(testcase.result, (Failure, Error))) def check(self, content): - """ Function for counting the number of JUnit failures in a specific text + """Function for counting the number of JUnit failures in a specific text The test cases with a ``classname`` that does not end with the ``name`` class attribute are ignored. @@ -151,9 +137,9 @@ def check(self, content): content (str): The content to parse Raises: - SystemExit: No suite with name ``self.name`` found. Returning error code -1. + SystemExit: No suite with name ``self.suite_name`` found. Returning error code -1. """ super().check(content) if not self.is_valid_suite_name and self.check_suite_name: - print('ERROR: No suite with name {!r} found. Returning error code -1.'.format(self.name)) + self.logger.error(f"No suite with name {self.suite_name!r} found. Returning error code -1.") sys.exit(-1) diff --git a/src/mlx/warnings/warnings.py b/src/mlx/warnings/warnings.py index 4357b56e..945cb8ff 100644 --- a/src/mlx/warnings/warnings.py +++ b/src/mlx/warnings/warnings.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- # SPDX-License-Identifier: Apache-2.0 import argparse import errno import glob import json +import logging +import os import subprocess import sys from importlib.metadata import distribution @@ -15,56 +16,51 @@ from .exceptions import WarningsConfigError from .junit_checker import JUnitChecker +from .polyspace_checker import PolyspaceChecker from .regex_checker import CoverityChecker, DoxyChecker, SphinxChecker, XMLRunnerChecker from .robot_checker import RobotChecker -from .polyspace_checker import PolyspaceChecker -__version__ = distribution('mlx.warnings').version +__version__ = distribution("mlx.warnings").version + +LOGGER = logging.getLogger(__name__) class WarningsPlugin: - def __init__(self, verbose=False, config_file=None, cq_enabled=False): - ''' + def __init__(self, cq_enabled=False): + """ Function for initializing the parsers Args: - verbose (bool): optional - enable verbose logging - config_file (Path): optional - configuration file with setup cq_enabled (bool): optional - enable generation of Code Quality report - ''' + """ self.activated_checkers = {} - self.verbose = verbose self.cq_enabled = cq_enabled - self.public_checkers = [SphinxChecker(self.verbose), DoxyChecker(self.verbose), JUnitChecker(self.verbose), - XMLRunnerChecker(self.verbose), CoverityChecker(self.verbose), - RobotChecker(self.verbose), PolyspaceChecker(self.verbose)] - - if config_file: - with open(config_file, 'r', encoding='utf-8') as open_file: - if config_file.suffix.lower().startswith('.y'): - config = YAML().load(open_file) - else: - config = json.load(open_file) - self.config_parser(config) - + self.public_checkers = (SphinxChecker, DoxyChecker, JUnitChecker, XMLRunnerChecker, CoverityChecker, + RobotChecker, PolyspaceChecker) self._minimum = 0 self._maximum = 0 self.count = 0 self.printout = False - def activate_checker(self, checker): - ''' + def activate_checker(self, checker_type, *logging_args): + """ Activate additional checkers after initialization Args: - checker (WarningsChecker): checker object - ''' - checker.cq_enabled = self.cq_enabled and checker.name in ('doxygen', 'sphinx', 'xmlrunner', 'polyspace', 'coverity') + checker_type (WarningsChecker): checker class + + Return: + WarningsChecker: activated checker object + """ + checker = checker_type(*logging_args) + checker.cq_enabled = self.cq_enabled and checker.name in ("doxygen", "sphinx", "xmlrunner", "polyspace", + "coverity") self.activated_checkers[checker.name] = checker + return checker - def activate_checker_name(self, name): - ''' + def activate_checker_name(self, name, *args): + """ Activates checker by name Args: @@ -72,35 +68,35 @@ def activate_checker_name(self, name): Returns: WarningsChecker: activated checker object, or None when no checker with the given name exists - ''' - for checker in self.public_checkers: - if checker.name == name: - self.activate_checker(checker) + """ + for checker_type in self.public_checkers: + if checker_type.name == name: + checker = self.activate_checker(checker_type, *args) return checker else: - print("Checker %s does not exist" % name) + LOGGER.error(f"Checker {name} does not exist") def get_checker(self, name): - ''' Get checker by name + """Get checker by name Args: name (str): checker name Return: checker object (WarningsChecker) - ''' + """ return self.activated_checkers[name] def check(self, content): - ''' + """ Count the number of warnings in a specified content Args: content (str): The content to parse - ''' + """ if self.printout: - print(content) + LOGGER.warning(content) if not self.activated_checkers: - print("No checkers activated. Please use activate_checker function") + LOGGER.error("No checkers activated. Please use activate_checker function") else: for checker in self.activated_checkers.values(): if checker.name == "polyspace": @@ -109,14 +105,14 @@ def check(self, content): checker.check(content) def check_logfile(self, file): - ''' + """ Count the number of warnings in a specified content Args: content (_io.TextIOWrapper): The open file to parse - ''' + """ if not self.activated_checkers: - print("No checkers activated. Please use activate_checker function") + LOGGER.error("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") @@ -127,25 +123,25 @@ def check_logfile(self, file): checker.check(content) def configure_maximum(self, maximum): - ''' Configure the maximum amount of warnings for each activated checker + """Configure the maximum amount of warnings for each activated checker Args: maximum (int): maximum amount of warnings allowed - ''' + """ for checker in self.activated_checkers.values(): checker.maximum = maximum def configure_minimum(self, minimum): - ''' Configure the minimum amount of warnings for each activated checker + """Configure the minimum amount of warnings for each activated checker Args: minimum (int): minimum amount of warnings allowed - ''' + """ for checker in self.activated_checkers.values(): checker.minimum = minimum def return_count(self, name=None): - ''' Getter function for the amount of found warnings + """Getter function for the amount of found warnings If the name parameter is set, this function will return the amount of warnings found by that checker. If not, the function will return the sum @@ -156,7 +152,7 @@ def return_count(self, name=None): Returns: int: Amount of found warnings - ''' + """ self.count = 0 if name is None: for checker in self.activated_checkers.values(): @@ -166,7 +162,7 @@ def return_count(self, name=None): return self.count def return_check_limits(self, name=None): - ''' Function for determining the return value of the script + """Function for determining the return value of the script If the name parameter is set, this function will check (and return) the return value of that checker. If not, this function checks whether the @@ -178,7 +174,7 @@ def return_check_limits(self, name=None): Return: int: 0 if the amount of warnings is within limits, the count of warnings otherwise (or 1 in case of a count of 0 warnings) - ''' + """ if name is None: for checker in self.activated_checkers.values(): retval = checker.return_check_limits() @@ -190,126 +186,133 @@ def return_check_limits(self, name=None): return 0 def toggle_printout(self, printout): - ''' Toggle printout of all the parsed content + """Toggle printout of all the parsed content Useful for command input where we want to print content as well Args: printout (bool): True enables the printout, False provides more silent mode - ''' + """ self.printout = printout - def config_parser(self, config): - ''' Parsing configuration dict extracted by previously opened JSON or YAML file + def config_parser(self, config, *logging_args): + """Parsing configuration dict extracted by previously opened JSON or YAML file Args: - config (dict): Content of configuration file - ''' - # activate checker - for checker in self.public_checkers: - try: - checker_config = config[checker.name] - if bool(checker_config['enabled']): - self.activate_checker(checker) - checker.parse_config(checker_config) - print("Config parsing for {name} completed".format(name=checker.name)) - except KeyError as err: - print("Incomplete config. Missing: {key}".format(key=err)) - - def write_counted_warnings(self, out_file): - ''' Writes counted warnings to the given file + config (dict/Path): Content or path of configuration file + """ + if isinstance(config, Path): + with open(config, encoding="utf-8") as open_file: + if config.suffix.lower().startswith(".y"): + config = YAML().load(open_file) + else: + config = json.load(open_file) - Args: - out_file (str): Location for the output file - ''' - Path(out_file).parent.mkdir(parents=True, exist_ok=True) - with open(out_file, 'w', encoding='utf-8', newline='\n') as open_file: - for checker in self.activated_checkers.values(): - open_file.write("\n".join(checker.counted_warnings) + "\n") + # activate checker + for checker_type in self.public_checkers: + if checker_type.name in config: + checker_config = config[checker_type.name] + try: + if bool(checker_config["enabled"]): + checker = self.activate_checker(checker_type, *logging_args) + checker.parse_config(checker_config) + LOGGER.info(f"{checker.name_repr}: Config parsing completed") + except KeyError as err: + raise WarningsConfigError(f"Incomplete config. Missing: {err}") from err def write_code_quality_report(self, out_file): - ''' Generates the Code Quality report artifact as a JSON file that implements a subset of the Code Climate spec + """Generates the Code Quality report artifact as a JSON file that implements a subset of the Code Climate spec Args: out_file (str): Location for the output file - ''' + """ results = [] for checker in self.activated_checkers.values(): results.extend(checker.cq_findings) content = json.dumps(results, indent=4, sort_keys=False) Path(out_file).parent.mkdir(parents=True, exist_ok=True) - with open(out_file, 'w', encoding='utf-8', newline='\n') as open_file: + with open(out_file, "w", encoding="utf-8", newline="\n") as open_file: open_file.write(f"{content}\n") def warnings_wrapper(args): - parser = argparse.ArgumentParser(prog='mlx-warnings') - group1 = parser.add_argument_group('Configuration command line options') - group1.add_argument('--coverity', dest='coverity', action='store_true') - group1.add_argument('-d', '--doxygen', dest='doxygen', action='store_true') - group1.add_argument('-j', '--junit', dest='junit', action='store_true') - group1.add_argument('-r', '--robot', dest='robot', action='store_true') - group1.add_argument('-s', '--sphinx', dest='sphinx', action='store_true') - group1.add_argument('-x', '--xmlrunner', dest='xmlrunner', action='store_true') - group1.add_argument('--name', default='', - help='Name of the Robot Framework test suite to check results of') - group1.add_argument('-m', '--maxwarnings', '--max-warnings', type=int, default=0, - help='Maximum amount of warnings accepted') - group1.add_argument('--minwarnings', '--min-warnings', type=int, default=0, - help='Minimum amount of warnings accepted') - group1.add_argument('--exact-warnings', type=int, default=0, - help='Exact amount of warnings expected') - group2 = parser.add_argument_group('Configuration file with options') - group2.add_argument('--config', dest='configfile', action='store', required=False, type=Path, - help='Config file in JSON or YAML format provides toggle of checkers and their limits') - group2.add_argument('--include-sphinx-deprecation', dest='include_sphinx_deprecation', action='store_true', + parser = argparse.ArgumentParser(prog="mlx-warnings") + group1 = parser.add_argument_group("Configuration command line options") + group1.add_argument("--coverity", dest="coverity", action="store_true") + group1.add_argument("-d", "--doxygen", dest="doxygen", action="store_true") + group1.add_argument("-j", "--junit", dest="junit", action="store_true") + group1.add_argument("-r", "--robot", dest="robot", action="store_true") + group1.add_argument("-s", "--sphinx", dest="sphinx", action="store_true") + group1.add_argument("-x", "--xmlrunner", dest="xmlrunner", action="store_true") + group1.add_argument("--name", default="", + help="Name of the Robot Framework test suite to check results of") + group1.add_argument("-m", "--maxwarnings", "--max-warnings", type=int, default=0, + help="Maximum amount of warnings accepted") + group1.add_argument("--minwarnings", "--min-warnings", type=int, default=0, + help="Minimum amount of warnings accepted") + group1.add_argument("--exact-warnings", type=int, default=0, + help="Exact amount of warnings expected") + group2 = parser.add_argument_group("Configuration file with options") + group2.add_argument("--config", dest="configfile", action="store", required=False, type=Path, + help="Config file in JSON or YAML format provides toggle of checkers and their limits") + group2.add_argument("--include-sphinx-deprecation", dest="include_sphinx_deprecation", action="store_true", help="Sphinx checker will include warnings matching (RemovedInSphinx\\d+Warning) regex") - parser.add_argument('-o', '--output', - help='Output file that contains all counted warnings') - parser.add_argument('-C', '--code-quality', - help='Output Code Quality report artifact for GitLab CI') - parser.add_argument('-v', '--verbose', dest='verbose', action='store_true') - parser.add_argument('--command', dest='command', action='store_true', - help='Treat program arguments as command to execute to obtain data') - parser.add_argument('--ignore-retval', dest='ignore', action='store_true', - help='Ignore return value of the executed command') - parser.add_argument('--version', action='version', version='%(prog)s {version}'.format(version=__version__)) - parser.add_argument('logfile', nargs='+', help='Logfile (or command) that might contain warnings') - parser.add_argument('flags', nargs=argparse.REMAINDER, - help='Possible not-used flags from above are considered as command flags') + parser.add_argument("-o", "--output", type=Path, + help="Output file that contains all counted warnings") + parser.add_argument("-C", "--code-quality", + help="Output Code Quality report artifact for GitLab CI") + parser.add_argument("-v", "--verbose", dest="verbose", action="store_true") + parser.add_argument("--command", dest="command", action="store_true", + help="Treat program arguments as command to execute to obtain data") + parser.add_argument("--ignore-retval", dest="ignore", action="store_true", + help="Ignore return value of the executed command") + parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}") + parser.add_argument("logfile", nargs="+", help="Logfile (or command) that might contain warnings") + parser.add_argument("flags", nargs=argparse.REMAINDER, + help="Possible not-used flags from above are considered as command flags") args = parser.parse_args(args) code_quality_enabled = bool(args.code_quality) + if args.output is not None and args.output.exists(): + os.remove(args.output) + + LOGGER.addHandler(logging.StreamHandler()) + LOGGER.setLevel(logging.WARNING) + if args.verbose: + LOGGER.setLevel(logging.INFO) + + logging_args = [args.verbose, args.output] + warnings = WarningsPlugin(cq_enabled=code_quality_enabled) # Read config file if args.configfile is not None: checker_flags = args.sphinx or args.doxygen or args.junit or args.coverity or args.xmlrunner or args.robot warning_args = args.maxwarnings or args.minwarnings or args.exact_warnings if checker_flags or warning_args: - print("Configfile cannot be provided with other arguments") + LOGGER.error("Configfile cannot be provided with other arguments") sys.exit(2) - warnings = WarningsPlugin(verbose=args.verbose, config_file=args.configfile, cq_enabled=code_quality_enabled) + warnings.config_parser(args.configfile, *logging_args) else: - warnings = WarningsPlugin(verbose=args.verbose, cq_enabled=code_quality_enabled) if args.sphinx: - warnings.activate_checker_name('sphinx') + warnings.activate_checker_name("sphinx", *logging_args) if args.doxygen: - warnings.activate_checker_name('doxygen') + warnings.activate_checker_name("doxygen", *logging_args) if args.junit: - warnings.activate_checker_name('junit') + warnings.activate_checker_name("junit", *logging_args) if args.xmlrunner: - warnings.activate_checker_name('xmlrunner') + warnings.activate_checker_name("xmlrunner", *logging_args) if args.coverity: - warnings.activate_checker_name('coverity') + warnings.activate_checker_name("coverity", *logging_args) if args.robot: - robot_checker = warnings.activate_checker_name('robot') - robot_checker.parse_config({ - 'suites': [{'name': args.name, 'min': 0, 'max': 0}], - 'check_suite_names': True, - }) + robot_checker = warnings.activate_checker_name("robot", *logging_args) + if robot_checker is not None: + robot_checker.parse_config({ + "suites": [{"name": args.name, "min": 0, "max": 0}], + "check_suite_names": True, + }) if args.exact_warnings: if args.maxwarnings | args.minwarnings: - print("expected-warnings cannot be provided with maxwarnings or minwarnings") + LOGGER.error("expected-warnings cannot be provided with maxwarnings or minwarnings") sys.exit(2) warnings.configure_maximum(args.exact_warnings) warnings.configure_minimum(args.exact_warnings) @@ -317,8 +320,8 @@ def warnings_wrapper(args): warnings.configure_maximum(args.maxwarnings) warnings.configure_minimum(args.minwarnings) - if args.include_sphinx_deprecation and 'sphinx' in warnings.activated_checkers.keys(): - warnings.get_checker('sphinx').include_sphinx_deprecation() + if args.include_sphinx_deprecation and "sphinx" in warnings.activated_checkers.keys(): + warnings.get_checker("sphinx").include_sphinx_deprecation() if args.command: if "polyspace" in warnings.activated_checkers: @@ -333,22 +336,20 @@ def warnings_wrapper(args): return retval else: if args.flags: - print(f"WARNING: Some keyword arguments have been ignored because they followed positional arguments: " - f"{' '.join(args.flags)!r}") + LOGGER.warning(f"Some keyword arguments have been ignored because they followed positional arguments: " + f"{' '.join(args.flags)!r}") retval = warnings_logfile(warnings, args.logfile) if retval != 0: return retval warnings.return_count() - if args.output: - warnings.write_counted_warnings(args.output) if args.code_quality: warnings.write_code_quality_report(args.code_quality) return warnings.return_check_limits() def warnings_command(warnings, cmd): - ''' Execute command to obtain input for parsing for warnings + """Execute command to obtain input for parsing for warnings Usually log files are output of the commands. To avoid this additional step this function runs a command instead and parses the stderr and stdout of the @@ -363,10 +364,9 @@ def warnings_command(warnings, cmd): Raises: OSError: When program is not installed. - ''' + """ try: - print("Executing: ", end='') - print(cmd) + LOGGER.info(f"Executing: {cmd}") proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, bufsize=1, universal_newlines=True) out, err = proc.communicate() @@ -385,12 +385,12 @@ def warnings_command(warnings, cmd): return proc.returncode except OSError as err: if err.errno == errno.ENOENT: - print("It seems like program " + str(cmd) + " is not installed.") + LOGGER.error("It seems like program " + str(cmd) + " is not installed.") raise def warnings_logfile(warnings, log): - ''' Parse logfile for warnings + """Parse logfile for warnings Args: warnings (WarningsPlugin): Object for warnings where errors should be logged @@ -399,7 +399,7 @@ def warnings_logfile(warnings, log): Return: 0: Log files existed and are parsed successfully 1: Log files did not exist - ''' + """ # args.logfile doesn't necessarily contain wildcards, but just to be safe, we # assume it does, and try to expand them. # This mechanism is put in place to allow wildcards to be passed on even when @@ -409,10 +409,10 @@ def warnings_logfile(warnings, log): for file_wildcard in log: if glob.glob(file_wildcard): for logfile in glob.glob(file_wildcard): - with open(logfile, "r") as file: + with open(logfile) as file: warnings.check_logfile(file) else: - print("FILE: %s does not exist" % file_wildcard) + LOGGER.error(f"FILE: {file_wildcard} does not exist") return 1 return 0 @@ -422,5 +422,5 @@ def main(): sys.exit(warnings_wrapper(sys.argv[1:])) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/src/mlx/warnings/warnings_checker.py b/src/mlx/warnings/warnings_checker.py index e4be5790..bb075b67 100644 --- a/src/mlx/warnings/warnings_checker.py +++ b/src/mlx/warnings/warnings_checker.py @@ -1,9 +1,10 @@ # SPDX-License-Identifier: Apache-2.0 import abc -from math import inf +import logging import os import re +from math import inf from string import Template from .exceptions import WarningsConfigError @@ -29,47 +30,85 @@ def substitute_envvar(checker_config, keys): from None +class DebugOnlyFilter(logging.Filter): + def filter(self, record: logging.LogRecord) -> bool: + if record.levelno <= logging.DEBUG: + return True + return False + + class WarningsChecker: - name = 'checker' + name = "checker" + logging_fmt = "{checker.name_repr}: {message}" - def __init__(self, verbose=False): - ''' Constructor + def __init__(self, verbose, output): + """Constructor + + The logging is configured. A handler is added only if a parent checker hasn't done this already. + A parent-checker uses the same logger as its sub-checkers, but each with their own LoggerAdapter. Args: - name (str): Name of the checker verbose (bool): Enable/disable verbose logging - ''' - self.verbose = verbose + output (Path/None): The path to the output file + """ self.count = 0 self._minimum = 0 self._maximum = 0 - self._counted_warnings = [] self._cq_findings = [] self.cq_enabled = False - self.cq_default_path = '.gitlab-ci.yml' - self._cq_description_template = Template('$description') + self.cq_default_path = ".gitlab-ci.yml" + self._cq_description_template = Template("$description") self.exclude_patterns = [] self.include_patterns = [] + self.logging_args = (verbose, output) + + self.logger = logging.getLogger(self.name) + self.logger.setLevel(logging.WARNING) + if output: + self.logger.setLevel(logging.DEBUG) + elif verbose: + self.logger.setLevel(logging.INFO) + formatter = logging.Formatter(fmt=self.logging_fmt, style="{") + if not self.logger.handlers: + self.logger.propagate = True # Propagate to parent loggers + handler = logging.StreamHandler() + handler.setFormatter(formatter) + if verbose: + handler.setLevel(logging.INFO) + else: + handler.setLevel(logging.WARNING) + self.logger.addHandler(handler) + if output is not None: + handler = logging.FileHandler(output, "a") + handler.setFormatter(formatter) + handler.setLevel(logging.DEBUG) + handler.addFilter(DebugOnlyFilter()) + self.logger.addHandler(handler) + logging_vars = {"checker": self} + self.logger = logging.LoggerAdapter(self.logger, extra=logging_vars) @property - def cq_findings(self): - ''' List[dict]: list of code quality findings''' - return self._cq_findings + def name_repr(self): + return self.name.replace("_sub", "").capitalize() + + @property + def is_sub_checker(self): + return self.name.endswith("_sub") @property - def counted_warnings(self): - ''' List[str]: list of counted warnings''' - return self._counted_warnings + def cq_findings(self): + """List[dict]: list of code quality findings""" + return self._cq_findings @property def cq_description_template(self): - ''' Template: string.Template instance based on the configured template string ''' + """Template: string.Template instance based on the configured template string""" return self._cq_description_template @cq_description_template.setter def cq_description_template(self, template_obj): try: - template_obj.template = template_obj.substitute(os.environ, description='$description') + template_obj.template = template_obj.substitute(os.environ, description="$description") except KeyError as err: raise WarningsConfigError(f"Failed to find environment variable from configuration value " f"'cq_description_template': {err}") from err @@ -77,11 +116,11 @@ def cq_description_template(self, template_obj): @property def maximum(self): - ''' Getter function for the maximum amount of warnings + """Getter function for the maximum amount of warnings Returns: int: Maximum amount of warnings - ''' + """ return self._maximum @maximum.setter @@ -95,11 +134,11 @@ def maximum(self, maximum): @property def minimum(self): - ''' Getter function for the minimum amount of warnings + """Getter function for the minimum amount of warnings Returns: int: Minimum amount of warnings - ''' + """ return self._minimum @minimum.setter @@ -111,20 +150,20 @@ def minimum(self, minimum): @abc.abstractmethod def check(self, content): - ''' Function for counting the number of warnings in a specific text + """Function for counting the number of warnings in a specific text Args: content (str): The content to parse - ''' + """ return def add_patterns(self, regexes, pattern_container): - ''' Adds regexes as patterns to the specified container + """Adds regexes as patterns to the specified container Args: regexes (list[str]|None): List of regexes to add pattern_container (list[re.Pattern]): Target storage container for patterns - ''' + """ if regexes: if not isinstance(regexes, list): raise TypeError("Expected a list value for exclude key in configuration file; got {}" @@ -133,70 +172,63 @@ def add_patterns(self, regexes, pattern_container): pattern_container.append(re.compile(regex)) def return_count(self): - ''' Getter function for the amount of warnings found + """Getter function for the amount of warnings found Returns: int: Number of warnings found - ''' - print("{0.count} {0.name} warnings found".format(self)) + """ return self.count def return_check_limits(self): - ''' Function for checking whether the warning count is within the configured limits + """Function for checking whether the warning count is within the configured limits + A checker instance with sub-checkers is responsible for printing 'Returning error code X.' + when the exit code is not 0. Returns: - int: 0 if the amount of warnings is within limits, the count of warnings otherwise + int: 0 if the amount of warnings is within limits, the count of (the sum of sub-checker) warnings otherwise (or 1 in case of a count of 0 warnings) - ''' + """ if self.count > self._maximum or self.count < self._minimum: return self._return_error_code() elif self._minimum == self._maximum and self.count == self._maximum: - print("Number of warnings ({0.count}) is exactly as expected. Well done." - .format(self)) + msg = f"number of warnings ({self.count}) is exactly as expected. Well done." else: - print("Number of warnings ({0.count}) is between limits {0._minimum} and {0._maximum}. Well done." - .format(self)) + msg = f"number of warnings ({self.count}) is between limits {self._minimum} and {self._maximum}. Well done." + self.logger.warning(msg) return 0 def _return_error_code(self): - ''' Function for determining the return code and message on failure + """Function for determining the return code and message on failure Returns: int: The count of warnings (or 1 in case of a count of 0 warnings) - ''' + """ if self.count > self._maximum: - error_reason = "higher than the maximum limit ({0._maximum})".format(self) + error_reason = f"higher than the maximum limit ({self._maximum})" else: - error_reason = "lower than the minimum limit ({0._minimum})".format(self) + error_reason = f"lower than the minimum limit ({self._minimum})" error_code = self.count if error_code == 0: error_code = 1 - print("Number of warnings ({0.count}) is {1}. Returning error code {2}." - .format(self, error_reason, error_code)) + string_to_print = f"number of warnings ({self.count}) is {error_reason}." + if not self.is_sub_checker: + string_to_print += f" Returning error code {error_code}." + self.logger.warning(string_to_print) return error_code - def print_when_verbose(self, message): - ''' Prints message only when verbose mode is enabled. - - Args: - message (str): Message to conditionally print - ''' - if self.verbose: - print(message) - def parse_config(self, config): - substitute_envvar(config, {'min', 'max'}) - self.maximum = int(config['max']) - self.minimum = int(config['min']) + substitute_envvar(config, {"min", "max"}) + self.maximum = int(config["max"]) + self.minimum = int(config["min"]) self.add_patterns(config.get("exclude"), self.exclude_patterns) - if 'cq_default_path' in config: - self.cq_default_path = config['cq_default_path'] - if 'cq_description_template' in config: - self.cq_description_template = Template(config['cq_description_template']) + if "cq_default_path" in config: + self.cq_default_path = config["cq_default_path"] + if "cq_description_template" in config: + self.cq_description_template = Template(config["cq_description_template"]) def _is_excluded(self, content): - ''' Checks if the specific text must be excluded based on the configured regexes for exclusion and inclusion. + """Checks if the specific text must be excluded based on the configured regexes for exclusion and inclusion. Inclusion has priority over exclusion. @@ -205,17 +237,16 @@ def _is_excluded(self, content): Returns: bool: True for exclusion, False for inclusion - ''' + """ matching_exclude_pattern = self._search_patterns(content, self.exclude_patterns) if not self._search_patterns(content, self.include_patterns) and matching_exclude_pattern: - self.print_when_verbose("Excluded {!r} because of configured regex {!r}" - .format(content, matching_exclude_pattern)) + self.logger.info(f"Excluded {content!r} because of configured regex {matching_exclude_pattern!r}") return True return False @staticmethod def _search_patterns(content, patterns): - ''' Returns the regex of the first pattern that matches specified content, None if nothing matches ''' + """Returns the regex of the first pattern that matches specified content, None if nothing matches""" for pattern in patterns: if pattern.search(content): return pattern.pattern diff --git a/tests/test_config.py b/tests/test_config.py index 6fef8791..193d2e6a 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,554 +1,596 @@ +import logging import os -from io import StringIO from pathlib import Path from unittest import TestCase -from unittest.mock import patch -from mlx.warnings import (JUnitChecker, DoxyChecker, SphinxChecker, XMLRunnerChecker, RobotChecker, WarningsPlugin, - WarningsConfigError) +import pytest -TEST_IN_DIR = Path(__file__).parent / 'test_in' +from mlx.warnings import ( + DoxyChecker, + JUnitChecker, + RobotChecker, + SphinxChecker, + WarningsConfigError, + WarningsPlugin, + XMLRunnerChecker, +) + +TEST_IN_DIR = Path(__file__).parent / "test_in" +logging.getLogger("mlx.warnings.warnings").setLevel(logging.DEBUG) class TestConfig(TestCase): + @pytest.fixture(autouse=True) + def caplog(self, caplog): + self.caplog = caplog + def setUp(self): - os.environ['MIN_SPHINX_WARNINGS'] = '0' - os.environ['MAX_SPHINX_WARNINGS'] = '0' + os.environ["MIN_SPHINX_WARNINGS"] = "0" + os.environ["MAX_SPHINX_WARNINGS"] = "0" def tearDown(self): - for var in ('MIN_SPHINX_WARNINGS', 'MAX_SPHINX_WARNINGS'): + for var in ("MIN_SPHINX_WARNINGS", "MAX_SPHINX_WARNINGS"): if var in os.environ: del os.environ[var] def test_configfile_parsing(self): - warnings = WarningsPlugin(config_file=(TEST_IN_DIR / "config_example.json")) - warnings.check('testfile.c:6: warning: group test: ignoring title "Some test functions" that does not match old title "Some freaky test functions"') + warnings = WarningsPlugin() + warnings.config_parser((TEST_IN_DIR / "config_example.json"), False, None) + warnings.check("testfile.c:6: warning: group test: ignoring title \"Some test functions\" that does not match " + "old title \"Some freaky test functions\"") self.assertEqual(warnings.return_count(), 1) - warnings.check('') + warnings.check("") self.assertEqual(warnings.return_count(), 1) - warnings.check("/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document u'installation'") + warnings.check("/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document " + "u'installation'") self.assertEqual(warnings.return_count(), 2) - warnings.check('This should not be treated as warning2') + warnings.check("This should not be treated as warning2") self.assertEqual(warnings.return_count(), 2) - warnings.check('ERROR [0.000s]: test_some_error_test (something.anything.somewhere)') + warnings.check("ERROR [0.000s]: test_some_error_test (something.anything.somewhere)") self.assertEqual(warnings.return_count(), 3) def test_configfile_parsing_missing_envvar(self): - del os.environ['MAX_SPHINX_WARNINGS'] + del os.environ["MAX_SPHINX_WARNINGS"] with self.assertRaises(WarningsConfigError) as c_m: - WarningsPlugin(config_file=(TEST_IN_DIR / "config_example.json")) + warnings = WarningsPlugin() + warnings.config_parser((TEST_IN_DIR / "config_example.json"), False, None) self.assertEqual( str(c_m.exception), - "Failed to find environment variable 'MAX_SPHINX_WARNINGS' for configuration value 'max'") + "Failed to find environment variable 'MAX_SPHINX_WARNINGS' for configuration value 'max'", + ) def _helper_exclude(self, warnings): - with patch('sys.stdout', new=StringIO()) as verbose_output: - warnings.check('testfile.c:6: warning: group test: ignoring title "Some test functions" that does not match old title "Some freaky test functions"') - self.assertEqual(warnings.return_count(), 0) - warnings.check('') - self.assertEqual(warnings.return_count(), 0) - deprecation_warning = 'sphinx/application.py:402: RemovedInSphinx20Warning: app.info() is now deprecated. Use sphinx.util.logging instead.' - warnings.check(deprecation_warning) - self.assertEqual(warnings.return_count(), 0) - toctree_warning = "/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document u'installation'" - warnings.check(toctree_warning) - self.assertEqual(warnings.return_count(), 0) # ignored because of configured "exclude" regex - warnings.check("home/bljah/test/index.rst:5: WARNING: this warning should not get excluded") - self.assertEqual(warnings.return_count(), 1) - warnings.check('This should not be treated as warning2') - self.assertEqual(warnings.return_count(), 1) - warnings.check('ERROR [0.000s]: test_some_error_test (something.anything.somewhere)') - self.assertEqual(warnings.return_count(), 1) - excluded_toctree_warning = "Excluded {!r} because of configured regex {!r}".format(toctree_warning, "WARNING: toctree") - self.assertIn(excluded_toctree_warning, verbose_output.getvalue()) + warnings.check("testfile.c:6: warning: group test: ignoring title \"Some test functions\" that does not match " + "old title \"Some freaky test functions\"") + self.assertEqual(warnings.return_count(), 0) + warnings.check("") + self.assertEqual(warnings.return_count(), 0) + deprecation_warning = "sphinx/application.py:402: RemovedInSphinx20Warning: app.info() is now deprecated. "\ + "Use sphinx.util.logging instead." + warnings.check(deprecation_warning) + self.assertEqual(warnings.return_count(), 0) + toctree_warning = "/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document "\ + "u'installation'" + warnings.check(toctree_warning) + self.assertEqual(warnings.return_count(), 0) # ignored because of configured "exclude" regex + warnings.check("home/bljah/test/index.rst:5: WARNING: this warning should not get excluded") + self.assertEqual(warnings.return_count(), 1) + warnings.check("This should not be treated as warning2") + self.assertEqual(warnings.return_count(), 1) + warnings.check("ERROR [0.000s]: test_some_error_test (something.anything.somewhere)") + self.assertEqual(warnings.return_count(), 1) + excluded_toctree_warning = "Excluded {!r} because of configured regex {!r}".format(toctree_warning, + "WARNING: toctree") + self.assertIn("Sphinx: Config parsing completed", self.caplog.messages) + self.assertIn(f"{excluded_toctree_warning}", self.caplog.messages) warning_echo = "home/bljah/test/index.rst:5: WARNING: this warning should not get excluded" - self.assertIn(warning_echo, verbose_output.getvalue()) + self.assertIn(f"{warning_echo}", self.caplog.messages) def test_configfile_parsing_exclude_json(self): - warnings = WarningsPlugin(verbose=True, config_file=(TEST_IN_DIR / "config_example_exclude.json")) + warnings = WarningsPlugin() + warnings.config_parser((TEST_IN_DIR / "config_example_exclude.json"), True, None) self._helper_exclude(warnings) def test_configfile_parsing_exclude_yml(self): - warnings = WarningsPlugin(verbose=True, config_file=(TEST_IN_DIR / "config_example_exclude.yml")) + warnings = WarningsPlugin() + warnings.config_parser((TEST_IN_DIR / "config_example_exclude.yml"), True, None) self._helper_exclude(warnings) def test_configfile_parsing_include_priority(self): - warnings = WarningsPlugin(verbose=True, config_file=(TEST_IN_DIR / "config_example_exclude.json")) - warnings.get_checker('sphinx').include_sphinx_deprecation() - deprecation_warning = 'sphinx/application.py:402: RemovedInSphinx20Warning: app.info() is now deprecated. Use sphinx.util.logging instead.' + warnings = WarningsPlugin() + warnings.config_parser((TEST_IN_DIR / "config_example_exclude.json"), True, None) + warnings.get_checker("sphinx").include_sphinx_deprecation() + deprecation_warning = "sphinx/application.py:402: RemovedInSphinx20Warning: app.info() is now deprecated. "\ + "Use sphinx.util.logging instead." warnings.check(deprecation_warning) self.assertEqual(warnings.return_count(), 1) def test_partial_sphinx_config_parsing(self): warnings = WarningsPlugin() tmpjson = { - 'sphinx': { - 'enabled': True, - 'min': 0, - 'max': 0 + "sphinx": { + "enabled": True, + "min": 0, + "max": 0 } } - warnings.config_parser(tmpjson) - warnings.check('testfile.c:6: warning: group test: ignoring title "Some test functions" that does not match old title "Some freaky test functions"') + warnings.config_parser(tmpjson, False, None) + warnings.check("testfile.c:6: warning: group test: ignoring title \"Some test functions\" that does not match " + "old title \"Some freaky test functions\"") self.assertEqual(warnings.return_count(), 0) - with open('tests/test_in/junit_single_fail.xml', 'r') as xmlfile: + with open("tests/test_in/junit_single_fail.xml") as xmlfile: warnings.check(xmlfile.read()) self.assertEqual(warnings.return_count(), 0) - warnings.check('ERROR [0.000s]: test_some_error_test (something.anything.somewhere)') + warnings.check("ERROR [0.000s]: test_some_error_test (something.anything.somewhere)") self.assertEqual(warnings.return_count(), 0) - warnings.check("/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document u'installation'") + warnings.check("/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document " + "u'installation'") self.assertEqual(warnings.return_count(), 1) def test_partial_doxygen_config_parsing(self): warnings = WarningsPlugin() tmpjson = { - 'doxygen': { - 'enabled': True, - 'min': 0, - 'max': 0 + "doxygen": { + "enabled": True, + "min": 0, + "max": 0 } } - warnings.config_parser(tmpjson) - with open('tests/test_in/junit_single_fail.xml', 'r') as xmlfile: + warnings.config_parser(tmpjson, False, None) + with open("tests/test_in/junit_single_fail.xml") as xmlfile: warnings.check(xmlfile.read()) self.assertEqual(warnings.return_count(), 0) - warnings.check("/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document u'installation'") + warnings.check("/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document " + "u'installation'") self.assertEqual(warnings.return_count(), 0) - warnings.check('ERROR [0.000s]: test_some_error_test (something.anything.somewhere)') + warnings.check("ERROR [0.000s]: test_some_error_test (something.anything.somewhere)") self.assertEqual(warnings.return_count(), 0) - warnings.check('testfile.c:6: warning: group test: ignoring title "Some test functions" that does not match old title "Some freaky test functions"') + warnings.check("testfile.c:6: warning: group test: ignoring title \"Some test functions\" that does not match " + "old title \"Some freaky test functions\"") self.assertEqual(warnings.return_count(), 1) def test_partial_junit_config_parsing(self): warnings = WarningsPlugin() tmpjson = { - 'junit': { - 'enabled': True, - 'min': 0, - 'max': 0 + "junit": { + "enabled": True, + "min": 0, + "max": 0 } } - warnings.config_parser(tmpjson) - warnings.check("/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document u'installation'") + warnings.config_parser(tmpjson, False, None) + warnings.check("/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document " + "u'installation'") self.assertEqual(warnings.return_count(), 0) - warnings.check('testfile.c:6: warning: group test: ignoring title "Some test functions" that does not match old title "Some freaky test functions"') + warnings.check("testfile.c:6: warning: group test: ignoring title \"Some test functions\" that does not match " + "old title \"Some freaky test functions\"") self.assertEqual(warnings.return_count(), 0) - warnings.check('ERROR [0.000s]: test_some_error_test (something.anything.somewhere)') + warnings.check("ERROR [0.000s]: test_some_error_test (something.anything.somewhere)") self.assertEqual(warnings.return_count(), 0) - with open('tests/test_in/junit_single_fail.xml', 'r') as xmlfile: + with open("tests/test_in/junit_single_fail.xml") as xmlfile: warnings.check(xmlfile.read()) self.assertEqual(warnings.return_count(), 1) def test_exclude_feature_type_error(self): warnings = WarningsPlugin() tmpjson = { - 'junit': { - 'enabled': True, - 'min': 0, - 'max': 0, + "junit": { + "enabled": True, + "min": 0, + "max": 0, "exclude": "able to trace this random failure msg" } } with self.assertRaises(TypeError) as c_m: - warnings.config_parser(tmpjson) + warnings.config_parser(tmpjson, False, None) self.assertEqual(str(c_m.exception), "Expected a list value for exclude key in configuration file; got str") def test_partial_junit_config_parsing_exclude_regex(self): warnings = WarningsPlugin() tmpjson = { - 'junit': { - 'enabled': True, - 'min': 0, - 'max': 0, + "junit": { + "enabled": True, + "min": 0, + "max": 0, "exclude": ["able to trace this random failure msg"] } } - warnings.config_parser(tmpjson) - with open('tests/test_in/junit_single_fail.xml', 'r') as xmlfile: + warnings.config_parser(tmpjson, False, None) + with open("tests/test_in/junit_single_fail.xml") as xmlfile: warnings.check(xmlfile.read()) self.assertEqual(warnings.return_count(), 0) def test_partial_robot_config_parsing_exclude_regex(self): - warnings = WarningsPlugin(verbose=True) + warnings = WarningsPlugin() tmpjson = { - 'robot': { - 'enabled': True, - 'suites': [ + "robot": { + "enabled": True, + "suites": [ { - 'name': 'Suite One', - 'min': 0, - 'max': 0, + "name": "Suite One", + "min": 0, + "max": 0, "exclude": ["does not exist"] # excludes failure in suite }, { - 'name': 'Suite Two', - 'min': 1, - 'max': 1, + "name": "Suite Two", + "min": 1, + "max": 1, "exclude": ["does not exist"] # no match for failure in suite } ] } } - warnings.config_parser(tmpjson) - with open('tests/test_in/robot_double_fail.xml', 'r') as xmlfile: - with patch('sys.stdout', new=StringIO()) as verbose_output: - warnings.check(xmlfile.read()) - count = warnings.return_count() - self.assertEqual(count, 1) - self.assertEqual(warnings.return_check_limits(), 0) + warnings.config_parser(tmpjson, True, None) + with open("tests/test_in/robot_double_fail.xml") as xmlfile: + warnings.check(xmlfile.read()) + retval = warnings.return_check_limits() + self.assertEqual(warnings.return_count(), 1) + self.assertEqual(retval, 0) self.assertEqual( - '\n'.join([ - r"Excluded 'Directory 'C:\\nonexistent' does not exist.' because of configured regex 'does not exist'", - "Suite One & Suite Two.Suite Two.Another test", - "Suite 'Suite One': 0 warnings found", - "Suite 'Suite Two': 1 warnings found", - ]) + '\n', - verbose_output.getvalue() + ["Robot: Config parsing completed", + "Excluded 'Directory 'C:\\\\nonexistent' does not exist.' because of configured regex " + "'does not exist'", + "Suite One & Suite Two.Suite Two.Another test", + "number of warnings (0) is exactly as expected. Well done.", + "number of warnings (1) is exactly as expected. Well done."], + self.caplog.messages ) def test_partial_robot_config_empty_name(self): - warnings = WarningsPlugin(verbose=True) + warnings = WarningsPlugin() tmpjson = { - 'robot': { - 'enabled': True, - 'suites': [ + "robot": { + "enabled": True, + "suites": [ { - 'name': '', - 'min': 1, - 'max': 1, + "name": "", + "min": 1, + "max": 1, "exclude": ["does not exist"] # excludes 1 out of 2 failures in suites } ] } } - warnings.config_parser(tmpjson) - with open('tests/test_in/robot_double_fail.xml', 'r') as xmlfile: - with patch('sys.stdout', new=StringIO()) as verbose_output: - warnings.check(xmlfile.read()) - count = warnings.return_count() + warnings.config_parser(tmpjson, True, None) + with open("tests/test_in/robot_double_fail.xml") as xmlfile: + warnings.check(xmlfile.read()) + count = warnings.return_count() self.assertEqual(count, 1) self.assertEqual(warnings.return_check_limits(), 0) self.assertEqual( - '\n'.join([ - r"Excluded 'Directory 'C:\\nonexistent' does not exist.' because of configured regex 'does not exist'", + [ + "Robot: Config parsing completed", + r"Excluded 'Directory 'C:\\nonexistent' does not exist.' because of configured regex " + r"'does not exist'", "Suite One & Suite Two.Suite Two.Another test", - "1 warnings found", - ]) + '\n', - verbose_output.getvalue() + "number of warnings (1) is exactly as expected. Well done." + ], + self.caplog.messages ) def test_partial_xmlrunner_config_parsing(self): warnings = WarningsPlugin() tmpjson = { - 'xmlrunner': { - 'enabled': True, - 'min': 0, - 'max': 0 + "xmlrunner": { + "enabled": True, + "min": 0, + "max": 0 } } - warnings.config_parser(tmpjson) - with open('tests/test_in/junit_single_fail.xml', 'r') as xmlfile: + warnings.config_parser(tmpjson, False, None) + with open("tests/test_in/junit_single_fail.xml") as xmlfile: warnings.check(xmlfile.read()) self.assertEqual(warnings.return_count(), 0) - warnings.check("/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document u'installation'") + warnings.check("/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document " + "u'installation'") self.assertEqual(warnings.return_count(), 0) - warnings.check('testfile.c:6: warning: group test: ignoring title "Some test functions" that does not match old title "Some freaky test functions"') + warnings.check("testfile.c:6: warning: group test: ignoring title \"Some test functions\" that does not match " + "old title \"Some freaky test functions\"") self.assertEqual(warnings.return_count(), 0) - warnings.check('ERROR [0.000s]: test_some_error_test (something.anything.somewhere)') + warnings.check("ERROR [0.000s]: test_some_error_test (something.anything.somewhere)") self.assertEqual(warnings.return_count(), 1) def test_doxy_junit_options_config_parsing(self): warnings = WarningsPlugin() tmpjson = { - 'doxygen': { - 'enabled': True, - 'min': 0, - 'max': 0 + "doxygen": { + "enabled": True, + "min": 0, + "max": 0 }, - 'junit': { - 'enabled': True, - 'min': 0, - 'max': 0 + "junit": { + "enabled": True, + "min": 0, + "max": 0 } } - warnings.config_parser(tmpjson) - warnings.check("/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document u'installation'") + warnings.config_parser(tmpjson, False, None) + warnings.check("/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document " + "u'installation'") self.assertEqual(warnings.return_count(), 0) - warnings.check('testfile.c:6: warning: group test: ignoring title "Some test functions" that does not match old title "Some freaky test functions"') + warnings.check("testfile.c:6: warning: group test: ignoring title \"Some test functions\" that does not match " + "old title \"Some freaky test functions\"") self.assertEqual(warnings.return_count(), 1) - with open('tests/test_in/junit_single_fail.xml', 'r') as xmlfile: + with open("tests/test_in/junit_single_fail.xml") as xmlfile: warnings.check(xmlfile.read()) self.assertEqual(warnings.return_count(), 2) def test_sphinx_doxy_config_parsing(self): warnings = WarningsPlugin() tmpjson = { - 'sphinx': { - 'enabled': True, - 'min': 0, - 'max': 0 + "sphinx": { + "enabled": True, + "min": 0, + "max": 0 }, - 'doxygen': { - 'enabled': True, - 'min': 0, - 'max': 0 + "doxygen": { + "enabled": True, + "min": 0, + "max": 0 } } - warnings.config_parser(tmpjson) - with open('tests/test_in/junit_single_fail.xml', 'r') as xmlfile: + warnings.config_parser(tmpjson, False, None) + with open("tests/test_in/junit_single_fail.xml") as xmlfile: warnings.check(xmlfile.read()) self.assertEqual(warnings.return_count(), 0) - warnings.check('testfile.c:6: warning: group test: ignoring title "Some test functions" that does not match old title "Some freaky test functions"') + warnings.check("testfile.c:6: warning: group test: ignoring title \"Some test functions\" that does not match " + "old title \"Some freaky test functions\"") self.assertEqual(warnings.return_count(), 1) - warnings.check("/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document u'installation'") + warnings.check("/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document " + "u'installation'") self.assertEqual(warnings.return_count(), 2) - with open('tests/test_in/junit_single_fail.xml', 'r') as xmlfile: + with open("tests/test_in/junit_single_fail.xml") as xmlfile: warnings.check(xmlfile.read()) self.assertEqual(warnings.return_count(), 2) - warnings.check("/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document u'installation'") + warnings.check("/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document " + "u'installation'") self.assertEqual(warnings.return_count(), 3) - warnings.check('testfile.c:6: warning: group test: ignoring title "Some test functions" that does not match old title "Some freaky test functions"') + warnings.check("testfile.c:6: warning: group test: ignoring title \"Some test functions\" that does not match " + "old title \"Some freaky test functions\"") self.assertEqual(warnings.return_count(), 4) def test_sphinx_config_max(self): warnings = WarningsPlugin() tmpjson = { - 'sphinx': { - 'enabled': True, - 'min': 0, - 'max': 5 + "sphinx": { + "enabled": True, + "min": 0, + "max": 5 } } - warnings.config_parser(tmpjson) - self.assertEqual(warnings.get_checker(SphinxChecker().name).maximum, 5) + warnings.config_parser(tmpjson, False, None) + self.assertEqual(warnings.get_checker(SphinxChecker.name).maximum, 5) def test_doxygen_config_max(self): warnings = WarningsPlugin() tmpjson = { - 'doxygen': { - 'enabled': True, - 'min': 0, - 'max': 5 + "doxygen": { + "enabled": True, + "min": 0, + "max": 5 } } - warnings.config_parser(tmpjson) - self.assertEqual(warnings.get_checker(DoxyChecker().name).maximum, 5) + warnings.config_parser(tmpjson, False, None) + self.assertEqual(warnings.get_checker(DoxyChecker.name).maximum, 5) def test_junit_config_max(self): warnings = WarningsPlugin() tmpjson = { - 'junit': { - 'enabled': True, - 'min': 0, - 'max': 5 + "junit": { + "enabled": True, + "min": 0, + "max": 5 } } - warnings.config_parser(tmpjson) - self.assertEqual(warnings.get_checker(JUnitChecker().name).maximum, 5) + warnings.config_parser(tmpjson, False, None) + self.assertEqual(warnings.get_checker(JUnitChecker.name).maximum, 5) def test_xmlrunner_config_max(self): warnings = WarningsPlugin() tmpjson = { - 'xmlrunner': { - 'enabled': True, - 'min': 0, - 'max': 5 + "xmlrunner": { + "enabled": True, + "min": 0, + "max": 5 } } - warnings.config_parser(tmpjson) - self.assertEqual(warnings.get_checker(XMLRunnerChecker().name).maximum, 5) + warnings.config_parser(tmpjson, False, None) + self.assertEqual(warnings.get_checker(XMLRunnerChecker.name).maximum, 5) def test_all_config_max(self): warnings = WarningsPlugin() tmpjson = { - 'sphinx': { - 'enabled': True, - 'min': 0, - 'max': 4 + "sphinx": { + "enabled": True, + "min": 0, + "max": 4 }, - 'doxygen': { - 'enabled': True, - 'min': 0, - 'max': 5 + "doxygen": { + "enabled": True, + "min": 0, + "max": 5 }, - 'junit': { - 'enabled': True, - 'min': 0, - 'max': 6 + "junit": { + "enabled": True, + "min": 0, + "max": 6 }, - 'xmlrunner': { - 'enabled': True, - 'min': 0, - 'max': 6 + "xmlrunner": { + "enabled": True, + "min": 0, + "max": 6 }, - 'robot': { - 'enabled': True, - 'suites': [ + "robot": { + "enabled": True, + "suites": [ { - 'name': 'dummy1', - 'min': 5, - 'max': 7, + "name": "dummy1", + "min": 5, + "max": 7, }, { - 'name': 'dummy2', - 'min': 1, - 'max': 9, + "name": "dummy2", + "min": 1, + "max": 9, }, { - 'name': 'dummy3', - 'min': 2, - 'max': 2, + "name": "dummy3", + "min": 2, + "max": 2, } ] } } - warnings.config_parser(tmpjson) - self.assertEqual(warnings.get_checker(SphinxChecker().name).maximum, 4) - self.assertEqual(warnings.get_checker(DoxyChecker().name).maximum, 5) - self.assertEqual(warnings.get_checker(JUnitChecker().name).maximum, 6) - self.assertEqual(warnings.get_checker(XMLRunnerChecker().name).maximum, 6) - self.assertEqual(warnings.get_checker(RobotChecker().name).maximum, 9) + warnings.config_parser(tmpjson, False, None) + self.assertEqual(warnings.get_checker(SphinxChecker.name).maximum, 4) + self.assertEqual(warnings.get_checker(DoxyChecker.name).maximum, 5) + self.assertEqual(warnings.get_checker(JUnitChecker.name).maximum, 6) + self.assertEqual(warnings.get_checker(XMLRunnerChecker.name).maximum, 6) + self.assertEqual(warnings.get_checker(RobotChecker.name).maximum, 9) def test_sphinx_config_min(self): warnings = WarningsPlugin() tmpjson = { - 'sphinx': { - 'enabled': True, - 'min': 5, - 'max': 7 + "sphinx": { + "enabled": True, + "min": 5, + "max": 7 } } - warnings.config_parser(tmpjson) - self.assertEqual(warnings.get_checker(SphinxChecker().name).minimum, 5) + warnings.config_parser(tmpjson, False, None) + self.assertEqual(warnings.get_checker(SphinxChecker.name).minimum, 5) def test_doxygen_config_min(self): warnings = WarningsPlugin() tmpjson = { - 'doxygen': { - 'enabled': True, - 'min': 5, - 'max': 7 + "doxygen": { + "enabled": True, + "min": 5, + "max": 7 } } - warnings.config_parser(tmpjson) - self.assertEqual(warnings.get_checker(DoxyChecker().name).minimum, 5) + warnings.config_parser(tmpjson, False, None) + self.assertEqual(warnings.get_checker(DoxyChecker.name).minimum, 5) def test_junit_config_min(self): warnings = WarningsPlugin() tmpjson = { - 'junit': { - 'enabled': True, - 'min': 5, - 'max': 7 + "junit": { + "enabled": True, + "min": 5, + "max": 7 } } - warnings.config_parser(tmpjson) - self.assertEqual(warnings.get_checker(JUnitChecker().name).minimum, 5) + warnings.config_parser(tmpjson, False, None) + self.assertEqual(warnings.get_checker(JUnitChecker.name).minimum, 5) def test_xmlrunner_config_min(self): warnings = WarningsPlugin() tmpjson = { - 'xmlrunner': { - 'enabled': True, - 'min': 5, - 'max': 7 + "xmlrunner": { + "enabled": True, + "min": 5, + "max": 7 } } - warnings.config_parser(tmpjson) - self.assertEqual(warnings.get_checker(XMLRunnerChecker().name).minimum, 5) + warnings.config_parser(tmpjson, False, None) + self.assertEqual(warnings.get_checker(XMLRunnerChecker.name).minimum, 5) def test_all_config_min(self): warnings = WarningsPlugin() tmpjson = { - 'sphinx': { - 'enabled': True, - 'min': 4, - 'max': 7 + "sphinx": { + "enabled": True, + "min": 4, + "max": 7 }, - 'doxygen': { - 'enabled': True, - 'min': 3, - 'max': 7 + "doxygen": { + "enabled": True, + "min": 3, + "max": 7 }, - 'junit': { - 'enabled': True, - 'min': 5, - 'max': 7 + "junit": { + "enabled": True, + "min": 5, + "max": 7 }, - 'xmlrunner': { - 'enabled': True, - 'min': 5, - 'max': 7 + "xmlrunner": { + "enabled": True, + "min": 5, + "max": 7 }, - 'robot': { - 'enabled': True, - 'suites': [ + "robot": { + "enabled": True, + "suites": [ { - 'name': 'dummy1', - 'min': 5, - 'max': 7, + "name": "dummy1", + "min": 5, + "max": 7, }, { - 'name': 'dummy2', - 'min': 1, - 'max': 9, + "name": "dummy2", + "min": 1, + "max": 9, }, { - 'name': 'dummy3', - 'min': 2, - 'max': 2, + "name": "dummy3", + "min": 2, + "max": 2, } ] } } - warnings.config_parser(tmpjson) - self.assertEqual(warnings.get_checker(SphinxChecker().name).minimum, 4) - self.assertEqual(warnings.get_checker(DoxyChecker().name).minimum, 3) - self.assertEqual(warnings.get_checker(JUnitChecker().name).minimum, 5) - self.assertEqual(warnings.get_checker(XMLRunnerChecker().name).minimum, 5) - self.assertEqual(warnings.get_checker(RobotChecker().name).minimum, 1) + warnings.config_parser(tmpjson, False, None) + self.assertEqual(warnings.get_checker(SphinxChecker.name).minimum, 4) + self.assertEqual(warnings.get_checker(DoxyChecker.name).minimum, 3) + self.assertEqual(warnings.get_checker(JUnitChecker.name).minimum, 5) + self.assertEqual(warnings.get_checker(XMLRunnerChecker.name).minimum, 5) + self.assertEqual(warnings.get_checker(RobotChecker.name).minimum, 1) def test_invalid_config(self): warnings = WarningsPlugin() tmpjson = { - 'robot': { - 'enabled': True, - 'suites': [ + "robot": { + "enabled": True, + "suites": [ { - 'name': '', - 'min': 5, - 'max': 7, + "name": "", + "min": 5, + "max": 7, }, { - 'name': 'dummy2', - 'min': 10, - 'max': 9, + "name": "dummy2", + "min": 10, + "max": 9, }, { - 'name': 'dummy3', - 'min': 2, - 'max': 2, + "name": "dummy3", + "min": 2, + "max": 2, } ] } } with self.assertRaises(ValueError) as c_m: - warnings.config_parser(tmpjson) + warnings.config_parser(tmpjson, False, None) self.assertEqual(str(c_m.exception), - 'Invalid argument: minimum limit must be lower than maximum limit (9); cannot set 10.') + "Invalid argument: minimum limit must be lower than maximum limit (9); cannot set 10.") diff --git a/tests/test_coverity.py b/tests/test_coverity.py index f91e40d4..a8e37e84 100644 --- a/tests/test_coverity.py +++ b/tests/test_coverity.py @@ -1,14 +1,15 @@ import filecmp import os -from io import StringIO from pathlib import Path -from unittest import TestCase, mock +from unittest import TestCase from unittest.mock import patch -from mlx.warnings import WarningsPlugin, warnings_wrapper, Finding +import pytest -TEST_IN_DIR = Path(__file__).parent / 'test_in' -TEST_OUT_DIR = Path(__file__).parent / 'test_out' +from mlx.warnings import Finding, WarningsPlugin, warnings_wrapper + +TEST_IN_DIR = Path(__file__).parent / "test_in" +TEST_OUT_DIR = Path(__file__).parent / "test_out" def ordered(obj): @@ -20,90 +21,98 @@ def ordered(obj): return obj -@mock.patch.dict(os.environ, { +@patch.dict(os.environ, { "MIN_UNCLASSIFIED": "8", "MAX_UNCLASSIFIED": "8", "MIN_INTENTIONAL": "1", "MAX_INTENTIONAL": "1", "MIN_FALSE_POSITIVE": "2", "MAX_FALSE_POSITIVE": "2", }) class TestCoverityWarnings(TestCase): + @pytest.fixture(autouse=True) + def caplog(self, caplog): + self.caplog = caplog + def setUp(self): Finding.fingerprints = {} - self.warnings = WarningsPlugin(verbose=True) - self.warnings.activate_checker_name('coverity') + self.warnings = WarningsPlugin() + self.warnings.activate_checker_name("coverity", True, None) def test_no_warning_normal_text(self): - dut = 'This should not be treated as warning' + dut = "This should not be treated as warning" self.warnings.check(dut) self.assertEqual(self.warnings.return_count(), 0) def test_no_warning_but_still_command_output(self): - dut = 'src/something/src/somefile.c:82: 1. misra_violation: Essential type of the left hand operand "0U" (unsigned) is not the same as that of the right operand "1U"(signed).' + dut = "src/something/src/somefile.c:82: 1. misra_violation: Essential type of the left hand operand \"0U\" "\ + "(unsigned) is not the same as that of the right operand \"1U\"(signed)." self.warnings.check(dut) self.assertEqual(self.warnings.return_count(), 0) def test_single_warning(self): - dut = '/src/somefile.c:82: CID 113396 (#2 of 2): Coding standard violation (MISRA C-2012 Rule 10.1): Unclassified, Unspecified, Undecided, owner is nobody, first detected on 2017-07-27.' - with patch('sys.stdout', new=StringIO()) as fake_out: - self.warnings.check(dut) + dut = "/src/somefile.c:82: CID 113396 (#2 of 2): Coding standard violation (MISRA C-2012 Rule 10.1): "\ + "Unclassified, Unspecified, Undecided, owner is nobody, first detected on 2017-07-27." + self.warnings.check(dut) self.assertEqual(self.warnings.return_count(), 1) - self.assertIn(dut, fake_out.getvalue()) + self.assertEqual([f"{dut}"], self.caplog.messages) def test_single_warning_count_one(self): - dut1 = '/src/somefile.c:80: CID 113396 (#1 of 2): Coding standard violation (MISRA C-2012 Rule 10.1): Unclassified, Unspecified, Undecided, owner is nobody, first detected on 2017-07-27.' - dut2 = '/src/somefile.c:82: CID 113396 (#2 of 2): Coding standard violation (MISRA C-2012 Rule 10.1): Unclassified, Unspecified, Undecided, owner is nobody, first detected on 2017-07-27.' - with patch('sys.stdout', new=StringIO()) as fake_out: - self.warnings.check(dut1) - self.warnings.check(dut2) + dut1 = "/src/somefile.c:80: CID 113396 (#1 of 2): Coding standard violation (MISRA C-2012 Rule 10.1): "\ + "Unclassified, Unspecified, Undecided, owner is nobody, first detected on 2017-07-27." + dut2 = "/src/somefile.c:82: CID 113396 (#2 of 2): Coding standard violation (MISRA C-2012 Rule 10.1): "\ + "Unclassified, Unspecified, Undecided, owner is nobody, first detected on 2017-07-27." + self.warnings.check(dut1) + self.warnings.check(dut2) self.assertEqual(self.warnings.return_count(), 1) - self.assertIn(dut2, fake_out.getvalue()) + self.assertEqual([f"{dut2}"], self.caplog.messages) def test_single_warning_real_output(self): - dut1 = '/src/somefile.c:80: CID 113396 (#1 of 2): Coding standard violation (MISRA C-2012 Rule 10.1): Unclassified, Unspecified, Undecided, owner is nobody, first detected on 2017-07-27.' - dut2 = '/src/somefile.c:82: CID 113396 (#2 of 2): Coding standard violation (MISRA C-2012 Rule 10.1): Unclassified, Unspecified, Undecided, owner is nobody, first detected on 2017-07-27.' - dut3 = 'src/something/src/somefile.c:82: 1. misra_violation: Essential type of the left hand operand "0U" (unsigned) is not the same as that of the right operand "1U"(signed).' - with patch('sys.stdout', new=StringIO()) as fake_out: - self.warnings.check(dut1) - self.warnings.check(dut2) - self.warnings.check(dut3) + dut1 = "/src/somefile.c:80: CID 113396 (#1 of 2): Coding standard violation (MISRA C-2012 Rule 10.1): "\ + "Unclassified, Unspecified, Undecided, owner is nobody, first detected on 2017-07-27." + dut2 = "/src/somefile.c:82: CID 113396 (#2 of 2): Coding standard violation (MISRA C-2012 Rule 10.1): "\ + "Unclassified, Unspecified, Undecided, owner is nobody, first detected on 2017-07-27." + dut3 = "src/something/src/somefile.c:82: 1. misra_violation: Essential type of the left hand operand \"0U\" "\ + "(unsigned) is not the same as that of the right operand \"1U\"(signed)." + self.warnings.check(dut1) + self.warnings.check(dut2) + self.warnings.check(dut3) self.assertEqual(self.warnings.return_count(), 1) - self.assertIn(dut2, fake_out.getvalue()) + self.assertEqual([f"{dut2}"], self.caplog.messages) def test_code_quality_without_config(self): - filename = 'coverity_cq.json' + filename = "coverity_cq.json" out_file = str(TEST_OUT_DIR / filename) ref_file = str(TEST_IN_DIR / filename) retval = warnings_wrapper([ - '--coverity', - '--code-quality', out_file, - str(TEST_IN_DIR / 'coverity_full.txt'), + "--coverity", + "--code-quality", out_file, + str(TEST_IN_DIR / "coverity_full.txt"), ]) self.assertEqual(11, retval) self.assertTrue(filecmp.cmp(out_file, ref_file)) def test_code_quality_with_config_pass(self): - filename = 'coverity_cq.json' + filename = "coverity_cq.json" out_file = str(TEST_OUT_DIR / filename) ref_file = str(TEST_IN_DIR / filename) retval = warnings_wrapper([ - '--code-quality', out_file, - '--config', str(TEST_IN_DIR / 'config_example_coverity.yml'), - str(TEST_IN_DIR / 'coverity_full.txt'), + "--code-quality", out_file, + "--config", str(TEST_IN_DIR / "config_example_coverity.yml"), + str(TEST_IN_DIR / "coverity_full.txt"), ]) self.assertEqual(0, retval) self.assertTrue(filecmp.cmp(out_file, ref_file)) - @mock.patch.dict(os.environ, { + @patch.dict(os.environ, { "MIN_UNCLASSIFIED": "11", "MAX_UNCLASSIFIED": "-1", "MIN_FALSE_POSITIVE": "0", "MAX_FALSE_POSITIVE": "1", }) def test_code_quality_with_config_fail(self): - filename = 'coverity_cq.json' + filename = "coverity_cq.json" out_file = str(TEST_OUT_DIR / filename) ref_file = str(TEST_IN_DIR / filename) retval = warnings_wrapper([ - '--code-quality', out_file, - '--config', str(TEST_IN_DIR / 'config_example_coverity.yml'), - str(TEST_IN_DIR / 'coverity_full.txt'), + "--code-quality", out_file, + "--config", str(TEST_IN_DIR / "config_example_coverity.yml"), + str(TEST_IN_DIR / "coverity_full.txt"), ]) self.assertEqual(10, retval) # 8 + 2 not within range 6 and 7 self.assertTrue(filecmp.cmp(out_file, ref_file)) diff --git a/tests/test_doxygen.py b/tests/test_doxygen.py index 6a7ee7d4..d3a6aecc 100644 --- a/tests/test_doxygen.py +++ b/tests/test_doxygen.py @@ -1,77 +1,80 @@ -from io import StringIO from unittest import TestCase -from unittest.mock import patch +import pytest from mlx.warnings import WarningsPlugin class TestDoxygenWarnings(TestCase): + @pytest.fixture(autouse=True) + def caplog(self, caplog): + self.caplog = caplog + def setUp(self): - self.warnings = WarningsPlugin(verbose=True) - self.warnings.activate_checker_name('doxygen') + self.warnings = WarningsPlugin() + self.warnings.activate_checker_name("doxygen", True, None) def test_no_warning(self): - dut = 'This should not be treated as warning' + dut = "This should not be treated as warning" self.warnings.check(dut) self.assertEqual(self.warnings.return_count(), 0) def test_single_warning(self): - dut = 'testfile.c:6: warning: group test: ignoring title "Some test functions" that does not match old title "Some freaky test functions"' - with patch('sys.stdout', new=StringIO()) as fake_out: - self.warnings.check(dut) + dut = "testfile.c:6: warning: group test: ignoring title \"Some test functions\" that does not match "\ + "old title \"Some freaky test functions\"" + self.warnings.check(dut) self.assertEqual(self.warnings.return_count(), 1) - self.assertRegex(fake_out.getvalue(), dut) + self.assertEqual([f"{dut}"], self.caplog.messages) def test_single_warning_mixed(self): - dut1 = 'This1 should not be treated as warning' - dut2 = 'testfile.c:6: warning: group test: ignoring title "Some test functions" that does not match old title "Some freaky test functions"' - dut3 = 'This should not be treated as warning2' - with patch('sys.stdout', new=StringIO()) as fake_out: - self.warnings.check(dut1) - self.warnings.check(dut2) - self.warnings.check(dut3) + dut1 = "This1 should not be treated as warning" + dut2 = "testfile.c:6: warning: group test: ignoring title \"Some test functions\" that does not match "\ + "old title \"Some freaky test functions\"" + dut3 = "This should not be treated as warning2" + self.warnings.check(dut1) + self.warnings.check(dut2) + self.warnings.check(dut3) self.assertEqual(self.warnings.return_count(), 1) - self.assertRegex(fake_out.getvalue(), dut2) + self.assertEqual([f"{dut2}"], self.caplog.messages) def test_multiline(self): - duterr1 = "testfile.c:6: warning: group test: ignoring title \"Some test functions\" that does not match old title \"Some freaky test functions\"\n" - duterr2 = "testfile.c:8: warning: group test: ignoring title \"Some test functions\" that does not match old title \"Some freaky test functions\"\n" + duterr1 = "testfile.c:6: warning: group test: ignoring title \"Some test functions\" that does not match "\ + "old title \"Some freaky test functions\"\n" + duterr2 = "testfile.c:8: warning: group test: ignoring title \"Some test functions\" that does not match "\ + "old title \"Some freaky test functions\"\n" dut = "This1 should not be treated as warning\n" dut += duterr1 dut += "This should not be treated as warning2\n" dut += duterr2 - with patch('sys.stdout', new=StringIO()) as fake_out: - self.warnings.check(dut) + self.warnings.check(dut) self.assertEqual(self.warnings.return_count(), 2) - self.assertRegex(fake_out.getvalue(), duterr1) - self.assertRegex(fake_out.getvalue(), duterr2) + self.assertEqual([f"{duterr1.strip()}", f"{duterr2.strip()}"], self.caplog.messages) def test_git_warning(self): - duterr1 = "testfile.c:6: warning: group test: ignoring title \"Some test functions\" that does not match old title \"Some freaky test functions\"\n" - duterr2 = "testfile.c:8: warning: group test: ignoring title \"Some test functions\" that does not match old title \"Some freaky test functions\"\n" + duterr1 = "testfile.c:6: warning: group test: ignoring title \"Some test functions\" that does not match "\ + "old title \"Some freaky test functions\"\n" + duterr2 = "testfile.c:8: warning: group test: ignoring title \"Some test functions\" that does not match "\ + "old title \"Some freaky test functions\"\n" dut = "warning: notes ref refs/notes/review is invalid should not be treated as warning\n" dut += duterr1 dut += "This should not be treated as warning2\n" dut += duterr2 - with patch('sys.stdout', new=StringIO()) as fake_out: - self.warnings.check(dut) + self.warnings.check(dut) self.assertEqual(self.warnings.return_count(), 2) - self.assertRegex(fake_out.getvalue(), duterr1) - self.assertRegex(fake_out.getvalue(), duterr2) + self.assertEqual([f"{duterr1.strip()}", f"{duterr2.strip()}"], self.caplog.messages) def test_sphinx_deprecation_warning(self): - duterr1 = "testfile.c:6: warning: group test: ignoring title \"Some test functions\" that does not match old title \"Some freaky test functions\"\n" - dut = "/usr/local/lib/python3.5/dist-packages/sphinx/application.py:402: RemovedInSphinx20Warning: app.info() "\ - "is now deprecated. Use sphinx.util.logging instead. RemovedInSphinx20Warning)\n" + duterr1 = "testfile.c:6: warning: group test: ignoring title \"Some test functions\" that does not match "\ + "old title \"Some freaky test functions\"\n" + dut = "/usr/local/lib/python3.5/dist-packages/sphinx/application.py:402: RemovedInSphinx20Warning: "\ + "app.info() is now deprecated. Use sphinx.util.logging instead. RemovedInSphinx20Warning)\n" dut += duterr1 - with patch('sys.stdout', new=StringIO()) as fake_out: - self.warnings.check(dut) + self.warnings.check(dut) self.assertEqual(self.warnings.return_count(), 1) - self.assertRegex(fake_out.getvalue(), duterr1) + self.assertEqual([f"{duterr1.strip()}"], self.caplog.messages) def test_doxygen_warnings_txt(self): - dut_file = 'tests/test_in/doxygen_warnings.txt' - with open(dut_file, 'r') as open_file: + dut_file = "tests/test_in/doxygen_warnings.txt" + with open(dut_file) as open_file: self.warnings.check(open_file.read()) self.assertEqual(self.warnings.return_count(), 22) diff --git a/tests/test_in/cov_out.txt b/tests/test_in/cov_out.txt new file mode 100644 index 00000000..3149d030 --- /dev/null +++ b/tests/test_in/cov_out.txt @@ -0,0 +1,11 @@ +Coverity: unclassified | some/path/boot.c:32:5: CID 446411 (#1 of 1): Infinite loop (INFINITE_LOOP): Unclassified, Unspecified, Undecided, owner is Unassigned, defect only exists locally. +Coverity: unclassified | some/path/boot.c:55:12: CID 446410 (#1 of 1): MISRA C-2012 The Essential Type Model (MISRA C-2012 Rule 10.3, Required): Unclassified, Unspecified, Undecided, owner is Unassigned, defect only exists locally. +Coverity: unclassified | some/path/boot.c:37:13: CID 446409 (#1 of 1): MISRA C-2012 Control Flow Expressions (MISRA C-2012 Rule 14.3, Required): Unclassified, Unspecified, Undecided, owner is Unassigned, defect only exists locally. +Coverity: unclassified | some/path/boot.c:37:13: CID 446408 (#1 of 1): Logically dead code (DEADCODE): Unclassified, Unspecified, Undecided, owner is Unassigned, defect only exists locally. +Coverity: unclassified | some/path/boot.c:36:13: CID 446407 (#1 of 1): MISRA C-2012 The Essential Type Model (MISRA C-2012 Rule 10.4, Required): Unclassified, Unspecified, Undecided, owner is Unassigned, defect only exists locally. +Coverity: unclassified | some/path/boot.c:32:5: CID 446406 (#1 of 1): MISRA C-2012 Control Flow Expressions (MISRA C-2012 Rule 14.3, Required): Unclassified, Unspecified, Undecided, owner is Unassigned, defect only exists locally. +Coverity: unclassified | some/path/boot.c:37:31: CID 446405 (#1 of 1): MISRA C-2012 Unused Code (MISRA C-2012 Rule 2.2, Required): Unclassified, Unspecified, Undecided, owner is Unassigned, defect only exists locally. +Coverity: intentional | some/path/dummy_int.h:34:12: CID 264736 (#1 of 1): MISRA C-2012 Standard C Environment (MISRA C-2012 Rule 1.2, Advisory): Intentional, Minor, Ignore, owner is Unassigned, defect only exists locally. +Coverity: false positive | some/path/dummy_fp.c:367:13: CID 423570 (#1 of 1): Out-of-bounds write (OVERRUN): False Positive, Minor, Ignore, owner is sfo, defect only exists locally. +Coverity: false positive | some/path/dummy_fp.c:367:13: CID 423568 (#1 of 1): MISRA C-2012 Pointers and Arrays (MISRA C-2012 Rule 18.1, Required): False Positive, Minor, Ignore, owner is sfo, defect only exists locally. +Coverity: unclassified | some/path/dummy_uncl.h:194:14: CID 431350 (#1 of 1): MISRA C-2012 Declarations and Definitions (MISRA C-2012 Rule 8.5, Required): Unclassified, Unspecified, Undecided, owner is Unassigned, defect only exists locally. diff --git a/tests/test_in/junit_double_fail_summary.txt b/tests/test_in/junit_double_fail_summary.txt index 298fa54c..4f4a29d7 100644 --- a/tests/test_in/junit_double_fail_summary.txt +++ b/tests/test_in/junit_double_fail_summary.txt @@ -1,2 +1,2 @@ -test_warn_plugin_double_fail.myfirstfai1ure: Is our warnings plugin able to trace this random failure msg? -test_warn_plugin_no_double_fail.mysecondfai1ure: Second failure +JUnit: test_warn_plugin_double_fail.myfirstfai1ure | Is our warnings plugin able to trace this random failure msg? +JUnit: test_warn_plugin_no_double_fail.mysecondfai1ure | Second failure diff --git a/tests/test_in/polyspace_output.txt b/tests/test_in/polyspace_output.txt new file mode 100644 index 00000000..aabb6e93 --- /dev/null +++ b/tests/test_in/polyspace_output.txt @@ -0,0 +1,74 @@ +Polyspace: run-time check : color : orange | ID '19928' +Polyspace: run-time check : color : orange | ID '19526' +Polyspace: run-time check : color : orange | ID '19424' +Polyspace: run-time check : color : orange | ID '19442' +Polyspace: run-time check : color : orange | ID '19375' +Polyspace: run-time check : color : orange | ID '19378' +Polyspace: run-time check : color : orange | ID '19377' +Polyspace: run-time check : color : orange | ID '19357' +Polyspace: run-time check : color : orange | ID '19352' +Polyspace: run-time check : color : orange | ID '19355' +Polyspace: run-time check : color : orange | ID '19354' +Polyspace: run-time check : color : orange | ID '19358' +Polyspace: run-time check : color : orange | ID '19360' +Polyspace: run-time check : color : orange | ID '19349' +Polyspace: run-time check : color : orange | ID '19345' +Polyspace: run-time check : color : orange | ID '19344' +Polyspace: run-time check : color : orange | ID '19338' +Polyspace: run-time check : color : orange | ID '19336' +Polyspace: run-time check : color : orange | ID '19335' +Polyspace: defect : information : impact: high | ID '17503' +Polyspace: defect : information : impact: high | ID '17504' +Polyspace: defect : information : impact: high | ID '17505' +Polyspace: defect : information : impact: high | ID '17506' +Polyspace: defect : information : impact: high | ID '17507' +Polyspace: defect : information : impact: high | ID '17508' +Polyspace: defect : information : impact: high | ID '17509' +Polyspace: defect : information : impact: high | ID '17510' +Polyspace: defect : information : impact: high | ID '17511' +Polyspace: defect : information : impact: high | ID '17512' +Polyspace: defect : information : impact: high | ID '17513' +Polyspace: defect : information : impact: high | ID '17514' +Polyspace: defect : information : impact: high | ID '17515' +Polyspace: defect : information : impact: high | ID '17516' +Polyspace: defect : information : impact: high | ID '17517' +Polyspace: defect : information : impact: high | ID '17518' +Polyspace: defect : information : impact: high | ID '17519' +Polyspace: defect : information : impact: high | ID '17520' +Polyspace: defect : information : impact: high | ID '17521' +Polyspace: defect : information : impact: high | ID '17522' +Polyspace: defect : information : impact: high | ID '17523' +Polyspace: defect : information : impact: high | ID '17524' +Polyspace: defect : information : impact: high | ID '17525' +Polyspace: defect : information : impact: high | ID '17526' +Polyspace: defect : information : impact: high | ID '17527' +Polyspace: defect : information : impact: high | ID '17528' +Polyspace: defect : information : impact: high | ID '17529' +Polyspace: defect : information : impact: high | ID '17530' +Polyspace: defect : information : impact: high | ID '17531' +Polyspace: defect : information : impact: high | ID '17532' +Polyspace: defect : information : impact: high | ID '17533' +Polyspace: defect : information : impact: high | ID '17534' +Polyspace: defect : information : impact: high | ID '17535' +Polyspace: defect : information : impact: high | ID '17536' +Polyspace: defect : information : impact: high | ID '17537' +Polyspace: defect : information : impact: high | ID '17538' +Polyspace: defect : information : impact: high | ID '17539' +Polyspace: defect : information : impact: high | ID '17540' +Polyspace: defect : information : impact: high | ID '17541' +Polyspace: defect : information : impact: high | ID '17542' +Polyspace: defect : information : impact: high | ID '17543' +Polyspace: defect : information : impact: high | ID '17544' +Polyspace: defect : information : impact: medium | ID '17559' +Polyspace: defect : information : impact: medium | ID '17560' +Polyspace: defect : information : impact: medium | ID '17561' +Polyspace: defect : information : impact: medium | ID '17562' +Polyspace: defect : information : impact: medium | ID '17563' +Polyspace: defect : information : impact: medium | ID '17568' +Polyspace: defect : information : impact: medium | ID '17569' +Polyspace: defect : information : impact: medium | ID '17570' +Polyspace: defect : information : impact: medium | ID '17571' +Polyspace: defect : information : impact: low | ID '17557' +Polyspace: defect : information : impact: low | ID '17564' +Polyspace: defect : information : impact: low | ID '17565' +Polyspace: defect : information : impact: low | ID '17566' diff --git a/tests/test_in/polyspace_short.tsv b/tests/test_in/polyspace_short.tsv new file mode 100644 index 00000000..21e37acf --- /dev/null +++ b/tests/test_in/polyspace_short.tsv @@ -0,0 +1,11 @@ +ID Family Group Color New Check Information Function File Status Severity Comment Key Line Col +19339 Run-time Check Numerical Orange no Overflow dummy_function() dummy_file_name.c Not a defect Unset 66CC91F4CA91263464 10 8 +19338 Run-time Check Numerical Orange no Overflow dummy_function() dummy_file_name.c Unreviewed Unset 66CC91F4CA91263468 9 9 +19336 Run-time Check Numerical Orange no Overflow dummy_function() dummy_file_name.c Unreviewed Unset 66CC91F4CA91263970 9 10 +19335 Run-time Check Numerical Orange no Overflow dummy_function() dummy_file_name.c Unreviewed Unset CC9923E995234C62C0C9 10 11 +17533 Defect Concurrency Red no Data race Impact: High File Scope dummy_file_name.c Unreviewed Unset 630711A054827D921468519C 20 15 +17544 Defect Concurrency Red no Data race Impact: High File Scope dummy_file_name.c Unreviewed Unset 070C2241A805FA2528D0A2316A 23 15 +17559 Defect Numerical Red no Sign change integer conversion overflow Impact: Medium dummy_function() dummy_file_name.c Unreviewed Unset 3366C89A94E893AF4390057274A84E1C 22 7 +17560 Defect Numerical Red no Sign change integer conversion overflow Impact: Medium dummy_function() dummy_file_name.c Unreviewed Unset 3364E09A94E893AF4390057274A80D1C 22 8 +17557 Defect Data flow Red no Dead code Impact: Low dummy_function() dummy_file_name.c Unreviewed Unset 0E1B3288150A44EBD0A7448AC489 24 16 +17564 Defect Data flow Red no Dead code Impact: Low dummy_function() dummy_file_name.c Unreviewed Unset 070C1B448A0522F568D3224568 25 7 diff --git a/tests/test_in/robot_double_fail_config_summary.txt b/tests/test_in/robot_double_fail_config_summary.txt index 2809884d..a54a9d0c 100644 --- a/tests/test_in/robot_double_fail_config_summary.txt +++ b/tests/test_in/robot_double_fail_config_summary.txt @@ -1,4 +1,4 @@ -Suite One & Suite Two.Suite One.First Test: Directory 'C:\nonexistent' does not exist. -Suite One & Suite Two.Suite One.First Test: Directory 'C:\nonexistent' does not exist. -Suite One & Suite Two.Suite Two.Another test: Expected str; got int. -Suite One & Suite Two.Suite Two.Another test: Expected str; got int. +Robot: suite 'Suite One' Suite One & Suite Two.Suite One.First Test | Directory 'C:\nonexistent' does not exist. +Robot: all test suites Suite One & Suite Two.Suite One.First Test | Directory 'C:\nonexistent' does not exist. +Robot: all test suites Suite One & Suite Two.Suite Two.Another test | Expected str; got int. +Robot: suite 'Suite Two' Suite One & Suite Two.Suite Two.Another test | Expected str; got int. diff --git a/tests/test_in/robot_double_fail_summary.txt b/tests/test_in/robot_double_fail_summary.txt index 4e9820d8..7fdafbed 100644 --- a/tests/test_in/robot_double_fail_summary.txt +++ b/tests/test_in/robot_double_fail_summary.txt @@ -1,2 +1,2 @@ -Suite One & Suite Two.Suite One.First Test: Directory 'C:\nonexistent' does not exist. -Suite One & Suite Two.Suite Two.Another test: Expected str; got int. +Robot: all test suites Suite One & Suite Two.Suite One.First Test | Directory 'C:\nonexistent' does not exist. +Robot: all test suites Suite One & Suite Two.Suite Two.Another test | Expected str; got int. diff --git a/tests/test_in/sphinx_double_deprecation_warning_summary.txt b/tests/test_in/sphinx_double_deprecation_warning_summary.txt index 498c10dc..e71712f7 100644 --- a/tests/test_in/sphinx_double_deprecation_warning_summary.txt +++ b/tests/test_in/sphinx_double_deprecation_warning_summary.txt @@ -1,2 +1,2 @@ -/usr/local/lib/python3.7/dist-packages/sphinx/util/docutils.py:286: RemovedInSphinx30Warning: function based directive support is now deprecated. Use class based directive instead. -/usr/local/lib/python3.7/dist-packages/sphinx_rtd_theme/search.html:20: RemovedInSphinx30Warning: To modify script_files in the theme is deprecated. Please insert a