Perf dashboard erroneously shows an old OS build in A/B testing range
[WebKit-https.git] / Websites / perf.webkit.org / tools / pull-os-versions.py
1 #!/usr/bin/python
2
3 import argparse
4 import json
5 import operator
6 import re
7 import sys
8 import subprocess
9 import time
10 import urllib
11 import urllib2
12
13 from datetime import datetime
14 from xml.dom.minidom import parseString as parseXmlString
15 from util import submit_commits
16 from util import text_content
17 from util import setup_auth
18
19
20 def main(argv):
21     parser = argparse.ArgumentParser()
22     parser.add_argument('--os-config-json', required=True, help='The path to a JSON that specifies how to fetch OS build information')
23     parser.add_argument('--server-config-json', required=True, help='The path to a JSON file that specifies the perf dashboard')
24     parser.add_argument('--seconds-to-sleep', type=float, default=43200, help='The seconds to sleep between iterations')
25     args = parser.parse_args()
26
27     with open(args.os_config_json) as os_config_json:
28         os_config_list = json.load(os_config_json)
29
30     with open(args.server_config_json) as server_config_json:
31         server_config = json.load(server_config_json)
32         setup_auth(server_config['server'])
33
34     fetchers = [OSBuildFetcher(os_config) for os_config in os_config_list]
35
36     while True:
37         for fetcher in fetchers:
38             fetcher.fetch_and_report_new_builds(server_config)
39         print "Sleeping for %d seconds" % args.seconds_to_sleep
40         time.sleep(args.seconds_to_sleep)
41
42
43 # FIXME: Move other static functions into this class.
44 class OSBuildFetcher:
45     def __init__(self, os_config):
46         self._os_config = os_config
47         self._reported_revisions = set()
48
49     def _fetch_available_builds(self):
50         config = self._os_config
51         repository_name = self._os_config['name']
52
53         if 'customCommands' in config:
54             available_builds = []
55             for command in config['customCommands']:
56                 print "Executing", ' '.join(command['command'])
57                 available_builds += available_builds_from_command(repository_name, command['command'], command['linesToIgnore'])
58         else:
59             url = config['buildSourceURL']
60             print "Fetching available builds from", url
61             available_builds = fetch_available_builds(repository_name, url, config['trainVersionMap'])
62         return available_builds
63
64     def fetch_and_report_new_builds(self, server_config):
65         available_builds = self._fetch_available_builds()
66         reported_revisions = self._reported_revisions
67         print 'Found %d builds' % len(available_builds)
68
69         available_builds = filter(lambda commit: commit['revision'] not in reported_revisions, available_builds)
70         self._assign_fake_timestamps(available_builds)
71
72         print "Submitting %d builds" % len(available_builds)
73         submit_commits(available_builds, server_config['server']['url'], server_config['slave']['name'], server_config['slave']['password'])
74         reported_revisions |= set(map(lambda commit: commit['revision'], available_builds))
75
76     @staticmethod
77     def _assign_fake_timestamps(builds):
78         build_name_regex = re.compile(r'(?P<major>\d+)(?P<kind>\w)(?P<minor>\d+)(?P<variant>\w*)')
79         for commit in builds:
80             match = build_name_regex.search(commit['revision'])
81             major = int(match.group('major'))
82             kind = ord(match.group('kind').upper()) - ord('A')
83             minor = int(match.group('minor'))
84             variant = ord(match.group('variant').upper()) - ord('A') + 1 if match.group('variant') else 0
85             # These fake times won't conflict with real commit time since even 99Z9999z is still in Feb 1973
86             fake_time = datetime.utcfromtimestamp((major * 100 + kind) * 10000 + minor + float(variant) / 100)
87             commit['time'] = fake_time.isoformat()
88
89
90 def available_builds_from_command(repository_name, command, lines_to_ignore):
91     try:
92         output = subprocess.check_output(command, stderr=subprocess.STDOUT)
93     except subprocess.CalledProcessError as error:
94         print "Failed:", str(error)
95
96     regex = re.compile(lines_to_ignore)
97     return [{'repository': repository_name, 'revision': line} for line in output.split('\n') if not regex.match(line)]
98
99
100 def fetch_available_builds(repository_name, url, train_version_map):
101     output = urllib2.urlopen(url).read()
102     try:
103         xml = parseXmlString(output)
104     except Exception, error:
105         raise Exception(error, output)
106     available_builds = []
107     for image in xml.getElementsByTagName('baseASR'):
108         id = text_content(image.getElementsByTagName('id')[0])
109         train = text_content(image.getElementsByTagName('train')[0])
110         build = text_content(image.getElementsByTagName('build')[0])
111         if train not in train_version_map:
112             continue
113         available_builds.append({
114             'repository': repository_name,
115             'revision': train_version_map[train] + ' ' + build})
116
117     return available_builds
118
119
120 if __name__ == "__main__":
121     main(sys.argv)