summaryrefslogtreecommitdiffstats
path: root/testing/mozbase/mozinfo
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /testing/mozbase/mozinfo
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/mozbase/mozinfo')
-rw-r--r--testing/mozbase/mozinfo/mozinfo/__init__.py58
-rwxr-xr-xtesting/mozbase/mozinfo/mozinfo/mozinfo.py363
-rw-r--r--testing/mozbase/mozinfo/mozinfo/string_version.py73
-rw-r--r--testing/mozbase/mozinfo/setup.cfg2
-rw-r--r--testing/mozbase/mozinfo/setup.py41
-rw-r--r--testing/mozbase/mozinfo/tests/manifest.toml4
-rw-r--r--testing/mozbase/mozinfo/tests/test.py176
7 files changed, 717 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..bb04be54c8
--- /dev/null
+++ b/testing/mozbase/mozinfo/mozinfo/mozinfo.py
@@ -0,0 +1,363 @@
+#!/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
+
+
+# get system information
+info = {
+ "os": unknown,
+ "processor": unknown,
+ "version": unknown,
+ "os_version": unknown,
+ "bits": unknown,
+ "has_sandbox": unknown,
+ "display": None,
+ "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, build_number, _, _) = os.sys.getwindowsversion()
+ version = "%d.%d.%d" % (major, minor, build_number)
+ if major == 10 and minor == 0 and build_number >= 22000:
+ major = 11
+ 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)
+
+ if os.environ.get("WAYLAND_DISPLAY"):
+ info["display"] = "wayland"
+ elif os.environ.get("DISPLAY"):
+ info["display"] = "x11"
+
+ 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_2009"] = False
+if info["os"] == "win" and version == "10.0.19045":
+ info["win10_2009"] = 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)
+info["is_ubuntu"] = "Ubuntu" in 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"
+elif processor == "arm" and bits == "64bit":
+ processor = "aarch64"
+
+bits = re.search(r"(\d+)bit", bits).group(1)
+info.update(
+ {
+ "processor": processor,
+ "bits": int(bits),
+ }
+)
+
+# we want to transition to this instead of using `!debug`, etc.
+info["arch"] = info["processor"]
+
+
+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..87db88d1e4
--- /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.3"
+
+# 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.toml b/testing/mozbase/mozinfo/tests/manifest.toml
new file mode 100644
index 0000000000..147e23872e
--- /dev/null
+++ b/testing/mozbase/mozinfo/tests/manifest.toml
@@ -0,0 +1,4 @@
+[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..f1d971d317
--- /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 == "1.2.5a"
+ assert "1.2.5a" == a
+
+
+def test_to_string():
+ assert "10.10" == str(mozinfo.StringVersion("10.10"))
+
+
+if __name__ == "__main__":
+ mozunit.main()