blob: b754d296f097a6c29857f20c991ab5a64ded8ccd [file] [log] [blame]
# 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
import private
FAILED_TESTS_REGEX = re.compile('^(FAIL:) (.*)$')
ERRORED_TESTS_REGEX = re.compile('^(ERROR:) (.*)$')
TESTS_SUMMARY_REGEX = re.compile('^(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, 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
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("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 ('Tests 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 results in test_report['failed']:
html_string += results
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 results in test_report['errored']:
html_string += results
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, results):
description = shell.ShellCommand.getText2(self, cmd, results)
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))
return results.FAILURE if fails > 0 or errors > 0 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)
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
def populateBuildFactory(factory):
"""This function populates the BuildFactory Object with
build steps."""
factory.addStep(git.Git(repourl='http://code.google.com/p/soc/',
mode='full', clobberOnFailure=True,
submodules=True,
name='Fetching Source code.'))
factory.addStep(shell.ShellCommand(
command=['virtualenv', 'venv'],
name='Creating virtualenv.'))
factory.addStep(shell.ShellCommand(
command=['venv/bin/pip','install', '-U', 'lxml', 'setuptools'],
name='Pip installs.'))
factory.addStep(shell.ShellCommand(
command=['venv/bin/python', 'bootstrap.py'],
name='Bootstrap.py'))
factory.addStep(shell.ShellCommand(
command=['bin/buildout'],
name='bin/buildout'))
factory.addStep(shell.ShellCommand(
command=['bin/gen-app-yaml', '-f', 'local-devel'],
name='Writing Application name.'))
factory.addStep(shell.ShellCommand(
command=['bin/paver', 'build'],
name='Building source.'))
factory.addStep(TestCommand(
command=['bin/run-tests'],
name='Test run.'))
# 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.'))
return factory