diff options
Diffstat (limited to 'testing/mozbase/mozversion')
-rw-r--r-- | testing/mozbase/mozversion/mozversion/__init__.py | 9 | ||||
-rw-r--r-- | testing/mozbase/mozversion/mozversion/errors.py | 29 | ||||
-rw-r--r-- | testing/mozbase/mozversion/mozversion/mozversion.py | 153 | ||||
-rw-r--r-- | testing/mozbase/mozversion/setup.cfg | 2 | ||||
-rw-r--r-- | testing/mozbase/mozversion/setup.py | 33 | ||||
-rw-r--r-- | testing/mozbase/mozversion/tests/manifest.ini | 5 | ||||
-rw-r--r-- | testing/mozbase/mozversion/tests/test_apk.py | 45 | ||||
-rw-r--r-- | testing/mozbase/mozversion/tests/test_binary.py | 157 |
8 files changed, 433 insertions, 0 deletions
diff --git a/testing/mozbase/mozversion/mozversion/__init__.py b/testing/mozbase/mozversion/mozversion/__init__.py new file mode 100644 index 0000000000..99c6b051d0 --- /dev/null +++ b/testing/mozbase/mozversion/mozversion/__init__.py @@ -0,0 +1,9 @@ +# 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/. + +import mozversion.errors + +from .errors import * +from .mozversion import cli, get_version diff --git a/testing/mozbase/mozversion/mozversion/errors.py b/testing/mozbase/mozversion/mozversion/errors.py new file mode 100644 index 0000000000..db005c367a --- /dev/null +++ b/testing/mozbase/mozversion/mozversion/errors.py @@ -0,0 +1,29 @@ +# 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/. + + +class VersionError(Exception): + def __init__(self, message): + Exception.__init__(self, message) + + +class AppNotFoundError(VersionError): + """Exception for the application not found""" + + def __init__(self, message): + VersionError.__init__(self, message) + + +class LocalAppNotFoundError(AppNotFoundError): + """Exception for local application not found""" + + def __init__(self, path): + AppNotFoundError.__init__(self, "Application not found at: %s" % path) + + +class RemoteAppNotFoundError(AppNotFoundError): + """Exception for remote application not found""" + + def __init__(self, message): + AppNotFoundError.__init__(self, message) diff --git a/testing/mozbase/mozversion/mozversion/mozversion.py b/testing/mozbase/mozversion/mozversion/mozversion.py new file mode 100644 index 0000000000..bea53e0a6d --- /dev/null +++ b/testing/mozbase/mozversion/mozversion/mozversion.py @@ -0,0 +1,153 @@ +# 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 argparse +import io +import os +import sys +import zipfile + +import mozlog +from six.moves import configparser + +from mozversion import errors + +INI_DATA_MAPPING = (("application", "App"), ("platform", "Build")) + + +class Version(object): + def __init__(self): + self._info = {} + self._logger = mozlog.get_default_logger(component="mozversion") + if not self._logger: + self._logger = mozlog.unstructured.getLogger("mozversion") + + def get_gecko_info(self, path): + for type, section in INI_DATA_MAPPING: + config_file = os.path.join(path, "%s.ini" % type) + if os.path.exists(config_file): + try: + with open(config_file) as fp: + self._parse_ini_file(fp, type, section) + except OSError: + self._logger.warning("Unable to read %s" % config_file) + else: + self._logger.warning("Unable to find %s" % config_file) + + def _parse_ini_file(self, fp, type, section): + config = configparser.RawConfigParser() + config.read_file(fp) + name_map = { + "codename": "display_name", + "milestone": "version", + "sourcerepository": "repository", + "sourcestamp": "changeset", + } + for key, value in config.items(section): + name = name_map.get(key, key).lower() + self._info["%s_%s" % (type, name)] = ( + config.has_option(section, key) and config.get(section, key) or None + ) + + if not self._info.get("application_display_name"): + self._info["application_display_name"] = self._info.get("application_name") + + +class LocalFennecVersion(Version): + def __init__(self, path, **kwargs): + Version.__init__(self, **kwargs) + self.get_gecko_info(path) + + def get_gecko_info(self, path): + archive = zipfile.ZipFile(path, "r") + archive_list = archive.namelist() + for type, section in INI_DATA_MAPPING: + filename = "%s.ini" % type + if filename in archive_list: + with io.TextIOWrapper(archive.open(filename)) as fp: + self._parse_ini_file(fp, type, section) + else: + self._logger.warning("Unable to find %s" % filename) + + if "package-name.txt" in archive_list: + with io.TextIOWrapper(archive.open("package-name.txt")) as fp: + self._info["package_name"] = fp.readlines()[0].strip() + + +class LocalVersion(Version): + def __init__(self, binary, **kwargs): + Version.__init__(self, **kwargs) + + if binary: + # on Windows, the binary may be specified with or without the + # .exe extension + if not os.path.exists(binary) and not os.path.exists(binary + ".exe"): + raise IOError("Binary path does not exist: %s" % binary) + path = os.path.dirname(os.path.realpath(binary)) + else: + path = os.getcwd() + + if not self.check_location(path): + if sys.platform == "darwin": + resources_path = os.path.join(os.path.dirname(path), "Resources") + if self.check_location(resources_path): + path = resources_path + else: + raise errors.LocalAppNotFoundError(path) + + else: + raise errors.LocalAppNotFoundError(path) + + self.get_gecko_info(path) + + def check_location(self, path): + return os.path.exists(os.path.join(path, "application.ini")) and os.path.exists( + os.path.join(path, "platform.ini") + ) + + +def get_version(binary=None): + """ + Returns the application version information as a dict. You can specify + a path to the binary of the application or an Android APK file (to get + version information for Firefox for Android). If this is omitted then the + current directory is checked for the existance of an application.ini + file. + + :param binary: Path to the binary for the application or Android APK file + """ + if ( + binary + and zipfile.is_zipfile(binary) + and "AndroidManifest.xml" in zipfile.ZipFile(binary, "r").namelist() + ): + version = LocalFennecVersion(binary) + else: + version = LocalVersion(binary) + + for (key, value) in sorted(version._info.items()): + if value: + version._logger.info("%s: %s" % (key, value)) + + return version._info + + +def cli(args=sys.argv[1:]): + parser = argparse.ArgumentParser( + description="Display version information for Mozilla applications" + ) + parser.add_argument("--binary", help="path to application binary or apk") + mozlog.commandline.add_logging_group( + parser, include_formatters=mozlog.commandline.TEXT_FORMATTERS + ) + + args = parser.parse_args() + + mozlog.commandline.setup_logging("mozversion", args, {"mach": sys.stdout}) + + get_version(binary=args.binary) + + +if __name__ == "__main__": + cli() diff --git a/testing/mozbase/mozversion/setup.cfg b/testing/mozbase/mozversion/setup.cfg new file mode 100644 index 0000000000..3c6e79cf31 --- /dev/null +++ b/testing/mozbase/mozversion/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 diff --git a/testing/mozbase/mozversion/setup.py b/testing/mozbase/mozversion/setup.py new file mode 100644 index 0000000000..332a65c32a --- /dev/null +++ b/testing/mozbase/mozversion/setup.py @@ -0,0 +1,33 @@ +# 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 = "2.3.0" + + +setup( + name="mozversion", + version=PACKAGE_VERSION, + description="Library to get version information for applications", + long_description="see https://firefox-source-docs.mozilla.org/mozbase/index.html", + classifiers=[ + "Programming Language :: Python :: 2.7", + "Programming Language :: Python :: 3.5", + ], + 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=["mozversion"], + include_package_data=True, + zip_safe=False, + install_requires=["mozlog >= 6.0", "six >= 1.13.0"], + entry_points=""" + # -*- Entry points: -*- + [console_scripts] + mozversion = mozversion:cli + """, +) diff --git a/testing/mozbase/mozversion/tests/manifest.ini b/testing/mozbase/mozversion/tests/manifest.ini new file mode 100644 index 0000000000..28936e5c23 --- /dev/null +++ b/testing/mozbase/mozversion/tests/manifest.ini @@ -0,0 +1,5 @@ +[DEFAULT] +subsuite = mozbase + +[test_binary.py] +[test_apk.py] diff --git a/testing/mozbase/mozversion/tests/test_apk.py b/testing/mozbase/mozversion/tests/test_apk.py new file mode 100644 index 0000000000..7ca4d4e068 --- /dev/null +++ b/testing/mozbase/mozversion/tests/test_apk.py @@ -0,0 +1,45 @@ +#!/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 zipfile + +import mozunit +import pytest +from mozversion import get_version + +"""test getting version information from an android .apk""" + +application_changeset = "a" * 40 +platform_changeset = "b" * 40 + + +@pytest.fixture(name="apk") +def fixture_apk(tmpdir): + path = str(tmpdir.join("apk.zip")) + with zipfile.ZipFile(path, "w") as z: + z.writestr( + "application.ini", """[App]\nSourceStamp=%s\n""" % application_changeset + ) + z.writestr("platform.ini", """[Build]\nSourceStamp=%s\n""" % platform_changeset) + z.writestr("AndroidManifest.xml", "") + return path + + +def test_basic(apk): + v = get_version(apk) + assert v.get("application_changeset") == application_changeset + assert v.get("platform_changeset") == platform_changeset + + +def test_with_package_name(apk): + with zipfile.ZipFile(apk, "a") as z: + z.writestr("package-name.txt", "org.mozilla.fennec") + v = get_version(apk) + assert v.get("package_name") == "org.mozilla.fennec" + + +if __name__ == "__main__": + mozunit.main() diff --git a/testing/mozbase/mozversion/tests/test_binary.py b/testing/mozbase/mozversion/tests/test_binary.py new file mode 100644 index 0000000000..9de6bb0e6b --- /dev/null +++ b/testing/mozbase/mozversion/tests/test_binary.py @@ -0,0 +1,157 @@ +#!/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 os +import shutil +import sys + +import mozunit +import pytest +from moztest.selftest.fixtures import binary_fixture # noqa: F401 +from mozversion import errors, get_version + +"""test getting application version information from a binary path""" + + +@pytest.fixture() +def fake_binary(tmpdir): + binary = tmpdir.join("binary") + binary.write("foobar") + return str(binary) + + +@pytest.fixture(name="application_ini") +def fixture_application_ini(tmpdir): + ini = tmpdir.join("application.ini") + ini.write( + """[App] +ID = AppID +Name = AppName +CodeName = AppCodeName +Version = AppVersion +BuildID = AppBuildID +SourceRepository = AppSourceRepo +SourceStamp = AppSourceStamp +Vendor = AppVendor""" + ) + return str(ini) + + +@pytest.fixture(name="platform_ini") +def fixture_platform_ini(tmpdir): + ini = tmpdir.join("platform.ini") + ini.write( + """[Build] +BuildID = PlatformBuildID +Milestone = PlatformMilestone +SourceStamp = PlatformSourceStamp +SourceRepository = PlatformSourceRepo""" + ) + return str(ini) + + +def test_real_binary(binary): # noqa: F811 + if not binary: + pytest.skip("No binary found") + v = get_version(binary) + assert isinstance(v, dict) + + +def test_binary(fake_binary, application_ini, platform_ini): + _check_version(get_version(fake_binary)) + + +@pytest.mark.skipif( + not hasattr(os, "symlink"), reason="os.symlink not supported on this platform" +) +def test_symlinked_binary(fake_binary, application_ini, platform_ini, tmpdir): + # create a symlink of the binary in another directory and check + # version against this symlink + symlink = str(tmpdir.join("symlink")) + os.symlink(fake_binary, symlink) + _check_version(get_version(symlink)) + + +def test_binary_in_current_path(fake_binary, application_ini, platform_ini, tmpdir): + os.chdir(str(tmpdir)) + _check_version(get_version()) + + +def test_with_ini_files_on_osx( + fake_binary, application_ini, platform_ini, monkeypatch, tmpdir +): + monkeypatch.setattr(sys, "platform", "darwin") + # get_version is working with ini files next to the binary + _check_version(get_version(binary=fake_binary)) + + # or if they are in the Resources dir + # in this case the binary must be in a Contents dir, next + # to the Resources dir + contents_dir = tmpdir.mkdir("Contents") + moved_binary = str(contents_dir.join(os.path.basename(fake_binary))) + shutil.move(fake_binary, moved_binary) + + resources_dir = str(tmpdir.mkdir("Resources")) + shutil.move(application_ini, resources_dir) + shutil.move(platform_ini, resources_dir) + + _check_version(get_version(binary=moved_binary)) + + +def test_invalid_binary_path(tmpdir): + with pytest.raises(IOError): + get_version(str(tmpdir.join("invalid"))) + + +def test_without_ini_files(fake_binary): + """With missing ini files an exception should be thrown""" + with pytest.raises(errors.AppNotFoundError): + get_version(fake_binary) + + +def test_without_platform_ini_file(fake_binary, application_ini): + """With a missing platform.ini file an exception should be thrown""" + with pytest.raises(errors.AppNotFoundError): + get_version(fake_binary) + + +def test_without_application_ini_file(fake_binary, platform_ini): + """With a missing application.ini file an exception should be thrown""" + with pytest.raises(errors.AppNotFoundError): + get_version(fake_binary) + + +def test_with_exe(application_ini, platform_ini, tmpdir): + """Test that we can resolve .exe files""" + binary = tmpdir.join("binary.exe") + binary.write("foobar") + _check_version(get_version(os.path.splitext(str(binary))[0])) + + +def test_not_found_with_binary_specified(fake_binary): + with pytest.raises(errors.LocalAppNotFoundError): + get_version(fake_binary) + + +def _check_version(version): + assert version.get("application_id") == "AppID" + assert version.get("application_name") == "AppName" + assert version.get("application_display_name") == "AppCodeName" + assert version.get("application_version") == "AppVersion" + assert version.get("application_buildid") == "AppBuildID" + assert version.get("application_repository") == "AppSourceRepo" + assert version.get("application_changeset") == "AppSourceStamp" + assert version.get("application_vendor") == "AppVendor" + assert version.get("platform_name") is None + assert version.get("platform_buildid") == "PlatformBuildID" + assert version.get("platform_repository") == "PlatformSourceRepo" + assert version.get("platform_changeset") == "PlatformSourceStamp" + assert version.get("invalid_key") is None + assert version.get("platform_version") == "PlatformMilestone" + + +if __name__ == "__main__": + mozunit.main() |