| # Copyright 2014 the Melange authors. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import re |
| |
| from buildbot.steps import shell |
| from buildbot.status import results |
| from buildbot.steps.source import git |
| from buildbot.process import properties |
| from buildbot import util, interfaces |
| from zope.interface import implements # pylint: disable=no-name-in-module |
| |
| import private |
| |
| FAILED_TESTS_REGEX = re.compile(r'^(FAIL:) (.*)$') |
| ERRORED_TESTS_REGEX = re.compile(r'^(ERROR:) (.*)$') |
| ERRORED_PYLINT_REGEX = re.compile(r'(make:) (\*\*\*) (\[pylint\] Error.*)') |
| TESTS_SUMMARY_REGEX = re.compile(r'^(Ran) ([0-9]+) tests in ([0-9]+.[0-9]+)s') |
| DOCS_LOCATION = private.buildbot_url |
| |
| |
| class PyUnitTestsObserver(object): |
| """Class reading the stdio log generated by PyUnit tests and returning |
| the tests results via some getter methods implemented.""" |
| |
| def __init__(self): |
| self.testsResults = {'errored': [], 'failed': []} |
| self.failedTestsCount = 0 |
| self.passedTestsCount = 0 |
| self.skippedTestsCount = 0 |
| self.errorTestsCount = 0 |
| self.totalTests = 0 |
| self.totalTime = 0 |
| self.regroupfailed = [(FAILED_TESTS_REGEX, 1)] |
| self.regroup_time = [TESTS_SUMMARY_REGEX] |
| self.regroup_error = [(ERRORED_TESTS_REGEX, 1)] |
| |
| def outLineReceived(self, line): |
| matched = False |
| |
| for regex in self.regroupfailed: |
| result = regex[0].search(line) |
| if result: |
| self.failedTestsCount += 1 |
| self.testsResults['failed'].append(result.groups()[regex[1]].strip()) |
| matched = True |
| |
| if not matched: |
| for regex in self.regroup_error: |
| result = regex[0].search(line) |
| if result: |
| self.testsResults['errored'].append( |
| result.groups()[regex[1]].strip()) |
| matched = True |
| |
| if not matched: |
| for regex in self.regroup_time: |
| result = regex.search(line) |
| if result: |
| self.totalTests = result.groups()[1] |
| self.totalTime = result.groups()[2] |
| matched = True |
| return |
| |
| if not matched: |
| if '.....' in line: |
| for chars in line: |
| if chars == 'S': |
| self.skippedTestsCount += 1 |
| if chars == '.': |
| self.passedTestsCount += 1 |
| if chars == 'E': |
| self.errorTestsCount += 1 |
| |
| |
| class TestCommand(shell.ShellCommand): |
| """ShellCommand class for Melange tests.""" |
| |
| def __init__(self, command=None, type=None, log_observer=None, **kwargs): |
| test_name = command[0] if command else "" |
| |
| shell.ShellCommand.__init__( |
| self, description=['Running ' + test_name], |
| descriptionDone=['Ran ' + test_name], |
| command=command, **kwargs) |
| |
| self.flunkOnFailure = True |
| self.warnOnWarnings = True |
| self.log_observer = log_observer |
| self.log_observer.__init__() |
| self.testsResults = None |
| self.type = type |
| |
| def createSummary(self, stdio): |
| self.updateStats(stdio) |
| |
| def updateStats(self, log): |
| stdio = log.getText() |
| stdio_lines = stdio.split('\n') |
| |
| total = fails = warnings = errors = 0 |
| hastests = False |
| |
| # This matches Nose and Django output |
| if not hastests: |
| fails += len(re.findall('FAIL:', stdio)) |
| errors += len( |
| re.findall('==========================================' |
| '============================\nERROR:', stdio)) |
| for number in re.findall(r"Ran (?P<count>[\d]+)", stdio): |
| total += int(number) |
| hastests = True |
| |
| self.log_observer = self.log_observer or PyUnitTestsObserver() |
| for lines in stdio_lines: |
| self.log_observer.outLineReceived(lines) |
| |
| self.testsResults = self.log_observer.testsResults |
| skippedTestsCount = self.log_observer.skippedTestsCount |
| |
| passed = total - (skippedTestsCount + fails + errors + warnings) |
| |
| # Update the step statistics. |
| if hastests: |
| self.step_status.setStatistic('total', total) |
| self.step_status.setStatistic('fails', fails) |
| self.step_status.setStatistic('errors', errors) |
| self.step_status.setStatistic('skipped', skippedTestsCount) |
| self.step_status.setStatistic('passed', passed) |
| |
| if fails > 0 or passed > 0 or errors > 0: |
| self.addHTMLLog(self.type + ' summary', self.createTestsSummary()) |
| |
| def createTestsSummary(self): |
| #TODO(piyush.devel): Use Templating to construct this HTML response. |
| html_open = '<html>' |
| html_closed = '</html>' |
| html_string = html_open |
| |
| test_report = self.testsResults |
| |
| html_string += str( |
| 'We ran ' + str(self.step_status.getStatistic('total')) |
| + ' tests </br> </br>') |
| html_string += str( |
| str(self.step_status.getStatistic('fails')) |
| + ' Tests failed. </br> </br>') |
| |
| if len(test_report['failed']): |
| html_string += 'Here is a list of failed tests </br> </br>' |
| for result in test_report['failed']: |
| html_string += result |
| html_string += '</br>' |
| html_string += '</br> </br> </br>' |
| |
| html_string += str(str(self.step_status.getStatistic('errors')) |
| + ' Tests exited with some error.</br>') |
| |
| if len(test_report['errored']): |
| html_string += 'Here is a list of errored tests </br> </br>' |
| for result in test_report['errored']: |
| html_string += result |
| html_string += '</br>' |
| html_string += '</br> </br> </br>' |
| |
| html_string += str(str(self.step_status.getStatistic('skipped')) |
| + ' Tests were skipped. </br> </br>') |
| return html_string+html_closed |
| |
| def getText2(self, cmd, result): |
| description = shell.ShellCommand.getText2(self, cmd, result) |
| if self.step_status.hasStatistic('total'): |
| def append(stat, fmtstring): |
| val = self.step_status.getStatistic(stat, 0) |
| if val: |
| description.append(fmtstring % val) |
| |
| append('total', '\n%d tests\n') |
| append('fails', '%d fails\n') |
| append('errors', '%d errors\n') |
| append('skipped', '%d skipped\n') |
| append('passed', '%d passed') |
| |
| return description |
| |
| def evaluateCommand(self, cmd): |
| stdio = self.getLog('stdio').getText() |
| fails = len(re.findall('FAIL:', stdio)) |
| errors = len(re.findall('========================\nERROR:', stdio)) |
| pylint_errors = len(re.findall(ERRORED_PYLINT_REGEX, stdio)) |
| |
| return results.FAILURE if any( |
| (fails, errors, pylint_errors)) else results.SUCCESS |
| |
| |
| class LinkDocs(shell.ShellCommand): |
| """This class inherits from shell.ShellCommand and defines a buildstep |
| to link epydoc documentation to specified url.""" |
| |
| def __init__(self, **kwargs): |
| """Calls the constructor of parent class in a buildbot-style, |
| non-super way.""" |
| shell.ShellCommand.__init__(self, **kwargs) |
| |
| def createSummary(self, log): |
| docs_location = DOCS_LOCATION |
| buildername = self.getProperty('buildername') |
| buildnumber = self.getProperty('buildnumber') |
| docs_url = '%sbuilds/%s/%d/docs/index.html' % ( |
| docs_location, buildername, buildnumber) |
| self.addURL('Documentation', docs_url) |
| |
| |
| class LinkBuild(shell.ShellCommand): |
| """This class inherits from shell.ShellCommand and defines a buildstep |
| to link the compressed build directory to specified url.""" |
| |
| def __init__(self, **kwargs): |
| """Calls the constructor of parent class in a buildbot-style, |
| non-super way.""" |
| shell.ShellCommand.__init__(self, **kwargs) |
| |
| def createSummary(self, log): |
| build_location = DOCS_LOCATION |
| buildername = self.getProperty('buildername') |
| buildnumber = self.getProperty('buildnumber') |
| build_url = '%sbuilds/%s/%d/build/build.tar.gz' % ( |
| build_location, buildername, buildnumber) |
| self.addURL('Compressed Build Directory', build_url) |
| |
| |
| def isImportant(change): |
| """Determines if a change should trigger a build.""" |
| not_important = ['LICENSE-2.0.txt', 'AUTHORS', 'README.md', '.*wiki'] |
| for file in change.files: |
| triggerBuild = True |
| for pattern in not_important: |
| match = re.match(pattern, file) |
| if match: |
| triggerBuild = False |
| break |
| if triggerBuild: |
| return True |
| return False |
| |
| |
| class UsersAreEmails(util.ComparableMixin): |
| implements(interfaces.IEmailLookup) |
| |
| @staticmethod |
| def getAddress(name): |
| return name |
| |
| |
| def populateBuildFactory(factory): |
| """This function populates the BuildFactory Object with |
| build steps.""" |
| |
| factory.addStep(git.Git( |
| repourl='https://melange.googlesource.com/soc/', |
| mode='full', clobberOnFailure=True, |
| submodules=True, |
| name='Fetching Source code.')) |
| |
| factory.addStep(shell.ShellCommand( |
| command=['make', 'initial-setup'], |
| name='Initial Setup.')) |
| factory.addStep(shell.ShellCommand( |
| command=['make', 'setup'], |
| name='Setup.')) |
| factory.addStep(TestCommand( |
| command=['make', 'pylint'], |
| name='PyLint', |
| type='PyLint')) |
| factory.addStep(shell.ShellCommand( |
| command=['make', 'jstest'], |
| name='Javascript Tests.')) |
| factory.addStep(TestCommand( |
| command=['make', 'functionaltest'], |
| name='Functional Tests', |
| type='Functional')) |
| factory.addStep(TestCommand( |
| command=['make', 'pyunit'], |
| name='PyUnit Tests', |
| type='PyUnit')) |
| factory.addStep(shell.ShellCommand( |
| command=['make', 'docs'], |
| name='Documentation.')) |
| |
| # Let's move the project generated documentation |
| factory.addStep(shell.ShellCommand( |
| command=['/bin/sh', '-c', |
| properties.WithProperties( |
| 'echo %s/%s | tee ../documentationsubdir', |
| 'buildername', 'buildnumber')], |
| description="Save build ID", |
| name='Saving build ID')) |
| factory.addStep(LinkDocs( |
| command=['/bin/sh', '../../../master/upload.sh'], |
| description='Linking Documentation.', |
| name='Generating Documentation.')) |
| factory.addStep(LinkBuild( |
| command=['/bin/sh', '../../../master/upload_build_dir.sh'], |
| description='Linking Build Directory.', |
| name='Linking Build Directory.')) |
| return factory |