summaryrefslogtreecommitdiffstats
path: root/chrome/test/chromedriver/run_buildbot_steps.py
blob: 4c1cee2d69036873f92a3fa409bc4b941446983f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
#!/usr/bin/env python
# Copyright (c) 2013 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.

"""Runs all the buildbot steps for ChromeDriver except for update/compile."""

import bisect
import csv
import datetime
import glob
import json
import optparse
import os
import platform as platform_module
import re
import shutil
import StringIO
import sys
import tempfile
import time
import urllib2

_THIS_DIR = os.path.abspath(os.path.dirname(__file__))
GS_CHROMEDRIVER_BUCKET = 'gs://chromedriver'
GS_CHROMEDRIVER_DATA_BUCKET = 'gs://chromedriver-data'
GS_CHROMEDRIVER_RELEASE_URL = 'http://chromedriver.storage.googleapis.com'
GS_CONTINUOUS_URL = GS_CHROMEDRIVER_DATA_BUCKET + '/continuous'
GS_PREBUILTS_URL = GS_CHROMEDRIVER_DATA_BUCKET + '/prebuilts'
GS_SERVER_LOGS_URL = GS_CHROMEDRIVER_DATA_BUCKET + '/server_logs'
SERVER_LOGS_LINK = (
    'http://chromedriver-data.storage.googleapis.com/server_logs')
TEST_LOG_FORMAT = '%s_log.json'
GS_GIT_LOG_URL = (
    'https://chromium.googlesource.com/chromium/src/+/%s?format=json')
GS_SEARCH_PATTERN = (
    r'Cr-Commit-Position: refs/heads/master@{#(\d+)}')
CR_REV_URL = 'https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/%s'

SCRIPT_DIR = os.path.join(_THIS_DIR, os.pardir, os.pardir, os.pardir, os.pardir,
                          os.pardir, os.pardir, os.pardir, 'scripts')
SITE_CONFIG_DIR = os.path.join(_THIS_DIR, os.pardir, os.pardir, os.pardir,
                               os.pardir, os.pardir, os.pardir, os.pardir,
                               'site_config')
sys.path.append(SCRIPT_DIR)
sys.path.append(SITE_CONFIG_DIR)

import archive
import chrome_paths
from slave import gsutil_download
from slave import slave_utils
import util


def _ArchivePrebuilts(commit_position):
  """Uploads the prebuilts to google storage."""
  util.MarkBuildStepStart('archive prebuilts')
  zip_path = util.Zip(os.path.join(chrome_paths.GetBuildDir(['chromedriver']),
                                   'chromedriver'))
  if slave_utils.GSUtilCopy(
      zip_path,
      '%s/%s' % (GS_PREBUILTS_URL, 'r%s.zip' % commit_position)):
    util.MarkBuildStepError()


def _ArchiveServerLogs():
  """Uploads chromedriver server logs to google storage."""
  util.MarkBuildStepStart('archive chromedriver server logs')
  for server_log in glob.glob(os.path.join(tempfile.gettempdir(),
                                           'chromedriver_*')):
    base_name = os.path.basename(server_log)
    util.AddLink(base_name, '%s/%s' % (SERVER_LOGS_LINK, base_name))
    slave_utils.GSUtilCopy(
        server_log,
        '%s/%s' % (GS_SERVER_LOGS_URL, base_name),
        mimetype='text/plain')


def _DownloadPrebuilts():
  """Downloads the most recent prebuilts from google storage."""
  util.MarkBuildStepStart('Download latest chromedriver')

  zip_path = os.path.join(util.MakeTempDir(), 'build.zip')
  if gsutil_download.DownloadLatestFile(GS_PREBUILTS_URL,
                                        GS_PREBUILTS_URL + '/r',
                                        zip_path):
    util.MarkBuildStepError()

  util.Unzip(zip_path, chrome_paths.GetBuildDir(['host_forwarder']))


def _GetTestResultsLog(platform):
  """Gets the test results log for the given platform.

  Args:
    platform: The platform that the test results log is for.

  Returns:
    A dictionary where the keys are commit positions and the values are booleans
    indicating whether the tests passed.
  """
  temp_log = tempfile.mkstemp()[1]
  log_name = TEST_LOG_FORMAT % platform
  result = slave_utils.GSUtilDownloadFile(
      '%s/%s' % (GS_CHROMEDRIVER_DATA_BUCKET, log_name), temp_log)
  if result:
    return {}
  with open(temp_log, 'rb') as log_file:
    json_dict = json.load(log_file)
  # Workaround for json encoding dictionary keys as strings.
  return dict([(int(v[0]), v[1]) for v in json_dict.items()])


def _PutTestResultsLog(platform, test_results_log):
  """Pushes the given test results log to google storage."""
  temp_dir = util.MakeTempDir()
  log_name = TEST_LOG_FORMAT % platform
  log_path = os.path.join(temp_dir, log_name)
  with open(log_path, 'wb') as log_file:
    json.dump(test_results_log, log_file)
  if slave_utils.GSUtilCopyFile(log_path, GS_CHROMEDRIVER_DATA_BUCKET):
    raise Exception('Failed to upload test results log to google storage')


def _UpdateTestResultsLog(platform, commit_position, passed):
  """Updates the test results log for the given platform.

  Args:
    platform: The platform name.
    commit_position: The commit position number.
    passed: Boolean indicating whether the tests passed at this commit position.
  """

  assert commit_position.isdigit(), 'The commit position must be a number'
  commit_position = int(commit_position)
  log = _GetTestResultsLog(platform)
  if len(log) > 500:
    del log[min(log.keys())]
  assert commit_position not in log, \
      'Results already exist for commit position %s' % commit_position
  log[commit_position] = bool(passed)
  _PutTestResultsLog(platform, log)


def _GetVersion():
  """Get the current chromedriver version."""
  with open(os.path.join(_THIS_DIR, 'VERSION'), 'r') as f:
    return f.read().strip()


def _GetSupportedChromeVersions():
  """Get the minimum and maximum supported Chrome versions.

  Returns:
    A tuple of the form (min_version, max_version).
  """
  # Minimum supported Chrome version is embedded as:
  # const int kMinimumSupportedChromeVersion[] = {27, 0, 1453, 0};
  with open(os.path.join(_THIS_DIR, 'chrome', 'version.cc'), 'r') as f:
    lines = f.readlines()
    chrome_min_version_line = [
        x for x in lines if 'kMinimumSupportedChromeVersion' in x]
  chrome_min_version = chrome_min_version_line[0].split('{')[1].split(',')[0]
  with open(os.path.join(chrome_paths.GetSrc(), 'chrome', 'VERSION'), 'r') as f:
    chrome_max_version = f.readlines()[0].split('=')[1].strip()
  return (chrome_min_version, chrome_max_version)


def _CommitPositionState(test_results_log, commit_position):
  """Check the state of tests at a given commit position.

  Considers tests as having passed at a commit position if they passed at
  revisions both before and after.

  Args:
    test_results_log: A test results log dictionary from _GetTestResultsLog().
    commit_position: The commit position to check at.

  Returns:
    'passed', 'failed', or 'unknown'
  """
  assert isinstance(commit_position, int), 'The commit position must be an int'
  keys = sorted(test_results_log.keys())
  # Return passed if the exact commit position passed on Android.
  if commit_position in test_results_log:
    return 'passed' if test_results_log[commit_position] else 'failed'
  # Tests were not run on this exact commit position on Android.
  index = bisect.bisect_right(keys, commit_position)
  # Tests have not yet run on Android at or above this commit position.
  if index == len(test_results_log):
    return 'unknown'
  # No log exists for any prior commit position, assume it failed.
  if index == 0:
    return 'failed'
  # Return passed if the commit position on both sides passed.
  if test_results_log[keys[index]] and test_results_log[keys[index - 1]]:
    return 'passed'
  return 'failed'


def _ArchiveGoodBuild(platform, commit_position):
  """Archive chromedriver binary if the build is green."""
  assert platform != 'android'
  util.MarkBuildStepStart('archive build')

  server_name = 'chromedriver'
  if util.IsWindows():
    server_name += '.exe'
  zip_path = util.Zip(os.path.join(chrome_paths.GetBuildDir([server_name]),
                                   server_name))

  build_name = 'chromedriver_%s_%s.%s.zip' % (
      platform, _GetVersion(), commit_position)
  build_url = '%s/%s' % (GS_CONTINUOUS_URL, build_name)
  if slave_utils.GSUtilCopy(zip_path, build_url):
    util.MarkBuildStepError()

  (latest_fd, latest_file) = tempfile.mkstemp()
  os.write(latest_fd, build_name)
  os.close(latest_fd)
  latest_url = '%s/latest_%s' % (GS_CONTINUOUS_URL, platform)
  if slave_utils.GSUtilCopy(latest_file, latest_url, mimetype='text/plain'):
    util.MarkBuildStepError()
  os.remove(latest_file)


def _WasReleased(version, platform):
  """Check if the specified version is released for the given platform."""
  result, _ = slave_utils.GSUtilListBucket(
      '%s/%s/chromedriver_%s.zip' % (GS_CHROMEDRIVER_BUCKET, version, platform),
      [])
  return result == 0


def _MaybeRelease(platform):
  """Releases a release candidate if conditions are right."""
  assert platform != 'android'

  version = _GetVersion()

  # Check if the current version has already been released.
  if _WasReleased(version, platform):
    return

  # Fetch Android test results.
  android_test_results = _GetTestResultsLog('android')

  # Fetch release candidates.
  result, output = slave_utils.GSUtilListBucket(
      '%s/chromedriver_%s_%s*' % (
          GS_CONTINUOUS_URL, platform, version),
      [])
  assert result == 0 and output, 'No release candidates found'
  candidate_pattern = re.compile(
      r'.*/chromedriver_%s_%s\.(\d+)\.zip$' % (platform, version))
  candidates = []
  for line in output.strip().split('\n'):
    result = candidate_pattern.match(line)
    if not result:
      print 'Ignored line "%s"' % line
      continue
    candidates.append(int(result.group(1)))

  # Release the latest candidate build that passed Android, if any.
  # In this way, if a hot fix is needed, we can delete the release from
  # the chromedriver bucket instead of bumping up the release version number.
  candidates.sort(reverse=True)
  for commit_position in candidates:
    android_result = _CommitPositionState(android_test_results, commit_position)
    if android_result == 'failed':
      print 'Android tests did not pass at commit position', commit_position
    elif android_result == 'passed':
      print 'Android tests passed at commit position', commit_position
      candidate = 'chromedriver_%s_%s.%s.zip' % (
          platform, version, commit_position)
      _Release('%s/%s' % (GS_CONTINUOUS_URL, candidate), version, platform)
      break
    else:
      print 'Android tests have not run at a commit position as recent as', \
          commit_position


def _Release(build, version, platform):
  """Releases the given candidate build."""
  release_name = 'chromedriver_%s.zip' % platform
  util.MarkBuildStepStart('releasing %s' % release_name)
  temp_dir = util.MakeTempDir()
  slave_utils.GSUtilCopy(build, temp_dir)
  zip_path = os.path.join(temp_dir, os.path.basename(build))

  if util.IsLinux():
    util.Unzip(zip_path, temp_dir)
    server_path = os.path.join(temp_dir, 'chromedriver')
    util.RunCommand(['strip', server_path])
    zip_path = util.Zip(server_path)

  slave_utils.GSUtilCopy(
      zip_path, '%s/%s/%s' % (GS_CHROMEDRIVER_BUCKET, version, release_name))

  _MaybeUploadReleaseNotes(version)
  _MaybeUpdateLatestRelease(version)


def _GetWebPageContent(url):
  """Return the content of the web page specified by the given url."""
  return urllib2.urlopen(url).read()


def _MaybeUploadReleaseNotes(version):
  """Upload release notes if conditions are right."""
  # Check if the current version has already been released.
  notes_name = 'notes.txt'
  notes_url = '%s/%s/%s' % (GS_CHROMEDRIVER_BUCKET, version, notes_name)
  prev_version = '.'.join([version.split('.')[0],
                           str(int(version.split('.')[1]) - 1)])
  prev_notes_url = '%s/%s/%s' % (
      GS_CHROMEDRIVER_BUCKET, prev_version, notes_name)

  result, _ = slave_utils.GSUtilListBucket(notes_url, [])
  if result == 0:
    return

  fixed_issues = []
  query = ('https://code.google.com/p/chromedriver/issues/csv?'
           'can=1&q=label%%3AChromeDriver-%s&colspec=ID%%20Summary' % version)
  issues = StringIO.StringIO(_GetWebPageContent(query).split('\n', 1)[1])
  for issue in csv.reader(issues):
    if not issue:
      continue
    issue_id = issue[0]
    desc = issue[1]
    labels = issue[2].split(', ')
    labels.remove('ChromeDriver-%s' % version)
    if 'Hotlist-GoodFirstBug' in labels:
      labels.remove('Hotlist-GoodFirstBug')
    fixed_issues += ['Resolved issue %s: %s [%s]' % (issue_id, desc, labels)]

  old_notes = ''
  temp_notes_fname = tempfile.mkstemp()[1]
  if not slave_utils.GSUtilDownloadFile(prev_notes_url, temp_notes_fname):
    with open(temp_notes_fname, 'rb') as f:
      old_notes = f.read()

  new_notes = '----------ChromeDriver v%s (%s)----------\n%s\n%s\n\n%s' % (
      version, datetime.date.today().isoformat(),
      'Supports Chrome v%s-%s' % _GetSupportedChromeVersions(),
      '\n'.join(fixed_issues),
      old_notes)
  with open(temp_notes_fname, 'w') as f:
    f.write(new_notes)

  if slave_utils.GSUtilCopy(temp_notes_fname, notes_url, mimetype='text/plain'):
    util.MarkBuildStepError()


def _MaybeUpdateLatestRelease(version):
  """Update the file LATEST_RELEASE with the latest release version number."""
  latest_release_fname = 'LATEST_RELEASE'
  latest_release_url = '%s/%s' % (GS_CHROMEDRIVER_BUCKET, latest_release_fname)

  # Check if LATEST_RELEASE is up-to-date.
  latest_released_version = _GetWebPageContent(
      '%s/%s' % (GS_CHROMEDRIVER_RELEASE_URL, latest_release_fname))
  if version == latest_released_version:
    return

  # Check if chromedriver was released on all supported platforms.
  supported_platforms = ['linux32', 'linux64', 'mac32', 'win32']
  for platform in supported_platforms:
    if not _WasReleased(version, platform):
      return

  util.MarkBuildStepStart('updating LATEST_RELEASE to %s' % version)

  temp_latest_release_fname = tempfile.mkstemp()[1]
  with open(temp_latest_release_fname, 'w') as f:
    f.write(version)
  if slave_utils.GSUtilCopy(temp_latest_release_fname, latest_release_url,
                            mimetype='text/plain'):
    util.MarkBuildStepError()


def _CleanTmpDir():
  tmp_dir = tempfile.gettempdir()
  print 'cleaning temp directory:', tmp_dir
  for file_name in os.listdir(tmp_dir):
    file_path = os.path.join(tmp_dir, file_name)
    if os.path.isdir(file_path):
      print 'deleting sub-directory', file_path
      shutil.rmtree(file_path, True)
    if file_name.startswith('chromedriver_'):
      print 'deleting file', file_path
      os.remove(file_path)


def _GetCommitPositionFromGitHash(snapshot_hashcode):
  json_url = GS_GIT_LOG_URL % snapshot_hashcode
  try:
    response = urllib2.urlopen(json_url)
  except urllib2.HTTPError as error:
    util.PrintAndFlush('HTTP Error %d' % error.getcode())
    return None
  except urllib2.URLError as error:
    util.PrintAndFlush('URL Error %s' % error.message)
    return None
  data = json.loads(response.read()[4:])
  if 'message' in data:
    message = data['message'].split('\n')
    message = [line for line in message if line.strip()]
    search_pattern = re.compile(GS_SEARCH_PATTERN)
    result = search_pattern.search(message[len(message)-1])
    if result:
      return result.group(1)
  util.PrintAndFlush('Failed to get commit position number for %s' %
                     snapshot_hashcode)
  return None


def _GetGitHashFromCommitPosition(commit_position):
  json_url = CR_REV_URL % commit_position
  try:
    response = urllib2.urlopen(json_url)
  except urllib2.HTTPError as error:
    util.PrintAndFlush('HTTP Error %d' % error.getcode())
    return None
  except urllib2.URLError as error:
    util.PrintAndFlush('URL Error %s' % error.message)
    return None
  data = json.loads(response.read())
  if 'git_sha' in data:
    return data['git_sha']
  util.PrintAndFlush('Failed to get git hash for %s' % commit_position)
  return None


def _WaitForLatestSnapshot(commit_position):
  util.MarkBuildStepStart('wait_for_snapshot')
  while True:
    snapshot_position = archive.GetLatestSnapshotVersion()
    if commit_position is not None and snapshot_position is not None:
      if int(snapshot_position) >= int(commit_position):
        break
      util.PrintAndFlush('Waiting for snapshot >= %s, found %s' %
                         (commit_position, snapshot_position))
    time.sleep(60)
  util.PrintAndFlush('Got snapshot commit position %s' % snapshot_position)


def _AddToolsToPath(platform_name):
  """Add some tools like Ant and Java to PATH for testing steps to use."""
  paths = []
  error_message = ''
  if platform_name == 'win32':
    paths = [
        # Path to Ant and Java, required for the java acceptance tests.
        'C:\\Program Files (x86)\\Java\\ant\\bin',
        'C:\\Program Files (x86)\\Java\\jre\\bin',
    ]
    error_message = ('Java test steps will fail as expected and '
                     'they can be ignored.\n'
                     'Ant, Java or others might not be installed on bot.\n'
                     'Please refer to page "WATERFALL" on site '
                     'go/chromedriver.')
  if paths:
    util.MarkBuildStepStart('Add tools to PATH')
    path_missing = False
    for path in paths:
      if not os.path.isdir(path) or not os.listdir(path):
        print 'Directory "%s" is not found or empty.' % path
        path_missing = True
    if path_missing:
      print error_message
      util.MarkBuildStepError()
      return
    os.environ['PATH'] += os.pathsep + os.pathsep.join(paths)


def main():
  parser = optparse.OptionParser()
  parser.add_option(
      '', '--android-packages',
      help=('Comma separated list of application package names, '
            'if running tests on Android.'))
  parser.add_option(
      '-r', '--revision', help='Chromium git revision hash')
  parser.add_option(
      '', '--update-log', action='store_true',
      help='Update the test results log (only applicable to Android)')
  options, _ = parser.parse_args()

  bitness = '32'
  if util.IsLinux() and platform_module.architecture()[0] == '64bit':
    bitness = '64'
  platform = '%s%s' % (util.GetPlatformName(), bitness)
  if options.android_packages:
    platform = 'android'

  _CleanTmpDir()

  if not options.revision:
    commit_position = None
  else:
    commit_position = _GetCommitPositionFromGitHash(options.revision)

  if platform == 'android':
    if not options.revision and options.update_log:
      parser.error('Must supply a --revision with --update-log')
    _DownloadPrebuilts()
  else:
    if not options.revision:
      parser.error('Must supply a --revision')
    if platform == 'linux64':
      _ArchivePrebuilts(commit_position)
    _WaitForLatestSnapshot(commit_position)

  _AddToolsToPath(platform)

  cmd = [
      sys.executable,
      os.path.join(_THIS_DIR, 'test', 'run_all_tests.py'),
  ]
  if platform == 'android':
    cmd.append('--android-packages=' + options.android_packages)

  passed = (util.RunCommand(cmd) == 0)

  _ArchiveServerLogs()

  if platform == 'android':
    if options.update_log:
      util.MarkBuildStepStart('update test result log')
      _UpdateTestResultsLog(platform, commit_position, passed)
  elif passed:
    _ArchiveGoodBuild(platform, commit_position)
    _MaybeRelease(platform)

  if not passed:
    # Make sure the build is red if there is some uncaught exception during
    # running run_all_tests.py.
    util.MarkBuildStepStart('run_all_tests.py')
    util.MarkBuildStepError()

  # Add a "cleanup" step so that errors from runtest.py or bb_device_steps.py
  # (which invoke this script) are kept in their own build step.
  util.MarkBuildStepStart('cleanup')


if __name__ == '__main__':
  main()