parent
044b8da55a
commit
759617c9c0
288 changed files with 13040 additions and 1 deletions
317
bots/tests-invoke
Executable file
317
bots/tests-invoke
Executable file
|
|
@ -0,0 +1,317 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# This file is part of Cockpit.
|
||||
#
|
||||
# Copyright (C) 2016 Red Hat, Inc.
|
||||
#
|
||||
# Cockpit is free software; you can redistribute it and/or modify it
|
||||
# under the terms of the GNU Lesser General Public License as published by
|
||||
# the Free Software Foundation; either version 2.1 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Cockpit is distributed in the hope that it will be useful, but
|
||||
# WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
||||
# Lesser General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Lesser General Public License
|
||||
# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
|
||||
from task import github
|
||||
from task import sink
|
||||
|
||||
HOSTNAME = socket.gethostname().split(".")[0]
|
||||
BOTS = os.path.abspath(os.path.dirname(__file__))
|
||||
BASE = os.path.normpath(os.path.join(BOTS, ".."))
|
||||
DEVNULL = open("/dev/null", "r+")
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Run integration tests')
|
||||
parser.add_argument('--rebase', help="Rebase onto the specific branch before testing")
|
||||
parser.add_argument('-o', "--offline", action='store_true',
|
||||
help="Work offline, don''t fetch new data from origin for rebase")
|
||||
parser.add_argument('--publish', dest='publish', default=os.environ.get("TEST_PUBLISH", ""),
|
||||
action='store', help='Publish results centrally to a sink')
|
||||
parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output')
|
||||
parser.add_argument('--pull-number', help="The number of the pull request to test")
|
||||
parser.add_argument('--html-logs', action='store_true', help="Use log.html to prettify the raw logs")
|
||||
parser.add_argument('context', help="The context or type of integration tests to run")
|
||||
opts = parser.parse_args()
|
||||
|
||||
name = os.environ.get("TEST_NAME", "tests")
|
||||
revision = os.environ.get("TEST_REVISION")
|
||||
test_project = os.environ.get("TEST_PROJECT")
|
||||
# branch name when explicitly given in the test status
|
||||
# e.g. PR opened against master for test `rhel-8-0@cockpit-project/cockpit/rhel-8.0`
|
||||
# TEST_BRANCH would be `rhel-8.0`
|
||||
test_branch = os.environ.get("TEST_BRANCH")
|
||||
|
||||
try:
|
||||
task = PullTask(name, revision, opts.context, opts.rebase,
|
||||
test_project=test_project, test_branch=test_branch,
|
||||
html_logs=opts.html_logs)
|
||||
ret = task.run(opts)
|
||||
except RuntimeError as ex:
|
||||
ret = str(ex)
|
||||
|
||||
if ret:
|
||||
sys.stderr.write("tests-invoke: {0}\n".format(ret))
|
||||
return 1
|
||||
return 0
|
||||
|
||||
class PullTask(object):
|
||||
def __init__(self, name, revision, context, base, test_project, test_branch, html_logs):
|
||||
self.name = name
|
||||
self.revision = revision
|
||||
self.context = context
|
||||
self.base = base
|
||||
self.test_project = test_project
|
||||
self.test_branch = test_branch
|
||||
self.html_logs = html_logs
|
||||
|
||||
self.sink = None
|
||||
self.github_status_data = None
|
||||
|
||||
def detect_collisions(self, opts):
|
||||
api = github.GitHub()
|
||||
|
||||
if opts.pull_number:
|
||||
pull = api.get("pulls/{0}".format(opts.pull_number))
|
||||
if pull:
|
||||
if pull["head"]["sha"] != self.revision:
|
||||
return "Newer revision available on GitHub for this pull request"
|
||||
if pull["state"] != "open":
|
||||
return "Pull request isn't open"
|
||||
|
||||
if not self.revision:
|
||||
return None
|
||||
|
||||
statuses = api.get("commits/{0}/statuses".format(self.revision))
|
||||
for status in statuses:
|
||||
if status.get("context") == self.context:
|
||||
if status.get("state") == "pending" and status.get("description") not in [github.NOT_TESTED, github.NOT_TESTED_DIRECT]:
|
||||
return "Status of context isn't pending or description is not in [{0}, {1}]".format(github.NOT_TESTED, github.NOT_TESTED_DIRECT)
|
||||
else: # only check the newest status of the supplied context
|
||||
return None
|
||||
|
||||
def start_publishing(self, host):
|
||||
api = github.GitHub()
|
||||
|
||||
# build a unique file name for this test run
|
||||
id_context = "-".join([self.test_project, self.test_branch or "", self.context])
|
||||
identifier = "-".join([
|
||||
self.name.replace("/", "-"),
|
||||
self.revision[0:8],
|
||||
id_context.replace("/", "-")
|
||||
])
|
||||
|
||||
description = "{0} [{1}]".format(github.TESTING, HOSTNAME)
|
||||
|
||||
# build a globally unique test context for GitHub statuses
|
||||
github_context = self.context
|
||||
if self.test_branch:
|
||||
github_context += "@" + self.test_project + "/" + self.test_branch
|
||||
elif self.test_project != api.repo : # disambiguate test name for foreign project tests
|
||||
github_context += "@" + self.test_project
|
||||
|
||||
self.github_status_data = {
|
||||
"state": "pending",
|
||||
"context": github_context,
|
||||
"description": description,
|
||||
"target_url": ":link"
|
||||
}
|
||||
|
||||
status = {
|
||||
"github": {
|
||||
"token": api.token,
|
||||
"requests": [
|
||||
# Set status to pending
|
||||
{ "method": "POST",
|
||||
"resource": api.qualify("commits/" + self.revision + "/statuses"),
|
||||
"data": self.github_status_data
|
||||
}
|
||||
],
|
||||
"watches": [{
|
||||
"resource": api.qualify("commits/" + self.revision + "/status?per_page=100"),
|
||||
"result": {
|
||||
"statuses": [
|
||||
{
|
||||
"context": github_context,
|
||||
"description": description,
|
||||
"target_url": ":link"
|
||||
}
|
||||
]
|
||||
}
|
||||
}]
|
||||
},
|
||||
"revision": self.revision,
|
||||
"onaborted": {
|
||||
"github": {
|
||||
"token": api.token,
|
||||
"requests": [
|
||||
# Set status to error
|
||||
{ "method": "POST",
|
||||
"resource": api.qualify("statuses/" + self.revision),
|
||||
"data": {
|
||||
"state": "error",
|
||||
"context": self.context,
|
||||
"description": "Aborted without status",
|
||||
"target_url": ":link"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
if self.html_logs:
|
||||
# explicit request for html logs
|
||||
status["link"] = "log.html"
|
||||
status["extras"] = [ "https://raw.githubusercontent.com/cockpit-project/cockpit/master/bots/task/log.html" ]
|
||||
elif self.test_project != "cockpit-project/cockpit":
|
||||
# third-party project, link directly to text log
|
||||
status["link"] = "log"
|
||||
else:
|
||||
# Testing cockpit itself, use HTML log from current
|
||||
# revision. Use log.html from code under test, but only
|
||||
# if we are on master. Other branches don't have a bots/
|
||||
# in their repo.
|
||||
status["link"] = "log.html"
|
||||
status["extras"] = [ "https://raw.githubusercontent.com/cockpit-project/cockpit/{0}/bots/task/log.html".format(self.revision if not self.base else "master") ]
|
||||
|
||||
|
||||
# Include information about which base we're testing against
|
||||
if self.base:
|
||||
subprocess.check_call([ "git", "fetch", "origin", self.base ])
|
||||
commit = subprocess.check_output([ "git", "rev-parse", "origin/" + self.base ],
|
||||
universal_newlines=True).strip()
|
||||
status["base"] = commit
|
||||
|
||||
if not self.base:
|
||||
status['irc'] = { } # Only send to IRC when master
|
||||
|
||||
# For other scripts to use
|
||||
os.environ["TEST_DESCRIPTION"] = description
|
||||
self.sink = sink.Sink(host, identifier, status)
|
||||
|
||||
def rebase(self):
|
||||
remote_base = "origin" + "/" + self.base
|
||||
|
||||
# Rebase this branch onto the base, but only if it's not already an ancestor
|
||||
try:
|
||||
if subprocess.call([ "git", "merge-base", "--is-ancestor", remote_base, "HEAD" ]) != 0:
|
||||
sha = subprocess.check_output([ "git", "rev-parse", remote_base ], universal_newlines=True).strip()
|
||||
sys.stderr.write("Rebasing onto {0} ({1}) ...\n".format(remote_base, sha))
|
||||
subprocess.check_call([ "git", "reset", "HEAD" ])
|
||||
subprocess.check_call([ "git", "rebase", remote_base ])
|
||||
except subprocess.CalledProcessError:
|
||||
subprocess.call([ "git", "rebase", "--abort" ])
|
||||
traceback.print_exc()
|
||||
return "Rebase failed"
|
||||
|
||||
return None
|
||||
|
||||
def stop_publishing(self, ret):
|
||||
sink = self.sink
|
||||
def mark_failed():
|
||||
if "github" in sink.status:
|
||||
self.github_status_data["state"] = "failure"
|
||||
if "irc" in sink.status: # Never send success messages to IRC
|
||||
sink.status["irc"]["channel"] = "#cockpit"
|
||||
def mark_passed():
|
||||
if "github" in sink.status:
|
||||
self.github_status_data["state"] = "success"
|
||||
if isinstance(ret, str):
|
||||
message = ret
|
||||
mark_failed()
|
||||
ret = 0
|
||||
elif ret == 0:
|
||||
message = "Tests passed"
|
||||
mark_passed()
|
||||
else:
|
||||
message = "Tests failed with code {0}".format(ret)
|
||||
mark_failed()
|
||||
ret = 0 # A failure, but not for this script
|
||||
sink.status["message"] = message
|
||||
if "github" in sink.status:
|
||||
self.github_status_data["description"] = message
|
||||
try:
|
||||
del sink.status["extras"]
|
||||
except KeyError:
|
||||
pass
|
||||
sink.flush()
|
||||
|
||||
return ret
|
||||
|
||||
def run(self, opts):
|
||||
# Collision detection
|
||||
ret = self.detect_collisions(opts)
|
||||
if ret:
|
||||
sys.stderr.write('Collision detected: {0}\n'.format(ret))
|
||||
return None
|
||||
|
||||
if opts.publish:
|
||||
self.start_publishing(opts.publish)
|
||||
os.environ["TEST_ATTACHMENTS"] = self.sink.attachments
|
||||
|
||||
head = subprocess.check_output([ "git", "rev-parse", "HEAD" ], universal_newlines=True).strip()
|
||||
if not self.revision:
|
||||
self.revision = head
|
||||
|
||||
# Retrieve information about our base branch and master (for bots/)
|
||||
if self.base and not opts.offline:
|
||||
subprocess.check_call([ "git", "fetch", "origin", self.base, "master" ])
|
||||
|
||||
# Clean out the test directory
|
||||
subprocess.check_call([ "git", "clean", "-d", "--force", "--quiet", "-x", "--", "test/" ])
|
||||
|
||||
os.environ["TEST_NAME"] = self.name
|
||||
os.environ["TEST_REVISION"] = self.revision
|
||||
|
||||
# Split OS and potential scenario
|
||||
(image, _, scenario) = self.context.partition("/")
|
||||
if scenario:
|
||||
os.environ["TEST_SCENARIO"] = scenario
|
||||
os.environ["TEST_OS"] = image
|
||||
|
||||
msg = "Testing {0} for {1} with {2} on {3}...\n".format(self.revision, self.name,
|
||||
self.context, HOSTNAME)
|
||||
sys.stderr.write(msg)
|
||||
|
||||
ret = None
|
||||
|
||||
if self.base:
|
||||
ret = self.rebase()
|
||||
|
||||
# Flush our own output before running command
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
|
||||
# Actually run the tests
|
||||
if not ret:
|
||||
entry_point = os.path.join(BOTS, "..", ".cockpit-ci", "run")
|
||||
alt_entry_point = os.path.join(BOTS, "..", "test", "run")
|
||||
if not os.path.exists(entry_point) and os.path.exists(alt_entry_point):
|
||||
entry_point = alt_entry_point
|
||||
cmd = [ "timeout", "120m", entry_point ]
|
||||
if opts.verbose:
|
||||
cmd.append("--verbose")
|
||||
ret = subprocess.call(cmd)
|
||||
|
||||
# All done
|
||||
if self.sink:
|
||||
ret = self.stop_publishing(ret)
|
||||
|
||||
return ret
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue