Repository: melexis/warnings-plugin

Issue:
Parse output stream of command
```
warning-plugin <plugin-thresholds> <command> <arguments>
```
should disregard the command return value and parse stdout and stderr for warnings in plugin thresholds or in some special json formatted conf file in the root of the project. This will make it a  lot more useable than generating log files to parse through plugin

Discussion:

Patch:
diff --git a/README.rst b/README.rst
index 139e744..e5af286 100644
--- a/README.rst
+++ b/README.rst
@@ -57,11 +57,16 @@ You can find more details in `Installation guide <docs/installation.rst>`_
 Usage
 =====
 
-Since warnings plugin parses log messages (so far), you will need to redirect
-your stderr to some text file. You can do that with shell pipes or with
+Warnings plugin parses log messages as well as direct command stream. In case you
+want to create log file, you will need to redirect your stderr to some text file.
+You can do that with shell pipes or with
 command line arguments to command (if it supports outputting errors to file
 instead of stderr). Be aware that some commands print warnings on stdout.
 
+Also warnings plugin log files need to be the last argument as otherwise the
+arguments after that are discarded, because they are considered as command
+arguments (with or without command flag).
+
 ------------
 Pipe example
 ------------
@@ -73,6 +78,16 @@ file.
 
     yourcommand 2>&1 | tee doc_log.txt
 
+---------------
+Command example
+---------------
+
+Below is the command example for the plugin (keep in mind that parse commands are
+required).
+
+.. code-block:: bash
+
+    mlx-warnings --command <yourcommand>
 
 ---------------
 Running command
@@ -104,20 +119,26 @@ 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.
 
-----------------------------
+-------------------------
 Parse for Sphinx warnings
-----------------------------
+-------------------------
 
 After you saved your Sphinx warnings to the file, you can parse it with
 command:
 
 .. code-block:: bash
 
-    # command line
+    # command line log file
     mlx-warnings doc_log.txt --sphinx
+    # command line command execution
+    mlx-warnings --command --sphinx <commandforsphinx>
+
+    # 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 doc_log.txt --sphinx
-    python -m mlx.warnings doc_log.txt --sphinx
+    python3 -m mlx.warnings --command --sphinx <commandforsphinx>
+    python -m mlx.warnings --command --sphinx <commandforsphinx>
 
 
 --------------------------
@@ -129,11 +150,17 @@ command:
 
 .. code-block:: bash
 
-    # command line
+    # command line log file
     mlx-warnings doc_log.txt --doxygen
+    # command line command execution
+    mlx-warnings --command --doxygen <commandfordoxygen>
+
+    # 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 doc_log.txt --doxygen
-    python -m mlx.warnings doc_log.txt --doxygen
+    python3 -m mlx.warnings --command --doxygen <commandfordoxygen>
+    python -m mlx.warnings --command --doxygen <commandfordoxygen>
 
 
 ------------------------
@@ -145,11 +172,17 @@ command:
 
 .. code-block:: bash
 
-    # command line
+    # command line log file
     mlx-warnings junit_output.xml --junit
+    # command line command execution
+    mlx-warnings --command --junit <commandforjunit>
+
+    # 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 junit_output.xml --junit
-    python -m mlx.warnings junit_output.xml --junit
+    python3 -m mlx.warnings --command --junit <commandforjunit>
+    python -m mlx.warnings --command --junit <commandforjunit>
 
 -------------
 Other options
diff --git a/src/mlx/warnings.py b/src/mlx/warnings.py
index 240e13b..325ad23 100644
--- a/src/mlx/warnings.py
+++ b/src/mlx/warnings.py
@@ -1,9 +1,13 @@
 #!/usr/bin/env python
 # -*- coding: utf-8 -*-
 
+from __future__ import print_function
+
 import argparse
+import os
 import pkg_resources
 import re
+import subprocess
 import sys
 import abc
 from junitparser import JUnitXml, Failure, Error
@@ -13,7 +17,7 @@ from setuptools_scm import get_version
 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)
 
-SPHINX_WARNING_REGEX = r"(.+?:(?:\d+|None)?):?\s*(DEBUG|INFO|WARNING|ERROR|SEVERE):\s*(.+)\n?"
+SPHINX_WARNING_REGEX = r"(.+?:(?:\d+|None)):\s*(DEBUG|INFO|WARNING|ERROR|SEVERE):\s*(.+)\n?"
 sphinx_pattern = re.compile(SPHINX_WARNING_REGEX)
 
 __version__ = get_version()
@@ -313,33 +317,76 @@ def warnings_wrapper(args):
     group.add_argument('-s', '--sphinx', dest='sphinx', action='store_true')
     group.add_argument('-j', '--junit', dest='junit', action='store_true')
     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('-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 that might contain warnings')
+    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)
 
+    if args.command:
+        cmd = args.logfile
+        if args.flags:
+            cmd.extend(args.flags)
+        warnings_command(warnings, cmd)
+    else:
+        warnings_logfile(warnings, args.logfile)
+
+    warnings.return_count()
+    return warnings.return_check_limits()
+
+
+def warnings_command(warnings, cmd):
+    try:
+        print("Executing: ", end='')
+        print(cmd)
+        proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
+                                stdin=subprocess.PIPE, bufsize=1, universal_newlines=True)
+        out, err = proc.communicate()
+        # Check stdout
+        if out:
+            try:
+                print(out.decode(encoding="utf-8"))
+                warnings.check(out.decode(encoding="utf-8"))
+            except AttributeError as e:
+                warnings.check(out)
+                print(out)
+        # Check stderr
+        if err:
+            try:
+                warnings.check(err.decode(encoding="utf-8"))
+                print(err.decode(encoding="utf-8"), file=sys.stderr)
+            except AttributeError as e:
+                warnings.check(err)
+                print(err, file=sys.stderr)
+    except OSError as e:
+        if e.errno == os.errno.ENOENT:
+            print("It seems like program " + str(cmd) + " is not installed.")
+        raise
+
+
+def warnings_logfile(warnings, log):
     # 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
     # executing the script on windows (in that case there is no shell expansion of wildcards)
     # so that the script can be used in the exact same way even when moving from one
     # OS to another.
-    for file_wildcard in args.logfile:
+    for file_wildcard in log:
         for logfile in glob.glob(file_wildcard):
             with open(logfile, 'r') as loghandle:
                 warnings.check(loghandle.read())
 
-    warnings.return_count()
-    return warnings.return_check_limits()
-
 
 def main():
     sys.exit(warnings_wrapper(sys.argv[1:]))
diff --git a/tox.ini b/tox.ini
index 33199f3..d369cc7 100644
--- a/tox.ini
+++ b/tox.ini
@@ -26,7 +26,6 @@ deps =
     pytest-cov
     mock
     junitparser>=1.0.0
-    setuptools_scm
     coverage
 commands =
     {posargs:py.test --cov=mlx --cov-report=term-missing -vv tests/}
@@ -35,8 +34,9 @@ commands =
     python -c 'import mlx.warnings;print(mlx.warnings.__version__)'
     python -m mlx.warnings -h
     python -m mlx.warnings --version
-    python -m mlx.warnings -j tests/junit*.xml --maxwarnings 3 --minwarnings 3
-    python -m mlx.warnings -j "tests/junit*.xml" --maxwarnings 3 --minwarnings 3 #emulate for windows (no shell expansion)
+    python -m mlx.warnings -j --maxwarnings 3 --minwarnings 3 tests/junit*.xml
+    python -m mlx.warnings -j --maxwarnings 3 --minwarnings 3 "tests/junit*.xml"  #emulate for windows (no shell expansion)
+    python -m mlx.warnings -j --command --maxwarnings 2 --minwarnings 2 cat tests/junit_double_fail.xml
 
 [testenv:bootstrap]
 deps =


Test patch:
diff --git a/tests/sphinx_double_warning.txt b/tests/sphinx_double_warning.txt
new file mode 100644
index 0000000..54d4f42
--- /dev/null
+++ b/tests/sphinx_double_warning.txt
@@ -0,0 +1,3 @@
+/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document u'installation'
+/home/bljah/test/index.rst:None: WARNING: toctree contains reference to nonexisting document u'installation'
+
diff --git a/tests/sphinx_single_warning.txt b/tests/sphinx_single_warning.txt
new file mode 100644
index 0000000..3dd77fc
--- /dev/null
+++ b/tests/sphinx_single_warning.txt
@@ -0,0 +1,2 @@
+/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document u'installation'
+
diff --git a/tests/test_integration.py b/tests/test_integration.py
index 0a06f45..97351a5 100644
--- a/tests/test_integration.py
+++ b/tests/test_integration.py
@@ -5,21 +5,6 @@ from mlx.warnings import warnings_wrapper
 
 class TestIntegration(TestCase):
 
-    def test_help(self):
-        with self.assertRaises(SystemExit) as ex:
-            warnings_wrapper(['--help'])
-        self.assertEqual(0, ex.exception.code)
-
-    def test_version(self):
-        with self.assertRaises(SystemExit) as ex:
-            warnings_wrapper(['--version'])
-        self.assertEqual(0, ex.exception.code)
-
-    def test_no_parser_selection(self):
-        with self.assertRaises(SystemExit) as ex:
-            warnings_wrapper([])
-        self.assertEqual(2, ex.exception.code)
-
     junit_warning_cnt = 3
 
     def test_single_argument(self):
@@ -30,6 +15,26 @@ class TestIntegration(TestCase):
         retval = warnings_wrapper(['--junit', 'tests/junit_single_fail.xml', 'tests/junit_double_fail.xml'])
         self.assertEqual(1 + 2, retval)
 
+    def test_single_command_argument(self):
+        retval = warnings_wrapper(['--junit', '--command', 'cat', 'tests/junit_single_fail.xml'])
+        self.assertEqual(1, retval)
+
+    def test_two_command_arguments(self):
+        retval = warnings_wrapper(['--sphinx', '--command', 'cat', 'tests/sphinx_single_warning.txt', 'tests/sphinx_double_warning.txt'])
+        self.assertEqual(1 + 2, retval)
+
+    def test_command_with_its_own_arguments(self):
+        retval = warnings_wrapper(['--sphinx', '--command', 'cat', '-A', 'tests/sphinx_single_warning.txt', 'tests/sphinx_double_warning.txt'])
+        self.assertEqual(1 + 2, retval)
+
+    def test_command_to_stderr(self):
+        retval = warnings_wrapper(['--sphinx', '--command', 'cat', 'tests/sphinx_single_warning.txt', '>&2'])
+        self.assertEqual(1, retval)
+
+    def test_faulty_command(self):
+        with self.assertRaises(OSError):
+            warnings_wrapper(['--sphinx', '--command', 'blahahahaha', 'tests/sphinx_single_warning.txt'])
+
     def test_wildcarded_arguments(self):
         # note: no shell expansion simulation (e.g. as in windows)
         retval = warnings_wrapper(['--junit', 'tests/junit*.xml'])
diff --git a/tests/test_sphinx.py b/tests/test_sphinx.py
index ac72886..7200652 100644
--- a/tests/test_sphinx.py
+++ b/tests/test_sphinx.py
@@ -26,18 +26,12 @@ class TestSphinxWarnings(TestCase):
     def test_warning_no_line_number(self):
         dut1 = "/home/bljah/test/index.rst:5: WARNING: toctree contains reference to nonexisting document u'installation'"
         dut2 = "/home/bljah/test/index.rst:None: WARNING: toctree contains reference to nonexisting document u'installation'"
-        dut3 = "/home/bljah/test/index.rst:: WARNING: toctree contains reference to nonexisting document u'installation'"
-        dut4 = "/home/bljah/test/SRS.rst: WARNING: item non_existing_requirement is not defined"
         with patch('sys.stdout', new=StringIO()) as fake_out:
             self.warnings.check(dut1)
             self.warnings.check(dut2)
-            self.warnings.check(dut3)
-            self.warnings.check(dut4)
-        self.assertEqual(self.warnings.return_count(), 4)
+        self.assertEqual(self.warnings.return_count(), 2)
         self.assertRegexpMatches(fake_out.getvalue(), dut1)
         self.assertRegexpMatches(fake_out.getvalue(), dut2)
-        self.assertRegexpMatches(fake_out.getvalue(), dut3)
-        self.assertRegexpMatches(fake_out.getvalue(), dut4)
 
     def test_single_warning_mixed(self):
         dut1 = 'This1 should not be treated as warning'