| Index: content/test/gpu/gather_swarming_json_results.py
|
| diff --git a/content/test/gpu/gather_swarming_json_results.py b/content/test/gpu/gather_swarming_json_results.py
|
| new file mode 100755
|
| index 0000000000000000000000000000000000000000..159d4cb4065d1671b28aafebe2e3b4b9da86c406
|
| --- /dev/null
|
| +++ b/content/test/gpu/gather_swarming_json_results.py
|
| @@ -0,0 +1,234 @@
|
| +#!/usr/bin/env python
|
| +# Copyright 2016 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.
|
| +
|
| +"""Script which gathers and merges the JSON results from multiple
|
| +swarming shards of a step on the waterfall.
|
| +
|
| +This is used to feed in the per-test times of previous runs of tests
|
| +to the browser_test_runner's sharding algorithm, to improve shard
|
| +distribution.
|
| +"""
|
| +
|
| +import argparse
|
| +import json
|
| +import os
|
| +import shutil
|
| +import subprocess
|
| +import sys
|
| +import tempfile
|
| +import urllib
|
| +import urllib2
|
| +
|
| +SWARMING_SERVICE = 'https://chromium-swarm.appspot.com'
|
| +
|
| +THIS_DIR = os.path.dirname(os.path.abspath(__file__))
|
| +SRC_DIR = os.path.dirname(os.path.dirname(os.path.dirname(THIS_DIR)))
|
| +SWARMING_CLIENT_DIR = os.path.join(SRC_DIR, 'tools', 'swarming_client')
|
| +
|
| +class Swarming:
|
| + @staticmethod
|
| + def CheckAuth():
|
| + output = subprocess.check_output([
|
| + os.path.join(SWARMING_CLIENT_DIR, 'auth.py'),
|
| + 'check',
|
| + '--service',
|
| + SWARMING_SERVICE])
|
| + if not output.startswith('user:'):
|
| + print 'Must run:'
|
| + print ' tools/swarming_client/auth.py login --service ' + \
|
| + SWARMING_SERVICE
|
| + print 'and authenticate with @google.com credentials.'
|
| + sys.exit(1)
|
| +
|
| + @staticmethod
|
| + def Collect(taskIDs, output_dir, verbose):
|
| + cmd = [
|
| + os.path.join(SWARMING_CLIENT_DIR, 'swarming.py'),
|
| + 'collect',
|
| + '-S',
|
| + SWARMING_SERVICE,
|
| + '--task-output-dir',
|
| + output_dir] + taskIDs
|
| + if verbose:
|
| + print 'Collecting Swarming results:'
|
| + print cmd
|
| + if verbose > 1:
|
| + # Print stdout from the collect command.
|
| + stdout = None
|
| + else:
|
| + fnull = open(os.devnull, 'w')
|
| + stdout = fnull
|
| + subprocess.check_call(cmd, stdout=stdout, stderr=subprocess.STDOUT)
|
| +
|
| + @staticmethod
|
| + def ExtractShardTaskIDs(urls):
|
| + SWARMING_URL = 'https://chromium-swarm.appspot.com/user/task/'
|
| + taskIDs = []
|
| + for k,v in urls.iteritems():
|
| + if not k.startswith('shard'):
|
| + raise Exception('Illegally formatted \'urls\' key %s' % k)
|
| + if not v.startswith(SWARMING_URL):
|
| + raise Exception('Illegally formatted \'urls\' value %s' % v)
|
| + taskIDs.append(v[len(SWARMING_URL):])
|
| + return taskIDs
|
| +
|
| +class Waterfall:
|
| + def __init__(self, waterfall):
|
| + self._waterfall = waterfall
|
| + self.BASE_URL = 'http://build.chromium.org/p/'
|
| + self.BASE_JSON_BUILDERS_URL = self.BASE_URL + '%s/json/builders'
|
| + self.BASE_JSON_BUILDS_URL = self.BASE_JSON_BUILDERS_URL + '/%s/builds'
|
| +
|
| + def GetJsonFromUrl(self, url):
|
| + conn = urllib2.urlopen(url)
|
| + result = conn.read()
|
| + conn.close()
|
| + return json.loads(result)
|
| +
|
| + def GetBuildNumbersForBot(self, bot):
|
| + builds_json = self.GetJsonFromUrl(
|
| + self.BASE_JSON_BUILDS_URL %
|
| + (self._waterfall, urllib.quote(bot)))
|
| + build_numbers = [int(k) for k in builds_json.keys()]
|
| + build_numbers.sort()
|
| + return build_numbers
|
| +
|
| + def GetMostRecentlyCompletedBuildNumberForBot(self, bot):
|
| + builds = self.GetBuildNumbersForBot(bot)
|
| + return builds[len(builds) - 1]
|
| +
|
| + def GetJsonForBuild(self, bot, build):
|
| + return self.GetJsonFromUrl(
|
| + (self.BASE_JSON_BUILDS_URL + '/%d') %
|
| + (self._waterfall, urllib.quote(bot), build))
|
| +
|
| +
|
| +def JsonLoadStrippingUnicode(file, **kwargs):
|
| + def StripUnicode(obj):
|
| + if isinstance(obj, unicode):
|
| + try:
|
| + return obj.encode('ascii')
|
| + except UnicodeEncodeError:
|
| + return obj
|
| +
|
| + if isinstance(obj, list):
|
| + return map(StripUnicode, obj)
|
| +
|
| + if isinstance(obj, dict):
|
| + new_obj = type(obj)(
|
| + (StripUnicode(k), StripUnicode(v)) for k, v in obj.iteritems() )
|
| + return new_obj
|
| +
|
| + return obj
|
| +
|
| + return StripUnicode(json.load(file, **kwargs))
|
| +
|
| +
|
| +def Merge(dest, src):
|
| + if isinstance(dest, list):
|
| + if not isinstance(src, list):
|
| + raise Exception('Both must be lists: ' + dest + ' and ' + src)
|
| + return dest + src
|
| +
|
| + if isinstance(dest, dict):
|
| + if not isinstance(src, dict):
|
| + raise Exception('Both must be dicts: ' + dest + ' and ' + src)
|
| + for k in src.iterkeys():
|
| + if k not in dest:
|
| + dest[k] = src[k]
|
| + else:
|
| + dest[k] = Merge(dest[k], src[k])
|
| + return dest
|
| +
|
| + return src
|
| +
|
| +
|
| +def main():
|
| + rest_args = sys.argv[1:]
|
| + parser = argparse.ArgumentParser(
|
| + description='Gather JSON results from a run of a Swarming test.',
|
| + formatter_class=argparse.ArgumentDefaultsHelpFormatter)
|
| + parser.add_argument('-v', '--verbose', action='count', default=0,
|
| + help='Enable verbose output (specify multiple times '
|
| + 'for more output)')
|
| + parser.add_argument('--waterfall', type=str, default='chromium.gpu.fyi',
|
| + help='Which waterfall to examine')
|
| + parser.add_argument('--bot', type=str, default='Linux Release (NVIDIA)',
|
| + help='Which bot on the waterfall to examine')
|
| + parser.add_argument('--build', default=-1, type=int,
|
| + help='Which build to fetch (-1 means most recent)')
|
| + parser.add_argument('--step', type=str, default='webgl2_conformance_tests',
|
| + help='Which step to fetch (treated as a prefix)')
|
| + parser.add_argument('--output', type=str, default='output.json',
|
| + help='Name of output file')
|
| + parser.add_argument('--leak-temp-dir', action='store_true', default=False,
|
| + help='Deliberately leak temporary directory')
|
| +
|
| + options = parser.parse_args(rest_args)
|
| +
|
| + Swarming.CheckAuth()
|
| +
|
| + waterfall = Waterfall(options.waterfall)
|
| + build = options.build
|
| + if build < 0:
|
| + build = waterfall.GetMostRecentlyCompletedBuildNumberForBot(options.bot)
|
| +
|
| + build_json = waterfall.GetJsonForBuild(options.bot, build)
|
| +
|
| + if options.verbose:
|
| + print 'Fetching information from %s, bot %s, build %s' % (
|
| + options.waterfall, options.bot, build)
|
| +
|
| + taskIDs = []
|
| + for s in build_json['steps']:
|
| + if s['name'].startswith(options.step):
|
| + # Found the step.
|
| + #
|
| + # The Swarming shards happen to be listed in the 'urls' property
|
| + # of the step. Iterate down them.
|
| + if 'urls' not in s or not s['urls']:
|
| + # Note: we could also just download json.output if it exists.
|
| + print ('%s on waterfall %s, bot %s, build %s doesn\'t '
|
| + 'look like a Swarmed task') % (
|
| + s['name'], options.waterfall, options.bot, build)
|
| + return 1
|
| + taskIDs = Swarming.ExtractShardTaskIDs(s['urls'])
|
| + if options.verbose:
|
| + print 'Found Swarming task IDs for step %s' % s['name']
|
| +
|
| + break
|
| + if not taskIDs:
|
| + print 'Problem gathering the Swarming task IDs for %s' % options.step
|
| + return 1
|
| +
|
| + # Collect the results.
|
| + tmpdir = tempfile.mkdtemp()
|
| + Swarming.Collect(taskIDs, tmpdir, options.verbose)
|
| +
|
| + # Shards' JSON outputs are in sequentially-numbered subdirectories
|
| + # of the output directory.
|
| + merged_json = None
|
| + for i in xrange(len(taskIDs)):
|
| + with open(os.path.join(tmpdir, str(i), 'output.json')) as f:
|
| + cur_json = JsonLoadStrippingUnicode(f)
|
| + if not merged_json:
|
| + merged_json = cur_json
|
| + else:
|
| + merged_json = Merge(merged_json, cur_json)
|
| +
|
| + with open(options.output, 'w') as f:
|
| + json.dump(merged_json, f, sort_keys=True, indent=2,
|
| + separators=(',', ': '))
|
| +
|
| + if options.leak_temp_dir:
|
| + print 'Temporary directory: %s' % tmpdir
|
| + else:
|
| + shutil.rmtree(tmpdir)
|
| +
|
| + return 0
|
| +
|
| +
|
| +if __name__ == "__main__":
|
| + sys.exit(main())
|
|
|