diff --git a/README.rst b/README.rst index 4c27a6f0..43da1ec1 100644 --- a/README.rst +++ b/README.rst @@ -123,7 +123,16 @@ The command returns (shell $? variable): - value 0 when the number of counted warnings is within the supplied minimum and maximum limits: ok, - number of counted warnings (positive) when the counter number is not within those limit. -------------------------- +--------------------------- +Simple Command line options +--------------------------- + +Plugin has two forms of passing the arguments to checkers. The command line +option which enables checkers and sets minimum and maximum to each checker +individually, or the configuration file option which provides more flexibility +and also traceability as it resides inside repository and provides option to +adjust minimum and maximum per individual checker. + Parse for Sphinx warnings ------------------------- @@ -135,17 +144,16 @@ command: # command line log file mlx-warnings doc_log.txt --sphinx # command line command execution - mlx-warnings --command --sphinx + mlx-warnings --sphinx --command # explicitly as python module for log file python3 -m mlx.warnings --sphinx doc_log.txt python -m mlx.warnings --sphinx doc_log.txt # explicitly as python module - python3 -m mlx.warnings --command --sphinx - python -m mlx.warnings --command --sphinx + python3 -m mlx.warnings --sphinx --command + python -m mlx.warnings --sphinx --command --------------------------- Parse for Doxygen warnings -------------------------- @@ -157,17 +165,16 @@ command: # command line log file mlx-warnings doc_log.txt --doxygen # command line command execution - mlx-warnings --command --doxygen + mlx-warnings --doxygen --command # explicitly as python module for log file python3 -m mlx.warnings --doxygen doc_log.txt python -m mlx.warnings --doxygen doc_log.txt # explicitly as python module - python3 -m mlx.warnings --command --doxygen - python -m mlx.warnings --command --doxygen + python3 -m mlx.warnings --doxygen --command + python -m mlx.warnings --doxygen --command ------------------------- Parse for JUnit failures ------------------------ @@ -179,14 +186,57 @@ command: # command line log file mlx-warnings junit_output.xml --junit # command line command execution - mlx-warnings --command --junit + mlx-warnings --junit --command # explicitly as python module for log file python3 -m mlx.warnings --junit junit_output.xml python -m mlx.warnings --junit junit_output.xml # explicitly as python module - python3 -m mlx.warnings --command --junit - python -m mlx.warnings --command --junit + python3 -m mlx.warnings --junit --command + python -m mlx.warnings --junit --command + + +---------------------------------- +Configuration file to pass options +---------------------------------- + +Beside command line, you can pass options through the configuration file. +Configuration file is in JSON format with a simple structure. + +.. code-block:: json + + { + "sphinx":{ + "enabled": true, + "min": 0, + "max": 0 + }, + "doxygen":{ + "enabled": false, + "min": 0, + "max": 0 + }, + "junit":{ + "enabled": false, + "min": 0, + "max": 0 + } + } + +First key is `checkername`, then it contains a boolean value for key `enabled`, +value for minimum number of warnings with key `min` and value for maximum +number of warnings with key `max`. This structure allows simple expansion. + +To run the plugin with configuration file you simply pass `--config` flag with +path to configuration file + +.. code-block:: bash + + # command line log file + mlx-warnings --config pathtoconfig.json junit_output.xml + # command line command execution + mlx-warnings --config patchtoconfig.json --command + ------------- Other options diff --git a/src/mlx/warnings.py b/src/mlx/warnings.py index 192d6d6f..4d0f0e56 100644 --- a/src/mlx/warnings.py +++ b/src/mlx/warnings.py @@ -4,6 +4,7 @@ from __future__ import print_function import argparse +import json import os import pkg_resources import subprocess @@ -17,7 +18,7 @@ class WarningsPlugin: - def __init__(self, sphinx = False, doxygen = False, junit = False, verbose = False): + def __init__(self, sphinx = False, doxygen = False, junit = False, verbose = False, configfile= None): ''' Function for initializing the parsers @@ -29,12 +30,17 @@ def __init__(self, sphinx = False, doxygen = False, junit = False, verbose = Fal ''' self.checkerList = {} self.verbose = verbose - if sphinx: - self.activate_checker(SphinxChecker(self.verbose)) - if doxygen: - self.activate_checker(DoxyChecker(self.verbose)) - if junit: - self.activate_checker(JUnitChecker(self.verbose)) + if configfile is not None: + with open(configfile, 'r') as f: + config = json.load(f) + self.config_parser_json(config) + else: + if sphinx: + self.activate_checker(SphinxChecker(self.verbose)) + if doxygen: + self.activate_checker(DoxyChecker(self.verbose)) + if junit: + self.activate_checker(JUnitChecker(self.verbose)) self.warn_min = 0 self.warn_max = 0 @@ -148,32 +154,58 @@ def toggle_printout(self, printout): ''' self.printout = printout + def config_parser_json(self, config): + ''' Parsing configuration dict extracted by previously opened json file + + Args: + config (dict): json dump of the configuration + ''' + self.publicCheckers = [SphinxChecker(), DoxyChecker(), JUnitChecker()] + # activate checker + for checker in self.publicCheckers: + try: + if bool(config[checker.name]['enabled']): + self.activate_checker(checker) + self.get_checker(checker.name).set_maximum(int(config[checker.name]['max'])) + self.get_checker(checker.name).set_minimum(int(config[checker.name]['min'])) + print("Config parsing for {name} completed".format(name=checker.name)) + except KeyError as e: + print("Incomplete config. Missing: {key}".format(key=e)) + def warnings_wrapper(args): parser = argparse.ArgumentParser(prog='mlx-warnings') - group = parser.add_mutually_exclusive_group(required=True) - group.add_argument('-d', '--doxygen', dest='doxygen', action='store_true') - group.add_argument('-s', '--sphinx', dest='sphinx', action='store_true') - group.add_argument('-j', '--junit', dest='junit', action='store_true') + group1 = parser.add_argument_group('Configuration command line options') + group1.add_argument('-d', '--doxygen', dest='doxygen', action='store_true') + group1.add_argument('-s', '--sphinx', dest='sphinx', action='store_true') + group1.add_argument('-j', '--junit', dest='junit', action='store_true') + group1.add_argument('-m', '--maxwarnings', type=int, required=False, default=0, + help='Maximum amount of warnings accepted') + group1.add_argument('--minwarnings', type=int, required=False, default=0, + help='Minimum amount of warnings accepted') + group2 = parser.add_argument_group('Configuration file with options') + group2.add_argument('--config', dest='configfile', action='store', required=False, help='Config file in JSON format provides toggle of checkers and their limits') 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('-m', '--maxwarnings', type=int, required=False, default=0, - help='Maximum amount of warnings accepted') - parser.add_argument('--minwarnings', type=int, required=False, default=0, - help='Minimum amount of warnings accepted') parser.add_argument('--version', action='version', version='%(prog)s {version}'.format(version=pkg_resources.require('mlx.warnings')[0].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) - warnings = WarningsPlugin(sphinx=args.sphinx, doxygen=args.doxygen, junit=args.junit, verbose=args.verbose) - warnings.set_maximum(args.maxwarnings) - warnings.set_minimum(args.minwarnings) + # Read config file + if args.configfile is not None: + if args.sphinx or args.doxygen or args.junit or (args.maxwarnings != 0) or (args.minwarnings != 0): + print("Configfile cannot be provided with other arguments") + sys.exit(2) + warnings = WarningsPlugin(configfile=args.configfile) + else: + warnings = WarningsPlugin(sphinx=args.sphinx, doxygen=args.doxygen, junit=args.junit, verbose=args.verbose) + warnings.set_maximum(args.maxwarnings) + warnings.set_minimum(args.minwarnings) if args.command: cmd = args.logfile diff --git a/src/mlx/warnings_checker.py b/src/mlx/warnings_checker.py index 4d4acb46..4727ea06 100644 --- a/src/mlx/warnings_checker.py +++ b/src/mlx/warnings_checker.py @@ -4,6 +4,8 @@ import abc import re from junitparser import JUnitXml, Failure, Error +from xml.etree.ElementTree import ParseError + DOXYGEN_WARNING_REGEX = r"(?:((?:[/.]|[A-Za-z]).+?):(-?\d+):\s*([Ww]arning|[Ee]rror)|<.+>:-?\d+(?::\s*([Ww]arning|[Ee]rror))?): (.+(?:(?!\s*(?:[Nn]otice|[Ww]arning|[Ee]rror): )[^/<\n][^:\n][^/\n].+)*)|\s*([Nn]otice|[Ww]arning|[Ee]rror): (.+)\n?" doxy_pattern = re.compile(DOXYGEN_WARNING_REGEX) @@ -166,14 +168,17 @@ def check(self, content): Args: content (str): The content to parse ''' - result = JUnitXml.fromstring(content.encode('utf-8')) - if self.verbose: - for suite in result: - for testcase in filter(lambda testcase: isinstance(testcase.result, (Failure, Error)), suite): - print('{classname}.{testname}'.format(classname=testcase.classname, - testname=testcase.name)) - result.update_statistics() - self.count += result.errors + result.failures + try: + result = JUnitXml.fromstring(content.encode('utf-8')) + if self.verbose: + for suite in result: + for testcase in filter(lambda testcase: isinstance(testcase.result, (Failure, Error)), suite): + print('{classname}.{testname}'.format(classname=testcase.classname, + testname=testcase.name)) + result.update_statistics() + self.count += result.errors + result.failures + except ParseError as _: + return diff --git a/tests/config_example.json b/tests/config_example.json new file mode 100644 index 00000000..206315b6 --- /dev/null +++ b/tests/config_example.json @@ -0,0 +1,17 @@ +{ + "sphinx":{ + "enabled": true, + "min": 0, + "max": 0 + }, + "doxygen":{ + "enabled": false, + "min": 0, + "max": 0 + }, + "junit":{ + "enabled": false, + "min": 0, + "max": 0 + } +} diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 00000000..209d138b --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,257 @@ +from unittest import TestCase + +from mlx.warnings import WarningsPlugin, SphinxChecker, DoxyChecker, JUnitChecker + + +class TestConfig(TestCase): + def test_configfile_parsing(self): + warnings = WarningsPlugin(configfile="tests/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"') + self.assertEqual(warnings.return_count(), 0) + warnings.check('') + self.assertEqual(warnings.return_count(), 0) + warnings.check("/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document u'installation'") + self.assertEqual(warnings.return_count(), 1) + warnings.check('This should not be treated as warning2') + self.assertEqual(warnings.return_count(), 1) + + def test_partial_sphinx_config_parsing(self): + warnings = WarningsPlugin() + tmpjson = { + 'sphinx': { + 'enabled': True, + 'min': 0, + 'max': 0 + } + } + + warnings.config_parser_json(tmpjson) + 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/junit_single_fail.xml', 'r') 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'") + self.assertEqual(warnings.return_count(), 1) + + def test_partial_doxygen_config_parsing(self): + warnings = WarningsPlugin() + tmpjson = { + 'doxygen': { + 'enabled': True, + 'min': 0, + 'max': 0 + } + } + + warnings.config_parser_json(tmpjson) + with open('tests/junit_single_fail.xml', 'r') 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'") + 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"') + self.assertEqual(warnings.return_count(), 1) + + def test_partial_junit_config_parsing(self): + warnings = WarningsPlugin() + tmpjson = { + 'junit': { + 'enabled': True, + 'min': 0, + 'max': 0 + } + } + + warnings.config_parser_json(tmpjson) + 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"') + self.assertEqual(warnings.return_count(), 0) + with open('tests/junit_single_fail.xml', 'r') as xmlfile: + warnings.check(xmlfile.read()) + self.assertEqual(warnings.return_count(), 1) + + def test_doxy_junit_options_config_parsing(self): + warnings = WarningsPlugin() + tmpjson = { + 'doxygen': { + 'enabled': True, + 'min': 0, + 'max': 0 + }, + 'junit': { + 'enabled': True, + 'min': 0, + 'max': 0 + } + + } + warnings.config_parser_json(tmpjson) + 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"') + self.assertEqual(warnings.return_count(), 1) + with open('tests/junit_single_fail.xml', 'r') 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 + }, + 'doxygen': { + 'enabled': True, + 'min': 0, + 'max': 0 + } + } + + warnings.config_parser_json(tmpjson) + with open('tests/junit_single_fail.xml', 'r') 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"') + self.assertEqual(warnings.return_count(), 1) + 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/junit_single_fail.xml', 'r') 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'") + 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"') + self.assertEqual(warnings.return_count(), 4) + + def test_sphinx_config_max(self): + warnings = WarningsPlugin() + tmpjson = { + 'sphinx': { + 'enabled': True, + 'min': 0, + 'max': 5 + } + } + + warnings.config_parser_json(tmpjson) + self.assertEqual(warnings.get_checker(SphinxChecker().name).get_maximum(), 5) + + def test_doxygen_config_max(self): + warnings = WarningsPlugin() + tmpjson = { + 'doxygen': { + 'enabled': True, + 'min': 0, + 'max': 5 + } + } + + warnings.config_parser_json(tmpjson) + self.assertEqual(warnings.get_checker(DoxyChecker().name).get_maximum(), 5) + + def test_junit_config_max(self): + warnings = WarningsPlugin() + tmpjson = { + 'junit': { + 'enabled': True, + 'min': 0, + 'max': 5 + } + } + + warnings.config_parser_json(tmpjson) + self.assertEqual(warnings.get_checker(JUnitChecker().name).get_maximum(), 5) + + def test_all_config_max(self): + warnings = WarningsPlugin() + tmpjson = { + 'sphinx': { + 'enabled': True, + 'min': 0, + 'max': 4 + }, + 'doxygen': { + 'enabled': True, + 'min': 0, + 'max': 5 + }, + 'junit': { + 'enabled': True, + 'min': 0, + 'max': 6 + } + } + + warnings.config_parser_json(tmpjson) + self.assertEqual(warnings.get_checker(SphinxChecker().name).get_maximum(), 4) + self.assertEqual(warnings.get_checker(DoxyChecker().name).get_maximum(), 5) + self.assertEqual(warnings.get_checker(JUnitChecker().name).get_maximum(), 6) + + def test_sphinx_config_min(self): + warnings = WarningsPlugin() + tmpjson = { + 'sphinx': { + 'enabled': True, + 'min': 5, + 'max': 7 + } + } + + warnings.config_parser_json(tmpjson) + self.assertEqual(warnings.get_checker(SphinxChecker().name).get_minimum(), 5) + + def test_doxygen_config_min(self): + warnings = WarningsPlugin() + tmpjson = { + 'doxygen': { + 'enabled': True, + 'min': 5, + 'max': 7 + } + } + + warnings.config_parser_json(tmpjson) + self.assertEqual(warnings.get_checker(DoxyChecker().name).get_minimum(), 5) + + def test_junit_config_min(self): + warnings = WarningsPlugin() + tmpjson = { + 'junit': { + 'enabled': True, + 'min': 5, + 'max': 7 + } + } + + warnings.config_parser_json(tmpjson) + self.assertEqual(warnings.get_checker(JUnitChecker().name).get_minimum(), 5) + + def test_all_config_min(self): + warnings = WarningsPlugin() + tmpjson = { + 'sphinx': { + 'enabled': True, + 'min': 4, + 'max': 7 + }, + 'doxygen': { + 'enabled': True, + 'min': 3, + 'max': 7 + }, + 'junit': { + 'enabled': True, + 'min': 5, + 'max': 7 + } + } + + warnings.config_parser_json(tmpjson) + self.assertEqual(warnings.get_checker(SphinxChecker().name).get_minimum(), 4) + self.assertEqual(warnings.get_checker(DoxyChecker().name).get_minimum(), 3) + self.assertEqual(warnings.get_checker(JUnitChecker().name).get_minimum(), 5) + diff --git a/tests/test_integration.py b/tests/test_integration.py index e9556a35..dd86e8fd 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -82,3 +82,13 @@ def test_min(self): def test_min_but_still_ok(self): retval = warnings_wrapper(['--junit', '--maxwarnings', '100', '--minwarnings', '2', 'tests/junit*.xml']) self.assertEqual(0, retval) + + def test_configfile_ok(self): + retval = warnings_wrapper(['--config', 'tests/config_example.json', 'tests/junit_single_fail.xml']) + self.assertEqual(0, retval) + + def test_configfile_exclude_commandline(self): + with self.assertRaises(SystemExit) as ex: + warnings_wrapper(['--config', 'tests/config_example.json', '--junit', 'tests/junit_single_fail.xml']) + self.assertEqual(2, ex.exception.code) + diff --git a/tests/test_junit.py b/tests/test_junit.py index 358dfcac..ce91b83a 100644 --- a/tests/test_junit.py +++ b/tests/test_junit.py @@ -6,7 +6,6 @@ from unittest import TestCase from mlx.warnings import WarningsPlugin -from xml.etree.ElementTree import ParseError class TestJUnitFailures(TestCase): @@ -34,7 +33,6 @@ def test_dual_warning(self): self.assertRegexpMatches(fake_out.getvalue(), 'mysecondfai1ure') def test_invalid_xml(self): - with self.assertRaises(ParseError): - self.warnings.check('this is not xml') + self.warnings.check('this is not xml') self.assertEqual(self.warnings.return_count(), 0) diff --git a/tests/test_warnings.py b/tests/test_warnings.py index d4dffbae..57f197ac 100644 --- a/tests/test_warnings.py +++ b/tests/test_warnings.py @@ -1,7 +1,6 @@ from unittest import TestCase from mlx.warnings import WarningsPlugin -from xml.etree.ElementTree import ParseError class TestWarningsPlugin(TestCase): @@ -50,14 +49,58 @@ def test_junit_warning_only(self): with open('tests/junit_single_fail.xml') as xmlfile: warnings.check(xmlfile.read()) self.assertEqual(warnings.return_count(), 1) - with self.assertRaises(ParseError): - 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) + 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 self.assertRaises(ParseError): - 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('This should not be treated as warning2') self.assertEqual(warnings.return_count(), 1) - with self.assertRaises(ParseError): - warnings.check('This should not be treated as warning2') + + def test_doxy_sphinx_warning(self): + warnings = WarningsPlugin(True, True, False) + 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'") + self.assertEqual(warnings.return_count(), 2) + with open('tests/junit_single_fail.xml') as xmlfile: + warnings.check(xmlfile.read()) + self.assertEqual(warnings.return_count(), 2) + warnings.check('This should not be treated as warning2') + self.assertEqual(warnings.return_count(), 2) + def test_doxy_junit_warning(self): + warnings = WarningsPlugin(False, True, True) + 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'") + self.assertEqual(warnings.return_count(), 1) + with open('tests/junit_single_fail.xml') as xmlfile: + warnings.check(xmlfile.read()) + self.assertEqual(warnings.return_count(), 2) + warnings.check('This should not be treated as warning2') + self.assertEqual(warnings.return_count(), 2) + + def test_sphinx_junit_warning(self): + warnings = WarningsPlugin(True, False, True) + 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("/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document u'installation'") + self.assertEqual(warnings.return_count(), 1) + with open('tests/junit_single_fail.xml') as xmlfile: + warnings.check(xmlfile.read()) + self.assertEqual(warnings.return_count(), 2) + warnings.check('This should not be treated as warning2') + self.assertEqual(warnings.return_count(), 2) + + def test_all_warning(self): + warnings = WarningsPlugin(True, True, True) + 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'") + self.assertEqual(warnings.return_count(), 2) + with open('tests/junit_single_fail.xml') as xmlfile: + warnings.check(xmlfile.read()) + self.assertEqual(warnings.return_count(), 3) + warnings.check('This should not be treated as warning2') + self.assertEqual(warnings.return_count(), 3)