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