Make use of Jenkins for Coversheet (#22)

This commit is contained in:
Cosmin Malutan 2014-06-11 18:26:15 +03:00 коммит произвёл Henrik Skupin
Родитель 5797da9f90
Коммит 438f2a081c
25 изменённых файлов: 449 добавлений и 1044 удалений

12
.gitignore поставляемый
Просмотреть файл

@ -1,3 +1,9 @@
*.DS_STORE
*.swp
*.egg-info
.DS_Store
*.pyc
*.bak
jenkins-env/
jenkins.out
*.log
*.war
log/
virtualenv-*

8
.travis.yml Normal file
Просмотреть файл

@ -0,0 +1,8 @@
language: python
python: 2.7
script: ./run_tests.sh
notifications:
email:
- dev-automation@lists.mozilla.org
irc:
- "irc.mozilla.org#automation"

1
README
Просмотреть файл

@ -1 +0,0 @@
See https://bugzilla.mozilla.org/show_bug.cgi?id=740228

106
README.md Normal file
Просмотреть файл

@ -0,0 +1,106 @@
# coversheet
Coversheet is a CI system for TPS, which allows to run tests for each daily
build of Firefox across all platforms.
## Setup
Before you can start the system the following commands have to be performed:
```bash
git clone git://github.com/mozilla/coversheet.git
cd coversheet
./setup.sh
```
You will need to have the Python header files installed:
* Ubuntu: Install the package via: `apt-get install python-dev`
* OSX, Windows: Install the latest [Python 2.7](http://www.python.org/getit/)
## Startup
To start Jenkins simply run `./start.py` from the coversheet directory. You
can tell when Jenkins is running by looking out for "Jenkins is fully up and
running" in the console output. You will also be able to view the web dashboard
by pointing your browser at http://localhost:8080/
## Jenkins URL
If you intend to connect to this Jenkins instance from another machine (for
example connecting additional nodes) you will need to update the `Jenkins URL`
to the IP or DNS name. This can be found in http://localhost:8080/configure
under the section headed "Jenkins Location".
## Adding new Nodes
To add Jenkins slaves to your master you have to create new nodes. You can use
one of the example nodes (Windows XP and Ubuntu) as a template. Once done the
nodes have to be connected to the master. Therefore Java has to be installed on
the node first.
### Windows:
Go to [www.java.com/download/](http://www.java.com/download/) and install the
latest version of Java JRE. Also make sure that the UAC is completely disabled,
and the screensaver and any energy settings have been turned off.
### Linux (Ubuntu):
Open the terminal or any other package manager and install the following
packages:
```bash
sudo add-apt-repository ppa:webupd8team/java
sudo apt-get update
sudo apt-get install oracle-java7-installer
```
Also make sure that the screensaver and any energy settings have been turned
off.
After Java has been installed open the appropriate node within Jenkins from the
nodes web browser like:
http://IP:8080/computer/windows_xp_32_01/
Now click the `Launch` button and the node should automatically connect to the
master. It will be used once a job for this type of platform has been requested
by the Pulse consumer.
## Using the Jenkins master as executor
If you want that the master node also executes jobs you will have to update its
labels and add/modify the appropriate platforms, e.g. `master mac 10.7 64bit`
for Mac OS X 10.7.
## Testing changes
In order to check that patches will apply and no Jenkins configuration changes
are missing from your changes you can run the `run_tests.sh` script. This uses
[Selenium](http://code.google.com/p/selenium/) and
[PhantomJS](http://phantomjs.org/) to save the configuration for each job and
reports any unexpected changes. Note that you will need to
[download](http://phantomjs.org/download.html) PhantomJS and put it in your
path in order for these tests to run.
## Merging branches
The main development on the coversheet code happens on the master branch. In
not yet specified intervals we are merging changesets into the staging branch.
It is used for testing all the new features before those go live on production.
When running those merge tasks you will have to obey the following steps:
1. Select the appropriate target branch
2. Run 'git rebase master' for staging or 'git rebase staging' for production
3. Run 'git pull' for the remote branch you want to push to
4. Ensure the merged patches are on top of the branch
5. Ensure that the Jenkins patch can be applied by running 'patch --dry-run -p1
<config/%BRANCH%/jenkins.patch'
6. Run 'git push' for the remote branch
For emergency fixes we are using cherry-pick to port individual fixes to the
staging and production branch:
1. Select the appropriate target branch
2. Run 'git cherry-pick %changeset%' to pick the specific changeset for the
current branch
3. Run 'git push' for the remote branch
Once the changes have been landed you will have to update the staging or
production machines. Run the following steps:
1. Run 'git reset --hard' to remove the locally applied patch
2. Pull the latest changes with 'git pull'
3. Apply the Jenkins patch with 'patch -p1 <config/%BRANCH%/jenkins.patch'
4. Restart Jenkins

Просмотреть файл

@ -1,23 +0,0 @@
{
"sync_account": {
"username": "crossweaveservices@mozilla.com",
"password": "crossweaveservicescrossweaveservices",
"passphrase": "r-jwcbc-zgf42-fjn72-p5vpp-iypmi"
},
"fx_account": {
"username": "crossweaveservices@restmail.net",
"password": "crossweaveservicescrossweaveservices"
},
"email": {
"username": "crossweave@mozilla.com",
"password": "",
"passednotificationlist": ["crossweave@mozilla.com"],
"notificationlist": ["crossweave@mozilla.com"]
},
"platform": "win32",
"os": "win7",
"es": "localhost:9200",
"serverURL": null,
"testdir": "__TESTDIR__",
"extensiondir": "__EXTENSIONDIR__"
}

Просмотреть файл

@ -1,38 +0,0 @@
# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (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.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is TPS.
#
# The Initial Developer of the Original Code is
# Mozilla Foundation.
# Portions created by the Initial Developer are Copyright (C) 2011
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Sam Garrett <samdgarrett@gmail.com>
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
#from pulse import TPSPulseMonitor

Просмотреть файл

@ -1,153 +0,0 @@
# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (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.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is TPS.
#
# The Initial Developer of the Original Code is
# Mozilla Foundation.
# Portions created by the Initial Developer are Copyright (C) 2011
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Jonathan Griffin <jgriffin@mozilla.com>
# Sam Garrett <samdgarrett@gmail.com>
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
import json
import logging
import optparse
import os
import sys
import time
import traceback
from pulse import TPSPulseMonitor
def main():
""" 1. Create virtualenv via virtualenv cov
2. Run python setup.py install
3. Call coversheet
"""
parser = optparse.OptionParser()
parser.add_option("--email-results",
action = "store_true", dest = "emailresults",
default = False,
help = "email the test results to the recipients defined "
"in the config file")
parser.add_option("--mobile",
action = "store_true", dest = "mobile",
default = False,
help = "run with mobile settings")
parser.add_option("--autolog",
action = "store_true", dest = "autolog",
default = False,
help = "post results to Autolog")
parser.add_option("--testfile",
action = "store", type = "string", dest = "testfile",
default = 'all_tests.json',
help = "path to the test file to run "
"[default: %default]")
parser.add_option("--logfile",
action = "store", type = "string", dest = "logfile",
default = 'tps.log',
help = "path to the log file [default: %default]")
parser.add_option("--resultfile",
action = "store", type = "string", dest = "resultfile",
default = 'tps_result.json',
help = "path to the result file [default: %default]")
parser.add_option("--binary",
action = "store", type = "string", dest = "binary",
default = None,
help = "path to the Firefox binary, specified either as "
"a local file or a url; if omitted, the PATH "
"will be searched;")
parser.add_option("--configfile",
action = "store", type = "string", dest = "configfile",
default = None,
help = "path to the config file to use "
"[default: %default]")
parser.add_option("--pulsefile",
action = "store", type = "string", dest = "pulsefile",
default = None,
help = "path to file containing a pulse message in "
"json format that you want to inject into the monitor")
parser.add_option("--ignore-unused-engines",
default=False,
action="store_true",
dest="ignore_unused_engines",
help="If defined, don't load unused engines in individual tests."
" Has no effect for pulse monitor.")
(options, args) = parser.parse_args()
configfile = options.configfile
if configfile is None:
if os.environ.get('VIRTUAL_ENV'):
configfile = os.path.join(os.path.dirname(__file__), 'config.json')
if configfile is None or not os.access(configfile, os.F_OK):
raise Exception("Unable to find config.json in a VIRTUAL_ENV; you must "
"specify a config file using the --configfile option")
configfile = os.path.abspath(configfile)
# load the config file
f = open(configfile, 'r')
configcontent = f.read()
f.close()
config = json.loads(configcontent)
options.resultfile = os.path.abspath(options.resultfile)
print 'using resultfile:', options.resultfile
if options.binary is None:
while True:
try:
# If no binary is specified, start the pulse build monitor, and wait
# until we receive build notifications before running tests.
monitor = TPSPulseMonitor(autolog=options.autolog,
emailresults=options.emailresults,
config=configfile,
testfile=options.testfile,
logfile=options.logfile,
resultfile=options.resultfile,
mobile=options.mobile,
ignore_unused_engines=options.ignore_unused_engines)
print "waiting for pulse build notifications"
if options.pulsefile:
# For testing purposes, inject a pulse message directly into
# the monitor.
builddata = json.loads(open(options.pulsefile, 'r').read())
monitor.on_build_complete(builddata)
monitor.listen()
except KeyboardInterrupt:
sys.exit()
except:
traceback.print_exc()
print 'sleeping 5 minutes'
time.sleep(300)
if __name__ == "__main__":
main()

Просмотреть файл

@ -1,158 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import datetime
def GenerateEmailBody(data, numpassed, numfailed, serverUrl, buildUrl):
now = datetime.datetime.now()
builddate = datetime.datetime.strptime(data['productversion']['buildid'],
'%Y%m%d%H%M%S')
tree = data['productversion']['repository']
row = """
<tr>
<td><a href="http://hg.mozilla.org/services/services-central/file/default/services/sync/tests/tps/{name}">{name}</a></td>
<td>{state}</td>
<td>{message}</td>
</tr>
"""
rowWithLog = """
<tr>
<td><a href="http://hg.mozilla.org/services/services-central/file/default/services/sync/tests/tps/{name}">{name}</a></td>
<td>{state}</td>
<td>{message} [<a href="{logurl}">view log</a>]</td>
</tr>
"""
rows = ""
for test in data['tests']:
if test.get('logurl'):
rows += rowWithLog.format(name=test['name'],
state=test['state'],
message=test['message'] if test['message'] else 'None',
logurl=test['logurl'])
else:
rows += row.format(name=test['name'],
state=test['state'],
message=test['message'] if test['message'] else 'None')
firefox_version = data['productversion']['version']
if buildUrl is not None:
firefox_version = "<a href='%s'>%s</a>" % (buildUrl, firefox_version)
body = """
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>TPS</title>
<style type="text/css">
#headertable {{ border: solid 1px black; margin-bottom: 2em; border-collapse: collapse; font-size: 0.8em; }}
#headertable th {{ border: solid 1px black; background-color: lightgray; padding: 4px; }}
#headertable td {{ border: solid 1px black; padding: 4px; }}
.light {{ color: gray; }}
.pass, a.pass:link, a.pass:visited {{ color: green; font-weight: bold; }}
.fail, a.fail:link, a.fail:visited {{ color: red; font-weight: bold; }}
.rightgray {{ text-align: right; background-color: lightgray; }}
#summarytable {{ border: solid 1px black; margin-bottom: 2em; border-collapse: collapse; font-size: 0.8em; }}
#summarytable th {{ border: solid 1px black; background-color: lightgray; padding: 4px; }}
#summarytable td {{ border: solid 1px black; padding: 4px; }}
</style>
</head>
<body>
<div id="content">
<h2>TPS Testrun Details</h2>
<table id="headertable">
<tr>
<td class="rightgray">Testrun Date</td>
<td>{date}</td>
</tr>
<tr>
<td class="rightgray">Firefox Version</td>
<td>{firefox_version}</td>
</tr>
<tr>
<td class="rightgray">Firefox Build Date</td>
<td>{firefox_date}</td>
</tr>
<tr>
<td class="rightgray">Firefox Sync Version / Type</td>
<td>{sync_version} / {sync_type}
</td>
</tr>
<tr>
<td class="rightgray">Firefox Sync Changeset</td>
<td>
<a href="{repository}/rev/{changeset}">
{changeset}</a> / {sync_tree}
</td>
</tr>
<tr>
<td class="rightgray">Sync Server</td>
<td>{server}</td>
</tr>
<tr>
<td class="rightgray">OS</td>
<td>{os}</td>
</tr>
<tr>
<td class="rightgray">Passed Tests</td>
<td>
<span class="{passclass}">{numpassed}</span>
</td>
</tr>
<tr>
<td class="rightgray">Failed Tests</td>
<td>
<span class="{failclass}">{numfailed}</span>
</td>
</tr>
</table>
<table id="summarytable">
<thead>
<tr>
<th>Testcase</th>
<th>Result</th>
<th>Message</th>
</tr>
</thead>
{rows}
</table>
</div>
</body>
</html>
""".format(date=now.ctime(),
firefox_version=firefox_version,
firefox_date=builddate.ctime(),
sync_version=data['addonversion']['version'],
sync_type=data['synctype'],
sync_tree=tree[tree.rfind("/") + 1:],
repository=data['productversion']['repository'],
changeset=data['productversion']['changeset'],
os=data['os'],
rows=rows,
numpassed=numpassed,
numfailed=numfailed,
passclass="pass" if numpassed > 0 else "light",
failclass="fail" if numfailed > 0 else "light",
server=serverUrl if serverUrl != "" else "default"
)
return body

Просмотреть файл

@ -1,120 +0,0 @@
# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (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.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is TPS.
#
# The Initial Developer of the Original Code is
# Mozilla Foundation.
# Portions created by the Initial Developer are Copyright (C) 2011
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Jonathan Griffin <jgriffin@mozilla.com>
# Sam Garrett <samdgarrett@gmail.com>
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
import json
import logging
import os
import socket
from pulsebuildmonitor import PulseBuildMonitor
from subproc import TPSSubproc
from results import Covresults
class TPSPulseMonitor(PulseBuildMonitor):
def __init__(self, platform='linux', config=None,
autolog=False, emailresults=False, testfile=None,
logfile=None, resultfile=None, mobile=False,
ignore_unused_engines=False, **kwargs):
self.buildtype = ['opt']
self.autolog = autolog
self.emailresults = emailresults
self.testfile = testfile
self.logfile = logfile
self.resultfile = resultfile
self.mobile = mobile
self.ignore_unused_engines = ignore_unused_engines
self.config = config
f = open(config, 'r')
configcontent = f.read()
f.close()
configjson = json.loads(configcontent)
self.tree = configjson.get('tree', ['services-central'])
self.platform = [configjson.get('platform', 'linux')]
self.label=('crossweave@mozilla.com|tps_build_monitor_' +
socket.gethostname())
self.logger = logging.getLogger('tps_pulse')
self.logger.setLevel(logging.DEBUG)
handler = logging.FileHandler('tps_pulse.log')
self.logger.addHandler(handler)
self.results = Covresults(configjson, self.autolog, self.emailresults,
self.resultfile)
PulseBuildMonitor.__init__(self,
trees=self.tree,
label=self.label,
logger=self.logger,
platforms=self.platform,
buildtypes=self.buildtype,
builds=True,
**kwargs)
def on_pulse_message(self, data):
key = data['_meta']['routing_key']
def on_build_complete(self, builddata):
print "================================================================="
print json.dumps(builddata)
print "================================================================="
# Don't run tests if some conditions aren't met
if not builddata.get('testsurl') or builddata.get('locale') != 'en-US' \
or builddata.get('status') != 0:
return
if os.access(self.resultfile, os.F_OK):
os.remove(self.resultfile)
mysub = TPSSubproc(builddata=builddata,
emailresults=self.emailresults,
autolog=self.autolog,
testfile=self.testfile,
logfile=self.logfile,
config=self.config,
mobile=self.mobile,
resultfile=self.resultfile,
ignore_unused_engines=self.ignore_unused_engines)
mysub.get_buildAndTests()
mysub.setup_tps()
mysub.update_config()
mysub.call_testrunners()
self.results.handleResults()

Просмотреть файл

@ -1,178 +0,0 @@
# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (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.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is TPS.
#
# The Initial Developer of the Original Code is
# Mozilla Foundation.
# Portions created by the Initial Developer are Copyright (C) 2011
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Jonathan Griffin <jgriffin@mozilla.com>
# Sam Garrett <samdgarrett@gmail.com>
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
import json
import socket
import traceback
class Covresults(object):
"""Class for handling coversheet test-run results."""
def __init__(self, config, autolog, emailresults, filename):
self.config = config
self.filename = filename
self.autolog = autolog
self.emailresults = emailresults
def readResults(self):
"""Reads in the results from the tps test run json file."""
f = open(self.filename, 'r')
fileContents = f.read()
f.close()
self.test = json.loads(fileContents)
def handleResults(self):
"""Handles sending of results to the appropriate destinations."""
self.readResults()
for result in self.test['results']:
print result
self.postdata = result
if result.has_key('numpassed'):
self.numpassed = result['numpassed']
if result.has_key('numfailed'):
self.numfailed = result['numfailed']
self.firefoxrunnerurl = result.get('firefoxrunnerurl', 'unknown')
self.synctype = result.get('synctype', '')
if result.has_key('body'):
body = result['body']
else:
body = None
sendTo = result['sendTo']
if self.autolog:
self.postToAutolog()
if self.emailresults:
try:
self.sendEmail(body, sendTo)
except:
traceback.print_exc()
def sendEmail(self, body=None, sendTo=None):
"""Send the result email"""
if self.config.get('email') and self.config['email'].get('username') \
and self.config['email'].get('password'):
from sendemail import SendEmail
from emailtemplate import GenerateEmailBody
if body is None:
buildUrl = None
if self.firefoxrunnerurl:
buildUrl = self.firefoxrunnerurl
body = GenerateEmailBody(self.postdata,
self.numpassed,
self.numfailed,
self.config['serverURL'],
buildUrl)
subj = "TPS Report: "
if self.numfailed == 0 and self.numpassed > 0:
subj += "YEEEAAAHHH"
else:
subj += "PC LOAD LETTER"
changeset = self.postdata['productversion']['changeset'] if \
self.postdata and self.postdata.get('productversion') and \
self.postdata['productversion'].get('changeset') \
else 'unknown'
subj +=", changeset " + changeset + "; " + str(self.numfailed) + \
" failed, " + str(self.numpassed) + " passed"
SendEmail(From=self.config['email']['username'],
To=sendTo,
Subject=subj,
HtmlData=body,
Username=self.config['email']['username'],
Password=self.config['email']['password'])
def postToAutolog(self):
from mozautolog import RESTfulAutologTestGroup as AutologTestGroup
for server in self.config.get('es'):
group = AutologTestGroup(
harness='crossweave',
testgroup='crossweave-%s' % self.synctype,
server=server,
restserver=self.config.get('restserver'),
machine=socket.gethostname(),
platform=self.config.get('platform', None),
os=self.config.get('os', None),
)
tree = self.postdata['productversion']['repository']
group.set_primary_product(
tree=tree[tree.rfind("/")+1:],
version=self.postdata['productversion']['version'],
buildid=self.postdata['productversion']['buildid'],
buildtype='opt',
revision=self.postdata['productversion']['changeset'],
)
group.add_test_suite(
passed=self.numpassed,
failed=self.numfailed,
todo=0,
)
for test in self.postdata['tests']:
if test['state'] != "TEST-PASS":
# XXX FIX ME
#errorlog = self.errorlogs.get(test['name'])
#errorlog_filename = errorlog.filename if errorlog else None
errorlog_filename = None
group.add_test_failure(
test = test['name'],
status = test['state'],
text = test['message'],
logfile = errorlog_filename
)
try:
group.submit()
except:
self.sendEmail('<pre>%s</pre>' % traceback.format_exc(),
sendTo='crossweave@mozilla.com')
return
# Iterate through all testfailure objects, and update the postdata
# dict with the testfailure logurl's, if any.
for tf in group.testsuites[-1].testfailures:
result = [x for x in self.postdata['tests'] if x.get('name') == tf.test]
if not result:
continue
result[0]['logurl'] = tf.logurl

Просмотреть файл

@ -1,50 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
def SendEmail(From=None, To=None, Subject='No Subject',
TextData=None, HtmlData=None,
Server='mail.mozilla.com', Port=465,
Username=None, Password=None):
"""Sends an e-mail.
From is an e-mail address, To is a list of e-mail adresses.
TextData and HtmlData are both strings. You can specify one or both.
If you specify both, the e-mail will be sent as a MIME multipart
alternative; i.e., the recipient will see the HTML content if his
viewer supports it, otherwise he'll see the text content.
"""
if From is None or To is None:
raise Exception("Both From and To must be specified")
if TextData is None and HtmlData is None:
raise Exception("Must specify either TextData or HtmlData")
server = smtplib.SMTP_SSL(Server, Port)
if Username is not None and Password is not None:
server.login(Username, Password)
if HtmlData is None:
msg = MIMEText(TextData)
elif TextData is None:
msg = MIMEMultipart()
msg.preamble = Subject
msg.attach(MIMEText(HtmlData, 'html'))
else:
msg = MIMEMultipart('alternative')
msg.attach(MIMEText(TextData, 'plain'))
msg.attach(MIMEText(HtmlData, 'html'))
msg['Subject'] = Subject
msg['From'] = From
msg['To'] = ', '.join(To)
server.sendmail(From, To, msg.as_string())
server.quit()

Просмотреть файл

@ -1,241 +0,0 @@
# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (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.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is TPS.
#
# The Initial Developer of the Original Code is
# Mozilla Foundation.
# Portions created by the Initial Developer are Copyright (C) 2011
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Jonathan Griffin <jgriffin@mozilla.com>
# Sam Garrett <samdgarrett@gmail.com>
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
import os
import shutil
import sys
import zipfile
import re
import subprocess
import json
import requests
import mozinfo
import mozinstall
class TPSSubproc():
def __init__(self, builddata=None, emailresults=False,
testfile=None, logfile=None, config=None, autolog=False,
mobile=False, ignore_unused_engines=False,
resultfile=None):
assert(builddata)
assert(config)
self.url = builddata['buildurl']
self.testurl = builddata['testsurl']
self.emailresults = emailresults
self.testfile = testfile
self.logfile = logfile
self.config = config
self.autolog = autolog
self.mobile = mobile
self.resultfile = resultfile
self.ignore_unused_engines = ignore_unused_engines
def update_download_progress(self, percent):
sys.stdout.write("===== Downloaded %d%% =====\r"%percent)
sys.stdout.flush()
if percent >= 100:
sys.stdout.write("\n")
def download_url(self, url, outputPath):
print "Downloading %s...\r" % url
with open(outputPath, 'wb') as handle:
request = requests.get(url, stream=True)
if request.status_code != 200:
print "Error downloading file status_code=%s" % request.status_code
bytes_so_far = 0.0
block_size = 16 * 1024
total_size = int(request.headers['content-length'])
for block in request.iter_content(block_size):
if not block:
continue
bytes_so_far += block_size
percent = (bytes_so_far / total_size) * 100
self.update_download_progress(percent)
handle.write(block)
def prepare_build(self, installdir='downloadedbuild', appname='firefox'):
self.installdir = os.path.abspath(installdir)
buildName = os.path.basename(self.url)
pathToBuild = os.path.join(os.path.dirname(os.path.abspath(__file__)),
buildName)
# delete the build if it already exists
if os.access(pathToBuild, os.F_OK):
os.remove(pathToBuild)
# download the build
self.download_url(self.url, pathToBuild)
# install the build
print "installing %s" % pathToBuild
shutil.rmtree(self.installdir, True)
installed_at = mozinstall.install(pathToBuild, self.installdir)
# remove the downloaded archive
os.remove(pathToBuild)
binary = mozinstall.get_binary(installed_at, appname)
return os.path.abspath(binary)
def download_tests(self, installdir='downloadedtests'):
self.testinstalldir = os.path.abspath(installdir)
testsName = os.path.basename(self.testurl)
pathToTests = os.path.join(os.path.dirname(os.path.abspath(__file__)),
testsName)
# delete tests if they already exist
if os.access(pathToTests, os.F_OK):
os.remove(pathToTests)
# download the tests
print "downloading tests from %s" % self.testurl
self.download_url(self.testurl, pathToTests)
print "extracting test files to %s" % pathToTests
tempZipFile = zipfile.ZipFile(pathToTests)
tempZipFile.extractall(path=self.testinstalldir)
print "finished downloading test files"
return self.testinstalldir
def get_buildAndTests(self):
if self.url is not None and ('http://' in self.url or 'ftp://' in self.url):
self.binary = self.prepare_build()
self.tests = self.download_tests()
else:
self.binary = self.binary
def setup_tps(self):
# Setup the downloaded TPS
self.tpswd = os.path.join(self.tests, "tps")
self.tpsenv = os.path.join(self.testinstalldir, "tpsenv")
if os.access(self.tpsenv, os.F_OK):
shutil.rmtree(self.tpsenv)
print "Installing tps in %s" % self.tpsenv
create_venv = os.path.join(self.tpswd, 'create_venv.py')
if os.path.exists(create_venv):
cmd_args = ["python", create_venv, self.tpsenv]
else:
cmd_args = ["sh", os.path.join(self.tpswd, "INSTALL.sh"),
self.tpsenv]
self.run_process(cmd_args, self.tpswd, ignoreFailures=True)
print "TPS setup complete"
def update_config(self):
f = open(self.config, 'r')
tpsconfig = f.read()
configjson = json.loads(tpsconfig)
configjson['testdir'] = os.path.join(self.tpswd, "tests")
configjson['extensiondir'] = os.path.join(self.tpswd, "extensions")
# Update our coversheet config file
updateFile = open(self.config, 'w')
updateFile.write(json.dumps(configjson))
updateFile.close()
print 'wrote config file to', self.config
# Update relative testfile paths to point to our downloaded tests.
if self.testfile and not os.path.isabs(self.testfile):
self.testfile = os.path.join(self.tpswd, "tests", self.testfile)
print "testfile: ", self.testfile
def call_testrunners(self):
# getting our python location
bin_dir = 'Scripts' if sys.platform.startswith('win') else 'bin'
python_exe = 'python.exe' if sys.platform.startswith('win') else 'python'
python_path = os.path.join(self.tpsenv, bin_dir, python_exe)
tps_cli = os.path.join(self.tpswd, "tps", "cli.py")
# standard call
self.run_process([python_path, tps_cli,
"--binary", self.binary,
"--testfile", self.testfile,
"--resultfile", self.resultfile,
"--logfile", self.logfile,
"--configfile", self.config,
"--mobile" if self.mobile else '',
"--ignore_unused_engines" if self.ignore_unused_engines else ''],
self.tpswd)
# mobile call
self.run_process([python_path, tps_cli,
"--binary", self.binary,
"--testfile", self.testfile,
"--resultfile", self.resultfile,
"--logfile", self.logfile,
"--configfile", self.config,
"--mobile",
"--ignore_unused_engines" if self.ignore_unused_engines else ''],
self.tpswd)
# ... and again via the staging server, if credentials are present
f = open(self.config, 'r')
configcontent = f.read()
f.close()
configjson = json.loads(configcontent)
stageaccount = configjson.get('stageaccount')
if stageaccount:
username = stageaccount.get('username')
password = stageaccount.get('password')
passphrase = stageaccount.get('passphrase')
if username and password and passphrase:
stageconfig = configjson.copy()
stageconfig['account'] = stageaccount.copy()
self.run_process([python_path, tps_cli,
"--binary", self.binary,
"--testfile", self.testfile,
"--resultfile", self.resultfile,
"--logfile", self.logfile,
"--configfile", stageconfig,
"--ignore_unused_engines" if self.ignore_unused_engines else ''],
self.tpswd)
def run_process(self, params, cwd=None, ignoreFailures=False):
process = subprocess.Popen(params, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, cwd=cwd)
stdout, stderr = process.communicate() #this blocks until the subprocess is finished
print stdout, stderr
retcode = process.returncode
if not ignoreFailures:
assert(retcode == 0)
return retcode

49
jenkins-master/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1,49 @@
.DS_Store
*.key
*.log
*.log.*
*.pyc
*.tmp
*.bak
builds/
lastStable
lastSuccessful
nextBuildNumber
.owner
fingerprints
hudson.maven.*
hudson.scm.CVSSCM.xml
hudson.scm.SubversionSCM.xml
hudson.tasks.Ant.xml
hudson.tasks.Maven.xml
hudson.tasks.Shell.xml
hudson.triggers.SCMTrigger.xml
jenkins.mvn.GlobalMavenConfig.xml
jobs/*/workspace/
!jobs/tools/workspace
logs
monitoring/
nodeMonitors.xml
plugins/*/
plugins/ant.jpi
plugins/antisamy-markup-formatter.jpi
plugins/cvs.jpi
plugins/credentials.jpi
plugins/external-monitor-job.jpi
plugins/javadoc.jpi
plugins/ldap.jpi
plugins/mailer.jpi
plugins/matrix-auth.jpi
plugins/maven-plugin.jpi
plugins/pam-auth.jpi
plugins/ssh-credentials.jpi
plugins/ssh-slaves.jpi
plugins/subversion.jpi
plugins/translation.jpi
plugins/windows-slaves.jpi
queue.xml
secret*
updates
userContent
users/
war/

48
jenkins-master/config.xml Normal file
Просмотреть файл

@ -0,0 +1,48 @@
<?xml version='1.0' encoding='UTF-8'?>
<hudson>
<disabledAdministrativeMonitors/>
<version>1.554.2</version>
<numExecutors>1</numExecutors>
<mode>NORMAL</mode>
<useSecurity>true</useSecurity>
<authorizationStrategy class="hudson.security.AuthorizationStrategy$Unsecured"/>
<securityRealm class="hudson.security.SecurityRealm$None"/>
<disableRememberMe>false</disableRememberMe>
<projectNamingStrategy class="jenkins.model.ProjectNamingStrategy$DefaultProjectNamingStrategy"/>
<workspaceDir>${ITEM_ROOTDIR}/workspace</workspaceDir>
<buildsDir>${ITEM_ROOTDIR}/builds</buildsDir>
<jdks/>
<viewsTabBar class="hudson.views.DefaultViewsTabBar"/>
<myViewsTabBar class="hudson.views.DefaultMyViewsTabBar"/>
<clouds/>
<slaves>
<slave>
<name>dummy</name>
<description></description>
<remoteFS></remoteFS>
<numExecutors>1</numExecutors>
<mode>NORMAL</mode>
<retentionStrategy class="hudson.slaves.RetentionStrategy$Always"/>
<launcher class="hudson.slaves.JNLPLauncher"/>
<label></label>
<nodeProperties/>
<userId>anonymous</userId>
</slave>
</slaves>
<quietPeriod>5</quietPeriod>
<scmCheckoutRetryCount>0</scmCheckoutRetryCount>
<views>
<hudson.model.AllView>
<owner class="hudson" reference="../../.."/>
<name>All</name>
<filterExecutors>false</filterExecutors>
<filterQueue>false</filterQueue>
<properties class="hudson.model.View$PropertyList"/>
</hudson.model.AllView>
</views>
<primaryView>All</primaryView>
<slaveAgentPort>0</slaveAgentPort>
<label>master</label>
<nodeProperties/>
<globalNodeProperties/>
</hudson>

Просмотреть файл

@ -0,0 +1,7 @@
<?xml version='1.0' encoding='UTF-8'?>
<sites>
<site>
<id>default</id>
<url>http://updates.jenkins-ci.org/stable/update-center.json</url>
</site>
</sites>

Просмотреть файл

@ -0,0 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
<hudson.tasks.Mailer_-DescriptorImpl plugin="mailer@1.6">
<useSsl>false</useSsl>
<charset>UTF-8</charset>
</hudson.tasks.Mailer_-DescriptorImpl>

Просмотреть файл

@ -0,0 +1,4 @@
<?xml version='1.0' encoding='UTF-8'?>
<jenkins.model.ArtifactManagerConfiguration>
<artifactManagerFactories/>
</jenkins.model.ArtifactManagerConfiguration>

Просмотреть файл

@ -0,0 +1,4 @@
<?xml version='1.0' encoding='UTF-8'?>
<jenkins.model.DownloadSettings>
<useBrowser>true</useBrowser>
</jenkins.model.DownloadSettings>

Просмотреть файл

@ -0,0 +1,5 @@
<?xml version='1.0' encoding='UTF-8'?>
<jenkins.model.JenkinsLocationConfiguration>
<adminAddress>address not configured yet &lt;nobody@nowhere&gt;</adminAddress>
<jenkinsUrl>http://localhost:8080/</jenkinsUrl>
</jenkins.model.JenkinsLocationConfiguration>

49
run_tests.sh Executable file
Просмотреть файл

@ -0,0 +1,49 @@
#!/usr/bin/env bash
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
# You can obtain one at http://mozilla.org/MPL/2.0/.
set -e
DIR_TEST_ENV="tests/venv"
DIR_JENKINS_ENV=jenkins-env
VERSION_VIRTUALENV=1.9.1
if [ -d "$DIR_JENKINS_ENV" ] && [ -z $CI ]; then
echo "Jenkins environment already exists!"
while true; do
read -p "Would you like to recreate it? " yn
case $yn in
[Yy]* ) echo "Running setup"; ./setup.sh $DIR_JENKINS_ENV; break;;
[Nn]* ) break;;
* ) echo "Please answer yes or no.";;
esac
done
else
echo "Running setup"
./setup.sh $DIR_JENKINS_ENV
fi
echo "Starting Jenkins"
./start.py > jenkins.out &
sleep 60
# Check if environment exists, if not, create a virtualenv:
if [ -d $DIR_TEST_ENV ]
then
echo "Using virtual environment in $DIR_TEST_ENV"
else
echo "Creating a virtual environment (version ${VERSION_VIRTUALENV}) in ${DIR_TEST_ENV}"
curl -O https://pypi.python.org/packages/source/v/virtualenv/virtualenv-${VERSION_VIRTUALENV}.tar.gz
tar xvfz virtualenv-${VERSION_VIRTUALENV}.tar.gz
python virtualenv-${VERSION_VIRTUALENV}/virtualenv.py ${DIR_TEST_ENV}
fi
. $DIR_TEST_ENV/bin/activate || exit $?
pip install selenium
python tests/configuration/save_config.py
echo "Killing Jenkins"
pid=$(lsof -i:8080 -t); kill -TERM $pid || kill -KILL $pid
git --no-pager diff --exit-code

Просмотреть файл

@ -1,79 +0,0 @@
# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License Version
# 1.1 (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.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS" basis,
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
# for the specific language governing rights and limitations under the
# License.
#
# The Original Code is TPS.
#
# The Initial Developer of the Original Code is
# Mozilla foundation
# Portions created by the Initial Developer are Copyright (C) 2011
# the Initial Developer. All Rights Reserved.
#
# Contributor(s):
# Sam Garrett <samdgarrett@gmail.com>
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
import sys
from setuptools import setup, find_packages
version = '0.2'
deps = ['requests >= 2.2.1',
'mozinstall >= 0.10',
'mozinfo >= 0.7',
'mozautolog >= 0.2.4',
'pulsebuildmonitor >= 0.80',]
# we only support python 2.6+ right now
assert sys.version_info[0] == 2
assert sys.version_info[1] >= 6
setup(name='coversheet',
version=version,
description='run automated multi-profile sync tests',
long_description="""\
""",
classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
keywords='',
author='Sam Garrett',
author_email='samdgarrett@gmail.com',
url='http://hg.mozilla.org/services/services-central',
license='MPL',
dependency_links = [
"http://people.mozilla.org/~jgriffin/packages/"
],
packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
include_package_data=True,
zip_safe=False,
install_requires=deps,
entry_points="""
# -*- Entry points: -*-
[console_scripts]
coversheet = coversheet.cli:main
""",
data_files=[
('coversheet', ['config/config.json']),
],
)

43
setup.sh Executable file
Просмотреть файл

@ -0,0 +1,43 @@
#!/usr/bin/env bash
# Link to the folder which contains the zip archives of virtualenv
URL_VIRTUALENV=https://codeload.github.com/pypa/virtualenv/zip/
VERSION_PULSEBUILDMONITOR=0.81
VERSION_PYTHON_JENKINS=0.2.1
VERSION_VIRTUALENV=1.9.1
VERSION_PYTHON=$(python -c "import sys;print sys.version[:3]")
DIR_BASE=$(cd $(dirname ${BASH_SOURCE}); pwd)
DIR_ENV=${DIR_BASE}/${1:-"jenkins-env"}
DIR_TMP=${DIR_BASE}/tmp
echo "Cleaning up existent jenkins env and tmp folders"
rm -r ${DIR_ENV} ${DIR_TMP}
echo "Fetching virtualenv ${VERSION_VIRTUALENV} and creating jenkins environment"
mkdir ${DIR_TMP}
curl ${URL_VIRTUALENV}${VERSION_VIRTUALENV} > ${DIR_TMP}/virtualenv.zip
unzip ${DIR_TMP}/virtualenv.zip -d ${DIR_TMP}
python ${DIR_TMP}/virtualenv-${VERSION_VIRTUALENV}/virtualenv.py ${DIR_ENV}
echo "Activating the new environment"
source ${DIR_ENV}/bin/activate
if [ ! -n "${VIRTUAL_ENV:+1}" ]; then
echo "### Failure in activating the new virtual environment: '${DIR_ENV}'"
rm -r ${DIR_ENV} ${DIR_TMP}
exit 1
fi
echo "Installing required dependencies"
pip install --upgrade python-jenkins==${VERSION_PYTHON_JENKINS}
pip install --upgrade pulsebuildmonitor==${VERSION_PULSEBUILDMONITOR}
echo "Deactivating the environment"
deactivate
echo "Successfully created the Jenkins environment: '${DIR_ENV}'"
echo "Run 'source ${DIR_ENV}/bin/activate' to activate the environment"
rm -r ${DIR_TMP}

64
start.py Executable file
Просмотреть файл

@ -0,0 +1,64 @@
#!/usr/bin/env python
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
import os
from subprocess import check_call, CalledProcessError
import sys
import urllib2
HERE = os.path.dirname(os.path.abspath(__file__))
JENKINS_VERSION = '1.554.2'
JENKINS_URL = 'http://mirrors.jenkins-ci.org/war-stable/%s/jenkins.war' % JENKINS_VERSION
JENKINS_ENV = os.path.join(HERE, 'jenkins-env', 'bin', 'activate_this.py')
JENKINS_WAR = os.path.join(HERE, 'jenkins-%s.war' % JENKINS_VERSION)
def download_jenkins():
"""Downloads Jenkins.war file"""
if os.path.isfile(JENKINS_WAR):
print "Jenkins already downloaded"
else:
print "Downloading Jenkins %s from %s" % (JENKINS_VERSION, JENKINS_URL)
# Download starts
tmp_file = JENKINS_WAR + ".part"
while True:
try:
r = urllib2.urlopen(JENKINS_URL)
CHUNK = 16 * 1024
with open(tmp_file, 'wb') as f:
for chunk in iter(lambda: r.read(CHUNK), ''):
f.write(chunk)
break
except (urllib2.HTTPError, urllib2.URLError):
print "Download failed."
raise
os.rename(tmp_file, JENKINS_WAR)
if __name__ == "__main__":
download_jenkins()
try:
# for more info see:
# http://www.virtualenv.org/en/latest/#using-virtualenv-without-bin-python
execfile(JENKINS_ENV, dict(__file__=JENKINS_ENV))
print "Virtual environment activated successfully."
except IOError:
print "Could not activate virtual environment."
print "Exiting."
sys.exit(IOError)
# TODO: Start Jenkins as daemon
print "Starting Jenkins"
os.environ['JENKINS_HOME'] = os.path.join(HERE, 'jenkins-master')
args = ['java', '-Xms2g', '-Xmx2g', '-XX:MaxPermSize=512M',
'-Xincgc', '-jar', JENKINS_WAR]
try:
check_call(args)
except CalledProcessError as e:
sys.exit(e.returncode)

1
tests/.gitignore поставляемый Normal file
Просмотреть файл

@ -0,0 +1 @@
venv

Просмотреть файл

@ -0,0 +1,47 @@
from selenium import webdriver
def main():
base_url = 'http://localhost:8080/'
driver = webdriver.PhantomJS()
driver.implicitly_wait(10)
print 'Saving main configuration...'
driver.get(base_url + 'configure')
driver.find_element_by_css_selector(
'#bottom-sticker .submit-button button').click()
print 'Saving node configurations...'
driver.get(base_url + 'computer/')
node_links = driver.find_elements_by_css_selector(
"tr[id*='node_'] > td:nth-child(2) > a")
nodes = [{'name': link.text, 'href': link.get_attribute('href')} for
link in node_links]
for i, node in enumerate(nodes):
driver.get(node['href'] + 'configure')
print '[%d/%d] %s' % (i + 1, len(nodes), node['name'])
driver.find_element_by_css_selector('.submit-button button').click()
driver.find_element_by_css_selector('#main-panel h1')
print 'Saving job configurations...'
driver.get(base_url)
job_links = driver.find_elements_by_css_selector(
"tr[id*='job_'] > td:nth-child(3) > a")
jobs = [{'name': link.text, 'href': link.get_attribute('href')} for
link in job_links]
assert len(jobs) == 0, 'No jobs configured in Jenkins!'
for i, job in enumerate(jobs):
driver.get(job['href'] + 'configure')
print '[%d/%d] %s' % (i + 1, len(jobs), job['name'])
driver.find_element_by_css_selector(
'#bottom-sticker .submit-button button').click()
driver.find_element_by_css_selector('#main-panel h1')
driver.quit()
if __name__ == "__main__":
main()