Index: tests/tools.py
diff --git a/tests/tools.py b/tests/tools.py
new file mode 100644
index 0000000000000000000000000000000000000000..7dfbfbc9463af0660bdfc7ad9b001f39e47c6035
--- /dev/null
+++ b/tests/tools.py
@@ -0,0 +1,198 @@
+# 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 zipfile
+import json
+import difflib
+
+from xml.etree import ElementTree
+
+from buildtools.build import processArgs
+from buildtools.packagerChrome import defaultLocale
+from buildtools.tests.conftest import MESSAGES_EN_US
+
+
+ALL_LANGUAGES = ['en_US', 'de', 'it']
+
+
+class Content(object):
+    """Base class for a unified ZipFile / Directory interface.
+
+    Base class for providing a unified context manager interface for
+    accessing files. This class is subclassed by ZipContent and DirContent,
+    which provide the additional methods "namelist()" and "read(path)".
+    """
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, exc_type, exc_value, exc_tb):
+        self._close()
+
+
+class ZipContent(Content):
+    """Provides a unified context manager for ZipFile access.
+
+    Inherits the context manager API from Content.
+    If desired, the specified ZipFile is deleted on exiting the manager.
+    """
+
+    def __init__(self, zip_path, delete_on_close=True):
+        """Constructor of ZipContent handling the file <zip_path>.
+
+        The parameter 'delete_on_close' causes the context manager to
+        delete the handled ZipFile (specified by zip_path) if set to
+        True (default).
+        """
+
+        self._zip_path = zip_path
+        self._zip_file = zipfile.ZipFile(zip_path)
+        self._delete_on_close = delete_on_close
+        super(ZipContent, self).__init__()
+
+    def _close(self):
+        self._zip_file.close()
+        if self._delete_on_close:
+            # if specified, delete the handled file
+            os.remove(self._zip_path)
+
+    def namelist(self):
+        return self._zip_file.namelist()
+
+    def read(self, path):
+        return self._zip_file.read(path)
+
+
+class DirContent(Content):
+    """Provides a unified context manager for directory access.
+
+    Inherits the context managet API from Content.
+    """
+
+    def __init__(self, path):
+        """Constructor of DirContent handling <path>.
+        """
+
+        self._path = path
+        super(DirContent, self).__init__()
+
+    def _close(self):
+        pass
+
+    def namelist(self):
+        """Generate a list of filenames, as expected from ZipFile.nameslist().
+        """
+
+        result = []
+        for parent, directories, files in os.walk(self._path):
+            for filename in files:
+                file_path = os.path.join(parent, filename)
+                rel_path = os.path.relpath(file_path, self._path)
+                result.append(rel_path)
+        return result
+
+    def read(self, path):
+        content = None
+        with open(os.path.join(self._path, path)) as fp:
+            content = fp.read()
+        return content
+
+
+def copy_metadata(filename, tmpdir):
+    """Copy the used metadata to the used temporary directory."""
+    path = os.path.join(os.path.dirname(__file__), filename)
+    destination = str(tmpdir.join(filename))
+    shutil.copy(path, destination)
+
+
+def run_webext_build(ext_type, build_opt, srcdir, buildnum=None, keyfile=None):
+    """Run a build process."""
+    if build_opt == 'build':
+        build_args = ['build']
+    elif build_opt == 'release':
+        build_args = ['build', '-r']
+    else:
+        build_args = ['devenv']
+
+    args = ['build.py', '-t', ext_type] + build_args
+
+    if keyfile:
+        args += ['-k', keyfile]
+    if buildnum:
+        args += ['-b', buildnum]
+
+    processArgs(str(srcdir), args)
+
+
+def locale_files(languages, rootfolder, srcdir):
+    """Generate example locales.
+
+    languages: tuple, list or set of languages to include
+    rootdir: folder-name to create the locale-folders in
+    """
+    for lang in languages:
+        subfolders = rootfolder.split(os.pathsep) + [lang, 'messages.json']
+        json_file = srcdir.ensure(*subfolders)
+        if lang == defaultLocale:
+            json_file.write(MESSAGES_EN_US)
+        else:
+            json_file.write('{}')
+
+
+def assert_all_locales_present(package, locales_path):
+    names = {x for x in package.namelist() if x.startswith(locales_path)}
+    assert names == {
+        os.path.join(locales_path, lang, 'messages.json')
+        for lang in ALL_LANGUAGES
+    }
+
+
+def compare_xml(a, b):
+    """Assert equal content in two manifests in XML format."""
+    def get_leafs_string(tree):
+        """Recursively build a string representing all xml leaf-nodes."""
+        root_str = '{}|{}|{}'.format(tree.tag, tree.tail, tree.text).strip()
+        result = []
+
+        if len(tree) > 0:
+            for subtree in tree:
+                for leaf in get_leafs_string(subtree):
+                    result.append('{}__{}'.format(root_str, leaf))
+        for k, v in tree.attrib.items():
+            result.append('{}__{}:{}'.format(root_str, k, v))
+        return result
+
+    # XML data itself shall not be sorted, hence we can safely sort
+    # our string representations in order to produce a valid unified diff.
+    strs_a = sorted(get_leafs_string(ElementTree.fromstring(a)))
+    strs_b = sorted(get_leafs_string(ElementTree.fromstring(b)))
+
+    diff = list(difflib.unified_diff(strs_a, strs_b, n=0))
+    assert len(diff) == 0, '\n'.join(diff)
+
+
+def compare_json(a, b):
+    """Assert equal content in two manifests in JSON format.
+
+    Compare the content of two JSON strings, respecting the order of items in
+    a list as well as possible duplicates.
+    Raise a unified diff if equality could not be asserted.
+    """
+    json_a = json.dumps(json.loads(a), sort_keys=True, indent=0).split('\n')
+    json_b = json.dumps(json.loads(b), sort_keys=True, indent=0).split('\n')
+
+    diff = list(difflib.unified_diff(json_a, json_b, n=0))
+    assert len(diff) == 0, '\n'.join(diff)
+
+
+def assert_manifest_content(manifest, expected_path):
+    extension = os.path.basename(expected_path).split('.')[-1]
+
+    with open(expected_path, 'r') as fp:
+        if extension == 'xml':
+            compare_xml(manifest, fp.read())
+        else:
+            compare_json(manifest, fp.read())
