summaryrefslogtreecommitdiffstats
path: root/tools/update-verify/release/compare-directories.py
diff options
context:
space:
mode:
Diffstat (limited to 'tools/update-verify/release/compare-directories.py')
-rwxr-xr-xtools/update-verify/release/compare-directories.py273
1 files changed, 273 insertions, 0 deletions
diff --git a/tools/update-verify/release/compare-directories.py b/tools/update-verify/release/compare-directories.py
new file mode 100755
index 0000000000..a45e78d62f
--- /dev/null
+++ b/tools/update-verify/release/compare-directories.py
@@ -0,0 +1,273 @@
+#! /usr/bin/env python3
+# 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 difflib
+import hashlib
+import logging
+import os
+import sys
+
+""" Define the transformations needed to make source + update == target
+
+Required:
+The files list describes the files which a transform may be used on.
+The 'side' is one of ('source', 'target') and defines where each transform is applied
+The 'channel_prefix' list controls which channels a transform may be used for, where a value of
+'beta' means all of beta, beta-localtest, beta-cdntest, etc.
+
+One or more:
+A 'deletion' specifies a start of line to match on, removing the whole line
+A 'substitution' is a list of full string to match and its replacement
+"""
+TRANSFORMS = [
+ # channel-prefs.js
+ {
+ # preprocessor comments, eg //@line 6 "/builds/worker/workspace/...
+ # this can be removed once each channel has a watershed above 59.0b2 (from bug 1431342)
+ "files": [
+ "defaults/pref/channel-prefs.js",
+ "Contents/Resources/defaults/pref/channel-prefs.js",
+ ],
+ "channel_prefix": ["aurora", "beta", "release", "esr"],
+ "side": "source",
+ "deletion": '//@line 6 "',
+ },
+ {
+ # updates from a beta to an RC build, the latter specifies the release channel
+ "files": [
+ "defaults/pref/channel-prefs.js",
+ "Contents/Resources/defaults/pref/channel-prefs.js",
+ ],
+ "channel_prefix": ["beta"],
+ "side": "target",
+ "substitution": [
+ 'pref("app.update.channel", "release");\n',
+ 'pref("app.update.channel", "beta");\n',
+ ],
+ },
+ {
+ # updates from an RC to a beta build
+ "files": [
+ "defaults/pref/channel-prefs.js",
+ "Contents/Resources/defaults/pref/channel-prefs.js",
+ ],
+ "channel_prefix": ["beta"],
+ "side": "source",
+ "substitution": [
+ 'pref("app.update.channel", "release");\n',
+ 'pref("app.update.channel", "beta");\n',
+ ],
+ },
+ {
+ # Warning comments from bug 1576546
+ # When updating from a pre-70.0 build to 70.0+ this removes the new comments in
+ # the target side. In the 70.0+ --> 70.0+ case with a RC we won't need this, and
+ # the channel munging above will make channel-prefs.js identical, allowing the code
+ # to break before applying this transform.
+ "files": [
+ "defaults/pref/channel-prefs.js",
+ "Contents/Resources/defaults/pref/channel-prefs.js",
+ ],
+ "channel_prefix": ["aurora", "beta", "release", "esr"],
+ "side": "target",
+ "deletion": "//",
+ },
+ # update-settings.ini
+ {
+ # updates from a beta to an RC build, the latter specifies the release channel
+ # on mac, we actually have both files. The second location is the real
+ # one but we copy to the first to run the linux64 updater
+ "files": ["update-settings.ini", "Contents/Resources/update-settings.ini"],
+ "channel_prefix": ["beta"],
+ "side": "target",
+ "substitution": [
+ "ACCEPTED_MAR_CHANNEL_IDS=firefox-mozilla-release\n",
+ "ACCEPTED_MAR_CHANNEL_IDS=firefox-mozilla-beta,firefox-mozilla-release\n",
+ ],
+ },
+ {
+ # updates from an RC to a beta build
+ # on mac, we only need to modify the legit file this time. unpack_build
+ # handles the copy for the updater in both source and target
+ "files": ["Contents/Resources/update-settings.ini"],
+ "channel_prefix": ["beta"],
+ "side": "source",
+ "substitution": [
+ "ACCEPTED_MAR_CHANNEL_IDS=firefox-mozilla-release\n",
+ "ACCEPTED_MAR_CHANNEL_IDS=firefox-mozilla-beta,firefox-mozilla-release\n",
+ ],
+ },
+]
+
+
+def walk_dir(path):
+ all_files = []
+ all_dirs = []
+
+ for root, dirs, files in os.walk(path):
+ all_dirs.extend([os.path.join(root, d) for d in dirs])
+ all_files.extend([os.path.join(root, f) for f in files])
+
+ # trim off directory prefix for easier comparison
+ all_dirs = [d[len(path) + 1 :] for d in all_dirs]
+ all_files = [f[len(path) + 1 :] for f in all_files]
+
+ return all_dirs, all_files
+
+
+def compare_listings(
+ source_list, target_list, label, source_dir, target_dir, ignore_missing=None
+):
+ obj1 = set(source_list)
+ obj2 = set(target_list)
+ difference_found = False
+ ignore_missing = ignore_missing or ()
+
+ left_diff = obj1 - obj2
+ if left_diff:
+ if left_diff - set(ignore_missing):
+ _log = logging.error
+ difference_found = True
+ else:
+ _log = logging.warning
+ _log("Ignoring missing files due to ignore_missing")
+
+ _log("{} only in {}:".format(label, source_dir))
+ for d in sorted(left_diff):
+ _log(" {}".format(d))
+
+ right_diff = obj2 - obj1
+ if right_diff:
+ logging.error("{} only in {}:".format(label, target_dir))
+ for d in sorted(right_diff):
+ logging.error(" {}".format(d))
+ difference_found = True
+
+ return difference_found
+
+
+def hash_file(filename):
+ h = hashlib.sha256()
+ with open(filename, "rb", buffering=0) as f:
+ for b in iter(lambda: f.read(128 * 1024), b""):
+ h.update(b)
+ return h.hexdigest()
+
+
+def compare_common_files(files, channel, source_dir, target_dir):
+ difference_found = False
+ for filename in files:
+ source_file = os.path.join(source_dir, filename)
+ target_file = os.path.join(target_dir, filename)
+
+ if os.stat(source_file).st_size != os.stat(target_file).st_size or hash_file(
+ source_file
+ ) != hash_file(target_file):
+ logging.info("Difference found in {}".format(filename))
+ file_contents = {
+ "source": open(source_file).readlines(),
+ "target": open(target_file).readlines(),
+ }
+
+ transforms = [
+ t
+ for t in TRANSFORMS
+ if filename in t["files"]
+ and channel.startswith(tuple(t["channel_prefix"]))
+ ]
+ logging.debug(
+ "Got {} transform(s) to consider for {}".format(
+ len(transforms), filename
+ )
+ )
+ for transform in transforms:
+ side = transform["side"]
+
+ if "deletion" in transform:
+ d = transform["deletion"]
+ logging.debug(
+ "Trying deleting lines starting {} from {}".format(d, side)
+ )
+ file_contents[side] = [
+ l for l in file_contents[side] if not l.startswith(d)
+ ]
+
+ if "substitution" in transform:
+ r = transform["substitution"]
+ logging.debug("Trying replacement for {} in {}".format(r, side))
+ file_contents[side] = [
+ l.replace(r[0], r[1]) for l in file_contents[side]
+ ]
+
+ if file_contents["source"] == file_contents["target"]:
+ logging.info("Transforms removed all differences")
+ break
+
+ if file_contents["source"] != file_contents["target"]:
+ difference_found = True
+ logging.error(
+ "{} still differs after transforms, residual diff:".format(filename)
+ )
+ for l in difflib.unified_diff(
+ file_contents["source"], file_contents["target"]
+ ):
+ logging.error(l.rstrip())
+
+ return difference_found
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(
+ "Compare two directories recursively, with transformations for expected diffs"
+ )
+ parser.add_argument("source", help="Directory containing updated Firefox")
+ parser.add_argument("target", help="Directory containing expected Firefox")
+ parser.add_argument("channel", help="Update channel used")
+ parser.add_argument(
+ "--verbose", "-v", action="store_true", help="Enable verbose logging"
+ )
+ parser.add_argument(
+ "--ignore-missing",
+ action="append",
+ metavar="<path>",
+ help="Ignore absence of <path> in the target",
+ )
+
+ args = parser.parse_args()
+ level = logging.INFO
+ if args.verbose:
+ level = logging.DEBUG
+ logging.basicConfig(level=level, format="%(message)s", stream=sys.stdout)
+
+ source = args.source
+ target = args.target
+ if not os.path.exists(source) or not os.path.exists(target):
+ logging.error("Source and/or target directory doesn't exist")
+ sys.exit(3)
+
+ logging.info("Comparing {} with {}...".format(source, target))
+ source_dirs, source_files = walk_dir(source)
+ target_dirs, target_files = walk_dir(target)
+
+ dir_list_diff = compare_listings(
+ source_dirs, target_dirs, "Directories", source, target
+ )
+ file_list_diff = compare_listings(
+ source_files, target_files, "Files", source, target, args.ignore_missing
+ )
+ file_diff = compare_common_files(
+ set(source_files) & set(target_files), args.channel, source, target
+ )
+
+ if file_diff:
+ # Use status of 2 since python will use 1 if there is an error running the script
+ sys.exit(2)
+ elif dir_list_diff or file_list_diff:
+ # this has traditionally been a WARN, but we don't have files on one
+ # side anymore so lets FAIL
+ sys.exit(2)
+ else:
+ logging.info("No differences found")