summaryrefslogtreecommitdiffstats
path: root/testing/mozbase/mozversion
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--testing/mozbase/mozversion/mozversion/__init__.py9
-rw-r--r--testing/mozbase/mozversion/mozversion/errors.py29
-rw-r--r--testing/mozbase/mozversion/mozversion/mozversion.py153
-rw-r--r--testing/mozbase/mozversion/setup.cfg2
-rw-r--r--testing/mozbase/mozversion/setup.py33
-rw-r--r--testing/mozbase/mozversion/tests/manifest.ini5
-rw-r--r--testing/mozbase/mozversion/tests/test_apk.py45
-rw-r--r--testing/mozbase/mozversion/tests/test_binary.py157
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()