blob: 99627845f5e1a45ecb99766e51667a02fc3af6ee [file] [log] [blame]
#!/usr/bin/env python
#
# Copyright 2012 The Chromium OS Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
from __future__ import print_function
import argparse
from contextlib import contextmanager
import logging
import os
import re
import subprocess
import sys
import tempfile
import factory_common # pylint: disable=unused-import
from cros.factory.utils import file_utils
from cros.factory.utils.process_utils import Spawn
from cros.factory.utils.sys_utils import MountPartition
STATEFUL_PARTITION_INDEX = 1
BLACKLIST = map(re.compile, [
'autotest/deps/',
'autotest/site_tests/.+/src/',
'autotest/site_tests/audiovideo_V4L2/media_v4l2_.*test$',
])
def DiffImages(mount_point_1, mount_point_2, out=sys.stdout):
'''Compares two images.
Args:
mount_point_1, mount_point_2: Mount points of stateful partitions of images
to compare.
out: Output to which differences should be printed.
'''
mount_points = [mount_point_1, mount_point_2]
# Hold in a variable since we want to change in an inner function.
differences = [0]
os_env = os.environ.copy()
os_env['LC_ALL'] = 'C'
for d in ['autotest', 'factory']:
process = Spawn(
['diff', '-qr'] +
# Skip client directory in autotest, since it causes a recursive
# directory loop in diff. This isn't perfect since it skips
# *everything* called client, but it'll do.
(['-x', 'client'] if d == 'autotest' else []) +
[os.path.join(x, 'dev_image', d) for x in mount_points],
read_stdout=True, log=True,
check_call=lambda returncode: returncode in [0, 1],
env=os_env)
for line in process.stdout_lines():
match = re.match(r'^Files (.+) and (.+) differ$|'
'^Only in (.+): (.+)$',
line)
assert match, 'Weird line in diff output: %r' % line
if match.group(1):
# Files exist in both trees, but differ
paths = [match.group(1), match.group(2)]
else:
assert match.group(3)
path = os.path.join(match.group(3), match.group(4))
if path.startswith(mount_points[0]):
paths = [path, None]
elif path.startswith(mount_points[1]):
paths = [None, path]
else:
assert False, (
"path doesn't start with either of %s" % mount_points)
stripped_paths = []
for i in (0, 1):
if paths[i]:
prefix = os.path.join(mount_points[i], 'dev_image', '')
assert paths[i].startswith(prefix)
# Strip the prefix
stripped_paths.append(paths[i][len(prefix):])
assert all(x == stripped_paths[0] for x in stripped_paths), (
stripped_paths)
stripped_path = stripped_paths[0]
blacklist_matches = [x for x in BLACKLIST
if x.match(stripped_path)]
if blacklist_matches:
logging.debug('Skipping %s since it matches %r',
stripped_path,
[x.pattern for x in blacklist_matches])
continue
def PrintHeader(message):
out.write('\n*** %s\n' % stripped_path)
out.write('*** %s\n' % message)
differences[0] += 1
if any(x is None for x in paths):
# We only have one or the other
PrintHeader('Only in image%d' % (1 if paths[0] else 2))
continue
# It's a real difference. Are either or both symlinks?
is_symlink = map(os.path.islink, paths)
if is_symlink[0] != is_symlink[1]:
def _IsSymlinkStr(value):
return 'is' if value else 'is not'
# That's a difference.
PrintHeader('%s symlink in image1 but %s in image2' %
(_IsSymlinkStr(is_symlink[0]),
_IsSymlinkStr(is_symlink[1])))
elif is_symlink[0]:
link_paths = map(os.readlink, paths)
if link_paths[0] != link_paths[1]:
PrintHeader('symlink to %r in image1 but %r in image2' %
tuple(link_paths))
else:
# They're both regular files; print a unified diff of the
# contents.
process = Spawn(
['diff', '-u'] + paths,
check_call=lambda returncode: returncode in [0, 1, 2],
read_stdout=True, env=os_env)
if process.returncode == 2:
if re.match(r'Binary files .+ differ\n$', process.stdout_data):
PrintHeader('Binary files differ')
else:
raise subprocess.CalledProcessError(process.returncode,
process.args)
else:
PrintHeader('Files differ; unified diff follows')
out.write(process.stdout_data)
return differences[0]
def main(argv=None, out=sys.stdout):
parser = argparse.ArgumentParser(
description=('Compares the autotest and factory directories in '
'two partitions.'))
parser.add_argument('--verbose', '-v', action='count')
parser.add_argument('images', metavar='IMAGE', nargs=2)
args = parser.parse_args(argv or sys.argv)
logging.basicConfig(level=logging.WARNING - 10 * (args.verbose or 0))
mount_points = [os.path.join(tempfile.gettempdir(), name)
for name in ('diff_image_1', 'diff_image_2')]
for f in mount_points:
file_utils.TryMakeDirs(f)
def MountOrReuse(index):
if os.path.isdir(args.images[index]):
@contextmanager
def Image():
yield args.images[index]
return Image()
else:
return MountPartition(args.images[index], STATEFUL_PARTITION_INDEX,
mount_points[index])
differences = 0
with MountOrReuse(0) as mount_point_0:
with MountOrReuse(1) as mount_point_1:
differences += DiffImages(mount_point_0, mount_point_1, out)
out.write('\nFound %d differences\n' % differences)
sys.exit(0 if differences == 0 else 1)
if __name__ == '__main__':
main()