diff options
Diffstat (limited to 'scripts/devscripts/test')
-rw-r--r-- | scripts/devscripts/test/__init__.py | 63 | ||||
-rw-r--r-- | scripts/devscripts/test/pylint.conf | 59 | ||||
-rw-r--r-- | scripts/devscripts/test/test_flake8.py | 53 | ||||
-rw-r--r-- | scripts/devscripts/test/test_help.py | 76 | ||||
-rw-r--r-- | scripts/devscripts/test/test_logger.py | 57 | ||||
-rw-r--r-- | scripts/devscripts/test/test_pylint.py | 68 |
6 files changed, 376 insertions, 0 deletions
diff --git a/scripts/devscripts/test/__init__.py b/scripts/devscripts/test/__init__.py new file mode 100644 index 0000000..335f1d4 --- /dev/null +++ b/scripts/devscripts/test/__init__.py @@ -0,0 +1,63 @@ +# Copyright (C) 2017, Benjamin Drung <benjamin.drung@profitbricks.com> +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import inspect +import os +import sys +import unittest + +SCRIPTS = [ + "debdiff-apply", + "reproducible-check", + "sadt", + "suspicious-source", + "wrap-and-sort", +] + + +def get_source_files(): + """Return a list of sources files/directories (to check with flake8/pylint)""" + modules = ["devscripts"] + py_files = ["setup.py"] + + files = [] + for code_file in SCRIPTS + modules + py_files: + is_script = code_file in SCRIPTS + if not os.path.exists(code_file): # pragma: no cover + # The alternative path is needed for Debian's pybuild + alternative = os.path.join(os.environ.get("OLDPWD", ""), code_file) + if os.path.exists(alternative): + code_file = alternative + if is_script: + with open(code_file, "rb") as script_file: + shebang = script_file.readline().decode("utf-8") + if ((sys.version_info[0] == 3 and "python3" in shebang) or + ("python" in shebang and "python3" not in shebang)): + files.append(code_file) + else: + files.append(code_file) + return files + + +def unittest_verbosity(): + """Return the verbosity setting of the currently running unittest + program, or None if none is running. + """ + frame = inspect.currentframe() + while frame: + self = frame.f_locals.get("self") + if isinstance(self, unittest.TestProgram): + return self.verbosity + frame = frame.f_back + return None # pragma: no cover diff --git a/scripts/devscripts/test/pylint.conf b/scripts/devscripts/test/pylint.conf new file mode 100644 index 0000000..b0a80f1 --- /dev/null +++ b/scripts/devscripts/test/pylint.conf @@ -0,0 +1,59 @@ +[MASTER] + +# Pickle collected data for later comparisons. +persistent=no + + +[MESSAGES CONTROL] + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +disable=fixme,invalid-name,locally-disabled,missing-docstring + + +[REPORTS] + +# Tells whether to display a full report or only the messages +reports=no + + +[TYPECHECK] + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). +ignored-classes=apt_pkg,magic + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=99 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + + +[BASIC] + +# Allow variables called e, f, lp +good-names=i,j,k,ex,Run,_,e,f,lp,fp + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=5 + + +[DESIGN] + +# Maximum number of arguments per function +max-args=10 diff --git a/scripts/devscripts/test/test_flake8.py b/scripts/devscripts/test/test_flake8.py new file mode 100644 index 0000000..8559f4d --- /dev/null +++ b/scripts/devscripts/test/test_flake8.py @@ -0,0 +1,53 @@ +# Copyright (C) 2017-2018, Benjamin Drung <bdrung@debian.org> +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""test_flake8.py - Run flake8 check""" + +import subprocess +import sys +import unittest + +from . import get_source_files, unittest_verbosity + + +class Flake8TestCase(unittest.TestCase): + """ + This unittest class provides a test that runs the flake8 code + checker (which combines pycodestyle and pyflakes) on the Python + source code. The list of source files is provided by the + get_source_files() function. + """ + + def test_flake8(self): + """Test: Run flake8 on Python source code""" + cmd = [sys.executable, "-m", "flake8", "--max-line-length=99"] + get_source_files() + if unittest_verbosity() >= 2: + sys.stderr.write( + "Running following command:\n{}\n".format(" ".join(cmd))) + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, close_fds=True) + + out, err = process.communicate() + if process.returncode != 0: # pragma: no cover + msgs = [] + if err: + msgs.append("flake8 exited with code {} and has unexpected output on stderr:\n{}" + .format(process.returncode, err.decode().rstrip())) + if out: + msgs.append("flake8 found issues:\n{}".format( + out.decode().rstrip())) + if not msgs: + msgs.append("flake8 exited with code {} and has no output on stdout or stderr." + .format(process.returncode)) + self.fail("\n".join(msgs)) diff --git a/scripts/devscripts/test/test_help.py b/scripts/devscripts/test/test_help.py new file mode 100644 index 0000000..42a7d10 --- /dev/null +++ b/scripts/devscripts/test/test_help.py @@ -0,0 +1,76 @@ +# test_help.py - Ensure scripts can run --help. +# +# Copyright (C) 2010, Stefano Rivera <stefanor@ubuntu.com> +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +import fcntl +import os +import select +import signal +import subprocess +import time +import unittest + +from . import SCRIPTS + +TIMEOUT = 5 + + +def load_tests(loader, tests, pattern): # pylint: disable=unused-argument + "Give HelpTestCase a chance to populate before loading its test cases" + suite = unittest.TestSuite() + HelpTestCase.populate() + suite.addTests(loader.loadTestsFromTestCase(HelpTestCase)) + return suite + + +class HelpTestCase(unittest.TestCase): + @classmethod + def populate(cls): + for script in SCRIPTS: + setattr(cls, 'test_' + script, cls.make_help_tester(script)) + + @classmethod + def make_help_tester(cls, script): + def tester(self): + with open('/dev/null', 'r') as null: + process = subprocess.Popen(['./' + script, '--help'], + close_fds=True, stdin=null, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + started = time.time() + out = [] + + fds = [process.stdout.fileno(), process.stderr.fileno()] + for fd in fds: + fcntl.fcntl(fd, fcntl.F_SETFL, + fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK) + + while time.time() - started < TIMEOUT: + for fd in select.select(fds, [], fds, TIMEOUT)[0]: + out.append(os.read(fd, 1024)) + if process.poll() is not None: + break + + if process.poll() is None: + os.kill(process.pid, signal.SIGTERM) + time.sleep(1) + if process.poll() is None: + os.kill(process.pid, signal.SIGKILL) + + self.assertEqual(process.poll(), 0, + "%s failed to return usage within %i seconds.\n" + "Output:\n%s" + % (script, TIMEOUT, ''.encode('ascii').join(out))) + return tester diff --git a/scripts/devscripts/test/test_logger.py b/scripts/devscripts/test/test_logger.py new file mode 100644 index 0000000..f03010f --- /dev/null +++ b/scripts/devscripts/test/test_logger.py @@ -0,0 +1,57 @@ +# test_logger.py - Test devscripts.logger.Logger. +# +# Copyright (C) 2012, Stefano Rivera <stefanor@debian.org> +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +import io +import sys + +from devscripts.logger import Logger +from devscripts.test import unittest + + +class LoggerTestCase(unittest.TestCase): + def setUp(self): + Logger.stdout = io.StringIO() + Logger.stderr = io.StringIO() + self._script_name = Logger.script_name + Logger.script_name = 'test' + self._verbose = Logger.verbose + + def tearDown(self): + Logger.stdout = sys.stdout + Logger.stderr = sys.stderr + Logger.script_name = self._script_name + Logger.verbose = self._verbose + + def testCommand(self): + # pylint: disable=no-member + Logger.command(('ls', 'a b')) + self.assertEqual(Logger.stdout.getvalue(), '') + Logger.set_verbosity(True) + Logger.command(('ls', 'a b')) + self.assertEqual(Logger.stdout.getvalue(), 'test: I: ls "a b"\n') + self.assertEqual(Logger.stderr.getvalue(), '') + + def testNoArgs(self): + # pylint: disable=no-member + Logger.normal('hello %s') + self.assertEqual(Logger.stdout.getvalue(), 'test: hello %s\n') + self.assertEqual(Logger.stderr.getvalue(), '') + + def testArgs(self): + # pylint: disable=no-member + Logger.normal('hello %s', 'world') + self.assertEqual(Logger.stdout.getvalue(), 'test: hello world\n') + self.assertEqual(Logger.stderr.getvalue(), '') diff --git a/scripts/devscripts/test/test_pylint.py b/scripts/devscripts/test/test_pylint.py new file mode 100644 index 0000000..ac08fda --- /dev/null +++ b/scripts/devscripts/test/test_pylint.py @@ -0,0 +1,68 @@ +# Copyright (C) 2010, Stefano Rivera <stefanor@debian.org> +# Copyright (C) 2017-2018, Benjamin Drung <bdrung@debian.org> +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +"""test_pylint.py - Run pylint""" + +import os +import re +import subprocess +import sys +import unittest + +from . import get_source_files, unittest_verbosity + +CONFIG = os.path.join(os.path.dirname(__file__), "pylint.conf") + + +class PylintTestCase(unittest.TestCase): + """ + This unittest class provides a test that runs the pylint code check + on the Python source code. The list of source files is provided by + the get_source_files() function and pylint is purely configured via + a config file. + """ + + def test_pylint(self): + """Test: Run pylint on Python source code""" + + cmd = [sys.executable, "-m", "pylint", "--rcfile=" + CONFIG, "--"] + get_source_files() + if unittest_verbosity() >= 2: + sys.stderr.write("Running following command:\n{}\n".format(" ".join(cmd))) + process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + close_fds=True) + out, err = process.communicate() + + if process.returncode != 0: # pragma: no cover + # Strip trailing summary (introduced in pylint 1.7). This summary might look like: + # + # ------------------------------------ + # Your code has been rated at 10.00/10 + # + out = re.sub("^(-+|Your code has been rated at .*)$", "", out.decode(), + flags=re.MULTILINE).rstrip() + + # Strip logging of used config file (introduced in pylint 1.8) + err = re.sub("^Using config file .*\n", "", err.decode()).rstrip() + + msgs = [] + if err: + msgs.append("pylint exited with code {} and has unexpected output on stderr:\n{}" + .format(process.returncode, err)) + if out: + msgs.append("pylint found issues:\n{}".format(out)) + if not msgs: + msgs.append("pylint exited with code {} and has no output on stdout or stderr." + .format(process.returncode)) + self.fail("\n".join(msgs)) |