blob: 21c00817e6ab098f6629fd120da044c2bf1cb368 [file] [log] [blame]
import os
import re
import json
import socket
from buildbot.scheduler import AnyBranchScheduler
from buildbot.schedulers.basic import SingleBranchScheduler
from buildbot.schedulers.timed import Nightly
from buildbot.schedulers.forcesched import ForceScheduler
from buildbot.process.factory import BuildFactory
from buildbot.config import BuilderConfig
from buildbot.process.properties import renderer
from buildbot.process.properties import Interpolate as I
from buildbot.process.properties import Property as P
from buildbot.changes.filter import ChangeFilter
from buildbot.steps.source.git import Git
from buildbot.steps.transfer import FileDownload
from buildbot.steps.shell import ShellCommand, Test, SetPropertyFromCommand
from buildbot.steps.master import SetProperty
from buildbot.status import words, results
# config
MEDIC_CONFIG_FILE = os.path.join(FP, 'cordova-config.json')
REPOS_CONFIG_FILE = os.path.join(FP, 'cordova-repos.json')
def parse_config_file(file_name):
with open(file_name, 'r') as config_file:
return json.load(config_file)
medic_config = parse_config_file(MEDIC_CONFIG_FILE)
repos_config = parse_config_file(REPOS_CONFIG_FILE)
# constants
BASE_WORKDIR = '.'
TEST_APP_NAME = 'mobilespec'
EXTRA_CONFIG_FILE_NAME = 'cordova-extra.conf'
NPM_CACHE_DIR_NAME = 'npm_cache'
NPM_TEMP_DIR_NAME = 'npm_tmp'
COUCHDB_URI = medic_config['couchdb']['uri']
ENTRY_POINT = medic_config['app']['entry']
TEST_RUN_TIMEOUT = medic_config['app']['timeout'] # in seconds
LOG_GETTING_TIMEOUT = 30 # in seconds
TEST_SUMMARY_FILE_NAME = 'test_summary.json'
MASTER_HOSTNAME = socket.gethostname()
GIT_RETRY_DELAY = 10
GIT_RETRY_TIMES = 3
CORDOVA_SUPPORTED_CATEGORY = 'cordova'
CORDOVA_UNSUPPORTED_CATEGORY = 'cordova-medic-unsupported'
CORE_PLUGINS = [
'cordova-plugins',
'cordova-plugin-test-framework',
'cordova-plugin-battery-status',
'cordova-plugin-camera',
'cordova-plugin-console',
'cordova-plugin-contacts',
'cordova-plugin-device',
'cordova-plugin-device-motion',
'cordova-plugin-device-orientation',
'cordova-plugin-dialogs',
'cordova-plugin-file',
'cordova-plugin-file-transfer',
'cordova-plugin-geolocation',
'cordova-plugin-globalization',
'cordova-plugin-inappbrowser',
'cordova-plugin-media',
'cordova-plugin-media-capture',
'cordova-plugin-network-information',
'cordova-plugin-splashscreen',
'cordova-plugin-statusbar',
'cordova-plugin-vibration',
'cordova-plugin-whitelist',
]
# NOTE:
# this is a special value that must be '' in order for
# all other non-Cordova builders on a master to work;
# more info is in the Buildbot documentation:
# http://docs.buildbot.net/0.8.10/manual/concepts.html#codebase
# http://docs.buildbot.net/0.8.10/manual/concepts.html#multiple-codebase-builds
SPECIAL_DEFAULT_CODEBASE_NAME = ''
# patterns
CORDOVA_REPO_PATTERN = r'.*(cordova-[^\.]+).*'
####### UTILITIES
# custom steps
class DisplayResults(Test):
def start(self):
test_summary = json.loads(self.getProperty('test_summary'))
total = test_summary['total']
failed = test_summary['failed']
passed = test_summary['passed']
warnings = test_summary['warnings']
self.setTestResults(total=total, failed=failed, passed=passed, warnings=warnings)
self.finished(results.SUCCESS if failed == 0 else results.WARNINGS)
self.step_status.setText(self.describe(True))
# helper functions
def codebase_name_from_repo_uri(uri):
match = re.match(CORDOVA_REPO_PATTERN, uri)
if match is not None:
return match.group(1)
return SPECIAL_DEFAULT_CODEBASE_NAME
def default_codebase_repo_uri(codebase_name):
codebase = repos_config[codebase_name]
all_repos = codebase['repositories']
default_repo = codebase['default_repository']
return all_repos[default_repo]
def default_codebase_branch(codebase_name):
return repos_config[codebase_name]['default_branch']
def slugify(string):
return string.lower().replace(' ', '-')
def make_codebase(codebase_name):
'''
Return a dict of default values for the given codebase.
'''
return {
'repository': default_codebase_repo_uri(codebase_name),
'branch': default_codebase_branch(codebase_name),
'revision': None,
}
def get_platform_codebase(platform):
'''
Return the name of the codebase for the given platform.
This is almost always 'cordova-[PLATFORM]', except for
'blackberry', where it's 'blackberry10'.
'''
repo_name = 'cordova-{0}'.format(platform)
if platform == 'blackberry10':
repo_name = 'cordova-blackberry'
return repo_name
# step wrappers
def DescribedStep(step_class, description, haltOnFailure=True, **kwargs):
return step_class(description=description, descriptionDone=description, name=slugify(description), haltOnFailure=haltOnFailure, **kwargs)
def SH(workdir=BASE_WORKDIR, timeout=TEST_RUN_TIMEOUT, **kwargs):
return DescribedStep(ShellCommand, workdir=workdir, timeout=timeout, **kwargs)
def NPM(npm_subcommand, command=list(), what='code', **kwargs):
return SH(
command = ['npm', npm_subcommand] + command,
description = 'npm ' + npm_subcommand + 'ing ' + what,
env = {'npm_config_prefix': P('npm_prefix_dir')},
**kwargs
)
def NPMInstall(command=list(), **kwargs):
# NOTE:
# adding the --cache parameter so that we don't use the global
# npm cache, which is shared with other processes
#
# adding the --tmp parameter so that even if the command doesn't
# exit cleanly, the folder will get removed during cleanup;
# refer to: https://docs.npmjs.com/files/folders#temp-files
return NPM('install', command=command + ['--cache', P('npm_cache_dir'), '--tmp', P('npm_temp_dir')], **kwargs)
def NPMTest(**kwargs):
return NPM('test', **kwargs)
def NPMLink(**kwargs):
return NPM('link', **kwargs)
def Clone(repourl, **kwargs):
return DescribedStep(Git, 'cloning', repourl=repourl, mode='full', method='clean', shallow=False, retryFetch=True, retry=(GIT_RETRY_DELAY, GIT_RETRY_TIMES), **kwargs)
def CodebaseClone(codebase, *args, **kwargs):
return Clone(repourl=I('%(src:' + codebase + ':repository)s'), codebase=codebase, workdir=codebase, **kwargs)
def Set(name, value, **kwargs):
return DescribedStep(SetProperty, 'setting ' + name, property=name, value=value, **kwargs)
def SetFromCommand(name, command, **kwargs):
return DescribedStep(SetPropertyFromCommand, 'setting ' + name, property=name, command=command, **kwargs)
def Download(mastersrc, slavedest, description, **kwargs):
# NOTE:
# the FileDownload step has a bug and requires
# the 'description' parameter to be a list
return FileDownload(mastersrc=mastersrc, slavedest=slavedest, description=[description], workdir=BASE_WORKDIR, **kwargs)
####### SLAVES
# NOTE:
# these slave names refer to the ones specified in master.cfg,
# and they must remain defined in master.cfg in order to work
# with the master.cfg used on Apache's Buildbot
OSX_SLAVES = ['cordova-osx-slave']
WINDOWS_SLAVES = ['cordova-windows-slave']
ALL_CORDOVA_SLAVES = OSX_SLAVES + WINDOWS_SLAVES
####### CHANGESOURCES
# None, because Apache Buildbot's master.cfg manages them, and since
# this file is shared with Apache Buildbot, we should not touch them.
####### CODEBASES
def cordovaCG(change_dict):
return codebase_name_from_repo_uri(change_dict['repository'])
def overrideCG(existingCG):
def newCG(change):
codebase = cordovaCG(change)
if codebase == SPECIAL_DEFAULT_CODEBASE_NAME:
codebase = existingCG(change)
return codebase
return newCG
# NOTE:
# codebaseGenerator is a magic function required by Buildbot
#
# if a codebaseGenerator is already defined, we must only extend it;
# we choose to take precedence over the other implementation, only
# calling it if our own returns the SPECIAL_DEFAULT_CODEBASE_NAME codebase
if 'codebaseGenerator' in c:
codebaseGenerator = overrideCG(c['codebaseGenerator'])
else:
codebaseGenerator = cordovaCG
c['codebaseGenerator'] = codebaseGenerator
CORDOVA_CODEBASES = {codebase_name: make_codebase(codebase_name) for codebase_name in repos_config.keys()}
####### STEPS
CORDOVA_STEPS_SET_SETTINGS = [
Set('build_id', I('%(prop:buildername)s-%(prop:buildnumber)s-' + MASTER_HOSTNAME)),
Set('test_summary_file', I('%(prop:builddir)s/' + TEST_SUMMARY_FILE_NAME)),
Set('npm_cache_dir', I('%(prop:builddir)s/' + NPM_CACHE_DIR_NAME)),
Set('npm_temp_dir', I('%(prop:builddir)s/' + NPM_TEMP_DIR_NAME)),
Set('npm_prefix_dir', I('%(prop:builddir)s/')),
SetFromCommand('npm_version', ['npm', '--version']),
SetFromCommand('node_version', ['node', '--version']),
SH(command=['npm', 'ls', '-g'], description='getting global modules'),
]
CORDOVA_STEPS_CLEAN_UP = [
SH(command=['node', 'cordova-medic/medic/medic.js', 'clean', '.', '--exclude', 'cordova-medic', '--exclude', NPM_CACHE_DIR_NAME], description='cleaning workspace'),
]
CORDOVA_STEPS_GET_MEDIC = [
CodebaseClone('cordova-medic'),
# NOTE:
# --production switch is used to speed up installation + fruitstrap dev dependency is not supported on Windows
NPMInstall(command=['--production'], what='cordova-medic', workdir='cordova-medic'),
]
CORDOVA_STEPS_GET_TOOLS = [
# clone cordova CLI tools
CodebaseClone('cordova-cli'),
CodebaseClone('cordova-lib'),
CodebaseClone('cordova-js'),
CodebaseClone('cordova-plugman'),
# install js
NPMInstall(workdir='cordova-js', what='cordova-js'),
NPMLink(workdir='cordova-js', what='cordova-js'),
# install common lib code
NPMInstall(workdir='cordova-lib/cordova-common', what='cordova-common'),
NPMLink(workdir='cordova-lib/cordova-common', what='cordova-common'),
# install lib
NPMLink(command=['cordova-js'], workdir='cordova-lib/cordova-lib', what='cloned cordova-js'),
NPMLink(command=['cordova-common'], workdir='cordova-lib/cordova-lib', what='cloned cordova-common'),
NPMInstall(workdir='cordova-lib/cordova-lib', what='cordova-lib'),
NPMLink(workdir='cordova-lib/cordova-lib', what='cordova-lib'),
# install cli
NPMLink(command=['cordova-lib'], workdir='cordova-cli', what='cloned cordova-lib'),
NPMInstall(workdir='cordova-cli', what='cordova-cli'),
NPMLink(workdir='cordova-cli', what='cordova-cli'),
# install plugman
NPMLink(command=['cordova-lib'], workdir='cordova-plugman', what='cloned cordova-lib'),
NPMInstall(workdir='cordova-plugman', what='cordova-plugman'),
# clone and install other tools
CodebaseClone('cordova-coho'),
CodebaseClone('cordova-mobile-spec'),
NPMInstall(workdir='cordova-coho', what='cordova-coho'),
NPMInstall(workdir='cordova-mobile-spec/createmobilespec', what='cordova-mobile-spec'),
]
def cordova_steps_get_plugins(plugins):
steps = []
for plugin in plugins:
steps.append(CodebaseClone(plugin))
return steps
def cordova_steps_create_mobilespec(platform):
return [
# get and install platform
CodebaseClone(get_platform_codebase(platform)),
NPMInstall(what='platform', workdir=get_platform_codebase(platform)),
# create mobilespec
SH(
command = [
'node',
'cordova-mobile-spec/createmobilespec/createmobilespec.js',
'--copywww',
'--skiplink',
'--' + platform,
TEST_APP_NAME
],
description='creating mobilespec app'
),
]
def cordova_steps_run_tests(platform, extra_args=list()):
return [
# download medic's config to slave
Download(mastersrc=MEDIC_CONFIG_FILE, slavedest='cordova-medic/config.json', description='downloading master\'s config'),
SH(
command = [
'node',
'cordova-medic/medic/medic.js',
'run',
'--id', P('build_id'),
'--platform', platform,
'--couchdb', COUCHDB_URI,
'--entry', ENTRY_POINT,
'--app', TEST_APP_NAME,
# NOTE:
# this timeout is smaller because TEST_RUN_TIMEOUT is used as the
# buildbot timeout, and the "run" command needs to time out before
# the buildbot wrapper times out so it can exit cleanly on timeout
'--timeout', TEST_RUN_TIMEOUT - 60
] + extra_args,
description = 'running tests',
haltOnFailure = True,
),
SH(
command = [
'node',
'cordova-medic/medic/medic.js',
'log',
'--platform', platform,
'--app', TEST_APP_NAME,
'--timeout', TEST_RUN_TIMEOUT
],
description = 'gathering logs',
timeout = LOG_GETTING_TIMEOUT,
haltOnFailure = False,
flunkOnFailure = False,
alwaysRun = True,
),
SH(
command = [
'node',
'cordova-medic/medic/medic.js',
'check',
'--id', P('build_id'),
'--couchdb', COUCHDB_URI,
'--file', P('test_summary_file'),
],
description = 'getting test results',
haltOnFailure = True,
),
SetFromCommand('test_summary', ['cat', P('test_summary_file')], hideStepIf=True),
DisplayResults(warnOnWarnings=True),
]
def makeRunSteps(platform, extra_args=list()):
factory = BuildFactory()
factory.addSteps(CORDOVA_STEPS_SET_SETTINGS)
factory.addSteps(CORDOVA_STEPS_GET_MEDIC)
factory.addSteps([
SH(command=['node', 'cordova-medic/medic/medic.js', 'kill', '--platform', platform], description='killing running tasks'),
])
factory.addSteps(CORDOVA_STEPS_CLEAN_UP)
factory.addSteps(CORDOVA_STEPS_GET_TOOLS)
factory.addSteps(cordova_steps_get_plugins(CORE_PLUGINS))
factory.addSteps(cordova_steps_create_mobilespec(platform))
factory.addSteps(cordova_steps_run_tests(platform, extra_args=extra_args))
factory.addSteps([
SH(command=['node', 'cordova-medic/medic/medic.js', 'kill', '--platform', platform], description='killing running tasks'),
])
return factory
####### BUILDERS
cordova_run_android = makeRunSteps('android')
cordova_run_ios = makeRunSteps('ios')
cordova_run_ws81 = makeRunSteps('windows', extra_args=['--winvers', 'store'])
cordova_run_wp81 = makeRunSteps('windows', extra_args=['--winvers', 'phone'])
# cordova_run_bbry = makeRunSteps('blackberry10')
c['builders'].extend([
BuilderConfig(name='cordova-android-osx', slavenames=OSX_SLAVES, factory=cordova_run_android, category=CORDOVA_SUPPORTED_CATEGORY),
BuilderConfig(name='cordova-android-win', slavenames=WINDOWS_SLAVES, factory=cordova_run_android, category=CORDOVA_SUPPORTED_CATEGORY),
BuilderConfig(name='cordova-ios', slavenames=OSX_SLAVES, factory=cordova_run_ios, category=CORDOVA_SUPPORTED_CATEGORY),
BuilderConfig(name='cordova-windows-store8.1', slavenames=WINDOWS_SLAVES, factory=cordova_run_ws81, category=CORDOVA_SUPPORTED_CATEGORY),
BuilderConfig(name='cordova-windows-phone8.1', slavenames=WINDOWS_SLAVES, factory=cordova_run_wp81, category=CORDOVA_SUPPORTED_CATEGORY),
# BuilderConfig(name='cordova-blackberry-win', slavenames=WINDOWS_SLAVES, factory=cordova_run_bbry, category=CORDOVA_UNSUPPORTED_CATEGORY),
# BuilderConfig(name='cordova-blackberry-osx', slavenames=OSX_SLAVES, factory=cordova_run_bbry, category=CORDOVA_UNSUPPORTED_CATEGORY),
])
####### STATUS TARGETS
c['status'].extend([])
####### CHANGE FILTERS
cordova_e2e_run_triggers = ChangeFilter(codebase=CORE_PLUGINS + [
'cordova-cli',
'cordova-js',
'cordova-lib',
'cordova-plugman',
'cordova-mobile-spec',
'cordova-medic',
'cordova-windows',
'cordova-android',
'cordova-ios',
# 'cordova-blackberry',
])
####### SCHEDULERS
c['schedulers'].extend([
Nightly(
name = 'cordova-e2e-periodic',
reason = 'periodic',
codebases = CORDOVA_CODEBASES,
branch = None, # None means default branch
hour = [0, 6, 12, 18],
minute = 0,
onlyIfChanged = False,
builderNames = [
'cordova-android-osx',
'cordova-android-win',
'cordova-ios',
'cordova-windows-store8.1',
'cordova-windows-phone8.1',
# 'cordova-blackberry-win',
# 'cordova-blackberry-osx',
],
),
SingleBranchScheduler(
name = 'cordova-e2e-commit',
reason = 'commit',
# NOTE:
# this means that the scheduler will use source stamps
# that it saw when the build was triggered; otherwise, builds that
# build "master" might build different code if master changes
# between two builders from a set of builds that should be identical
createAbsoluteSourceStamps = True,
# NOTE:
# in addition to 'change_filter', the 'codebases' argument ALSO functions
# as a filter, and codebases not present in it will not trigger the scheduler
change_filter = cordova_e2e_run_triggers,
codebases = CORDOVA_CODEBASES,
builderNames = [
'cordova-android-osx',
'cordova-android-win',
'cordova-ios',
'cordova-windows-store8.1',
'cordova-windows-phone8.1',
# 'cordova-blackberry-win',
# 'cordova-blackberry-osx',
],
),
ForceScheduler(
name = 'cordova-e2e-force',
codebases = CORDOVA_CODEBASES,
builderNames = [
'cordova-android-osx',
'cordova-android-win',
'cordova-ios',
'cordova-windows-store8.1',
'cordova-windows-phone8.1',
# 'cordova-blackberry-win',
# 'cordova-blackberry-osx',
],
),
])
####### EXTRA CONFIG
# run the extra config file as if it was pasted
# below, passing it a copy of our globals
extra_config_path = os.path.join(FP, EXTRA_CONFIG_FILE_NAME)
if os.path.exists(extra_config_path):
print 'Loading extra Cordova config'
globals_copy = globals().copy()
execfile(extra_config_path, globals_copy, globals_copy)
else:
print 'No extra Cordova config found'