diff options
Diffstat (limited to 'testing/mozbase/mozinfo')
-rw-r--r-- | testing/mozbase/mozinfo/mozinfo/__init__.py | 58 | ||||
-rwxr-xr-x | testing/mozbase/mozinfo/mozinfo/mozinfo.py | 387 | ||||
-rw-r--r-- | testing/mozbase/mozinfo/mozinfo/string_version.py | 73 | ||||
-rw-r--r-- | testing/mozbase/mozinfo/setup.cfg | 2 | ||||
-rw-r--r-- | testing/mozbase/mozinfo/setup.py | 41 | ||||
-rw-r--r-- | testing/mozbase/mozinfo/tests/manifest.ini | 3 | ||||
-rw-r--r-- | testing/mozbase/mozinfo/tests/test.py | 176 |
7 files changed, 740 insertions, 0 deletions
diff --git a/testing/mozbase/mozinfo/mozinfo/__init__.py b/testing/mozbase/mozinfo/mozinfo/__init__.py new file mode 100644 index 0000000000..9e091e0c91 --- /dev/null +++ b/testing/mozbase/mozinfo/mozinfo/__init__.py @@ -0,0 +1,58 @@ +# 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/. + +""" +interface to transform introspected system information to a format palatable to +Mozilla + +Module variables: + +.. attribute:: bits + + 32 or 64 + +.. attribute:: isBsd + + Returns ``True`` if the operating system is BSD + +.. attribute:: isLinux + + Returns ``True`` if the operating system is Linux + +.. attribute:: isMac + + Returns ``True`` if the operating system is Mac + +.. attribute:: isWin + + Returns ``True`` if the operating system is Windows + +.. attribute:: os + + Operating system [``'win'``, ``'mac'``, ``'linux'``, ...] + +.. attribute:: processor + + Processor architecture [``'x86'``, ``'x86_64'``, ``'ppc'``, ...] + +.. attribute:: version + + Operating system version string. For windows, the service pack information is also included + +.. attribute:: info + + Returns information identifying the current system. + + * :attr:`bits` + * :attr:`os` + * :attr:`processor` + * :attr:`version` + +""" + +from . import mozinfo +from .mozinfo import * + +__all__ = mozinfo.__all__ diff --git a/testing/mozbase/mozinfo/mozinfo/mozinfo.py b/testing/mozbase/mozinfo/mozinfo/mozinfo.py new file mode 100755 index 0000000000..667364f1ef --- /dev/null +++ b/testing/mozbase/mozinfo/mozinfo/mozinfo.py @@ -0,0 +1,387 @@ +#!/usr/bin/env python + +# 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/. + +# TODO: it might be a good idea of adding a system name (e.g. 'Ubuntu' for +# linux) to the information; I certainly wouldn't want anyone parsing this +# information and having behaviour depend on it + +import os +import platform +import re +import sys +from ctypes.util import find_library + +from .string_version import StringVersion + +# keep a copy of the os module since updating globals overrides this +_os = os + + +class unknown(object): + """marker class for unknown information""" + + # pylint: disable=W1629 + def __nonzero__(self): + return False + + def __bool__(self): + return False + + def __str__(self): + return "UNKNOWN" + + +unknown = unknown() # singleton + + +def get_windows_version(): + import ctypes + + class OSVERSIONINFOEXW(ctypes.Structure): + _fields_ = [ + ("dwOSVersionInfoSize", ctypes.c_ulong), + ("dwMajorVersion", ctypes.c_ulong), + ("dwMinorVersion", ctypes.c_ulong), + ("dwBuildNumber", ctypes.c_ulong), + ("dwPlatformId", ctypes.c_ulong), + ("szCSDVersion", ctypes.c_wchar * 128), + ("wServicePackMajor", ctypes.c_ushort), + ("wServicePackMinor", ctypes.c_ushort), + ("wSuiteMask", ctypes.c_ushort), + ("wProductType", ctypes.c_byte), + ("wReserved", ctypes.c_byte), + ] + + os_version = OSVERSIONINFOEXW() + os_version.dwOSVersionInfoSize = ctypes.sizeof(os_version) + retcode = ctypes.windll.Ntdll.RtlGetVersion(ctypes.byref(os_version)) + if retcode != 0: + raise OSError + + return ( + os_version.dwMajorVersion, + os_version.dwMinorVersion, + os_version.dwBuildNumber, + ) + + +# get system information +info = { + "os": unknown, + "processor": unknown, + "version": unknown, + "os_version": unknown, + "bits": unknown, + "has_sandbox": unknown, + "automation": bool(os.environ.get("MOZ_AUTOMATION", False)), +} +(system, node, release, version, machine, processor) = platform.uname() +(bits, linkage) = platform.architecture() + +# get os information and related data +if system in ["Microsoft", "Windows"]: + info["os"] = "win" + # There is a Python bug on Windows to determine platform values + # http://bugs.python.org/issue7860 + if "PROCESSOR_ARCHITEW6432" in os.environ: + processor = os.environ.get("PROCESSOR_ARCHITEW6432", processor) + else: + processor = os.environ.get("PROCESSOR_ARCHITECTURE", processor) + system = os.environ.get("OS", system).replace("_", " ") + (major, minor, _, _, service_pack) = os.sys.getwindowsversion() + info["service_pack"] = service_pack + if major >= 6 and minor >= 2: + # On windows >= 8.1 the system call that getwindowsversion uses has + # been frozen to always return the same values. In this case we call + # the RtlGetVersion API directly, which still provides meaningful + # values, at least for now. + major, minor, build_number = get_windows_version() + version = "%d.%d.%d" % (major, minor, build_number) + + os_version = "%d.%d" % (major, minor) +elif system.startswith(("MINGW", "MSYS_NT")): + # windows/mingw python build (msys) + info["os"] = "win" + os_version = version = unknown +elif system == "Linux": + # Attempt to use distro package to determine Linux distribution first. + # Failing that, fall back to use the platform method. + # Note that platform.linux_distribution() will be deprecated as of 3.8 + # and this block will be removed once support for 2.7/3.5 is dropped. + try: + from distro import linux_distribution + except ImportError: + from platform import linux_distribution + + output = linux_distribution() + (distribution, os_version, codename) = tuple(str(item.title()) for item in output) + + if not processor: + processor = machine + if not distribution: + distribution = "lfs" + if not os_version: + os_version = release + if not codename: + codename = "unknown" + version = "%s %s" % (distribution, os_version) + + info["os"] = "linux" + info["linux_distro"] = distribution +elif system in ["DragonFly", "FreeBSD", "NetBSD", "OpenBSD"]: + info["os"] = "bsd" + version = os_version = sys.platform +elif system == "Darwin": + (release, versioninfo, machine) = platform.mac_ver() + version = "OS X %s" % release + versionNums = release.split(".")[:2] + os_version = "%s.%s" % (versionNums[0], versionNums[1]) + info["os"] = "mac" +elif sys.platform in ("solaris", "sunos5"): + info["os"] = "unix" + os_version = version = sys.platform +else: + os_version = version = unknown + +info["apple_silicon"] = False +if ( + info["os"] == "mac" + and float(os_version) > 10.15 + and processor == "arm" + and bits == "64bit" +): + info["apple_silicon"] = True + +info["apple_catalina"] = False +if info["os"] == "mac" and float(os_version) == 10.15: + info["apple_catalina"] = True + +info["win10_2004"] = False +if info["os"] == "win" and version == "10.0.19041": + info["win10_2004"] = True + +info["win11_2009"] = False +if info["os"] == "win" and version == "10.0.22621": + info["win11_2009"] = True + +info["version"] = version +info["os_version"] = StringVersion(os_version) + + +# processor type and bits +if processor in ["i386", "i686"]: + if bits == "32bit": + processor = "x86" + elif bits == "64bit": + processor = "x86_64" +elif processor.upper() == "AMD64": + bits = "64bit" + processor = "x86_64" +elif processor.upper() == "ARM64": + bits = "64bit" + processor = "aarch64" +elif processor == "Power Macintosh": + processor = "ppc" +bits = re.search("(\d+)bit", bits).group(1) +info.update( + { + "processor": processor, + "bits": int(bits), + } +) + +if info["os"] == "linux": + import ctypes + import errno + + PR_SET_SECCOMP = 22 + SECCOMP_MODE_FILTER = 2 + ctypes.CDLL(find_library("c"), use_errno=True).prctl( + PR_SET_SECCOMP, SECCOMP_MODE_FILTER, 0 + ) + info["has_sandbox"] = ctypes.get_errno() == errno.EFAULT +else: + info["has_sandbox"] = True + +# standard value of choices, for easy inspection +choices = { + "os": ["linux", "bsd", "win", "mac", "unix"], + "bits": [32, 64], + "processor": ["x86", "x86_64", "ppc"], +} + + +def sanitize(info): + """Do some sanitization of input values, primarily + to handle universal Mac builds.""" + if "processor" in info and info["processor"] == "universal-x86-x86_64": + # If we're running on OS X 10.6 or newer, assume 64-bit + if release[:4] >= "10.6": # Note this is a string comparison + info["processor"] = "x86_64" + info["bits"] = 64 + else: + info["processor"] = "x86" + info["bits"] = 32 + + +# method for updating information + + +def update(new_info): + """ + Update the info. + + :param new_info: Either a dict containing the new info or a path/url + to a json file containing the new info. + """ + from six import string_types + + if isinstance(new_info, string_types): + # lazy import + import json + + import mozfile + + f = mozfile.load(new_info) + new_info = json.loads(f.read()) + f.close() + + info.update(new_info) + sanitize(info) + globals().update(info) + + # convenience data for os access + for os_name in choices["os"]: + globals()["is" + os_name.title()] = info["os"] == os_name + # unix is special + if isLinux or isBsd: # noqa + globals()["isUnix"] = True + + +def find_and_update_from_json(*dirs, **kwargs): + """Find a mozinfo.json file, load it, and update global symbol table. + + This method will first check the relevant objdir directory for the + necessary mozinfo.json file, if the current script is being run from a + Mozilla objdir. + + If the objdir directory did not supply the necessary data, this method + will then look for the required mozinfo.json file from the provided + tuple of directories. + + If file is found, the global symbols table is updated via a helper method. + + If no valid files are found, this method no-ops unless the raise_exception + kwargs is provided with explicit boolean value of True. + + :param tuple dirs: Directories in which to look for the file. + :param dict kwargs: optional values: + raise_exception: if True, exceptions are raised. + False by default. + :returns: None: default behavior if mozinfo.json cannot be found. + json_path: string representation of mozinfo.json path. + :raises: IOError: if raise_exception is True and file is not found. + """ + # First, see if we're in an objdir + try: + from mozboot.mozconfig import MozconfigFindException + from mozbuild.base import BuildEnvironmentNotFoundException, MozbuildObject + + build = MozbuildObject.from_environment() + json_path = _os.path.join(build.topobjdir, "mozinfo.json") + if _os.path.isfile(json_path): + update(json_path) + return json_path + except ImportError: + pass + except (BuildEnvironmentNotFoundException, MozconfigFindException): + pass + + for d in dirs: + d = _os.path.abspath(d) + json_path = _os.path.join(d, "mozinfo.json") + if _os.path.isfile(json_path): + update(json_path) + return json_path + + # by default, exceptions are suppressed. Set this to True if otherwise + # desired. + if kwargs.get("raise_exception", False): + raise IOError("mozinfo.json could not be found.") + return None + + +def output_to_file(path): + import json + + with open(path, "w") as f: + f.write(json.dumps(info)) + + +update({}) + +# exports +__all__ = list(info.keys()) +__all__ += ["is" + os_name.title() for os_name in choices["os"]] +__all__ += [ + "info", + "unknown", + "main", + "choices", + "update", + "find_and_update_from_json", + "output_to_file", + "StringVersion", +] + + +def main(args=None): + + # parse the command line + from optparse import OptionParser + + parser = OptionParser(description=__doc__) + for key in choices: + parser.add_option( + "--%s" % key, + dest=key, + action="store_true", + default=False, + help="display choices for %s" % key, + ) + options, args = parser.parse_args() + + # args are JSON blobs to override info + if args: + # lazy import + import json + + for arg in args: + if _os.path.exists(arg): + string = open(arg).read() + else: + string = arg + update(json.loads(string)) + + # print out choices if requested + flag = False + for key, value in options.__dict__.items(): + if value is True: + print( + "%s choices: %s" + % (key, " ".join([str(choice) for choice in choices[key]])) + ) + flag = True + if flag: + return + + # otherwise, print out all info + for key, value in info.items(): + print("%s: %s" % (key, value)) + + +if __name__ == "__main__": + main() diff --git a/testing/mozbase/mozinfo/mozinfo/string_version.py b/testing/mozbase/mozinfo/mozinfo/string_version.py new file mode 100644 index 0000000000..fc1c5b46c6 --- /dev/null +++ b/testing/mozbase/mozinfo/mozinfo/string_version.py @@ -0,0 +1,73 @@ +# 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 re + +import six + + +class StringVersion(six.text_type): + """ + A string version that can be compared with comparison operators. + """ + + # Pick out numeric and non-numeric parts (a match group for each type). + pat = re.compile(r"(\d+)|([^\d.]+)") + + def __init__(self, vstring): + super(StringVersion, self).__init__() + + # We'll use unicode internally. + # This check is mainly for python2 strings (which are bytes). + if isinstance(vstring, bytes): + vstring = vstring.decode("ascii") + + self.vstring = vstring + + # Store parts as strings to ease comparisons. + self.version = [] + parts = self.pat.findall(vstring) + # Pad numeric parts with leading zeros for ordering. + for i, obj in enumerate(parts): + if obj[0]: + self.version.append(obj[0].zfill(8)) + else: + self.version.append(obj[1]) + + def __str__(self): + return self.vstring + + def __repr__(self): + return "StringVersion ('%s')" % str(self) + + def _cmp(self, other): + if not isinstance(other, StringVersion): + other = StringVersion(other) + + if self.version == other.version: + return 0 + if self.version < other.version: + return -1 + if self.version > other.version: + return 1 + + def __hash__(self): + # pylint --py3k: W1641 + return hash(self.version) + + # operator overloads + def __eq__(self, other): + return self._cmp(other) == 0 + + def __lt__(self, other): + return self._cmp(other) < 0 + + def __le__(self, other): + return self._cmp(other) <= 0 + + def __gt__(self, other): + return self._cmp(other) > 0 + + def __ge__(self, other): + return self._cmp(other) >= 0 diff --git a/testing/mozbase/mozinfo/setup.cfg b/testing/mozbase/mozinfo/setup.cfg new file mode 100644 index 0000000000..3c6e79cf31 --- /dev/null +++ b/testing/mozbase/mozinfo/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff --git a/testing/mozbase/mozinfo/setup.py b/testing/mozbase/mozinfo/setup.py new file mode 100644 index 0000000000..2b508dbd9f --- /dev/null +++ b/testing/mozbase/mozinfo/setup.py @@ -0,0 +1,41 @@ +# 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 + +PACKAGE_VERSION = "1.2.2" + +# dependencies +deps = [ + "distro >= 1.4.0", + "mozfile >= 0.12", +] + +setup( + name="mozinfo", + version=PACKAGE_VERSION, + description="Library to get system information for use in Mozilla testing", + long_description="see https://firefox-source-docs.mozilla.org/mozbase/index.html", + classifiers=[ + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.5", + "Development Status :: 5 - Production/Stable", + ], + # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers + keywords="mozilla", + author="Mozilla Automation and Testing Team", + author_email="tools@lists.mozilla.org", + url="https://wiki.mozilla.org/Auto-tools/Projects/Mozbase", + license="MPL", + packages=["mozinfo"], + include_package_data=True, + zip_safe=False, + install_requires=deps, + entry_points=""" + # -*- Entry points: -*- + [console_scripts] + mozinfo = mozinfo:main + """, +) diff --git a/testing/mozbase/mozinfo/tests/manifest.ini b/testing/mozbase/mozinfo/tests/manifest.ini new file mode 100644 index 0000000000..72aff7539a --- /dev/null +++ b/testing/mozbase/mozinfo/tests/manifest.ini @@ -0,0 +1,3 @@ +[DEFAULT] +subsuite = mozbase +[test.py] diff --git a/testing/mozbase/mozinfo/tests/test.py b/testing/mozbase/mozinfo/tests/test.py new file mode 100644 index 0000000000..14e516b7fc --- /dev/null +++ b/testing/mozbase/mozinfo/tests/test.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python +# +# 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 json +import os +import sys +from importlib import reload +from unittest import mock + +import mozinfo +import mozunit +import pytest + + +@pytest.fixture(autouse=True) +def on_every_test(): + # per-test set up + reload(mozinfo) + + # When running from an objdir mozinfo will use a build generated json file + # instead of the ones created for testing. Prevent that from happening. + # See bug 896038 for details. + sys.modules["mozbuild"] = None + + yield + + # per-test tear down + del sys.modules["mozbuild"] + + +def test_basic(): + """Test that mozinfo has a few attributes.""" + assert mozinfo.os is not None + # should have isFoo == True where os == "foo" + assert getattr(mozinfo, "is" + mozinfo.os[0].upper() + mozinfo.os[1:]) + + +def test_update(): + """Test that mozinfo.update works.""" + mozinfo.update({"foo": 123}) + assert mozinfo.info["foo"] == 123 + + +def test_update_file(tmpdir): + """Test that mozinfo.update can load a JSON file.""" + j = os.path.join(tmpdir, "mozinfo.json") + with open(j, "w") as f: + f.write(json.dumps({"foo": "xyz"})) + mozinfo.update(j) + assert mozinfo.info["foo"] == "xyz" + + +def test_update_file_invalid_json(tmpdir): + """Test that mozinfo.update handles invalid JSON correctly""" + j = os.path.join(tmpdir, "test.json") + with open(j, "w") as f: + f.write('invalid{"json":') + with pytest.raises(ValueError): + mozinfo.update([j]) + + +def test_find_and_update_file(tmpdir): + """Test that mozinfo.find_and_update_from_json can + find mozinfo.json in a directory passed to it.""" + j = os.path.join(tmpdir, "mozinfo.json") + with open(j, "w") as f: + f.write(json.dumps({"foo": "abcdefg"})) + assert mozinfo.find_and_update_from_json(tmpdir) == j + assert mozinfo.info["foo"] == "abcdefg" + + +def test_find_and_update_file_no_argument(): + """Test that mozinfo.find_and_update_from_json no-ops on not being + given any arguments. + """ + assert mozinfo.find_and_update_from_json() is None + + +def test_find_and_update_file_invalid_json(tmpdir): + """Test that mozinfo.find_and_update_from_json can + handle invalid JSON""" + j = os.path.join(tmpdir, "mozinfo.json") + with open(j, "w") as f: + f.write('invalid{"json":') + with pytest.raises(ValueError): + mozinfo.find_and_update_from_json(tmpdir) + + +def test_find_and_update_file_raise_exception(): + """Test that mozinfo.find_and_update_from_json raises + an IOError when exceptions are unsuppressed. + """ + with pytest.raises(IOError): + mozinfo.find_and_update_from_json(raise_exception=True) + + +def test_find_and_update_file_suppress_exception(): + """Test that mozinfo.find_and_update_from_json suppresses + an IOError exception if a False boolean value is + provided as the only argument. + """ + assert mozinfo.find_and_update_from_json(raise_exception=False) is None + + +def test_find_and_update_file_mozbuild(tmpdir): + """Test that mozinfo.find_and_update_from_json can + find mozinfo.json using the mozbuild module.""" + j = os.path.join(tmpdir, "mozinfo.json") + with open(j, "w") as f: + f.write(json.dumps({"foo": "123456"})) + m = mock.MagicMock() + # Mock the value of MozbuildObject.from_environment().topobjdir. + m.MozbuildObject.from_environment.return_value.topobjdir = tmpdir + + mocked_modules = { + "mozbuild": m, + "mozbuild.base": m, + "mozbuild.mozconfig": m, + } + with mock.patch.dict(sys.modules, mocked_modules): + assert mozinfo.find_and_update_from_json() == j + assert mozinfo.info["foo"] == "123456" + + +def test_output_to_file(tmpdir): + """Test that mozinfo.output_to_file works.""" + path = os.path.join(tmpdir, "mozinfo.json") + mozinfo.output_to_file(path) + assert open(path).read() == json.dumps(mozinfo.info) + + +def test_os_version_is_a_StringVersion(): + assert isinstance(mozinfo.os_version, mozinfo.StringVersion) + + +def test_compare_to_string(): + version = mozinfo.StringVersion("10.10") + + assert version > "10.2" + assert "11" > version + assert version >= "10.10" + assert "10.11" >= version + assert version == "10.10" + assert "10.10" == version + assert version != "10.2" + assert "11" != version + assert version < "11.8.5" + assert "10.2" < version + assert version <= "11" + assert "10.10" <= version + + # Can have non-numeric versions (Bug 1654915) + assert version != mozinfo.StringVersion("Testing") + assert mozinfo.StringVersion("Testing") != version + assert mozinfo.StringVersion("") == "" + assert "" == mozinfo.StringVersion("") + + a = mozinfo.StringVersion("1.2.5a") + b = mozinfo.StringVersion("1.2.5b") + assert a < b + assert b > a + + # Make sure we can compare against unicode (for python 2). + assert a == u"1.2.5a" + assert u"1.2.5a" == a + + +def test_to_string(): + assert "10.10" == str(mozinfo.StringVersion("10.10")) + + +if __name__ == "__main__": + mozunit.main() |