[ews-build] Do not print worker environment variables in each build 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, trigger
27 from buildbot.steps.source import git
28 from buildbot.steps.worker import CompositeStepMixin
29 from twisted.internet import defer
30
31 import json
32 import re
33 import requests
34
35 BUG_SERVER_URL = 'https://bugs.webkit.org/'
36 S3URL = 'https://s3-us-west-2.amazonaws.com/'
37 WithProperties = properties.WithProperties
38 Interpolate = properties.Interpolate
39
40
41 class ConfigureBuild(buildstep.BuildStep):
42     name = 'configure-build'
43     description = ['configuring build']
44     descriptionDone = ['Configured build']
45
46     def __init__(self, platform, configuration, architectures, buildOnly, triggers, additionalArguments):
47         super(ConfigureBuild, self).__init__()
48         self.platform = platform
49         if platform != 'jsc-only':
50             self.platform = platform.split('-', 1)[0]
51         self.fullPlatform = platform
52         self.configuration = configuration
53         self.architecture = ' '.join(architectures) if architectures else None
54         self.buildOnly = buildOnly
55         self.triggers = triggers
56         self.additionalArguments = additionalArguments
57
58     def start(self):
59         if self.platform and self.platform != '*':
60             self.setProperty('platform', self.platform, 'config.json')
61         if self.fullPlatform and self.fullPlatform != '*':
62             self.setProperty('fullPlatform', self.fullPlatform, 'ConfigureBuild')
63         if self.configuration:
64             self.setProperty('configuration', self.configuration, 'config.json')
65         if self.architecture:
66             self.setProperty('architecture', self.architecture, 'config.json')
67         if self.buildOnly:
68             self.setProperty('buildOnly', self.buildOnly, 'config.json')
69         if self.triggers:
70             self.setProperty('triggers', self.triggers, 'config.json')
71         if self.additionalArguments:
72             self.setProperty('additionalArguments', self.additionalArguments, 'config.json')
73
74         self.add_patch_id_url()
75         self.finished(SUCCESS)
76         return defer.succeed(None)
77
78     def add_patch_id_url(self):
79         patch_id = self.getProperty('patch_id', '')
80         if patch_id:
81             self.addURL('Patch {}'.format(patch_id), self.getPatchURL(patch_id))
82
83     def getPatchURL(self, patch_id):
84         if not patch_id:
85             return None
86         return '{}attachment.cgi?id={}&action=prettypatch'.format(BUG_SERVER_URL, patch_id)
87
88
89 class CheckOutSource(git.Git):
90     name = 'clean-and-update-working-directory'
91     CHECKOUT_DELAY_AND_MAX_RETRIES_PAIR = (0, 2)
92
93     def __init__(self, **kwargs):
94         self.repourl = 'https://git.webkit.org/git/WebKit.git'
95         super(CheckOutSource, self).__init__(repourl=self.repourl,
96                                                 retry=self.CHECKOUT_DELAY_AND_MAX_RETRIES_PAIR,
97                                                 timeout=2 * 60 * 60,
98                                                 alwaysUseLatest=True,
99                                                 logEnviron=False,
100                                                 method='clean',
101                                                 progress=True,
102                                                 **kwargs)
103
104     def getResultSummary(self):
105         if self.results != SUCCESS:
106             return {u'step': u'Failed to updated working directory'}
107         else:
108             return {u'step': u'Cleaned and updated working directory'}
109
110
111 class CheckOutSpecificRevision(shell.ShellCommand):
112     name = 'checkout-specific-revision'
113     descriptionDone = ['Checked out required revision']
114     flunkOnFailure = False
115     haltOnFailure = False
116
117     def doStepIf(self, step):
118         return self.getProperty('ews_revision', False)
119
120     def hideStepIf(self, results, step):
121         return not self.doStepIf(step)
122
123     def start(self):
124         self.setCommand(['git', 'checkout', self.getProperty('ews_revision')])
125         return shell.ShellCommand.start(self)
126
127
128 class CleanWorkingDirectory(shell.ShellCommand):
129     name = 'clean-working-directory'
130     description = ['clean-working-directory running']
131     descriptionDone = ['Cleaned working directory']
132     flunkOnFailure = True
133     haltOnFailure = True
134     command = ['Tools/Scripts/clean-webkit']
135
136
137 class ApplyPatch(shell.ShellCommand, CompositeStepMixin):
138     name = 'apply-patch'
139     description = ['applying-patch']
140     descriptionDone = ['Applied patch']
141     flunkOnFailure = True
142     haltOnFailure = True
143     command = ['Tools/Scripts/svn-apply', '--force', '.buildbot-diff']
144
145     def __init__(self, **kwargs):
146         super(ApplyPatch, self).__init__(timeout=5 * 60, logEnviron=False, **kwargs)
147
148     def _get_patch(self):
149         sourcestamp = self.build.getSourceStamp(self.getProperty('codebase', ''))
150         if not sourcestamp or not sourcestamp.patch:
151             return None
152         return sourcestamp.patch[1]
153
154     def start(self):
155         patch = self._get_patch()
156         if not patch:
157             self.finished(FAILURE)
158             return None
159
160         d = self.downloadFileContentToWorker('.buildbot-diff', patch)
161         d.addCallback(lambda res: shell.ShellCommand.start(self))
162
163     def hideStepIf(self, results, step):
164         return results == SUCCESS and self.getProperty('validated', '') == False
165
166     def getResultSummary(self):
167         if self.results != SUCCESS:
168             return {u'step': u'Patch does not apply'}
169         return super(ApplyPatch, self).getResultSummary()
170
171
172 class CheckPatchRelevance(buildstep.BuildStep):
173     name = 'check-patch-relevance'
174     description = ['check-patch-relevance running']
175     descriptionDone = ['Checked patch relevance']
176     flunkOnFailure = True
177     haltOnFailure = True
178
179     bindings_paths = [
180         'Source/WebCore',
181         'Tools',
182     ]
183
184     jsc_paths = [
185         'JSTests/',
186         'Source/JavaScriptCore/',
187         'Source/WTF/',
188         'Source/bmalloc/',
189         'Makefile',
190         'Makefile.shared',
191         'Source/Makefile',
192         'Source/Makefile.shared',
193         'Tools/Scripts/build-webkit',
194         'Tools/Scripts/build-jsc',
195         'Tools/Scripts/jsc-stress-test-helpers/',
196         'Tools/Scripts/run-jsc',
197         'Tools/Scripts/run-jsc-benchmarks',
198         'Tools/Scripts/run-jsc-stress-tests',
199         'Tools/Scripts/run-javascriptcore-tests',
200         'Tools/Scripts/run-layout-jsc',
201         'Tools/Scripts/update-javascriptcore-test-results',
202         'Tools/Scripts/webkitdirs.pm',
203     ]
204
205     webkitpy_paths = [
206         'Tools/Scripts/webkitpy/',
207         'Tools/QueueStatusServer/',
208     ]
209
210     group_to_paths_mapping = {
211         'bindings': bindings_paths,
212         'jsc': jsc_paths,
213         'webkitpy': webkitpy_paths,
214     }
215
216     def _patch_is_relevant(self, patch, builderName):
217         group = [group for group in self.group_to_paths_mapping.keys() if group in builderName.lower()]
218         if not group:
219             # This builder doesn't have paths defined, all patches are relevant.
220             return True
221
222         relevant_paths = self.group_to_paths_mapping[group[0]]
223
224         for change in patch.splitlines():
225             for path in relevant_paths:
226                 if re.search(path, change, re.IGNORECASE):
227                     return True
228         return False
229
230     def _get_patch(self):
231         sourcestamp = self.build.getSourceStamp(self.getProperty('codebase', ''))
232         if not sourcestamp or not sourcestamp.patch:
233             return None
234         return sourcestamp.patch[1]
235
236     @defer.inlineCallbacks
237     def _addToLog(self, logName, message):
238         try:
239             log = self.getLog(logName)
240         except KeyError:
241             log = yield self.addLog(logName)
242         log.addStdout(message)
243
244     def start(self):
245         patch = self._get_patch()
246         if not patch:
247             # This build doesn't have a patch, it might be a force build.
248             self.finished(SUCCESS)
249             return None
250
251         if self._patch_is_relevant(patch, self.getProperty('buildername', '')):
252             self._addToLog('stdio', 'This patch contains relevant changes.')
253             self.finished(SUCCESS)
254             return None
255
256         self._addToLog('stdio', 'This patch does not have relevant changes.')
257         self.finished(FAILURE)
258         self.build.results = SKIPPED
259         self.build.buildFinished(['Patch {} doesn\'t have relevant changes'.format(self.getProperty('patch_id', ''))], SKIPPED)
260         return None
261
262
263 class ValidatePatch(buildstep.BuildStep):
264     name = 'validate-patch'
265     description = ['validate-patch running']
266     descriptionDone = ['Validated patch']
267     flunkOnFailure = True
268     haltOnFailure = True
269     bug_open_statuses = ['UNCONFIRMED', 'NEW', 'ASSIGNED', 'REOPENED']
270     bug_closed_statuses = ['RESOLVED', 'VERIFIED', 'CLOSED']
271
272     @defer.inlineCallbacks
273     def _addToLog(self, logName, message):
274         try:
275             log = self.getLog(logName)
276         except KeyError:
277             log = yield self.addLog(logName)
278         log.addStdout(message)
279
280     def fetch_data_from_url(self, url):
281         response = None
282         try:
283             response = requests.get(url)
284         except Exception as e:
285             if response:
286                 self._addToLog('stdio', 'Failed to access {url} with status code {status_code}.\n'.format(url=url, status_code=response.status_code))
287             else:
288                 self._addToLog('stdio', 'Failed to access {url} with exception: {exception}\n'.format(url=url, exception=e))
289             return None
290         if response.status_code != 200:
291             self._addToLog('stdio', 'Accessed {url} with unexpected status code {status_code}.\n'.format(url=url, status_code=response.status_code))
292             return None
293         return response
294
295     def get_patch_json(self, patch_id):
296         patch_url = '{}rest/bug/attachment/{}'.format(BUG_SERVER_URL, patch_id)
297         patch = self.fetch_data_from_url(patch_url)
298         if not patch:
299             return None
300         patch_json = patch.json().get('attachments')
301         if not patch_json or len(patch_json) == 0:
302             return None
303         return patch_json.get(str(patch_id))
304
305     def get_bug_json(self, bug_id):
306         bug_url = '{}rest/bug/{}'.format(BUG_SERVER_URL, bug_id)
307         bug = self.fetch_data_from_url(bug_url)
308         if not bug:
309             return None
310         bugs_json = bug.json().get('bugs')
311         if not bugs_json or len(bugs_json) == 0:
312             return None
313         return bugs_json[0]
314
315     def get_bug_id_from_patch(self, patch_id):
316         patch_json = self.get_patch_json(patch_id)
317         if not patch_json:
318             self._addToLog('stdio', 'Unable to fetch patch {}.\n'.format(patch_id))
319             return -1
320         return patch_json.get('bug_id')
321
322     def _is_patch_obsolete(self, patch_id):
323         patch_json = self.get_patch_json(patch_id)
324         if not patch_json:
325             self._addToLog('stdio', 'Unable to fetch patch {}.\n'.format(patch_id))
326             return -1
327
328         if str(patch_json.get('id')) != self.getProperty('patch_id', ''):
329             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', '')))
330             return -1
331
332         patch_author = patch_json.get('creator')
333         self.addURL('Patch by: {}'.format(patch_author), 'mailto:{}'.format(patch_author))
334         return patch_json.get('is_obsolete')
335
336     def _is_patch_review_denied(self, patch_id):
337         patch_json = self.get_patch_json(patch_id)
338         if not patch_json:
339             self._addToLog('stdio', 'Unable to fetch patch {}.\n'.format(patch_id))
340             return -1
341
342         for flag in patch_json.get('flags', []):
343             if flag.get('name') == 'review' and flag.get('status') == '-':
344                 return 1
345         return 0
346
347     def _is_bug_closed(self, bug_id):
348         if not bug_id:
349             self._addToLog('stdio', 'Skipping bug status validation since bug id is None.\n')
350             return -1
351
352         bug_json = self.get_bug_json(bug_id)
353         if not bug_json or not bug_json.get('status'):
354             self._addToLog('stdio', 'Unable to fetch bug {}.\n'.format(bug_id))
355             return -1
356
357         bug_title = bug_json.get('summary')
358         self.addURL(u'Bug {} {}'.format(bug_id, bug_title), '{}show_bug.cgi?id={}'.format(BUG_SERVER_URL, bug_id))
359         if bug_json.get('status') in self.bug_closed_statuses:
360             return 1
361         return 0
362
363     def skip_build(self, reason):
364         self._addToLog('stdio', reason)
365         self.finished(FAILURE)
366         self.build.results = SKIPPED
367         self.build.buildFinished([reason], SKIPPED)
368
369     def start(self):
370         patch_id = self.getProperty('patch_id', '')
371         if not patch_id:
372             self._addToLog('stdio', 'No patch_id found. Unable to proceed without patch_id.\n')
373             self.finished(FAILURE)
374             return None
375
376         bug_id = self.getProperty('bug_id', '') or self.get_bug_id_from_patch(patch_id)
377
378         bug_closed = self._is_bug_closed(bug_id)
379         if bug_closed == 1:
380             self.skip_build('Bug {} is already closed'.format(bug_id))
381             return None
382
383         obsolete = self._is_patch_obsolete(patch_id)
384         if obsolete == 1:
385             self.skip_build('Patch {} is obsolete'.format(patch_id))
386             return None
387
388         review_denied = self._is_patch_review_denied(patch_id)
389         if review_denied == 1:
390             self.skip_build('Patch {} is marked r-'.format(patch_id))
391             return None
392
393         if obsolete == -1 or review_denied == -1 or bug_closed == -1:
394             self.finished(WARNINGS)
395             self.setProperty('validated', False)
396             return None
397
398         self._addToLog('stdio', 'Bug is open.\nPatch is not obsolete.\nPatch is not marked r-.\n')
399         self.finished(SUCCESS)
400         return None
401
402
403 class UnApplyPatchIfRequired(CleanWorkingDirectory):
404     name = 'unapply-patch'
405     descriptionDone = ['Unapplied patch']
406
407     def doStepIf(self, step):
408         return self.getProperty('patchFailedToBuild') or self.getProperty('patchFailedTests')
409
410     def hideStepIf(self, results, step):
411         return not self.doStepIf(step)
412
413
414 class Trigger(trigger.Trigger):
415     def __init__(self, schedulerNames, **kwargs):
416         set_properties = self.propertiesToPassToTriggers() or {}
417         super(Trigger, self).__init__(schedulerNames=schedulerNames, set_properties=set_properties, **kwargs)
418
419     def propertiesToPassToTriggers(self):
420         return {
421             'patch_id': properties.Property('patch_id'),
422             'bug_id': properties.Property('bug_id'),
423             'configuration': properties.Property('configuration'),
424             'platform': properties.Property('platform'),
425             'fullPlatform': properties.Property('fullPlatform'),
426             'architecture': properties.Property('architecture'),
427             'owner': properties.Property('owner'),
428             'ews_revision': properties.Property('got_revision'),
429         }
430
431
432 class TestWithFailureCount(shell.Test):
433     failedTestsFormatString = '%d test%s failed'
434     failedTestCount = 0
435
436     def start(self):
437         self.log_observer = logobserver.BufferLogObserver(wantStderr=True)
438         self.addLogObserver('stdio', self.log_observer)
439         return shell.Test.start(self)
440
441     def countFailures(self, cmd):
442         raise NotImplementedError
443
444     def commandComplete(self, cmd):
445         shell.Test.commandComplete(self, cmd)
446         self.failedTestCount = self.countFailures(cmd)
447         self.failedTestPluralSuffix = '' if self.failedTestCount == 1 else 's'
448
449     def evaluateCommand(self, cmd):
450         if self.failedTestCount:
451             return FAILURE
452
453         if cmd.rc != 0:
454             return FAILURE
455
456         return SUCCESS
457
458     def getResultSummary(self):
459         status = self.name
460
461         if self.results != SUCCESS and self.failedTestCount:
462             status = self.failedTestsFormatString % (self.failedTestCount, self.failedTestPluralSuffix)
463
464         if self.results != SUCCESS:
465             status += u' ({})'.format(Results[self.results])
466
467         return {u'step': status}
468
469
470 class CheckStyle(TestWithFailureCount):
471     name = 'check-webkit-style'
472     description = ['check-webkit-style running']
473     descriptionDone = ['check-webkit-style']
474     flunkOnFailure = True
475     failedTestsFormatString = '%d style error%s'
476     command = ['Tools/Scripts/check-webkit-style']
477
478     def countFailures(self, cmd):
479         log_text = self.log_observer.getStdout() + self.log_observer.getStderr()
480
481         match = re.search(r'Total errors found: (?P<errors>\d+) in (?P<files>\d+) files', log_text)
482         if not match:
483             return 0
484         return int(match.group('errors'))
485
486
487 class RunBindingsTests(shell.ShellCommand):
488     name = 'bindings-tests'
489     description = ['bindings-tests running']
490     descriptionDone = ['bindings-tests']
491     flunkOnFailure = True
492     jsonFileName = 'bindings_test_results.json'
493     logfiles = {'json': jsonFileName}
494     command = ['Tools/Scripts/run-bindings-tests', '--json-output={0}'.format(jsonFileName)]
495
496     def __init__(self, **kwargs):
497         super(RunBindingsTests, self).__init__(timeout=5 * 60, **kwargs)
498
499     def start(self):
500         self.log_observer = logobserver.BufferLogObserver()
501         self.addLogObserver('json', self.log_observer)
502         return shell.ShellCommand.start(self)
503
504     def getResultSummary(self):
505         if self.results == SUCCESS:
506             message = 'Passed bindings tests'
507             self.build.buildFinished([message], SUCCESS)
508             return {u'step': unicode(message)}
509
510         logLines = self.log_observer.getStdout()
511         json_text = ''.join([line for line in logLines.splitlines()])
512         try:
513             webkitpy_results = json.loads(json_text)
514         except Exception as ex:
515             self._addToLog('stderr', 'ERROR: unable to parse data, exception: {}'.format(ex))
516             return super(RunBindingsTests, self).getResultSummary()
517
518         failures = webkitpy_results.get('failures')
519         if not failures:
520             return super(RunBindingsTests, self).getResultSummary()
521         pluralSuffix = 's' if len(failures) > 1 else ''
522         failures_string = ', '.join([failure.replace('(JS) ', '') for failure in failures])
523         message = 'Found {} Binding test failure{}: {}'.format(len(failures), pluralSuffix, failures_string)
524         self.build.buildFinished([message], FAILURE)
525         return {u'step': unicode(message)}
526
527     @defer.inlineCallbacks
528     def _addToLog(self, logName, message):
529         try:
530             log = self.getLog(logName)
531         except KeyError:
532             log = yield self.addLog(logName)
533         log.addStdout(message)
534
535
536 class RunWebKitPerlTests(shell.ShellCommand):
537     name = 'webkitperl-tests'
538     description = ['webkitperl-tests running']
539     descriptionDone = ['webkitperl-tests']
540     flunkOnFailure = True
541     command = ['Tools/Scripts/test-webkitperl']
542
543     def __init__(self, **kwargs):
544         super(RunWebKitPerlTests, self).__init__(timeout=2 * 60, **kwargs)
545
546
547 class RunWebKitPyTests(shell.ShellCommand):
548     name = 'webkitpy-tests'
549     description = ['webkitpy-tests running']
550     descriptionDone = ['webkitpy-tests']
551     flunkOnFailure = True
552     jsonFileName = 'webkitpy_test_results.json'
553     logfiles = {'json': jsonFileName}
554     command = ['Tools/Scripts/test-webkitpy', '--json-output={0}'.format(jsonFileName)]
555
556     def __init__(self, **kwargs):
557         super(RunWebKitPyTests, self).__init__(timeout=2 * 60, **kwargs)
558
559     def start(self):
560         self.log_observer = logobserver.BufferLogObserver()
561         self.addLogObserver('json', self.log_observer)
562         return shell.ShellCommand.start(self)
563
564     def getResultSummary(self):
565         if self.results == SUCCESS:
566             message = 'Passed webkitpy tests'
567             self.build.buildFinished([message], SUCCESS)
568             return {u'step': unicode(message)}
569
570         logLines = self.log_observer.getStdout()
571         json_text = ''.join([line for line in logLines.splitlines()])
572         try:
573             webkitpy_results = json.loads(json_text)
574         except Exception as ex:
575             self._addToLog('stderr', 'ERROR: unable to parse data, exception: {}'.format(ex))
576             return super(RunWebKitPyTests, self).getResultSummary()
577
578         failures = webkitpy_results.get('failures') + webkitpy_results.get('errors')
579         if not failures:
580             return super(RunWebKitPyTests, self).getResultSummary()
581         pluralSuffix = 's' if len(failures) > 1 else ''
582         failures_string = ', '.join([failure.get('name').replace('webkitpy.', '') for failure in failures])
583         message = 'Found {} WebKitPy test failure{}: {}'.format(len(failures), pluralSuffix, failures_string)
584         self.build.buildFinished([message], FAILURE)
585         return {u'step': unicode(message)}
586
587     @defer.inlineCallbacks
588     def _addToLog(self, logName, message):
589         try:
590             log = self.getLog(logName)
591         except KeyError:
592             log = yield self.addLog(logName)
593         log.addStdout(message)
594
595
596 def appendCustomBuildFlags(step, platform, fullPlatform):
597     # FIXME: Make a common 'supported platforms' list.
598     if platform not in ('gtk', 'wincairo', 'ios', 'jsc-only', 'wpe'):
599         return
600     if fullPlatform.startswith('ios-simulator'):
601         platform = 'ios-simulator'
602     elif platform == 'ios':
603         platform = 'device'
604     step.setCommand(step.command + ['--' + platform])
605
606
607 class CompileWebKit(shell.Compile):
608     name = 'compile-webkit'
609     description = ['compiling']
610     descriptionDone = ['Compiled WebKit']
611     env = {'MFLAGS': ''}
612     warningPattern = '.*arning: .*'
613     haltOnFailure = False
614     command = ['perl', 'Tools/Scripts/build-webkit', WithProperties('--%(configuration)s')]
615
616     def start(self):
617         platform = self.getProperty('platform')
618         buildOnly = self.getProperty('buildOnly')
619         architecture = self.getProperty('architecture')
620         additionalArguments = self.getProperty('additionalArguments')
621
622         if additionalArguments:
623             self.setCommand(self.command + additionalArguments)
624         if platform in ('mac', 'ios') and architecture:
625             self.setCommand(self.command + ['ARCHS=' + architecture])
626             if platform == 'ios':
627                 self.setCommand(self.command + ['ONLY_ACTIVE_ARCH=NO'])
628         if platform in ('mac', 'ios') and buildOnly:
629             # For build-only bots, the expectation is that tests will be run on separate machines,
630             # so we need to package debug info as dSYMs. Only generating line tables makes
631             # this much faster than full debug info, and crash logs still have line numbers.
632             self.setCommand(self.command + ['DEBUG_INFORMATION_FORMAT=dwarf-with-dsym'])
633             self.setCommand(self.command + ['CLANG_DEBUG_INFORMATION_LEVEL=line-tables-only'])
634
635         appendCustomBuildFlags(self, platform, self.getProperty('fullPlatform'))
636
637         return shell.Compile.start(self)
638
639     def evaluateCommand(self, cmd):
640         if cmd.didFail():
641             self.setProperty('patchFailedToBuild', True)
642             self.build.addStepsAfterCurrentStep([UnApplyPatchIfRequired(), CompileWebKitToT(), AnalyzeCompileWebKitResults()])
643         else:
644             self.build.addStepsAfterCurrentStep([ArchiveBuiltProduct(), UploadBuiltProduct(), TransferToS3()])
645
646         return super(CompileWebKit, self).evaluateCommand(cmd)
647
648
649 class CompileWebKitToT(CompileWebKit):
650     name = 'compile-webkit-tot'
651     haltOnFailure = False
652
653     def doStepIf(self, step):
654         return self.getProperty('patchFailedToBuild') or self.getProperty('patchFailedTests')
655
656     def hideStepIf(self, results, step):
657         return not self.doStepIf(step)
658
659     def evaluateCommand(self, cmd):
660         return shell.Compile.evaluateCommand(self, cmd)
661
662
663 class AnalyzeCompileWebKitResults(buildstep.BuildStep):
664     name = 'analyze-compile-webkit-results'
665     description = ['analyze-compile-webkit-results']
666     descriptionDone = ['analyze-compile-webkit-results']
667
668     def start(self):
669         compile_webkit_tot_result = self.getStepResult(CompileWebKitToT.name)
670
671         if compile_webkit_tot_result == FAILURE:
672             self.finished(FAILURE)
673             message = 'Unable to build WebKit without patch, retrying build'
674             self.descriptionDone = message
675             self.build.buildFinished([message], RETRY)
676             return defer.succeed(None)
677
678         self.finished(FAILURE)
679         self.build.results = FAILURE
680         message = 'Patch does not build'
681         self.descriptionDone = message
682         self.build.buildFinished([message], FAILURE)
683
684         return defer.succeed(None)
685
686     def getStepResult(self, step_name):
687         for step in self.build.executedSteps:
688             if step.name == step_name:
689                 return step.results
690
691
692 class CompileJSCOnly(CompileWebKit):
693     name = 'build-jsc'
694     descriptionDone = ['Compiled JSC']
695     command = ['perl', 'Tools/Scripts/build-jsc', WithProperties('--%(configuration)s')]
696
697
698 class CompileJSCOnlyToT(CompileJSCOnly):
699     name = 'build-jsc-tot'
700
701     def doStepIf(self, step):
702         return self.getProperty('patchFailedToBuild')
703
704     def hideStepIf(self, results, step):
705         return not self.doStepIf(step)
706
707
708 class RunJavaScriptCoreTests(shell.Test):
709     name = 'jscore-test'
710     description = ['jscore-tests running']
711     descriptionDone = ['jscore-tests']
712     flunkOnFailure = True
713     jsonFileName = 'jsc_results.json'
714     logfiles = {'json': jsonFileName}
715     command = ['perl', 'Tools/Scripts/run-javascriptcore-tests', '--no-build', '--no-fail-fast', '--json-output={0}'.format(jsonFileName), WithProperties('--%(configuration)s')]
716
717     def start(self):
718         appendCustomBuildFlags(self, self.getProperty('platform'), self.getProperty('fullPlatform'))
719         return shell.Test.start(self)
720
721     def evaluateCommand(self, cmd):
722         if cmd.didFail():
723             self.setProperty('patchFailedTests', True)
724
725         return super(RunJavaScriptCoreTests, self).evaluateCommand(cmd)
726
727
728 class ReRunJavaScriptCoreTests(RunJavaScriptCoreTests):
729     name = 'jscore-test-rerun'
730
731     def doStepIf(self, step):
732         return self.getProperty('patchFailedTests')
733
734     def hideStepIf(self, results, step):
735         return not self.doStepIf(step)
736
737     def evaluateCommand(self, cmd):
738         self.setProperty('patchFailedTests', cmd.didFail())
739         return super(RunJavaScriptCoreTests, self).evaluateCommand(cmd)
740
741
742 class RunJavaScriptCoreTestsToT(RunJavaScriptCoreTests):
743     name = 'jscore-test-tot'
744     jsonFileName = 'jsc_results.json'
745     command = ['perl', 'Tools/Scripts/run-javascriptcore-tests', '--no-fail-fast', '--json-output={0}'.format(jsonFileName), WithProperties('--%(configuration)s')]
746
747     def doStepIf(self, step):
748         return self.getProperty('patchFailedTests')
749
750     def hideStepIf(self, results, step):
751         return not self.doStepIf(step)
752
753
754 class CleanBuild(shell.Compile):
755     name = 'delete-WebKitBuild-directory'
756     description = ['deleting WebKitBuild directory']
757     descriptionDone = ['Deleted WebKitBuild directory']
758     command = ['python', 'Tools/BuildSlaveSupport/clean-build', WithProperties('--platform=%(fullPlatform)s'), WithProperties('--%(configuration)s')]
759
760
761 class KillOldProcesses(shell.Compile):
762     name = 'kill-old-processes'
763     description = ['killing old processes']
764     descriptionDone = ['Killed old processes']
765     command = ['python', 'Tools/BuildSlaveSupport/kill-old-processes', 'buildbot']
766
767     def __init__(self, **kwargs):
768         super(KillOldProcesses, self).__init__(timeout=60, logEnviron=False, **kwargs)
769
770
771 class RunWebKitTests(shell.Test):
772     name = 'layout-tests'
773     description = ['layout-tests running']
774     descriptionDone = ['layout-tests']
775     resultDirectory = 'layout-test-results'
776     command = ['python', 'Tools/Scripts/run-webkit-tests',
777                '--no-build',
778                '--no-new-test-results',
779                '--no-show-results',
780                '--exit-after-n-failures', '30',
781                '--skip-failing-tests',
782                WithProperties('--%(configuration)s')]
783
784     def start(self):
785         platform = self.getProperty('platform')
786         appendCustomBuildFlags(self, platform, self.getProperty('fullPlatform'))
787         additionalArguments = self.getProperty('additionalArguments')
788
789         self.setCommand(self.command + ['--results-directory', self.resultDirectory])
790         self.setCommand(self.command + ['--debug-rwt-logging'])
791
792         if additionalArguments:
793             self.setCommand(self.command + additionalArguments)
794         return shell.Test.start(self)
795
796     def evaluateCommand(self, cmd):
797         rc = super(RunWebKitTests, self).evaluateCommand(cmd)
798         if rc == SUCCESS:
799             message = 'Passed layout tests'
800             self.descriptionDone = message
801             self.build.results = SUCCESS
802             self.build.buildFinished([message], SUCCESS)
803         else:
804             self.build.addStepsAfterCurrentStep([ArchiveTestResults(), UploadTestResults(), ExtractTestResults(), ReRunWebKitTests()])
805         return rc
806
807
808 class ReRunWebKitTests(RunWebKitTests):
809     name = 're-run-layout-tests'
810
811     def evaluateCommand(self, cmd):
812         rc = shell.Test.evaluateCommand(self, cmd)
813         if rc == SUCCESS:
814             message = 'Passed layout tests'
815             self.descriptionDone = message
816             self.build.results = SUCCESS
817             self.build.buildFinished([message], SUCCESS)
818         else:
819             self.setProperty('patchFailedTests', True)
820             self.build.addStepsAfterCurrentStep([ArchiveTestResults(), UploadTestResults(identifier='rerun'), ExtractTestResults(identifier='rerun'), UnApplyPatchIfRequired(), CompileWebKitToT(), RunWebKitTestsWithoutPatch()])
821         return rc
822
823
824 class RunWebKitTestsWithoutPatch(RunWebKitTests):
825     name = 'run-layout-tests-without-patch'
826
827     def evaluateCommand(self, cmd):
828         rc = shell.Test.evaluateCommand(self, cmd)
829         self.build.addStepsAfterCurrentStep([ArchiveTestResults(), UploadTestResults(identifier='clean-tree'), ExtractTestResults(identifier='clean-tree')])
830         return rc
831
832
833 class RunWebKit1Tests(RunWebKitTests):
834     def start(self):
835         self.setCommand(self.command + ['--dump-render-tree'])
836
837         return RunWebKitTests.start(self)
838
839
840 class ArchiveBuiltProduct(shell.ShellCommand):
841     command = ['python', 'Tools/BuildSlaveSupport/built-product-archive',
842                WithProperties('--platform=%(fullPlatform)s'), WithProperties('--%(configuration)s'), 'archive']
843     name = 'archive-built-product'
844     description = ['archiving built product']
845     descriptionDone = ['Archived built product']
846     haltOnFailure = True
847
848     def __init__(self, **kwargs):
849         super(ArchiveBuiltProduct, self).__init__(logEnviron=False, **kwargs)
850
851
852 class UploadBuiltProduct(transfer.FileUpload):
853     name = 'upload-built-product'
854     workersrc = WithProperties('WebKitBuild/%(configuration)s.zip')
855     masterdest = WithProperties('public_html/archives/%(fullPlatform)s-%(architecture)s-%(configuration)s/%(patch_id)s.zip')
856     descriptionDone = ['Uploaded built product']
857     haltOnFailure = True
858
859     def __init__(self, **kwargs):
860         kwargs['workersrc'] = self.workersrc
861         kwargs['masterdest'] = self.masterdest
862         kwargs['mode'] = 0644
863         kwargs['blocksize'] = 1024 * 256
864         transfer.FileUpload.__init__(self, **kwargs)
865
866     def getResultSummary(self):
867         if self.results != SUCCESS:
868             return {u'step': u'Failed to upload built product'}
869         return super(UploadBuiltProduct, self).getResultSummary()
870
871
872 class TransferToS3(master.MasterShellCommand):
873     name = 'transfer-to-s3'
874     description = ['transferring to s3']
875     descriptionDone = ['Transferred archive to S3']
876     archive = WithProperties('public_html/archives/%(fullPlatform)s-%(architecture)s-%(configuration)s/%(patch_id)s.zip')
877     identifier = WithProperties('%(fullPlatform)s-%(architecture)s-%(configuration)s')
878     patch_id = WithProperties('%(patch_id)s')
879     command = ['python', '../Shared/transfer-archive-to-s3', '--patch_id', patch_id, '--identifier', identifier, '--archive', archive]
880     haltOnFailure = True
881     flunkOnFailure = True
882
883     def __init__(self, **kwargs):
884         kwargs['command'] = self.command
885         master.MasterShellCommand.__init__(self, logEnviron=False, **kwargs)
886
887     def start(self):
888         self.log_observer = logobserver.BufferLogObserver(wantStderr=True)
889         self.addLogObserver('stdio', self.log_observer)
890         return super(TransferToS3, self).start()
891
892     def finished(self, results):
893         log_text = self.log_observer.getStdout() + self.log_observer.getStderr()
894         match = re.search(r'S3 URL: (?P<url>[^\s]+)', log_text)
895         # Sample log: S3 URL: https://s3-us-west-2.amazonaws.com/ews-archives.webkit.org/ios-simulator-12-x86_64-release/123456.zip
896         if match:
897             self.addURL('uploaded archive', match.group('url'))
898
899         if results == SUCCESS:
900             triggers = self.getProperty('triggers', None)
901             if triggers:
902                 self.build.addStepsAfterCurrentStep([Trigger(schedulerNames=triggers)])
903
904         return super(TransferToS3, self).finished(results)
905
906     def hideStepIf(self, results, step):
907         return results == SUCCESS and self.getProperty('validated', '') == False
908
909     def getResultSummary(self):
910         if self.results != SUCCESS:
911             return {u'step': u'Failed to transfer archive to S3'}
912         return super(TransferToS3, self).getResultSummary()
913
914
915 class DownloadBuiltProduct(shell.ShellCommand):
916     command = ['python', 'Tools/BuildSlaveSupport/download-built-product',
917         WithProperties('--%(configuration)s'),
918         WithProperties(S3URL + 'ews-archives.webkit.org/%(fullPlatform)s-%(architecture)s-%(configuration)s/%(patch_id)s.zip')]
919     name = 'download-built-product'
920     description = ['downloading built product']
921     descriptionDone = ['Downloaded built product']
922     haltOnFailure = True
923     flunkOnFailure = True
924
925     def getResultSummary(self):
926         if self.results != SUCCESS:
927             return {u'step': u'Failed to download built product from S3'}
928         return super(DownloadBuiltProduct, self).getResultSummary()
929
930     def __init__(self, **kwargs):
931         super(DownloadBuiltProduct, self).__init__(logEnviron=False, **kwargs)
932
933
934 class ExtractBuiltProduct(shell.ShellCommand):
935     command = ['python', 'Tools/BuildSlaveSupport/built-product-archive',
936                WithProperties('--platform=%(fullPlatform)s'), WithProperties('--%(configuration)s'), 'extract']
937     name = 'extract-built-product'
938     description = ['extracting built product']
939     descriptionDone = ['Extracted built product']
940     haltOnFailure = True
941     flunkOnFailure = True
942
943     def __init__(self, **kwargs):
944         super(ExtractBuiltProduct, self).__init__(logEnviron=False, **kwargs)
945
946
947 class RunAPITests(TestWithFailureCount):
948     name = 'run-api-tests'
949     description = ['api tests running']
950     descriptionDone = ['api-tests']
951     jsonFileName = 'api_test_results.json'
952     logfiles = {'json': jsonFileName}
953     command = ['python', 'Tools/Scripts/run-api-tests', '--no-build',
954                WithProperties('--%(configuration)s'), '--verbose', '--json-output={0}'.format(jsonFileName)]
955     failedTestsFormatString = '%d api test%s failed or timed out'
956
957     def __init__(self, **kwargs):
958         super(RunAPITests, self).__init__(logEnviron=False, **kwargs)
959
960     def start(self):
961         appendCustomBuildFlags(self, self.getProperty('platform'), self.getProperty('fullPlatform'))
962         return TestWithFailureCount.start(self)
963
964     def countFailures(self, cmd):
965         log_text = self.log_observer.getStdout() + self.log_observer.getStderr()
966
967         match = re.search(r'Ran (?P<ran>\d+) tests of (?P<total>\d+) with (?P<passed>\d+) successful', log_text)
968         if not match:
969             return 0
970         return int(match.group('ran')) - int(match.group('passed'))
971
972     def evaluateCommand(self, cmd):
973         rc = super(RunAPITests, self).evaluateCommand(cmd)
974         if rc == SUCCESS:
975             message = 'Passed API tests'
976             self.descriptionDone = message
977             self.build.results = SUCCESS
978             self.build.buildFinished([message], SUCCESS)
979         else:
980             self.build.addStepsAfterCurrentStep([ReRunAPITests()])
981         return rc
982
983
984 class ReRunAPITests(RunAPITests):
985     name = 're-run-api-tests'
986
987     def evaluateCommand(self, cmd):
988         rc = TestWithFailureCount.evaluateCommand(self, cmd)
989         if rc == SUCCESS:
990             message = 'Passed API tests'
991             self.descriptionDone = message
992             self.build.results = SUCCESS
993             self.build.buildFinished([message], SUCCESS)
994         else:
995             self.setProperty('patchFailedTests', True)
996             self.build.addStepsAfterCurrentStep([UnApplyPatchIfRequired(), CompileWebKitToT(), RunAPITestsWithoutPatch(), AnalyzeAPITestsResults()])
997         return rc
998
999
1000 class RunAPITestsWithoutPatch(RunAPITests):
1001     name = 'run-api-tests-without-patch'
1002
1003     def evaluateCommand(self, cmd):
1004         return TestWithFailureCount.evaluateCommand(self, cmd)
1005
1006
1007 class AnalyzeAPITestsResults(buildstep.BuildStep):
1008     name = 'analyze-api-tests-results'
1009     description = ['analyze-api-test-results']
1010     descriptionDone = ['analyze-api-tests-results']
1011
1012     def start(self):
1013         self.results = {}
1014         d = self.getTestsResults(RunAPITests.name)
1015         d.addCallback(lambda res: self.getTestsResults(ReRunAPITests.name))
1016         d.addCallback(lambda res: self.getTestsResults(RunAPITestsWithoutPatch.name))
1017         d.addCallback(lambda res: self.analyzeResults())
1018         return defer.succeed(None)
1019
1020     def analyzeResults(self):
1021         if not self.results or len(self.results) == 0:
1022             self._addToLog('stderr', 'Unable to parse API test results: {}'.format(self.results))
1023             self.finished(RETRY)
1024             self.build.buildFinished(['Unable to parse API test results'], RETRY)
1025             return -1
1026
1027         first_run_results = self.results.get(RunAPITests.name)
1028         second_run_results = self.results.get(ReRunAPITests.name)
1029         clean_tree_results = self.results.get(RunAPITestsWithoutPatch.name)
1030
1031         if not (first_run_results and second_run_results and clean_tree_results):
1032             self.finished(RETRY)
1033             self.build.buildFinished(['Unable to parse API test results'], RETRY)
1034             return -1
1035
1036         def getAPITestFailures(result):
1037             # TODO: Analyze Time-out, Crash and Failure independently
1038             return set([failure.get('name') for failure in result.get('Timedout', [])] +
1039                 [failure.get('name') for failure in result.get('Crashed', [])] +
1040                 [failure.get('name') for failure in result.get('Failed', [])])
1041
1042         first_run_failures = getAPITestFailures(first_run_results)
1043         second_run_failures = getAPITestFailures(second_run_results)
1044         clean_tree_failures = getAPITestFailures(clean_tree_results)
1045
1046         failures_with_patch = first_run_failures.intersection(second_run_failures)
1047         flaky_failures = first_run_failures.union(second_run_failures) - first_run_failures.intersection(second_run_failures)
1048         flaky_failures_string = ', '.join([failure_name.replace('TestWebKitAPI.', '') for failure_name in flaky_failures])
1049         new_failures = failures_with_patch - clean_tree_failures
1050         new_failures_string = ', '.join([failure_name.replace('TestWebKitAPI.', '') for failure_name in new_failures])
1051
1052         self._addToLog('stderr', '\nFailures in API Test first run: {}'.format(first_run_failures))
1053         self._addToLog('stderr', '\nFailures in API Test second run: {}'.format(second_run_failures))
1054         self._addToLog('stderr', '\nFlaky Tests: {}'.format(flaky_failures))
1055         self._addToLog('stderr', '\nFailures in API Test on clean tree: {}'.format(clean_tree_failures))
1056
1057         if new_failures:
1058             self._addToLog('stderr', '\nNew failures: {}\n'.format(new_failures))
1059             self.finished(FAILURE)
1060             self.build.results = FAILURE
1061             pluralSuffix = 's' if len(new_failures) > 1 else ''
1062             message = 'Found {} new API Test failure{}: {}'.format(len(new_failures), pluralSuffix, new_failures_string)
1063             self.descriptionDone = message
1064             self.build.buildFinished([message], FAILURE)
1065         else:
1066             self._addToLog('stderr', '\nNo new failures\n')
1067             self.finished(SUCCESS)
1068             self.build.results = SUCCESS
1069             self.descriptionDone = 'Passed API tests'
1070             pluralSuffix = 's' if len(clean_tree_failures) > 1 else ''
1071             message = 'Found {} pre-existing API test failure{}'.format(len(clean_tree_failures), pluralSuffix)
1072             if flaky_failures:
1073                 message += '. Flaky tests: {}'.format(flaky_failures_string)
1074             self.build.buildFinished([message], SUCCESS)
1075
1076     @defer.inlineCallbacks
1077     def _addToLog(self, logName, message):
1078         try:
1079             log = self.getLog(logName)
1080         except KeyError:
1081             log = yield self.addLog(logName)
1082         log.addStdout(message)
1083
1084     def getBuildStepByName(self, name):
1085         for step in self.build.executedSteps:
1086             if step.name == name:
1087                 return step
1088         return None
1089
1090     @defer.inlineCallbacks
1091     def getTestsResults(self, name):
1092         step = self.getBuildStepByName(name)
1093         if not step:
1094             self._addToLog('stderr', 'ERROR: step not found: {}'.format(step))
1095             defer.returnValue(None)
1096
1097         logs = yield self.master.db.logs.getLogs(step.stepid)
1098         log = next((log for log in logs if log['name'] == u'json'), None)
1099         if not log:
1100             self._addToLog('stderr', 'ERROR: log for step not found: {}'.format(step))
1101             defer.returnValue(None)
1102
1103         lastline = int(max(0, log['num_lines'] - 1))
1104         logLines = yield self.master.db.logs.getLogLines(log['id'], 0, lastline)
1105         if log['type'] == 's':
1106             logLines = ''.join([line[1:] for line in logLines.splitlines()])
1107
1108         try:
1109             self.results[name] = json.loads(logLines)
1110         except Exception as ex:
1111             self._addToLog('stderr', 'ERROR: unable to parse data, exception: {}'.format(ex))
1112
1113
1114 class ArchiveTestResults(shell.ShellCommand):
1115     command = ['python', 'Tools/BuildSlaveSupport/test-result-archive',
1116                Interpolate('--platform=%(prop:platform)s'), Interpolate('--%(prop:configuration)s'), 'archive']
1117     name = 'archive-test-results'
1118     description = ['archiving test results']
1119     descriptionDone = ['Archived test results']
1120     haltOnFailure = True
1121
1122
1123 class UploadTestResults(transfer.FileUpload):
1124     name = 'upload-test-results'
1125     descriptionDone = ['Uploaded test results']
1126     workersrc = 'layout-test-results.zip'
1127     haltOnFailure = True
1128
1129     def __init__(self, identifier='', **kwargs):
1130         if identifier and not identifier.startswith('-'):
1131             identifier = '-{}'.format(identifier)
1132         kwargs['workersrc'] = self.workersrc
1133         kwargs['masterdest'] = Interpolate('public_html/results/%(prop:buildername)s/r%(prop:patch_id)s-%(prop:buildnumber)s{}.zip'.format(identifier))
1134         kwargs['mode'] = 0644
1135         kwargs['blocksize'] = 1024 * 256
1136         transfer.FileUpload.__init__(self, **kwargs)
1137
1138
1139 class ExtractTestResults(master.MasterShellCommand):
1140     name = 'extract-test-results'
1141     descriptionDone = ['Extracted test results']
1142     renderables = ['resultDirectory', 'zipFile']
1143     haltOnFailure = False
1144     flunkOnFailure = False
1145
1146     def __init__(self, identifier=''):
1147         if identifier and not identifier.startswith('-'):
1148             identifier = '-{}'.format(identifier)
1149
1150         self.zipFile = Interpolate('public_html/results/%(prop:buildername)s/r%(prop:patch_id)s-%(prop:buildnumber)s{}.zip'.format(identifier))
1151         self.resultDirectory = Interpolate('public_html/results/%(prop:buildername)s/r%(prop:patch_id)s-%(prop:buildnumber)s{}'.format(identifier))
1152         self.command = ['unzip', self.zipFile, '-d', self.resultDirectory]
1153
1154         super(ExtractTestResults, self).__init__(self.command)
1155
1156     def resultDirectoryURL(self):
1157         return self.resultDirectory.replace('public_html/', '/') + '/'
1158
1159     def resultsDownloadURL(self):
1160         return self.zipFile.replace('public_html/', '/')
1161
1162     def addCustomURLs(self):
1163         self.addURL('view layout test results', self.resultDirectoryURL() + 'results.html')
1164         self.addURL('download layout test results', self.resultsDownloadURL())
1165
1166     def finished(self, result):
1167         self.addCustomURLs()
1168         return master.MasterShellCommand.finished(self, result)
1169
1170
1171 class PrintConfiguration(steps.ShellSequence):
1172     name = 'configuration'
1173     description = ['configuration']
1174     haltOnFailure = False
1175     flunkOnFailure = False
1176     warnOnFailure = False
1177     logEnviron = False
1178     command_list = [['hostname'],
1179                     ['df', '-hl'],
1180                     ['date'],
1181                     ['sw_vers'],
1182                     ['xcodebuild', '-sdk', '-version']]
1183
1184     def __init__(self, **kwargs):
1185         super(PrintConfiguration, self).__init__(timeout=60, **kwargs)
1186         self.commands = []
1187         self.log_observer = logobserver.BufferLogObserver(wantStderr=True)
1188         self.addLogObserver('stdio', self.log_observer)
1189         # FIXME: Check platform before running platform specific commands.
1190         for command in self.command_list:
1191             self.commands.append(util.ShellArg(command=command, logfile='stdio'))
1192
1193     def convert_build_to_os_name(self, build):
1194         if not build:
1195             return 'Unknown'
1196
1197         build_to_name_mapping = {
1198             '10.15': 'Catalina',
1199             '10.14': 'Mojave',
1200             '10.13': 'High Sierra',
1201             '10.12': 'Sierra',
1202             '10.11': 'El Capitan',
1203             '10.10': 'Yosemite',
1204             '10.9': 'Maverick',
1205             '10.8': 'Mountain Lion',
1206             '10.7': 'Lion',
1207             '10.6': 'Snow Leopard',
1208             '10.5': 'Leopard',
1209         }
1210
1211         for key, value in build_to_name_mapping.iteritems():
1212             if build.startswith(key):
1213                 return value
1214         return 'Unknown'
1215
1216     def getResultSummary(self):
1217         if self.results != SUCCESS:
1218             return {u'step': u'Failed to print configuration'}
1219         logText = self.log_observer.getStdout() + self.log_observer.getStderr()
1220         configuration = u''
1221         match = re.search('ProductVersion:[ \t]*(.+?)\n', logText)
1222         if match:
1223             os_version = match.group(1).strip()
1224             os_name = self.convert_build_to_os_name(os_version)
1225             configuration = u'OS: {} ({})'.format(os_name, os_version)
1226
1227         xcode_re = sdk_re = 'Xcode[ \t]+?([0-9.]+?)\n'
1228         match = re.search(xcode_re, logText)
1229         if match:
1230             xcode_version = match.group(1).strip()
1231             configuration += u', Xcode: {}'.format(xcode_version)
1232         return {u'step': configuration}