diff options
Diffstat (limited to 'tools/isolate')
-rwxr-xr-x | tools/isolate/isolate_test.py | 24 | ||||
-rwxr-xr-x | tools/isolate/merge_gyp.py | 301 | ||||
-rwxr-xr-x | tools/isolate/merge_gyp_test.py | 188 | ||||
-rwxr-xr-x | tools/isolate/trace_inputs.py | 98 | ||||
-rwxr-xr-x | tools/isolate/trace_inputs_smoke_test.py | 163 | ||||
-rwxr-xr-x | tools/isolate/trace_inputs_test.py | 194 |
6 files changed, 808 insertions, 160 deletions
diff --git a/tools/isolate/isolate_test.py b/tools/isolate/isolate_test.py index 6d19212..3e05c226 100755 --- a/tools/isolate/isolate_test.py +++ b/tools/isolate/isolate_test.py @@ -3,6 +3,7 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. +import cStringIO import hashlib import json import logging @@ -225,14 +226,21 @@ class Isolate(unittest.TestCase): # cmd[0] is not generated from infiles[0] so it's not using a relative path. self._expected_result( False, ['isolate_test.py'], ['isolate_test.py', '--ok'], False) - expected = ( - "{\n 'variables': {\n" - " 'isolate_files': [\n" - " '<(DEPTH)/isolate_test.py',\n" - " ],\n" - " 'isolate_dirs': [\n" - " ],\n },\n},\n") - self.assertEquals(expected, out) + + expected_value = { + 'conditions': [ + ['OS=="%s"' % self.isolate.trace_inputs.get_flavor(), { + 'variables': { + 'isolate_files': [ + '<(DEPTH)/isolate_test.py', + ], + }, + }], + ], + } + expected_buffer = cStringIO.StringIO() + self.isolate.trace_inputs.pretty_print(expected_value, expected_buffer) + self.assertEquals(expected_buffer.getvalue(), out) def main(): diff --git a/tools/isolate/merge_gyp.py b/tools/isolate/merge_gyp.py new file mode 100755 index 0000000..fd4a0d3 --- /dev/null +++ b/tools/isolate/merge_gyp.py @@ -0,0 +1,301 @@ +#!/usr/bin/env python +# Copyright (c) 2012 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Merges multiple OS-specific gyp dependency lists into one that works on all +of them. + + +The logic is relatively simple. Takes the current conditions, add more +condition, find the strict subset. Done. +""" + +import copy +import logging +import optparse +import re +import sys + +import trace_inputs + + +def union(lhs, rhs): + """Merges two compatible datastructures composed of dict/list/set.""" + assert lhs is not None or rhs is not None + if lhs is None: + return copy.deepcopy(rhs) + if rhs is None: + return copy.deepcopy(lhs) + assert type(lhs) == type(rhs), (lhs, rhs) + if isinstance(lhs, dict): + return dict((k, union(lhs.get(k), rhs.get(k))) for k in set(lhs).union(rhs)) + elif isinstance(lhs, set): + # Do not go inside the set. + return lhs.union(rhs) + elif isinstance(lhs, list): + # Do not go inside the list. + return lhs + rhs + assert False, type(lhs) + + +def process_variables(for_os, variables): + """Extracts files and dirs from the |variables| dict. + + Returns a list of exactly two items. Each item is a dict that maps a string + to a set (of strings). + + In the first item, the keys are file names, and the values are sets of OS + names, like "win" or "mac". In the second item, the keys are directory names, + and the values are sets of OS names too. + """ + VALID_VARIABLES = ['isolate_files', 'isolate_dirs'] + + # Verify strictness. + assert isinstance(variables, dict), variables + assert set(VALID_VARIABLES).issuperset(set(variables)), variables.keys() + for items in variables.itervalues(): + assert isinstance(items, list), items + assert all(isinstance(i, basestring) for i in items), items + + # Returns [files, dirs] + return [ + dict((name, set([for_os])) for name in variables.get(var, [])) + for var in VALID_VARIABLES + ] + + +def eval_content(content): + """Evaluates a GYP file and return the value defined in it.""" + globs = {'__builtins__': None} + locs = {} + value = eval(content, globs, locs) + assert locs == {}, locs + assert globs == {'__builtins__': None}, globs + return value + + +def _process_inner(for_os, inner, old_files, old_dirs, old_os): + """Processes the variables inside a condition. + + Only meant to be called by parse_gyp_dict(). + + Args: + - for_os: OS where the references are tracked for. + - inner: Inner dictionary to process. + - old_files: Previous list of files to union with. + - old_dirs: Previous list of directories to union with. + - old_os: Previous list of OSes referenced to union with. + + Returns: + - A tuple of (files, dirs, os) where each list is a union of the new + dependencies found for this OS, as referenced by for_os, and the previous + list. + """ + assert isinstance(inner, dict), inner + assert set(['variables']).issuperset(set(inner)), inner.keys() + new_files, new_dirs = process_variables(for_os, inner.get('variables', {})) + if new_files or new_dirs: + old_os = old_os.union([for_os.lstrip('!')]) + return union(old_files, new_files), union(old_dirs, new_dirs), old_os + + +def parse_gyp_dict(value): + """Parses a gyp dict as returned by eval_content(). + + |value| is the loaded dictionary that was defined in the gyp file. + + Returns a 3-tuple, where the first two items are the same as the items + returned by process_variable() in the same order, and the last item is a set + of strings of all OSs seen in the input dict. + + The expected format is strict, anything diverting from the format below will + fail: + { + 'variables': { + 'isolate_files': [ + ... + ], + 'isolate_dirs: [ + ... + ], + }, + 'conditions': [ + ['OS=="<os>"', { + 'variables': { + ... + }, + }, { # else + 'variables': { + ... + }, + }], + ... + ], + } + """ + assert isinstance(value, dict), value + VALID_ROOTS = ['variables', 'conditions'] + assert set(VALID_ROOTS).issuperset(set(value)), value.keys() + + # Global level variables. + oses = set() + files, dirs = process_variables(None, value.get('variables', {})) + + # OS specific variables. + conditions = value.get('conditions', []) + assert isinstance(conditions, list), conditions + for condition in conditions: + assert isinstance(condition, list), condition + assert 2 <= len(condition) <= 3, condition + m = re.match(r'OS==\"([a-z]+)\"', condition[0]) + assert m, condition[0] + condition_os = m.group(1) + + files, dirs, oses = _process_inner( + condition_os, condition[1], files, dirs, oses) + + if len(condition) == 3: + files, dirs, oses = _process_inner( + '!' + condition_os, condition[2], files, dirs, oses) + + # TODO(maruel): _expand_negative() should be called here, because otherwise + # the OSes the negative condition represents is lost once the gyps are merged. + # This cause an invalid expansion in reduce_inputs() call. + return files, dirs, oses + + +def parse_gyp_dicts(gyps): + """Parses each gyp file and returns the merged results. + + It only loads what parse_gyp_dict() can process. + + Return values: + files: dict(filename, set(OS where this filename is a dependency)) + dirs: dict(dirame, set(OS where this dirname is a dependency)) + oses: set(all the OSes referenced) + """ + files = {} + dirs = {} + oses = set() + for gyp in gyps: + with open(gyp, 'rb') as gyp_file: + content = gyp_file.read() + gyp_files, gyp_dirs, gyp_oses = parse_gyp_dict(eval_content(content)) + files = union(gyp_files, files) + dirs = union(gyp_dirs, dirs) + oses |= gyp_oses + return files, dirs, oses + + +def _expand_negative(items, oses): + """Converts all '!foo' value in the set by oses.difference('foo').""" + assert None not in oses and len(oses) >= 2, oses + for name in items: + if None in items[name]: + # Shortcut any item having None in their set. An item listed in None means + # the item is a dependency on all OSes. As such, there is no need to list + # any OS. + items[name] = set([None]) + continue + for neg in [o for o in items[name] if o.startswith('!')]: + # Replace it with the inverse. + items[name] = items[name].union(oses.difference([neg[1:]])) + items[name].remove(neg) + if items[name] == oses: + items[name] = set([None]) + + +def _compact_negative(items, oses): + """Converts all oses.difference('foo') to '!foo'. + + It is doing the reverse of _expand_negative(). + """ + assert None not in oses and len(oses) >= 3, oses + for name in items: + missing = oses.difference(items[name]) + if len(missing) == 1: + # Replace it with a negative. + items[name] = set(['!' + tuple(missing)[0]]) + + +def reduce_inputs(files, dirs, oses): + """Reduces the variables to their strictest minimum.""" + # Construct the inverse map first. + # Look at each individual file and directory, map where they are used and + # reconstruct the inverse dictionary. + # First, expands all '!' builders into the reverse. + # TODO(maruel): This is too late to call _expand_negative(). The exact list + # negative OSes condition it represents is lost at that point. + _expand_negative(files, oses) + _expand_negative(dirs, oses) + + # Do not convert back to negative if only 2 OSes were merged. It is easier to + # read this way. + if len(oses) > 2: + _compact_negative(files, oses) + _compact_negative(dirs, oses) + + return files, dirs + + +def convert_to_gyp(files, dirs): + """Regenerates back a gyp-like configuration dict from files and dirs + mappings. + + Sort the lists. + """ + # First, inverse the mapping to make it dict first. + config = {} + def to_cond(items, name): + for item, oses in items.iteritems(): + for cond_os in oses: + condition_values = config.setdefault( + None if cond_os is None else cond_os.lstrip('!'), + [{}, {}]) + # If condition is negative, use index 1, else use index 0. + condition_value = condition_values[int((cond_os or '').startswith('!'))] + # The list of items (files or dirs). Append the new item and keep the + # list sorted. + l = condition_value.setdefault('variables', {}).setdefault(name, []) + l.append(item) + l.sort() + + to_cond(files, 'isolate_files') + to_cond(dirs, 'isolate_dirs') + + out = {} + for o in sorted(config): + d = config[o] + if o is None: + assert not d[1] + out = union(out, d[0]) + else: + c = out.setdefault('conditions', []) + if d[1]: + c.append(['OS=="%s"' % o] + d) + else: + c.append(['OS=="%s"' % o] + d[0:1]) + return out + + +def main(): + parser = optparse.OptionParser( + usage='%prog <options> [file1] [file2] ...') + parser.add_option( + '-v', '--verbose', action='count', default=0, help='Use multiple times') + + options, args = parser.parse_args() + level = [logging.ERROR, logging.INFO, logging.DEBUG][min(2, options.verbose)] + logging.basicConfig( + level=level, + format='%(levelname)5s %(module)15s(%(lineno)3d):%(message)s') + + trace_inputs.pretty_print( + convert_to_gyp(*reduce_inputs(*parse_gyp_dicts(args))), + sys.stdout) + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/isolate/merge_gyp_test.py b/tools/isolate/merge_gyp_test.py new file mode 100755 index 0000000..9826c68 --- /dev/null +++ b/tools/isolate/merge_gyp_test.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python +# Copyright (c) 2012 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import unittest + +import merge_gyp + + +class MergeGyp(unittest.TestCase): + def test_unknown_key(self): + try: + merge_gyp.process_variables(None, {'foo': [],}) + self.fail() + except AssertionError: + pass + + def test_unknown_var(self): + try: + merge_gyp.process_variables(None, {'variables': {'foo': [],}}) + self.fail() + except AssertionError: + pass + + def test_parse_gyp_dict_empty(self): + f, d, o = merge_gyp.parse_gyp_dict({}) + self.assertEquals({}, f) + self.assertEquals({}, d) + self.assertEquals(set(), o) + + def test_parse_gyp_dict(self): + value = { + 'variables': { + 'isolate_files': [ + 'a', + ], + 'isolate_dirs': [ + 'b', + ], + }, + 'conditions': [ + ['OS=="atari"', { + 'variables': { + 'isolate_files': [ + 'c', + 'x', + ], + 'isolate_dirs': [ + 'd', + ], + }, + }, { # else + 'variables': { + 'isolate_files': [ + 'e', + 'x', + ], + 'isolate_dirs': [ + 'f', + ], + }, + }], + ['OS=="amiga"', { + 'variables': { + 'isolate_files': [ + 'g', + ], + }, + }], + ['OS=="inexistent"', { + }], + ['OS=="coleco"', { + }, { # else + 'variables': { + 'isolate_dirs': [ + 'h', + ], + }, + }], + ], + } + expected_files = { + 'a': set([None]), + 'c': set(['atari']), + 'e': set(['!atari']), + 'g': set(['amiga']), + 'x': set(['!atari', 'atari']), # potential for reduction + } + expected_dirs = { + 'b': set([None]), + 'd': set(['atari']), + 'f': set(['!atari']), + 'h': set(['!coleco']), + } + # coleco is included even if only negative. + expected_oses = set(['atari', 'amiga', 'coleco']) + actual_files, actual_dirs, actual_oses = merge_gyp.parse_gyp_dict(value) + self.assertEquals(expected_files, actual_files) + self.assertEquals(expected_dirs, actual_dirs) + self.assertEquals(expected_oses, actual_oses) + + def test_reduce_inputs(self): + value_files = { + 'a': set([None]), + 'c': set(['atari']), + 'e': set(['!atari']), + 'g': set(['amiga']), + 'x': set(['!atari', 'atari']), + } + value_dirs = { + 'b': set([None]), + 'd': set(['atari']), + 'f': set(['!atari']), + 'h': set(['!coleco']), + } + value_oses = set(['atari', 'amiga', 'coleco']) + expected_files = { + 'a': set([None]), + 'c': set(['atari']), + 'e': set(['!atari']), + 'g': set(['amiga']), + 'x': set([None]), # Reduced. + } + expected_dirs = { + 'b': set([None]), + 'd': set(['atari']), + 'f': set(['!atari']), + 'h': set(['!coleco']), + } + actual_files, actual_dirs = merge_gyp.reduce_inputs( + value_files, value_dirs, value_oses) + self.assertEquals(expected_files, actual_files) + self.assertEquals(expected_dirs, actual_dirs) + + def test_convert_to_gyp(self): + files = { + 'a': set([None]), + 'x': set([None]), + + 'g': set(['amiga']), + + 'c': set(['atari']), + 'e': set(['!atari']), + } + dirs = { + 'b': set([None]), + + 'd': set(['atari']), + 'f': set(['!atari']), + + 'h': set(['!coleco']), + } + expected = { + 'variables': { + 'isolate_dirs': ['b'], + 'isolate_files': ['a', 'x'], + }, + 'conditions': [ + ['OS=="amiga"', { + 'variables': { + 'isolate_files': ['g'], + }, + }], + ['OS=="atari"', { + 'variables': { + 'isolate_dirs': ['d'], + 'isolate_files': ['c'], + }, + }, { + 'variables': { + 'isolate_dirs': ['f'], + 'isolate_files': ['e'], + }, + }], + ['OS=="coleco"', { + }, { + 'variables': { + 'isolate_dirs': ['h'], + }, + }], + ], + } + self.assertEquals(expected, merge_gyp.convert_to_gyp(files, dirs)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tools/isolate/trace_inputs.py b/tools/isolate/trace_inputs.py index 16cce27..b52b80d 100755 --- a/tools/isolate/trace_inputs.py +++ b/tools/isolate/trace_inputs.py @@ -22,6 +22,19 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__)) ROOT_DIR = os.path.dirname(os.path.dirname(BASE_DIR)) +def get_flavor(): + """Returns the system default flavor. Copied from gyp/pylib/gyp/common.py.""" + flavors = { + 'cygwin': 'win', + 'win32': 'win', + 'darwin': 'mac', + 'sunos5': 'solaris', + 'freebsd7': 'freebsd', + 'freebsd8': 'freebsd', + } + return flavors.get(sys.platform, 'linux') + + def isEnabledFor(level): return logging.getLogger().isEnabledFor(level) @@ -635,6 +648,59 @@ def extract_directories(files, root): return sorted(files) +def pretty_print(variables, stdout): + """Outputs a gyp compatible list from the decoded variables.""" + # Similar to pprint.print() but with NIH syndrome. + + def loop_list(indent, items): + for item in items: + if isinstance(item, basestring): + stdout.write('%s\'%s\',\n' % (indent, item)) + elif isinstance(item, dict): + stdout.write('%s{\n' % indent) + loop_dict(indent + ' ', item) + stdout.write('%s},\n' % indent) + elif isinstance(item, list): + # A list inside a list will write the first item embedded. + stdout.write('%s[' % indent) + for index, i in enumerate(item): + if isinstance(i, basestring): + stdout.write('\'%s\', ' % i) + elif isinstance(i, dict): + stdout.write('{\n') + loop_dict(indent + ' ', i) + if index != len(item) - 1: + x = ', ' + else: + x = '' + stdout.write('%s}%s' % (indent, x)) + else: + assert False + stdout.write('],\n') + else: + assert False + + def loop_dict(indent, items): + # Use reversed sorting since it happens we always want it that way. + for key in sorted(items, reverse=True): + item = items[key] + stdout.write("%s'%s': " % (indent, key)) + if isinstance(item, dict): + stdout.write('{\n') + loop_dict(indent + ' ', item) + stdout.write(indent + '},\n') + elif isinstance(item, list): + stdout.write('[\n') + loop_list(indent + ' ', item) + stdout.write(indent + '],\n') + else: + assert False + + stdout.write('{\n') + loop_dict(' ', variables) + stdout.write('}\n') + + def trace_inputs(logfile, cmd, root_dir, cwd_dir, product_dir, force_trace): """Tries to load the logs if available. If not, trace the test. @@ -672,12 +738,13 @@ def trace_inputs(logfile, cmd, root_dir, cwd_dir, product_dir, force_trace): if cwd_dir is None: print(txt) - if sys.platform == 'linux2': + flavor = get_flavor() + if flavor == 'linux': api = Strace() - elif sys.platform == 'darwin': + elif flavor == 'mac': api = Dtrace() else: - print >> sys.stderr, 'Unsupported platform' + print >> sys.stderr, 'Unsupported platform %s' % sys.platform return 1 if not os.path.isfile(logfile) or force_trace: @@ -745,18 +812,19 @@ def trace_inputs(logfile, cmd, root_dir, cwd_dir, product_dir, force_trace): corrected = [fix(f) for f in simplified] files = [f for f in corrected if not f.endswith('/')] dirs = [f for f in corrected if f.endswith('/')] - # Constructs the python code manually. - print( - '{\n' - ' \'variables\': {\n' - ' \'isolate_files\': [\n') + ( - ''.join(' \'%s\',\n' % f for f in files)) + ( - ' ],\n' - ' \'isolate_dirs\': [\n') + ( - ''.join(' \'%s\',\n' % f for f in dirs)) + ( - ' ],\n' - ' },\n' - '},') + variables = {} + if files: + variables['isolate_files'] = files + if dirs: + variables['isolate_dirs'] = dirs + value = { + 'conditions': [ + ['OS=="%s"' % flavor, { + 'variables': variables, + }], + ], + } + pretty_print(value, sys.stdout) return 0 diff --git a/tools/isolate/trace_inputs_smoke_test.py b/tools/isolate/trace_inputs_smoke_test.py new file mode 100755 index 0000000..13bb1e9 --- /dev/null +++ b/tools/isolate/trace_inputs_smoke_test.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python +# Copyright (c) 2012 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +import cStringIO +import logging +import os +import shutil +import subprocess +import sys +import tempfile +import unittest + +ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) +FILENAME = os.path.basename(__file__) +VERBOSE = False + + +class CalledProcessError(subprocess.CalledProcessError): + """Makes 2.6 version act like 2.7""" + def __init__(self, returncode, cmd, output, cwd): + super(CalledProcessError, self).__init__(returncode, cmd) + self.output = output + self.cwd = cwd + + def __str__(self): + return super(CalledProcessError, self).__str__() + ( + '\n' + 'cwd=%s\n%s') % (self.cwd, self.output) + + +class TraceInputs(unittest.TestCase): + def setUp(self): + self.tempdir = tempfile.mkdtemp() + self.log = os.path.join(self.tempdir, 'log') + os.chdir(ROOT_DIR) + + def tearDown(self): + shutil.rmtree(self.tempdir) + + def _execute(self, is_gyp): + cmd = [ + sys.executable, os.path.join('..', 'trace_inputs.py'), + '--log', self.log, + '--root-dir', ROOT_DIR, + ] + if is_gyp: + cmd.extend( + [ + '--cwd', 'data', + '--product', '.', # Not tested. + ]) + cmd.append(os.path.join('..', FILENAME)) + if is_gyp: + # When the gyp argument is not specified, the command is started from + # --root-dir directory. + cmd.append('--child-gyp') + else: + # When the gyp argument is specified, the command is started from --cwd + # directory. + cmd.append('--child') + + cwd = 'data' + p = subprocess.Popen( + cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=cwd) + out = p.communicate()[0] + if p.returncode: + raise CalledProcessError(p.returncode, cmd, out, cwd) + return out + + def test_trace(self): + if sys.platform not in ('linux2', 'darwin'): + print 'WARNING: unsupported: %s' % sys.platform + return + expected_end = [ + "Interesting: 4 reduced to 3", + " data/trace_inputs/", + " trace_inputs.py", + " %s" % FILENAME, + ] + actual = self._execute(False).splitlines() + self.assertTrue(actual[0].startswith('Tracing... [')) + self.assertTrue(actual[1].startswith('Loading traces... ')) + self.assertTrue(actual[2].startswith('Total: ')) + self.assertEquals("Non existent: 0", actual[3]) + # Ignore any Unexpected part. + # TODO(maruel): Make sure there is no Unexpected part, even in the case of + # virtualenv usage. + self.assertEquals(expected_end, actual[-len(expected_end):]) + + def test_trace_gyp(self): + if sys.platform not in ('linux2', 'darwin'): + print 'WARNING: unsupported: %s' % sys.platform + return + import trace_inputs + expected_value = { + 'conditions': [ + ['OS=="%s"' % trace_inputs.get_flavor(), { + 'variables': { + 'isolate_files': [ + '<(DEPTH)/trace_inputs.py', + '<(DEPTH)/%s' % FILENAME, + ], + 'isolate_dirs': [ + 'trace_inputs/', + ], + }, + }], + ], + } + expected_buffer = cStringIO.StringIO() + trace_inputs.pretty_print(expected_value, expected_buffer) + + actual = self._execute(True) + self.assertEquals(expected_buffer.getvalue(), actual) + + +def child(): + """When the gyp argument is not specified, the command is started from + --root-dir directory. + """ + print 'child' + # Force file opening with a non-normalized path. + open(os.path.join('data', '..', 'trace_inputs.py'), 'rb').close() + # Do not wait for the child to exit. + # Use relative directory. + subprocess.Popen( + ['python', 'child2.py'], cwd=os.path.join('data', 'trace_inputs')) + return 0 + + +def child_gyp(): + """When the gyp argument is specified, the command is started from --cwd + directory. + """ + print 'child_gyp' + # Force file opening. + open(os.path.join('..', 'trace_inputs.py'), 'rb').close() + # Do not wait for the child to exit. + # Use relative directory. + subprocess.Popen(['python', 'child2.py'], cwd='trace_inputs') + return 0 + + +def main(): + global VERBOSE + VERBOSE = '-v' in sys.argv + level = logging.DEBUG if VERBOSE else logging.ERROR + logging.basicConfig(level=level) + if len(sys.argv) == 1: + unittest.main() + + if sys.argv[1] == '--child': + return child() + if sys.argv[1] == '--child-gyp': + return child_gyp() + + unittest.main() + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/isolate/trace_inputs_test.py b/tools/isolate/trace_inputs_test.py index 0ef644c..3830b79 100755 --- a/tools/isolate/trace_inputs_test.py +++ b/tools/isolate/trace_inputs_test.py @@ -3,153 +3,73 @@ # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. -import logging -import os -import shutil -import subprocess -import sys -import tempfile +import cStringIO import unittest -ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) - -VERBOSE = False - - -class CalledProcessError(subprocess.CalledProcessError): - """Makes 2.6 version act like 2.7""" - def __init__(self, returncode, cmd, output, cwd): - super(CalledProcessError, self).__init__(returncode, cmd) - self.output = output - self.cwd = cwd - - def __str__(self): - return super(CalledProcessError, self).__str__() + ( - '\n' - 'cwd=%s\n%s') % (self.cwd, self.output) +import trace_inputs class TraceInputs(unittest.TestCase): - def setUp(self): - self.tempdir = tempfile.mkdtemp() - self.log = os.path.join(self.tempdir, 'log') - os.chdir(ROOT_DIR) - - def tearDown(self): - shutil.rmtree(self.tempdir) - - def _execute(self, is_gyp): - cmd = [ - sys.executable, os.path.join('..', 'trace_inputs.py'), - '--log', self.log, - '--root-dir', ROOT_DIR, - ] - if is_gyp: - cmd.extend( - [ - '--cwd', 'data', - '--product', '.', # Not tested. - ]) - cmd.append(os.path.join('..', 'trace_inputs_test.py')) - if is_gyp: - # When the gyp argument is not specified, the command is started from - # --root-dir directory. - cmd.append('--child-gyp') - else: - # When the gyp argument is specified, the command is started from --cwd - # directory. - cmd.append('--child') - - cwd = 'data' - p = subprocess.Popen( - cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=cwd) - out = p.communicate()[0] - if p.returncode: - raise CalledProcessError(p.returncode, cmd, out, cwd) - return out - - def test_trace(self): - if sys.platform not in ('linux2', 'darwin'): - print 'WARNING: unsupported: %s' % sys.platform - return - expected_end = [ - "Interesting: 4 reduced to 3", - " data/trace_inputs/", - " trace_inputs.py", - " trace_inputs_test.py", - ] - actual = self._execute(False).splitlines() - self.assertTrue(actual[0].startswith('Tracing... [')) - self.assertTrue(actual[1].startswith('Loading traces... ')) - self.assertTrue(actual[2].startswith('Total: ')) - self.assertEquals("Non existent: 0", actual[3]) - # Ignore any Unexpected part. - # TODO(maruel): Make sure there is no Unexpected part, even in the case of - # virtualenv usage. - self.assertEquals(expected_end, actual[-len(expected_end):]) - - def test_trace_gyp(self): - if sys.platform not in ('linux2', 'darwin'): - print 'WARNING: unsupported: %s' % sys.platform - return + def _test(self, value, expected): + actual = cStringIO.StringIO() + trace_inputs.pretty_print(value, actual) + self.assertEquals(expected, actual.getvalue()) + + def test_pretty_print_empty(self): + self._test({}, '{\n}\n') + + def test_pretty_print_mid_size(self): + value = { + 'variables': { + 'bar': [ + 'file1', + 'file2', + ], + }, + 'conditions': [ + ['OS=\"foo\"', { + 'variables': { + 'isolate': [ + 'dir1', + 'dir2', + ], + }, + }], + ['OS=\"bar\"', { + 'variables': {}, + }, { + 'variables': {}, + }], + ], + } expected = ( "{\n" " 'variables': {\n" - " 'isolate_files': [\n" - " '<(DEPTH)/trace_inputs.py',\n" - " '<(DEPTH)/trace_inputs_test.py',\n" - " ],\n" - " 'isolate_dirs': [\n" - " 'trace_inputs/',\n" + " 'bar': [\n" + " 'file1',\n" + " 'file2',\n" " ],\n" " },\n" - "},\n") - actual = self._execute(True) - self.assertEquals(expected, actual) - - -def child(): - """When the gyp argument is not specified, the command is started from - --root-dir directory. - """ - print 'child' - # Force file opening with a non-normalized path. - open(os.path.join('data', '..', 'trace_inputs.py'), 'rb').close() - # Do not wait for the child to exit. - # Use relative directory. - subprocess.Popen( - ['python', 'child2.py'], cwd=os.path.join('data', 'trace_inputs')) - return 0 - - -def child_gyp(): - """When the gyp argument is specified, the command is started from --cwd - directory. - """ - print 'child_gyp' - # Force file opening. - open(os.path.join('..', 'trace_inputs.py'), 'rb').close() - # Do not wait for the child to exit. - # Use relative directory. - subprocess.Popen(['python', 'child2.py'], cwd='trace_inputs') - return 0 - - -def main(): - global VERBOSE - VERBOSE = '-v' in sys.argv - level = logging.DEBUG if VERBOSE else logging.ERROR - logging.basicConfig(level=level) - if len(sys.argv) == 1: - unittest.main() - - if sys.argv[1] == '--child': - return child() - if sys.argv[1] == '--child-gyp': - return child_gyp() - - unittest.main() + " 'conditions': [\n" + " ['OS=\"foo\"', {\n" + " 'variables': {\n" + " 'isolate': [\n" + " 'dir1',\n" + " 'dir2',\n" + " ],\n" + " },\n" + " }],\n" + " ['OS=\"bar\"', {\n" + " 'variables': {\n" + " },\n" + " }, {\n" + " 'variables': {\n" + " },\n" + " }],\n" + " ],\n" + "}\n") + self._test(value, expected) if __name__ == '__main__': - sys.exit(main()) + unittest.main() |