diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:44:51 +0000 |
commit | 9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /config/mozunit | |
parent | Initial commit. (diff) | |
download | thunderbird-upstream.tar.xz thunderbird-upstream.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'config/mozunit')
-rw-r--r-- | config/mozunit/mozunit/__init__.py | 6 | ||||
-rw-r--r-- | config/mozunit/mozunit/mozunit.py | 336 | ||||
-rw-r--r-- | config/mozunit/mozunit/pytest.ini | 4 | ||||
-rw-r--r-- | config/mozunit/mozunit/pytest_plugin.py | 26 | ||||
-rw-r--r-- | config/mozunit/setup.py | 19 |
5 files changed, 391 insertions, 0 deletions
diff --git a/config/mozunit/mozunit/__init__.py b/config/mozunit/mozunit/__init__.py new file mode 100644 index 0000000000..51f02c874e --- /dev/null +++ b/config/mozunit/mozunit/__init__.py @@ -0,0 +1,6 @@ +# flake8: noqa +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +from .mozunit import * diff --git a/config/mozunit/mozunit/mozunit.py b/config/mozunit/mozunit/mozunit.py new file mode 100644 index 0000000000..b295ad881f --- /dev/null +++ b/config/mozunit/mozunit/mozunit.py @@ -0,0 +1,336 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import inspect +import io +import os +import sys +import unittest +from unittest import TestResult as _TestResult +from unittest import TextTestRunner as _TestRunner + +import pytest +import six +from six import BytesIO, StringIO + +here = os.path.abspath(os.path.dirname(__file__)) + +try: + # buildconfig doesn't yet support Python 3, so we can use pathlib to + # resolve the topsrcdir relative to our current location. + from pathlib import Path + + topsrcdir = Path(here).parents[2] +except ImportError: + from mozbuild.base import MozbuildObject + + build = MozbuildObject.from_environment(cwd=here) + topsrcdir = build.topsrcdir + +"""Helper to make python unit tests report the way that the Mozilla +unit test infrastructure expects tests to report. + +Usage: + +import mozunit + +if __name__ == '__main__': + mozunit.main() +""" + + +class _MozTestResult(_TestResult): + def __init__(self, stream, descriptions): + _TestResult.__init__(self) + self.stream = stream + self.descriptions = descriptions + + def getDescription(self, test): + if self.descriptions: + return test.shortDescription() or str(test) + else: + return str(test) + + def printStatus(self, status, test, message=""): + line = "{status} | {file} | {klass}.{test}{sep}{message}".format( + status=status, + file=inspect.getfile(test.__class__), + klass=test.__class__.__name__, + test=test._testMethodName, + sep=", " if message else "", + message=message, + ) + self.stream.writeln(line) + + def addSuccess(self, test): + _TestResult.addSuccess(self, test) + self.printStatus("TEST-PASS", test) + + def addSkip(self, test, reason): + _TestResult.addSkip(self, test, reason) + self.printStatus("TEST-SKIP", test) + + def addExpectedFailure(self, test, err): + _TestResult.addExpectedFailure(self, test, err) + self.printStatus("TEST-KNOWN-FAIL", test) + + def addUnexpectedSuccess(self, test): + _TestResult.addUnexpectedSuccess(self, test) + self.printStatus("TEST-UNEXPECTED-PASS", test) + + def addError(self, test, err): + _TestResult.addError(self, test, err) + self.printFail(test, err) + self.stream.writeln("ERROR: {0}".format(self.getDescription(test))) + self.stream.writeln(self.errors[-1][1]) + + def addFailure(self, test, err): + _TestResult.addFailure(self, test, err) + self.printFail(test, err) + self.stream.writeln("FAIL: {0}".format(self.getDescription(test))) + self.stream.writeln(self.failures[-1][1]) + + def printFail(self, test, err): + exctype, value, tb = err + message = value or "NO MESSAGE" + if hasattr(value, "message"): + message = value.message.splitlines()[0] + # Skip test runner traceback levels + while tb and self._is_relevant_tb_level(tb): + tb = tb.tb_next + if tb: + _, ln, _ = inspect.getframeinfo(tb)[:3] + message = "line {0}: {1}".format(ln, message) + self.printStatus("TEST-UNEXPECTED-FAIL", test, message) + + +class MozTestRunner(_TestRunner): + def _makeResult(self): + return _MozTestResult(self.stream, self.descriptions) + + def run(self, test): + result = self._makeResult() + test(result) + return result + + +def _mocked_file(cls): + """Create a mocked file class that inherits from the given class.""" + + class MockedFile(cls): + def __init__(self, context, filename, content): + self.context = context + self.name = filename + cls.__init__(self, content) + + def close(self): + self.context.files[self.name] = self.getvalue() + cls.close(self) + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.close() + + return MockedFile + + +MockedStringFile = _mocked_file(StringIO) +MockedBytesFile = _mocked_file(BytesIO) + + +def normcase(path): + """ + Normalize the case of `path`. + + Don't use `os.path.normcase` because that also normalizes forward slashes + to backslashes on Windows. + """ + if sys.platform.startswith("win"): + return path.lower() + return path + + +class _MockBaseOpen(object): + """Callable that acts like the open() function; see MockedOpen for more + info. + """ + + def __init__(self, open, files): + self.open = open + self.files = files + + def __call__( + self, + name, + mode="r", + buffering=None, + encoding=None, + newline=None, + errors=None, + opener=None, + ): + # open() can be called with an integer "name" (i.e. a file descriptor). + # We don't generally do this in our codebase, but internal Python + # libraries sometimes do and we want to handle that cleanly. + if isinstance(name, int): + return self.open( + name, + mode=mode, + buffering=buffering, + encoding=encoding, + newline=newline, + errors=errors, + opener=opener, + ) + # buffering is ignored. + absname = normcase(os.path.abspath(name)) + if "w" in mode: + file = self._mocked_file(absname, mode) + elif absname in self.files: + content = self.files[absname] + if content is None: + raise IOError(2, "No such file or directory") + file = self._mocked_file(absname, mode, content) + elif "a" in mode: + read_mode = "rb" if "b" in mode else "r" + file = self._mocked_file(absname, mode, self.open(name, read_mode).read()) + else: + file = self.open(name, mode) + if "a" in mode: + file.seek(0, os.SEEK_END) + return file + + def _mocked_file(self, name, mode, content=None): + raise NotImplementedError("subclass must implement") + + +class _MockPy2Open(_MockBaseOpen): + def _mocked_file(self, name, mode, content=None): + content = six.ensure_binary(content or b"") + return MockedBytesFile(self, name, content) + + +class _MockOpen(_MockBaseOpen): + def _mocked_file(self, name, mode, content=None): + if "b" in mode: + content = six.ensure_binary(content or b"") + return MockedBytesFile(self, name, content) + else: + content = six.ensure_text(content or u"") + return MockedStringFile(self, name, content) + + +class MockedOpen(object): + """ + Context manager diverting the open builtin such that opening files + can open "virtual" file instances given when creating a MockedOpen. + + with MockedOpen({'foo': 'foo', 'bar': 'bar'}): + f = open('foo', 'r') + + will thus open the virtual file instance for the file 'foo' to f. + + If the content of a file is given as None, then that file will be + represented as not existing (even if it does, actually, exist). + + MockedOpen also masks writes, so that creating or replacing files + doesn't touch the file system, while subsequently opening the file + will return the recorded content. + + with MockedOpen(): + f = open('foo', 'w') + f.write('foo') + self.assertRaises(Exception,f.open('foo', 'r')) + """ + + def __init__(self, files={}): + self.files = {} + for name, content in files.items(): + self.files[normcase(os.path.abspath(name))] = content + + def __enter__(self): + import six.moves.builtins + + self.open = six.moves.builtins.open + self.io_open = io.open + self._orig_path_exists = os.path.exists + self._orig_path_isdir = os.path.isdir + self._orig_path_isfile = os.path.isfile + builtin_cls = _MockPy2Open if six.PY2 else _MockOpen + six.moves.builtins.open = builtin_cls(self.open, self.files) + io.open = _MockOpen(self.io_open, self.files) + os.path.exists = self._wrapped_exists + os.path.isdir = self._wrapped_isdir + os.path.isfile = self._wrapped_isfile + + def __exit__(self, type, value, traceback): + import six.moves.builtins + + six.moves.builtins.open = self.open + io.open = self.io_open + os.path.exists = self._orig_path_exists + os.path.isdir = self._orig_path_isdir + os.path.isfile = self._orig_path_isfile + + def _wrapped_exists(self, p): + return self._wrapped_isfile(p) or self._wrapped_isdir(p) + + def _wrapped_isfile(self, p): + p = normcase(p) + if p in self.files: + return self.files[p] is not None + + abspath = normcase(os.path.abspath(p)) + if abspath in self.files: + return self.files[abspath] is not None + + return self._orig_path_isfile(p) + + def _wrapped_isdir(self, p): + p = normcase(p) + p = p if p.endswith(("/", "\\")) else p + os.sep + if any(f.startswith(p) for f in self.files): + return True + + abspath = normcase(os.path.abspath(p) + os.sep) + if any(f.startswith(abspath) for f in self.files): + return True + + return self._orig_path_isdir(p) + + +def main(*args, **kwargs): + runwith = kwargs.pop("runwith", "pytest") + + if runwith == "unittest": + unittest.main(testRunner=MozTestRunner(), *args, **kwargs) + else: + args = list(args) + if os.environ.get("MACH_STDOUT_ISATTY") and not any( + a.startswith("--color") for a in args + ): + args.append("--color=yes") + + module = __import__("__main__") + args.extend( + [ + "--rootdir", + str(topsrcdir), + "-c", + os.path.join(here, "pytest.ini"), + "-vv", + "--tb=short", + "-p", + "mozlog.pytest_mozlog.plugin", + "-p", + "mozunit.pytest_plugin", + "-p", + "no:cacheprovider", + "-rsx", # show reasons for skip / xfail + module.__file__, + ] + ) + sys.exit(pytest.main(args)) diff --git a/config/mozunit/mozunit/pytest.ini b/config/mozunit/mozunit/pytest.ini new file mode 100644 index 0000000000..db7632389c --- /dev/null +++ b/config/mozunit/mozunit/pytest.ini @@ -0,0 +1,4 @@ +[pytest] +console_output_style=classic +python_classes=PyTest +xfail_strict=true diff --git a/config/mozunit/mozunit/pytest_plugin.py b/config/mozunit/mozunit/pytest_plugin.py new file mode 100644 index 0000000000..e05fc579e4 --- /dev/null +++ b/config/mozunit/mozunit/pytest_plugin.py @@ -0,0 +1,26 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +import pytest + + +def pytest_addoption(parser): + group = parser.getgroup("mozunit") + group.addoption( + "--run-slow", action="store_true", default=False, help="run slow tests" + ) + + +def pytest_configure(config): + config.addinivalue_line("markers", "slow: mark test as slow to run") + + +def pytest_collection_modifyitems(config, items): + if config.getoption("--run-slow"): + # --run-slow given in cli: do not skip slow tests + return + skip_slow = pytest.mark.skip(reason="need --run-slow option to run") + for item in items: + if "slow" in item.keywords: + item.add_marker(skip_slow) diff --git a/config/mozunit/setup.py b/config/mozunit/setup.py new file mode 100644 index 0000000000..c2771b9053 --- /dev/null +++ b/config/mozunit/setup.py @@ -0,0 +1,19 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this file, +# You can obtain one at http://mozilla.org/MPL/2.0/. + +from setuptools import setup + +setup( + name="mozunit", + version="1.0", + description="Make unit tests report the way Mozilla infrastructure expects", + classifiers=["Programming Language :: Python :: 2.7"], + keywords="mozilla", + author="Mozilla Automation and Tools team", + author_email="tools@lists.mozilla.org", + license="MPL", + packages=["mozunit"], + include_package_data=True, + zip_safe=False, +) |