Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(79)

Side by Side Diff: tools/bisect-builds.py

Issue 28313002: Add Flash binary path option to bisect script. (Closed) Base URL: https://src.chromium.org/chrome/trunk/src/
Patch Set: Created 7 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/env python 1 #!/usr/bin/env python
2 # Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 # Copyright (c) 2012 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Snapshot Build Bisect Tool 6 """Snapshot Build Bisect Tool
7 7
8 This script bisects a snapshot archive using binary search. It starts at 8 This script bisects a snapshot archive using binary search. It starts at
9 a bad revision (it will try to guess HEAD) and asks for a last known-good 9 a bad revision (it will try to guess HEAD) and asks for a last known-good
10 revision. It will then binary search across this revision range by downloading, 10 revision. It will then binary search across this revision range by downloading,
(...skipping 11 matching lines...) Expand all
22 # Changelogs URL. 22 # Changelogs URL.
23 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ 23 CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \
24 'perf/dashboard/ui/changelog.html?' \ 24 'perf/dashboard/ui/changelog.html?' \
25 'url=/trunk/src&range=%d%%3A%d' 25 'url=/trunk/src&range=%d%%3A%d'
26 26
27 # Official Changelogs URL. 27 # Official Changelogs URL.
28 OFFICIAL_CHANGELOG_URL = 'http://omahaproxy.appspot.com/'\ 28 OFFICIAL_CHANGELOG_URL = 'http://omahaproxy.appspot.com/'\
29 'changelog?old_version=%s&new_version=%s' 29 'changelog?old_version=%s&new_version=%s'
30 30
31 # DEPS file URL. 31 # DEPS file URL.
32 DEPS_FILE= 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d' 32 DEPS_FILE = 'http://src.chromium.org/viewvc/chrome/trunk/src/DEPS?revision=%d'
33 # Blink Changelogs URL. 33 # Blink Changelogs URL.
34 BLINK_CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \ 34 BLINK_CHANGELOG_URL = 'http://build.chromium.org/f/chromium/' \
35 'perf/dashboard/ui/changelog_blink.html?' \ 35 'perf/dashboard/ui/changelog_blink.html?' \
36 'url=/trunk&range=%d%%3A%d' 36 'url=/trunk&range=%d%%3A%d'
37 37
38 DONE_MESSAGE_GOOD_MIN = 'You are probably looking for a change made after %s ' \ 38 DONE_MESSAGE_GOOD_MIN = 'You are probably looking for a change made after %s ' \
39 '(known good), but no later than %s (first known bad).' 39 '(known good), but no later than %s (first known bad).'
40 DONE_MESSAGE_GOOD_MAX = 'You are probably looking for a change made after %s ' \ 40 DONE_MESSAGE_GOOD_MAX = 'You are probably looking for a change made after %s ' \
41 '(known bad), but no later than %s (first known good).' 41 '(known bad), but no later than %s (first known good).'
42 42
43 ############################################################################### 43 ###############################################################################
44 44
45 import json 45 import json
46 import math
47 import optparse 46 import optparse
48 import os 47 import os
49 import pipes
50 import re 48 import re
51 import shlex 49 import shlex
52 import shutil 50 import shutil
53 import subprocess 51 import subprocess
54 import sys 52 import sys
55 import tempfile 53 import tempfile
56 import threading 54 import threading
57 import urllib 55 import urllib
58 from distutils.version import LooseVersion 56 from distutils.version import LooseVersion
59 from xml.etree import ElementTree 57 from xml.etree import ElementTree
60 import zipfile 58 import zipfile
61 59
62 60
63 class PathContext(object): 61 class PathContext(object):
64 """A PathContext is used to carry the information used to construct URLs and 62 """A PathContext is used to carry the information used to construct URLs and
65 paths when dealing with the storage server and archives.""" 63 paths when dealing with the storage server and archives."""
66 def __init__(self, base_url, platform, good_revision, bad_revision, 64 def __init__(self, base_url, platform, good_revision, bad_revision,
67 is_official, is_aura): 65 is_official, is_aura, flash_path = None):
68 super(PathContext, self).__init__() 66 super(PathContext, self).__init__()
69 # Store off the input parameters. 67 # Store off the input parameters.
70 self.base_url = base_url 68 self.base_url = base_url
71 self.platform = platform # What's passed in to the '-a/--archive' option. 69 self.platform = platform # What's passed in to the '-a/--archive' option.
72 self.good_revision = good_revision 70 self.good_revision = good_revision
73 self.bad_revision = bad_revision 71 self.bad_revision = bad_revision
74 self.is_official = is_official 72 self.is_official = is_official
75 self.is_aura = is_aura 73 self.is_aura = is_aura
74 self.flash_path = flash_path
76 75
77 # The name of the ZIP file in a revision directory on the server. 76 # The name of the ZIP file in a revision directory on the server.
78 self.archive_name = None 77 self.archive_name = None
79 78
80 # Set some internal members: 79 # Set some internal members:
81 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'. 80 # _listing_platform_dir = Directory that holds revisions. Ends with a '/'.
82 # _archive_extract_dir = Uncompressed directory in the archive_name file. 81 # _archive_extract_dir = Uncompressed directory in the archive_name file.
83 # _binary_name = The name of the executable to run. 82 # _binary_name = The name of the executable to run.
84 if self.platform in ('linux', 'linux64', 'linux-arm'): 83 if self.platform in ('linux', 'linux64', 'linux-arm'):
85 self._binary_name = 'chrome' 84 self._binary_name = 'chrome'
(...skipping 194 matching lines...) Expand 10 before | Expand all | Expand 10 after
280 # If we are bisecting only official builds (without --aura), 279 # If we are bisecting only official builds (without --aura),
281 # we can not include builds which ends with '.1' or '.2' since 280 # we can not include builds which ends with '.1' or '.2' since
282 # they have different folder hierarchy inside. 281 # they have different folder hierarchy inside.
283 elif (not self.IsAuraBuild(str(build_number)) and 282 elif (not self.IsAuraBuild(str(build_number)) and
284 not self.IsASANBuild(str(build_number))): 283 not self.IsASANBuild(str(build_number))):
285 final_list.append(str(build_number)) 284 final_list.append(str(build_number))
286 except urllib.HTTPError, e: 285 except urllib.HTTPError, e:
287 pass 286 pass
288 return final_list 287 return final_list
289 288
290 def UnzipFilenameToDir(filename, dir): 289 def UnzipFilenameToDir(filename, directory):
291 """Unzip |filename| to directory |dir|.""" 290 """Unzip |filename| to |directory|."""
292 cwd = os.getcwd() 291 cwd = os.getcwd()
293 if not os.path.isabs(filename): 292 if not os.path.isabs(filename):
294 filename = os.path.join(cwd, filename) 293 filename = os.path.join(cwd, filename)
295 zf = zipfile.ZipFile(filename) 294 zf = zipfile.ZipFile(filename)
296 # Make base. 295 # Make base.
297 if not os.path.isdir(dir): 296 if not os.path.isdir(directory):
298 os.mkdir(dir) 297 os.mkdir(directory)
299 os.chdir(dir) 298 os.chdir(directory)
300 # Extract files. 299 # Extract files.
301 for info in zf.infolist(): 300 for info in zf.infolist():
302 name = info.filename 301 name = info.filename
303 if name.endswith('/'): # dir 302 if name.endswith('/'): # dir
304 if not os.path.isdir(name): 303 if not os.path.isdir(name):
305 os.makedirs(name) 304 os.makedirs(name)
306 else: # file 305 else: # file
307 dir = os.path.dirname(name) 306 directory = os.path.dirname(name)
308 if not os.path.isdir(dir): 307 if not os.path.isdir(directory):
309 os.makedirs(dir) 308 os.makedirs(directory)
310 out = open(name, 'wb') 309 out = open(name, 'wb')
311 out.write(zf.read(name)) 310 out.write(zf.read(name))
312 out.close() 311 out.close()
313 # Set permissions. Permission info in external_attr is shifted 16 bits. 312 # Set permissions. Permission info in external_attr is shifted 16 bits.
314 os.chmod(name, info.external_attr >> 16L) 313 os.chmod(name, info.external_attr >> 16L)
315 os.chdir(cwd) 314 os.chdir(cwd)
316 315
317 316
318 def FetchRevision(context, rev, filename, quit_event=None, progress_event=None): 317 def FetchRevision(context, rev, filename, quit_event=None, progress_event=None):
319 """Downloads and unzips revision |rev|. 318 """Downloads and unzips revision |rev|.
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after
356 355
357 # Create a temp directory and unzip the revision into it. 356 # Create a temp directory and unzip the revision into it.
358 cwd = os.getcwd() 357 cwd = os.getcwd()
359 tempdir = tempfile.mkdtemp(prefix='bisect_tmp') 358 tempdir = tempfile.mkdtemp(prefix='bisect_tmp')
360 UnzipFilenameToDir(zipfile, tempdir) 359 UnzipFilenameToDir(zipfile, tempdir)
361 os.chdir(tempdir) 360 os.chdir(tempdir)
362 361
363 # Run the build as many times as specified. 362 # Run the build as many times as specified.
364 testargs = ['--user-data-dir=%s' % profile] + args 363 testargs = ['--user-data-dir=%s' % profile] + args
365 # The sandbox must be run as root on Official Chrome, so bypass it. 364 # The sandbox must be run as root on Official Chrome, so bypass it.
366 if context.is_official and context.platform.startswith('linux'): 365 if ((context.is_official or context.flash_path) and
366 context.platform.startswith('linux')):
367 testargs.append('--no-sandbox') 367 testargs.append('--no-sandbox')
368 if context.flash_path:
369 testargs.append('--ppapi-flash-path=%s' % context.flash_path)
370 # We have to pass a large enough Flash version, which currently needs not
371 # be correct. Instead of requiring the user of the script to figure out and
372 # pass the correct version we just spoof it.
373 testargs.append('--ppapi-flash-version=99.9.999.999')
368 374
369 runcommand = [] 375 runcommand = []
370 for token in shlex.split(command): 376 for token in shlex.split(command):
371 if token == "%a": 377 if token == "%a":
372 runcommand.extend(testargs) 378 runcommand.extend(testargs)
373 else: 379 else:
374 runcommand.append( \ 380 runcommand.append( \
375 token.replace('%p', context.GetLaunchPath()) \ 381 token.replace('%p', context.GetLaunchPath()) \
376 .replace('%s', ' '.join(testargs))) 382 .replace('%s', ' '.join(testargs)))
377 383
(...skipping 67 matching lines...) Expand 10 before | Expand all | Expand 10 after
445 def Bisect(base_url, 451 def Bisect(base_url,
446 platform, 452 platform,
447 official_builds, 453 official_builds,
448 is_aura, 454 is_aura,
449 good_rev=0, 455 good_rev=0,
450 bad_rev=0, 456 bad_rev=0,
451 num_runs=1, 457 num_runs=1,
452 command="%p %a", 458 command="%p %a",
453 try_args=(), 459 try_args=(),
454 profile=None, 460 profile=None,
461 flash_path=None,
455 evaluate=AskIsGoodBuild): 462 evaluate=AskIsGoodBuild):
456 """Given known good and known bad revisions, run a binary search on all 463 """Given known good and known bad revisions, run a binary search on all
457 archived revisions to determine the last known good revision. 464 archived revisions to determine the last known good revision.
458 465
459 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.). 466 @param platform Which build to download/run ('mac', 'win', 'linux64', etc.).
460 @param official_builds Specify build type (Chromium or Official build). 467 @param official_builds Specify build type (Chromium or Official build).
461 @param good_rev Number/tag of the known good revision. 468 @param good_rev Number/tag of the known good revision.
462 @param bad_rev Number/tag of the known bad revision. 469 @param bad_rev Number/tag of the known bad revision.
463 @param num_runs Number of times to run each build for asking good/bad. 470 @param num_runs Number of times to run each build for asking good/bad.
464 @param try_args A tuple of arguments to pass to the test application. 471 @param try_args A tuple of arguments to pass to the test application.
(...skipping 13 matching lines...) Expand all
478 is run on rev 75. 485 is run on rev 75.
479 486
480 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test 487 - If rev 50 is bad, the download of rev 75 is cancelled, and the next test
481 is run on rev 25. 488 is run on rev 25.
482 """ 489 """
483 490
484 if not profile: 491 if not profile:
485 profile = 'profile' 492 profile = 'profile'
486 493
487 context = PathContext(base_url, platform, good_rev, bad_rev, 494 context = PathContext(base_url, platform, good_rev, bad_rev,
488 official_builds, is_aura) 495 official_builds, is_aura, flash_path)
489 cwd = os.getcwd() 496 cwd = os.getcwd()
490 497
491 print "Downloading list of known revisions..." 498 print "Downloading list of known revisions..."
492 _GetDownloadPath = lambda rev: os.path.join(cwd, 499 _GetDownloadPath = lambda rev: os.path.join(cwd,
493 '%s-%s' % (str(rev), context.archive_name)) 500 '%s-%s' % (str(rev), context.archive_name))
494 if official_builds: 501 if official_builds:
495 revlist = context.GetOfficialBuildsList() 502 revlist = context.GetOfficialBuildsList()
496 else: 503 else:
497 revlist = context.GetRevList() 504 revlist = context.GetRevList()
498 505
(...skipping 48 matching lines...) Expand 10 before | Expand all | Expand 10 after
547 stderr = None 554 stderr = None
548 try: 555 try:
549 (status, stdout, stderr) = RunRevision(context, 556 (status, stdout, stderr) = RunRevision(context,
550 rev, 557 rev,
551 fetch.zipfile, 558 fetch.zipfile,
552 profile, 559 profile,
553 num_runs, 560 num_runs,
554 command, 561 command,
555 try_args) 562 try_args)
556 except Exception, e: 563 except Exception, e:
557 print >>sys.stderr, e 564 print >> sys.stderr, e
558 565
559 # Call the evaluate function to see if the current revision is good or bad. 566 # Call the evaluate function to see if the current revision is good or bad.
560 # On that basis, kill one of the background downloads and complete the 567 # On that basis, kill one of the background downloads and complete the
561 # other, as described in the comments above. 568 # other, as described in the comments above.
562 try: 569 try:
563 answer = evaluate(rev, official_builds, status, stdout, stderr) 570 answer = evaluate(rev, official_builds, status, stdout, stderr)
564 if answer == 'g' and good_rev < bad_rev or \ 571 if answer == 'g' and good_rev < bad_rev or \
565 answer == 'b' and bad_rev < good_rev: 572 answer == 'b' and bad_rev < good_rev:
566 fetch.Stop() 573 fetch.Stop()
567 minrev = pivot 574 minrev = pivot
(...skipping 146 matching lines...) Expand 10 before | Expand all | Expand 10 after
714 help = 'The buildbot archive to bisect [%s].' % 721 help = 'The buildbot archive to bisect [%s].' %
715 '|'.join(choices)) 722 '|'.join(choices))
716 parser.add_option('-o', action="store_true", dest='official_builds', 723 parser.add_option('-o', action="store_true", dest='official_builds',
717 help = 'Bisect across official ' + 724 help = 'Bisect across official ' +
718 'Chrome builds (internal only) instead of ' + 725 'Chrome builds (internal only) instead of ' +
719 'Chromium archives.') 726 'Chromium archives.')
720 parser.add_option('-b', '--bad', type = 'str', 727 parser.add_option('-b', '--bad', type = 'str',
721 help = 'A bad revision to start bisection. ' + 728 help = 'A bad revision to start bisection. ' +
722 'May be earlier or later than the good revision. ' + 729 'May be earlier or later than the good revision. ' +
723 'Default is HEAD.') 730 'Default is HEAD.')
731 parser.add_option('-f', '--flash_path', type = 'str',
732 help = 'Absolute path to a recent Adobe Pepper Flash ' +
733 'binary to be used in this bisection (e.g. ' +
734 'on Windows C:\...\pepflashplayer.dll and on Linux ' +
735 '/opt/google/chrome/PepperFlash/libpepflashplayer.so).')
724 parser.add_option('-g', '--good', type = 'str', 736 parser.add_option('-g', '--good', type = 'str',
725 help = 'A good revision to start bisection. ' + 737 help = 'A good revision to start bisection. ' +
726 'May be earlier or later than the bad revision. ' + 738 'May be earlier or later than the bad revision. ' +
727 'Default is 0.') 739 'Default is 0.')
728 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str', 740 parser.add_option('-p', '--profile', '--user-data-dir', type = 'str',
729 help = 'Profile to use; this will not reset every run. ' + 741 help = 'Profile to use; this will not reset every run. ' +
730 'Defaults to a clean profile.', default = 'profile') 742 'Defaults to a clean profile.', default = 'profile')
731 parser.add_option('-t', '--times', type = 'int', 743 parser.add_option('-t', '--times', type = 'int',
732 help = 'Number of times to run each build before asking ' + 744 help = 'Number of times to run each build before asking ' +
733 'if it\'s good or bad. Temporary profiles are reused.', 745 'if it\'s good or bad. Temporary profiles are reused.',
734 default = 1) 746 default = 1)
735 parser.add_option('-c', '--command', type = 'str', 747 parser.add_option('-c', '--command', type = 'str',
736 help = 'Command to execute. %p and %a refer to Chrome ' + 748 help = 'Command to execute. %p and %a refer to Chrome ' +
737 'executable and specified extra arguments respectively. ' + 749 'executable and specified extra arguments respectively. ' +
738 'Use %s to specify all extra arguments as one string. ' + 750 'Use %s to specify all extra arguments as one string. ' +
739 'Defaults to "%p %a". Note that any extra paths ' + 751 'Defaults to "%p %a". Note that any extra paths ' +
740 'specified should be absolute.', 752 'specified should be absolute.',
741 default = '%p %a'); 753 default = '%p %a')
742 parser.add_option('-l', '--blink', action='store_true', 754 parser.add_option('-l', '--blink', action='store_true',
743 help = 'Use Blink bisect instead of Chromium. ') 755 help = 'Use Blink bisect instead of Chromium. ')
744 parser.add_option('--aura', 756 parser.add_option('--aura',
745 dest='aura', 757 dest='aura',
746 action='store_true', 758 action='store_true',
747 default=False, 759 default=False,
748 help='Allow the script to bisect aura builds') 760 help='Allow the script to bisect aura builds')
749 761
750 (opts, args) = parser.parse_args() 762 (opts, args) = parser.parse_args()
751 763
752 if opts.archive is None: 764 if opts.archive is None:
753 print 'Error: missing required parameter: --archive' 765 print 'Error: missing required parameter: --archive'
754 print 766 print
755 parser.print_help() 767 parser.print_help()
756 return 1 768 return 1
757 769
758 if opts.aura: 770 if opts.aura:
759 if opts.archive != 'win' or not opts.official_builds: 771 if opts.archive != 'win' or not opts.official_builds:
760 print 'Error: Aura is supported only on Windows platform '\ 772 print 'Error: Aura is supported only on Windows platform '\
761 'and official builds.' 773 'and official builds.'
762 return 1 774 return 1
763 775
764 if opts.blink: 776 if opts.blink:
765 base_url = WEBKIT_BASE_URL 777 base_url = WEBKIT_BASE_URL
766 else: 778 else:
767 base_url = CHROMIUM_BASE_URL 779 base_url = CHROMIUM_BASE_URL
768 780
769 # Create the context. Initialize 0 for the revisions as they are set below. 781 # Create the context. Initialize 0 for the revisions as they are set below.
770 context = PathContext(base_url, opts.archive, 0, 0, 782 context = PathContext(base_url, opts.archive, 0, 0,
771 opts.official_builds, opts.aura) 783 opts.official_builds, opts.aura, None)
772 # Pick a starting point, try to get HEAD for this. 784 # Pick a starting point, try to get HEAD for this.
773 if opts.bad: 785 if opts.bad:
774 bad_rev = opts.bad 786 bad_rev = opts.bad
775 else: 787 else:
776 bad_rev = '999.0.0.0' 788 bad_rev = '999.0.0.0'
777 if not opts.official_builds: 789 if not opts.official_builds:
778 bad_rev = GetChromiumRevision(context.GetLastChangeURL()) 790 bad_rev = GetChromiumRevision(context.GetLastChangeURL())
779 791
780 # Find out when we were good. 792 # Find out when we were good.
781 if opts.good: 793 if opts.good:
782 good_rev = opts.good 794 good_rev = opts.good
783 else: 795 else:
784 good_rev = '0.0.0.0' if opts.official_builds else 0 796 good_rev = '0.0.0.0' if opts.official_builds else 0
785 797
798 if opts.flash_path:
799 flash_path = opts.flash_path
800 msg = 'Could not find Flash binary at %s' % flash_path
801 assert os.path.exists(flash_path), msg
802
786 if opts.official_builds: 803 if opts.official_builds:
787 good_rev = LooseVersion(good_rev) 804 good_rev = LooseVersion(good_rev)
788 bad_rev = LooseVersion(bad_rev) 805 bad_rev = LooseVersion(bad_rev)
789 else: 806 else:
790 good_rev = int(good_rev) 807 good_rev = int(good_rev)
791 bad_rev = int(bad_rev) 808 bad_rev = int(bad_rev)
792 809
793 if opts.times < 1: 810 if opts.times < 1:
794 print('Number of times to run (%d) must be greater than or equal to 1.' % 811 print('Number of times to run (%d) must be greater than or equal to 1.' %
795 opts.times) 812 opts.times)
796 parser.print_help() 813 parser.print_help()
797 return 1 814 return 1
798 815
799 (min_chromium_rev, max_chromium_rev) = Bisect( 816 (min_chromium_rev, max_chromium_rev) = Bisect(
800 base_url, opts.archive, opts.official_builds, opts.aura, good_rev, 817 base_url, opts.archive, opts.official_builds, opts.aura, good_rev,
801 bad_rev, opts.times, opts.command, args, opts.profile) 818 bad_rev, opts.times, opts.command, args, opts.profile, opts.flash_path)
802 819
803 # Get corresponding blink revisions. 820 # Get corresponding blink revisions.
804 try: 821 try:
805 min_blink_rev = GetBlinkRevisionForChromiumRevision(context, 822 min_blink_rev = GetBlinkRevisionForChromiumRevision(context,
806 min_chromium_rev) 823 min_chromium_rev)
807 max_blink_rev = GetBlinkRevisionForChromiumRevision(context, 824 max_blink_rev = GetBlinkRevisionForChromiumRevision(context,
808 max_chromium_rev) 825 max_chromium_rev)
809 except Exception, e: 826 except Exception, e:
810 # Silently ignore the failure. 827 # Silently ignore the failure.
811 min_blink_rev, max_blink_rev = 0, 0 828 min_blink_rev, max_blink_rev = 0, 0
(...skipping 21 matching lines...) Expand all
833 "you might also want to do a Blink bisect.") 850 "you might also want to do a Blink bisect.")
834 851
835 print 'CHANGELOG URL:' 852 print 'CHANGELOG URL:'
836 if opts.official_builds: 853 if opts.official_builds:
837 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev) 854 print OFFICIAL_CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
838 else: 855 else:
839 print ' ' + CHANGELOG_URL % (min_chromium_rev, max_chromium_rev) 856 print ' ' + CHANGELOG_URL % (min_chromium_rev, max_chromium_rev)
840 857
841 if __name__ == '__main__': 858 if __name__ == '__main__':
842 sys.exit(main()) 859 sys.exit(main())
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698