[ews-build] Improve summary for PrintConfiguration step
[WebKit-https.git] / Tools / BuildSlaveSupport / ews-build / steps.py
1 # Copyright (C) 2018-2019 Apple Inc. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or without
4 # modification, are permitted provided that the following conditions
5 # are met:
6 # 1.  Redistributions of source code must retain the above copyright
7 #     notice, this list of conditions and the following disclaimer.
8 # 2.  Redistributions in binary form must reproduce the above copyright
9 #     notice, this list of conditions and the following disclaimer in the
10 #     documentation and/or other materials provided with the distribution.
11 #
12 # THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
13 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
14 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
15 # DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
16 # ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
17 # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
18 # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
19 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
20 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
21 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22
23 from buildbot.plugins import steps, util
24 from buildbot.process import buildstep, logobserver, properties
25 from buildbot.process.results import Results, SUCCESS, FAILURE, WARNINGS, SKIPPED, EXCEPTION, RETRY
26 from buildbot.steps import master, shell, transfer
27 from buildbot.steps.source import git
28 from buildbot.steps.worker import CompositeStepMixin
29 from twisted.internet import defer
30
31 import re
32 import requests
33
34 BUG_SERVER_URL = 'https://bugs.webkit.org/'
35 EWS_URL = 'https://ews-build.webkit.org/'
36 WithProperties = properties.WithProperties
37 Interpolate = properties.Interpolate
38
39
40 class ConfigureBuild(buildstep.BuildStep):
41     name = "configure-build"
42     description = ["configuring build"]
43     descriptionDone = ["Configured build"]
44
45     def __init__(self, platform, configuration, architectures, buildOnly, additionalArguments):
46         super(ConfigureBuild, self).__init__()
47         self.platform = platform
48         if platform != 'jsc-only':
49             self.platform = platform.split('-', 1)[0]
50         self.fullPlatform = platform
51         self.configuration = configuration
52         self.architecture = " ".join(architectures) if architectures else None
53         self.buildOnly = buildOnly
54         self.additionalArguments = additionalArguments
55
56     def start(self):
57         if self.platform and self.platform != '*':
58             self.setProperty('platform', self.platform, 'config.json')
59         if self.fullPlatform and self.fullPlatform != '*':
60             self.setProperty('fullPlatform', self.fullPlatform, 'ConfigureBuild')
61         if self.configuration:
62             self.setProperty('configuration', self.configuration, 'config.json')
63         if self.architecture:
64             self.setProperty('architecture', self.architecture, 'config.json')
65         if self.buildOnly:
66             self.setProperty("buildOnly", self.buildOnly, 'config.json')
67         if self.additionalArguments:
68             self.setProperty("additionalArguments", self.additionalArguments, 'config.json')
69
70         self.add_patch_id_url()
71         self.finished(SUCCESS)
72         return defer.succeed(None)
73
74     def add_patch_id_url(self):
75         patch_id = self.getProperty('patch_id', '')
76         if patch_id:
77             self.addURL('Patch {}'.format(patch_id), self.getPatchURL(patch_id))
78
79     def getPatchURL(self, patch_id):
80         if not patch_id:
81             return None
82         return '{}attachment.cgi?id={}'.format(BUG_SERVER_URL, patch_id)
83
84
85 class CheckOutSource(git.Git):
86     name = 'clean-and-update-working-directory'
87     CHECKOUT_DELAY_AND_MAX_RETRIES_PAIR = (0, 2)
88
89     def __init__(self, **kwargs):
90         self.repourl = 'https://git.webkit.org/git/WebKit.git'
91         super(CheckOutSource, self).__init__(repourl=self.repourl,
92                                                 retry=self.CHECKOUT_DELAY_AND_MAX_RETRIES_PAIR,
93                                                 timeout=2 * 60 * 60,
94                                                 alwaysUseLatest=True,
95                                                 progress=True,
96                                                 **kwargs)
97
98     def getResultSummary(self):
99         if self.results != SUCCESS:
100             return {u'step': u'Failed to updated working directory'}
101         else:
102             return {u'step': u'Cleaned and updated working directory'}
103
104
105 class CleanWorkingDirectory(shell.ShellCommand):
106     name = 'clean-working-directory'
107     description = ['clean-working-directory running']
108     descriptionDone = ['Cleaned working directory']
109     flunkOnFailure = True
110     haltOnFailure = True
111     command = ['Tools/Scripts/clean-webkit']
112
113
114 class ApplyPatch(shell.ShellCommand, CompositeStepMixin):
115     name = 'apply-patch'
116     description = ['applying-patch']
117     descriptionDone = ['Applied patch']
118     flunkOnFailure = True
119     haltOnFailure = True
120     command = ['Tools/Scripts/svn-apply', '--force', '.buildbot-diff']
121
122     def _get_patch(self):
123         sourcestamp = self.build.getSourceStamp(self.getProperty('codebase', ''))
124         if not sourcestamp or not sourcestamp.patch:
125             return None
126         return sourcestamp.patch[1]
127
128     def start(self):
129         patch = self._get_patch()
130         if not patch:
131             self.finished(FAILURE)
132             return None
133
134         d = self.downloadFileContentToWorker('.buildbot-diff', patch)
135         d.addCallback(lambda _: self.downloadFileContentToWorker('.buildbot-patched', 'patched\n'))
136         d.addCallback(lambda res: shell.ShellCommand.start(self))
137
138
139 class CheckPatchRelevance(buildstep.BuildStep):
140     name = 'check-patch-relevance'
141     description = ['check-patch-relevance running']
142     descriptionDone = ['Checked patch relevance']
143     flunkOnFailure = True
144     haltOnFailure = True
145
146     bindings_paths = [
147         "Source/WebCore",
148         "Tools",
149     ]
150
151     jsc_paths = [
152         "JSTests/",
153         "Source/JavaScriptCore/",
154         "Source/WTF/",
155         "Source/bmalloc/",
156         "Makefile",
157         "Makefile.shared",
158         "Source/Makefile",
159         "Source/Makefile.shared",
160         "Tools/Scripts/build-webkit",
161         "Tools/Scripts/build-jsc",
162         "Tools/Scripts/jsc-stress-test-helpers/",
163         "Tools/Scripts/run-jsc",
164         "Tools/Scripts/run-jsc-benchmarks",
165         "Tools/Scripts/run-jsc-stress-tests",
166         "Tools/Scripts/run-javascriptcore-tests",
167         "Tools/Scripts/run-layout-jsc",
168         "Tools/Scripts/update-javascriptcore-test-results",
169         "Tools/Scripts/webkitdirs.pm",
170     ]
171
172     webkitpy_paths = [
173         "Tools/Scripts/webkitpy/",
174         "Tools/QueueStatusServer/",
175     ]
176
177     group_to_paths_mapping = {
178         'bindings': bindings_paths,
179         'jsc': jsc_paths,
180         'webkitpy': webkitpy_paths,
181     }
182
183     def _patch_is_relevant(self, patch, builderName):
184         group = [group for group in self.group_to_paths_mapping.keys() if group in builderName.lower()]
185         if not group:
186             # This builder doesn't have paths defined, all patches are relevant.
187             return True
188
189         relevant_paths = self.group_to_paths_mapping[group[0]]
190
191         for change in patch.splitlines():
192             for path in relevant_paths:
193                 if re.search(path, change, re.IGNORECASE):
194                     return True
195         return False
196
197     def _get_patch(self):
198         sourcestamp = self.build.getSourceStamp(self.getProperty('codebase', ''))
199         if not sourcestamp or not sourcestamp.patch:
200             return None
201         return sourcestamp.patch[1]
202
203     @defer.inlineCallbacks
204     def _addToLog(self, logName, message):
205         try:
206             log = self.getLog(logName)
207         except KeyError:
208             log = yield self.addLog(logName)
209         log.addStdout(message)
210
211     def start(self):
212         patch = self._get_patch()
213         if not patch:
214             # This build doesn't have a patch, it might be a force build.
215             self.finished(SUCCESS)
216             return None
217
218         if self._patch_is_relevant(patch, self.getProperty('buildername', '')):
219             self._addToLog('stdio', 'This patch contains relevant changes.')
220             self.finished(SUCCESS)
221             return None
222
223         self._addToLog('stdio', 'This patch does not have relevant changes.')
224         self.finished(FAILURE)
225         self.build.results = SKIPPED
226         self.build.buildFinished(['Patch {} doesn\'t have relevant changes'.format(self.getProperty('patch_id', ''))], SKIPPED)
227         return None
228
229
230 class ValidatePatch(buildstep.BuildStep):
231     name = 'validate-patch'
232     description = ['validate-patch running']
233     descriptionDone = ['Validated patch']
234     flunkOnFailure = True
235     haltOnFailure = True
236     bug_open_statuses = ["UNCONFIRMED", "NEW", "ASSIGNED", "REOPENED"]
237     bug_closed_statuses = ["RESOLVED", "VERIFIED", "CLOSED"]
238
239     @defer.inlineCallbacks
240     def _addToLog(self, logName, message):
241         try:
242             log = self.getLog(logName)
243         except KeyError:
244             log = yield self.addLog(logName)
245         log.addStdout(message)
246
247     def fetch_data_from_url(self, url):
248         response = None
249         try:
250             response = requests.get(url)
251         except Exception as e:
252             if response:
253                 self._addToLog('stdio', 'Failed to access {url} with status code {status_code}.\n'.format(url=url, status_code=response.status_code))
254             else:
255                 self._addToLog('stdio', 'Failed to access {url} with exception: {exception}\n'.format(url=url, exception=e))
256             return None
257         if response.status_code != 200:
258             self._addToLog('stdio', 'Accessed {url} with unexpected status code {status_code}.\n'.format(url=url, status_code=response.status_code))
259             return None
260         return response
261
262     def get_patch_json(self, patch_id):
263         patch_url = '{}rest/bug/attachment/{}'.format(BUG_SERVER_URL, patch_id)
264         patch = self.fetch_data_from_url(patch_url)
265         if not patch:
266             return None
267         patch_json = patch.json().get('attachments')
268         if not patch_json or len(patch_json) == 0:
269             return None
270         return patch_json.get(str(patch_id))
271
272     def get_bug_json(self, bug_id):
273         bug_url = '{}rest/bug/{}'.format(BUG_SERVER_URL, bug_id)
274         bug = self.fetch_data_from_url(bug_url)
275         if not bug:
276             return None
277         bugs_json = bug.json().get('bugs')
278         if not bugs_json or len(bugs_json) == 0:
279             return None
280         return bugs_json[0]
281
282     def get_bug_id_from_patch(self, patch_id):
283         patch_json = self.get_patch_json(patch_id)
284         if not patch_json:
285             self._addToLog('stdio', 'Unable to fetch patch {}.\n'.format(patch_id))
286             return -1
287         return patch_json.get('bug_id')
288
289     def _is_patch_obsolete(self, patch_id):
290         patch_json = self.get_patch_json(patch_id)
291         if not patch_json:
292             self._addToLog('stdio', 'Unable to fetch patch {}.\n'.format(patch_id))
293             return -1
294
295         if str(patch_json.get('id')) != self.getProperty('patch_id', ''):
296             self._addToLog('stdio', 'Fetched patch id {} does not match with requested patch id {}. Unable to validate.\n'.format(patch_json.get('id'), self.getProperty('patch_id', '')))
297             return -1
298
299         patch_author = patch_json.get('creator')
300         self.addURL('Patch by: {}'.format(patch_author), 'mailto:{}'.format(patch_author))
301         return patch_json.get('is_obsolete')
302
303     def _is_patch_review_denied(self, patch_id):
304         patch_json = self.get_patch_json(patch_id)
305         if not patch_json:
306             self._addToLog('stdio', 'Unable to fetch patch {}.\n'.format(patch_id))
307             return -1
308
309         for flag in patch_json.get('flags', []):
310             if flag.get('name') == 'review' and flag.get('status') == '-':
311                 return 1
312         return 0
313
314     def _is_bug_closed(self, bug_id):
315         if not bug_id:
316             self._addToLog('stdio', 'Skipping bug status validation since bug id is None.\n')
317             return -1
318
319         bug_json = self.get_bug_json(bug_id)
320         if not bug_json or not bug_json.get('status'):
321             self._addToLog('stdio', 'Unable to fetch bug {}.\n'.format(bug_id))
322             return -1
323
324         bug_title = bug_json.get('summary')
325         self.addURL('Bug {} {}'.format(bug_id, bug_title), '{}show_bug.cgi?id={}'.format(BUG_SERVER_URL, bug_id))
326         if bug_json.get('status') in self.bug_closed_statuses:
327             return 1
328         return 0
329
330     def skip_build(self, reason):
331         self._addToLog('stdio', reason)
332         self.finished(FAILURE)
333         self.build.results = SKIPPED
334         self.build.buildFinished([reason], SKIPPED)
335
336     def start(self):
337         patch_id = self.getProperty('patch_id', '')
338         if not patch_id:
339             self._addToLog('stdio', 'No patch_id found. Unable to proceed without patch_id.\n')
340             self.finished(FAILURE)
341             return None
342
343         bug_id = self.getProperty('bug_id', '') or self.get_bug_id_from_patch(patch_id)
344
345         bug_closed = self._is_bug_closed(bug_id)
346         if bug_closed == 1:
347             self.skip_build('Bug {} is already closed'.format(bug_id))
348             return None
349
350         obsolete = self._is_patch_obsolete(patch_id)
351         if obsolete == 1:
352             self.skip_build('Patch {} is obsolete'.format(patch_id))
353             return None
354
355         review_denied = self._is_patch_review_denied(patch_id)
356         if review_denied == 1:
357             self.skip_build('Patch {} is marked r-'.format(patch_id))
358             return None
359
360         if obsolete == -1 or review_denied == -1 or bug_closed == -1:
361             self.finished(WARNINGS)
362             return None
363
364         self._addToLog('stdio', 'Bug is open.\nPatch is not obsolete.\nPatch is not marked r-.\n')
365         self.finished(SUCCESS)
366         return None
367
368
369 class UnApplyPatchIfRequired(CleanWorkingDirectory):
370     name = 'unapply-patch'
371     descriptionDone = ['Unapplied patch']
372
373     def doStepIf(self, step):
374         return self.getProperty('patchFailedToBuild') or self.getProperty('patchFailedJSCTests')
375
376     def hideStepIf(self, results, step):
377         return not self.doStepIf(step)
378
379
380 class TestWithFailureCount(shell.Test):
381     failedTestsFormatString = "%d test%s failed"
382     failedTestCount = 0
383
384     def start(self):
385         self.log_observer = logobserver.BufferLogObserver(wantStderr=True)
386         self.addLogObserver('stdio', self.log_observer)
387         return shell.Test.start(self)
388
389     def countFailures(self, cmd):
390         raise NotImplementedError
391
392     def commandComplete(self, cmd):
393         shell.Test.commandComplete(self, cmd)
394         self.failedTestCount = self.countFailures(cmd)
395         self.failedTestPluralSuffix = "" if self.failedTestCount == 1 else "s"
396
397     def evaluateCommand(self, cmd):
398         if self.failedTestCount:
399             return FAILURE
400
401         if cmd.rc != 0:
402             return FAILURE
403
404         return SUCCESS
405
406     def getResultSummary(self):
407         status = self.name
408
409         if self.results != SUCCESS and self.failedTestCount:
410             status = self.failedTestsFormatString % (self.failedTestCount, self.failedTestPluralSuffix)
411
412         if self.results != SUCCESS:
413             status += u' ({})'.format(Results[self.results])
414
415         return {u'step': status}
416
417
418 class CheckStyle(TestWithFailureCount):
419     name = 'check-webkit-style'
420     description = ['check-webkit-style running']
421     descriptionDone = ['check-webkit-style']
422     flunkOnFailure = True
423     failedTestsFormatString = '%d style error%s'
424     command = ['Tools/Scripts/check-webkit-style']
425
426     def countFailures(self, cmd):
427         log_text = self.log_observer.getStdout() + self.log_observer.getStderr()
428
429         match = re.search(r'Total errors found: (?P<errors>\d+) in (?P<files>\d+) files', log_text)
430         if not match:
431             return 0
432         return int(match.group('errors'))
433
434
435 class RunBindingsTests(shell.ShellCommand):
436     name = 'bindings-tests'
437     description = ['bindings-tests running']
438     descriptionDone = ['bindings-tests']
439     flunkOnFailure = True
440     jsonFileName = 'bindings_test_results.json'
441     logfiles = {'json': jsonFileName}
442     command = ['Tools/Scripts/run-bindings-tests', '--json-output={0}'.format(jsonFileName)]
443
444
445 class RunWebKitPerlTests(shell.ShellCommand):
446     name = 'webkitperl-tests'
447     description = ['webkitperl-tests running']
448     descriptionDone = ['webkitperl-tests']
449     flunkOnFailure = True
450     command = ['Tools/Scripts/test-webkitperl']
451
452     def __init__(self, **kwargs):
453         super(RunWebKitPerlTests, self).__init__(timeout=2 * 60, **kwargs)
454
455
456 class RunWebKitPyTests(shell.ShellCommand):
457     name = 'webkitpy-tests'
458     description = ['webkitpy-tests running']
459     descriptionDone = ['webkitpy-tests']
460     flunkOnFailure = True
461     jsonFileName = 'webkitpy_test_results.json'
462     logfiles = {'json': jsonFileName}
463     command = ['Tools/Scripts/test-webkitpy', '--json-output={0}'.format(jsonFileName)]
464
465     def __init__(self, **kwargs):
466         super(RunWebKitPyTests, self).__init__(timeout=2 * 60, **kwargs)
467
468
469 def appendCustomBuildFlags(step, platform, fullPlatform):
470     # FIXME: Make a common 'supported platforms' list.
471     if platform not in ('gtk', 'wincairo', 'ios', 'jsc-only', 'wpe'):
472         return
473     if fullPlatform.startswith('ios-simulator'):
474         platform = 'ios-simulator'
475     elif platform == 'ios':
476         platform = 'device'
477     step.setCommand(step.command + ['--' + platform])
478
479
480 class CompileWebKit(shell.Compile):
481     name = "compile-webkit"
482     description = ["compiling"]
483     descriptionDone = ["Compiled WebKit"]
484     env = {'MFLAGS': ''}
485     warningPattern = ".*arning: .*"
486     haltOnFailure = False
487     command = ["perl", "Tools/Scripts/build-webkit", WithProperties("--%(configuration)s")]
488
489     def start(self):
490         platform = self.getProperty('platform')
491         buildOnly = self.getProperty('buildOnly')
492         architecture = self.getProperty('architecture')
493         additionalArguments = self.getProperty('additionalArguments')
494
495         if additionalArguments:
496             self.setCommand(self.command + additionalArguments)
497         if platform in ('mac', 'ios') and architecture:
498             self.setCommand(self.command + ['ARCHS=' + architecture])
499             if platform == 'ios':
500                 self.setCommand(self.command + ['ONLY_ACTIVE_ARCH=NO'])
501         if platform in ('mac', 'ios') and buildOnly:
502             # For build-only bots, the expectation is that tests will be run on separate machines,
503             # so we need to package debug info as dSYMs. Only generating line tables makes
504             # this much faster than full debug info, and crash logs still have line numbers.
505             self.setCommand(self.command + ['DEBUG_INFORMATION_FORMAT=dwarf-with-dsym'])
506             self.setCommand(self.command + ['CLANG_DEBUG_INFORMATION_LEVEL=line-tables-only'])
507
508         appendCustomBuildFlags(self, platform, self.getProperty('fullPlatform'))
509
510         return shell.Compile.start(self)
511
512     def evaluateCommand(self, cmd):
513         if cmd.didFail():
514             self.setProperty('patchFailedToBuild', True)
515
516         return super(CompileWebKit, self).evaluateCommand(cmd)
517
518
519 class CompileWebKitToT(CompileWebKit):
520     name = 'compile-webkit-tot'
521     haltOnFailure = True
522
523     def doStepIf(self, step):
524         return self.getProperty('patchFailedToBuild')
525
526     def hideStepIf(self, results, step):
527         return not self.doStepIf(step)
528
529
530 class CompileJSCOnly(CompileWebKit):
531     name = "build-jsc"
532     descriptionDone = ["Compiled JSC"]
533     command = ["perl", "Tools/Scripts/build-jsc", WithProperties("--%(configuration)s")]
534
535
536 class CompileJSCOnlyToT(CompileJSCOnly):
537     name = 'build-jsc-tot'
538
539     def doStepIf(self, step):
540         return self.getProperty('patchFailedToBuild')
541
542     def hideStepIf(self, results, step):
543         return not self.doStepIf(step)
544
545
546 class RunJavaScriptCoreTests(shell.Test):
547     name = 'jscore-test'
548     description = ['jscore-tests running']
549     descriptionDone = ['jscore-tests']
550     flunkOnFailure = True
551     jsonFileName = 'jsc_results.json'
552     logfiles = {"json": jsonFileName}
553     command = ['perl', 'Tools/Scripts/run-javascriptcore-tests', '--no-build', '--no-fail-fast', '--json-output={0}'.format(jsonFileName), WithProperties('--%(configuration)s')]
554
555     def start(self):
556         appendCustomBuildFlags(self, self.getProperty('platform'), self.getProperty('fullPlatform'))
557         return shell.Test.start(self)
558
559     def evaluateCommand(self, cmd):
560         if cmd.didFail():
561             self.setProperty('patchFailedJSCTests', True)
562
563         return super(RunJavaScriptCoreTests, self).evaluateCommand(cmd)
564
565
566 class ReRunJavaScriptCoreTests(RunJavaScriptCoreTests):
567     name = 'jscore-test-rerun'
568
569     def doStepIf(self, step):
570         return self.getProperty('patchFailedJSCTests')
571
572     def hideStepIf(self, results, step):
573         return not self.doStepIf(step)
574
575     def evaluateCommand(self, cmd):
576         self.setProperty('patchFailedJSCTests', cmd.didFail())
577         return super(RunJavaScriptCoreTests, self).evaluateCommand(cmd)
578
579
580 class RunJavaScriptCoreTestsToT(RunJavaScriptCoreTests):
581     name = 'jscore-test-tot'
582     jsonFileName = 'jsc_results.json'
583     command = ['perl', 'Tools/Scripts/run-javascriptcore-tests', '--no-fail-fast', '--json-output={0}'.format(jsonFileName), WithProperties('--%(configuration)s')]
584
585     def doStepIf(self, step):
586         return self.getProperty('patchFailedJSCTests')
587
588     def hideStepIf(self, results, step):
589         return not self.doStepIf(step)
590
591
592 class CleanBuild(shell.Compile):
593     name = "delete-WebKitBuild-directory"
594     description = ["deleting WebKitBuild directory"]
595     descriptionDone = ["Deleted WebKitBuild directory"]
596     command = ["python", "Tools/BuildSlaveSupport/clean-build", WithProperties("--platform=%(fullPlatform)s"), WithProperties("--%(configuration)s")]
597
598
599 class KillOldProcesses(shell.Compile):
600     name = "kill-old-processes"
601     description = ["killing old processes"]
602     descriptionDone = ["Killed old processes"]
603     command = ["python", "Tools/BuildSlaveSupport/kill-old-processes", "buildbot"]
604
605     def __init__(self, **kwargs):
606         super(KillOldProcesses, self).__init__(timeout=60, **kwargs)
607
608
609 class RunWebKitTests(shell.Test):
610     name = 'layout-tests'
611     description = ['layout-tests running']
612     descriptionDone = ['layout-tests']
613     resultDirectory = 'layout-test-results'
614     command = ['python', 'Tools/Scripts/run-webkit-tests',
615                '--no-build',
616                '--no-new-test-results',
617                '--no-show-results',
618                '--exit-after-n-failures', '30',
619                '--skip-failing-tests',
620                WithProperties('--%(configuration)s')]
621
622     def start(self):
623         platform = self.getProperty('platform')
624         appendCustomBuildFlags(self, platform, self.getProperty('fullPlatform'))
625         additionalArguments = self.getProperty('additionalArguments')
626
627         self.setCommand(self.command + ['--results-directory', self.resultDirectory])
628         self.setCommand(self.command + ['--debug-rwt-logging'])
629
630         if additionalArguments:
631             self.setCommand(self.command + additionalArguments)
632         return shell.Test.start(self)
633
634
635 class RunWebKit1Tests(RunWebKitTests):
636     def start(self):
637         self.setCommand(self.command + ['--dump-render-tree'])
638
639         return RunWebKitTests.start(self)
640
641
642 class ArchiveBuiltProduct(shell.ShellCommand):
643     command = ['python', 'Tools/BuildSlaveSupport/built-product-archive',
644                WithProperties('--platform=%(fullPlatform)s'), WithProperties('--%(configuration)s'), 'archive']
645     name = 'archive-built-product'
646     description = ['archiving built product']
647     descriptionDone = ['Archived built product']
648     haltOnFailure = True
649
650
651 class UploadBuiltProduct(transfer.FileUpload):
652     name = 'upload-built-product'
653     workersrc = WithProperties('WebKitBuild/%(configuration)s.zip')
654     masterdest = WithProperties('public_html/archives/%(fullPlatform)s-%(architecture)s-%(configuration)s/%(patch_id)s.zip')
655     haltOnFailure = True
656
657     def __init__(self, **kwargs):
658         kwargs['workersrc'] = self.workersrc
659         kwargs['masterdest'] = self.masterdest
660         kwargs['mode'] = 0644
661         kwargs['blocksize'] = 1024 * 256
662         transfer.FileUpload.__init__(self, **kwargs)
663
664
665 class DownloadBuiltProduct(shell.ShellCommand):
666     command = ['python', 'Tools/BuildSlaveSupport/download-built-product',
667         WithProperties('--platform=%(platform)s'), WithProperties('--%(configuration)s'),
668         WithProperties(EWS_URL + 'archives/%(fullPlatform)s-%(architecture)s-%(configuration)s/%(patch_id)s.zip')]
669     name = 'download-built-product'
670     description = ['downloading built product']
671     descriptionDone = ['Downloaded built product']
672     haltOnFailure = True
673     flunkOnFailure = True
674
675
676 class ExtractBuiltProduct(shell.ShellCommand):
677     command = ['python', 'Tools/BuildSlaveSupport/built-product-archive',
678                WithProperties('--platform=%(fullPlatform)s'), WithProperties('--%(configuration)s'), 'extract']
679     name = 'extract-built-product'
680     description = ['extracting built product']
681     descriptionDone = ['Extracted built product']
682     haltOnFailure = True
683     flunkOnFailure = True
684
685
686 class RunAPITests(TestWithFailureCount):
687     name = 'run-api-tests'
688     description = ['api tests running']
689     descriptionDone = ['api-tests']
690     jsonFileName = 'api_test_results.json'
691     logfiles = {'json': jsonFileName}
692     command = ['python', 'Tools/Scripts/run-api-tests', '--no-build',
693                WithProperties('--%(configuration)s'), '--verbose', '--json-output={0}'.format(jsonFileName)]
694     failedTestsFormatString = '%d api test%s failed or timed out'
695
696     def start(self):
697         appendCustomBuildFlags(self, self.getProperty('platform'), self.getProperty('fullPlatform'))
698         return TestWithFailureCount.start(self)
699
700     def countFailures(self, cmd):
701         log_text = self.log_observer.getStdout() + self.log_observer.getStderr()
702
703         match = re.search(r'Ran (?P<ran>\d+) tests of (?P<total>\d+) with (?P<passed>\d+) successful', log_text)
704         if not match:
705             return 0
706         return int(match.group('ran')) - int(match.group('passed'))
707
708
709 class ArchiveTestResults(shell.ShellCommand):
710     command = ['python', 'Tools/BuildSlaveSupport/test-result-archive',
711                Interpolate('--platform=%(prop:platform)s'), Interpolate('--%(prop:configuration)s'), 'archive']
712     name = 'archive-test-results'
713     description = ['archiving test results']
714     descriptionDone = ['Archived test results']
715     haltOnFailure = True
716
717
718 class UploadTestResults(transfer.FileUpload):
719     name = 'upload-test-results'
720     descriptionDone = ['Uploaded test results']
721     workersrc = 'layout-test-results.zip'
722     masterdest = Interpolate('public_html/results/%(prop:buildername)s/r%(prop:patch_id)s-%(prop:buildnumber)s.zip')
723     haltOnFailure = True
724
725     def __init__(self, **kwargs):
726         kwargs['workersrc'] = self.workersrc
727         kwargs['masterdest'] = self.masterdest
728         kwargs['mode'] = 0644
729         kwargs['blocksize'] = 1024 * 256
730         transfer.FileUpload.__init__(self, **kwargs)
731
732
733 class ExtractTestResults(master.MasterShellCommand):
734     name = 'extract-test-results'
735     zipFile = Interpolate('public_html/results/%(prop:buildername)s/r%(prop:patch_id)s-%(prop:buildnumber)s.zip')
736     resultDirectory = Interpolate('public_html/results/%(prop:buildername)s/r%(prop:patch_id)s-%(prop:buildnumber)s')
737
738     descriptionDone = ['Extracted test results']
739     command = ['unzip', zipFile, '-d', resultDirectory]
740     renderables = ['resultDirectory']
741
742     def __init__(self):
743         super(ExtractTestResults, self).__init__(self.command)
744
745     def resultDirectoryURL(self):
746         return self.resultDirectory.replace('public_html/', '/') + '/'
747
748     def addCustomURLs(self):
749         self.addURL('view layout test results', self.resultDirectoryURL() + 'results.html')
750
751     def finished(self, result):
752         self.addCustomURLs()
753         return master.MasterShellCommand.finished(self, result)
754
755
756 class PrintConfiguration(steps.ShellSequence):
757     name = 'configuration'
758     description = ['configuration']
759     haltOnFailure = False
760     flunkOnFailure = False
761     warnOnFailure = False
762     logEnviron = False
763     command_list = [['hostname'],
764                     ['df', '-hl'],
765                     ['date'],
766                     ['sw_vers'],
767                     ['xcodebuild', '-sdk', '-version']]
768
769     def __init__(self, **kwargs):
770         super(PrintConfiguration, self).__init__(timeout=60, **kwargs)
771         self.commands = []
772         self.log_observer = logobserver.BufferLogObserver(wantStderr=True)
773         self.addLogObserver('stdio', self.log_observer)
774         # FIXME: Check platform before running platform specific commands.
775         for command in self.command_list:
776             self.commands.append(util.ShellArg(command=command, logfile='stdio'))
777
778     def convert_build_to_os_name(self, build):
779         if not build:
780             return 'Unknown'
781
782         build_to_name_mapping = {
783             '10.14': 'Mojave',
784             '10.13': 'High Sierra',
785             '10.12': 'Sierra',
786             '10.11': 'El Capitan',
787             '10.10': 'Yosemite',
788             '10.9': 'Maverick',
789             '10.8': 'Mountain Lion',
790             '10.7': 'Lion',
791             '10.6': 'Snow Leopard',
792             '10.5': 'Leopard',
793         }
794
795         for key, value in build_to_name_mapping.iteritems():
796             if build.startswith(key):
797                 return value
798         return 'Unknown'
799
800     def getResultSummary(self):
801         if self.results != SUCCESS:
802             return {u'step': u'Failed to print configuration'}
803         logText = self.log_observer.getStdout() + self.log_observer.getStderr()
804         configuration = u''
805         match = re.search('ProductVersion:[ \t]*(.+?)\n', logText)
806         if match:
807             os_version = match.group(1).strip()
808             os_name = self.convert_build_to_os_name(os_version)
809             configuration = u'OS: {} ({})'.format(os_name, os_version)
810
811         sdk_re = 'MacOSX[\s\S]*?SDKVersion:[ \t]*(.+?)\n'
812         is_ios_builder = 'iOS' in self.getProperty('buildername', '')
813         if is_ios_builder:
814             sdk_re = 'iPhoneSimulator[\s\S]*?SDKVersion:[ \t]*(.+?)\n'
815         match = re.search(sdk_re, logText)
816         if match:
817             xcode_version = match.group(1).strip()
818             configuration += u', Xcode: {}'.format(xcode_version)
819         return {u'step': configuration}