#!/usr/bin/env python3
from __future__ import print_function, unicode_literals
import argparse
import coverage
import json
import logging
import multiprocessing
import os
import shutil
import subprocess
import sys
import threading
import time
RUNNER_PY = "nosetests"
RUNNER_PY2 = "nosetests-2"
RUNNER_PY3 = "nosetests-3"
COVER_PY = "coverage"
COVER_PY2 = "coverage2"
COVER_PY3 = "coverage3"
LASTLEN = None
NUMREMAINING = None
PRINTLOCK = None
RUNNING = []
FAILED = []
NUMPROCS = multiprocessing.cpu_count() - 1
if os.environ.get('BUILD_ID'):
NUMPROCS = multiprocessing.cpu_count()
LOG = logging.getLogger(__name__)
def setup_parser():
""" Set up the command line arguments supported and return the arguments
"""
parser = argparse.ArgumentParser(description="Run the Pagure tests")
parser.add_argument(
"--debug",
dest="debug",
action="store_true",
default=False,
help="Increase the level of data logged.",
)
subparsers = parser.add_subparsers(title="actions")
# RUN
parser_run = subparsers.add_parser("run", help="Run the tests")
parser_run.add_argument(
"--py2",
dest="py2",
action="store_true",
default=False,
help="Runs the tests only in python2 instead of both python2 and python3",
)
parser_run.add_argument(
"--py3",
dest="py3",
action="store_true",
default=False,
help="Runs the tests only in python3 instead of both python2 and python3",
)
parser_run.add_argument(
"--results",
default="results",
help="Specify a folder in which the results should be placed "
"(defaults to `results`)",
)
parser_run.add_argument(
"-f",
"--force",
default=False,
action="store_true",
help="Override the results and newfailed file without asking you",
)
parser_run.add_argument(
"--with-coverage",
default=False,
action="store_true",
help="Also build coverage report",
)
parser_run.add_argument(
"failed_tests",
nargs="?",
help="File containing a JSON list of the failed tests to run or "
"pointing to a test file to run.",
)
parser_run.set_defaults(func=do_run)
# RERUN
parser_run = subparsers.add_parser("rerun", help="Run failed tests")
parser_run.add_argument(
"--debug",
dest="debug",
action="store_true",
default=False,
help="Expand the level of data returned.",
)
parser_run.add_argument(
"--py2",
dest="py2",
action="store_true",
default=False,
help="Runs the tests only in python2 instead of both python2 and python3",
)
parser_run.add_argument(
"--py3",
dest="py3",
action="store_true",
default=False,
help="Runs the tests only in python3 instead of both python2 and python3",
)
parser_run.add_argument(
"--results",
default="results",
help="Specify a folder in which the results should be placed "
"(defaults to `results`)",
)
parser_run.add_argument(
"--with-coverage",
default=False,
action="store_true",
help="Also build coverage report",
)
parser_run.set_defaults(func=do_rerun)
# LIST
parser_run = subparsers.add_parser("list", help="List failed tests")
parser_run.add_argument(
"--results",
default="results",
help="Specify a folder in which the results should be placed "
"(defaults to `results`)",
)
parser_run.add_argument(
"--show",
default=False,
action="store_true",
help="Show the error files using `less`",
)
parser_run.add_argument(
"-n", default=None, nargs="?", type=int,
help="Number of failed test to show",
)
parser_run.set_defaults(func=do_list)
# SHOW-COVERAGE
parser_run = subparsers.add_parser(
"show-coverage",
help="Shows the coverage report from the data in the results folder")
parser_run.add_argument(
"--debug",
dest="debug",
action="store_true",
default=False,
help="Expand the level of data returned.",
)
parser_run.add_argument(
"--py2",
dest="py2",
action="store_true",
default=False,
help="Runs the tests only in python2 instead of both python2 and python3",
)
parser_run.add_argument(
"--py3",
dest="py3",
action="store_true",
default=False,
help="Runs the tests only in python3 instead of both python2 and python3",
)
parser_run.add_argument(
"--results",
default="results",
help="Specify a folder in which the results should be placed "
"(defaults to `results`)",
)
parser_run.set_defaults(func=do_show_coverage)
return parser
def clean_line():
global LASTLEN
with PRINTLOCK:
if LASTLEN is not None:
print(" " * LASTLEN, end="\r")
LASTLEN = None
def print_running():
global LASTLEN
with PRINTLOCK:
msg = "Running %d suites: %d remaining, %d failed" % (
len(RUNNING),
NUMREMAINING,
len(FAILED),
)
LASTLEN = len(msg)
print(msg, end="\r")
def add_running(suite):
global NUMREMAINING
with PRINTLOCK:
NUMREMAINING -= 1
RUNNING.append(suite)
clean_line()
print_running()
def remove_running(suite, failed):
with PRINTLOCK:
RUNNING.remove(suite)
clean_line()
status = 'passed'
if failed:
status = 'FAILED'
print("Test suite %s: %s" % (status, suite))
print_running()
class WorkerThread(threading.Thread):
def __init__(self, sem, pyver, suite, results, with_cover):
name = "py%s-%s" % (pyver, suite)
super(WorkerThread, self).__init__(name="worker-%s" % name)
self.name = name
self.sem = sem
self.pyver = pyver
self.suite = suite
self.failed = None
self.results = results
self.with_cover = with_cover
def run(self):
with self.sem:
add_running(self.name)
with open(os.path.join(self.results, self.name), "w") as resfile:
if self.pyver == 2:
runner = RUNNER_PY2
elif self.pyver == 3:
runner = RUNNER_PY3
else:
runner = RUNNER_PY
cmd = [runner, "-v", "tests.%s" % self.suite]
if self.with_cover:
cmd.append("--with-cover")
env = os.environ.copy()
env.update({
"PAGURE_CONFIG": "../tests/test_config",
"COVERAGE_FILE": os.path.join(
self.results, "%s.coverage" % self.name
),
"LANG": "en_US.UTF-8",
})
proc = subprocess.Popen(
cmd, cwd=".", stdout=resfile, stderr=subprocess.STDOUT, env=env
)
res = proc.wait()
if res == 0:
self.failed = False
else:
self.failed = True
if not self.failed is not True:
with PRINTLOCK:
FAILED.append(self.name)
remove_running(self.name, self.failed)
def do_run(args):
""" Performs some checks and runs the tests.
"""
# Some pre-flight checks
if not os.path.exists("./.git") or not os.path.exists("./nosetests3"):
print("Please run from a single level into the Pagure codebase")
return 1
if os.path.exists(args.results):
if not args.force:
print(
"Results folder exists, please remove it so we do not clobber"
" or use --force"
)
return 1
else:
shutil.rmtree(args.results)
os.mkdir(args.results)
print("Pre-flight checks passed")
suites = []
if args.failed_tests:
here = os.path.join(os.path.dirname(os.path.abspath(__file__)))
failed_tests_fullpath = os.path.join(here, args.failed_tests)
if not os.path.exists(failed_tests_fullpath):
print("Could not find the specified file:%s" % failed_tests_fullpath)
return 1
print("Loading failed tests")
try:
with open(failed_tests_fullpath, "r") as ffile:
suites = json.loads(ffile.read())
except json.decoder.JSONDecodeError:
bname = os.path.basename(args.failed_tests)
if bname.endswith(".py") and bname.startswith("test_"):
suites.append(bname.replace(".py", ""))
if len(suites) == 0:
print("Loading all tests")
for fname in os.listdir("./tests"):
if not fname.endswith(".py"):
continue
if not fname.startswith("test_"):
continue
suites.append(fname.replace(".py", ""))
return _run_test_suites(args, suites)
def do_rerun(args):
""" Re-run tests that failed the last/specified run.
"""
# Some pre-flight checks
if not os.path.exists("./.git") or not os.path.exists("./pagure"):
print("Please run from a single level into the Pagure codebase")
return 1
if not os.path.exists(args.results):
print("Could not find an existing results folder at: %s" % args.results)
return 1
if not os.path.exists(os.path.join(args.results, "newfailed")):
print(
"Could not find an failed tests in the results folder at: %s" % args.results
)
return 1
print("Pre-flight checks passed")
suites = []
tmp = []
print("Loading failed tests")
try:
with open(os.path.join(args.results, "newfailed"), "r") as ffile:
tmp = json.loads(ffile.read())
except json.decoder.JSONDecodeError:
print("File containing the failed tests is not JSON")
return 1
for suite in tmp:
if suite.startswith(("py2-", "py3-")):
suites.append(suite[4:])
return _run_test_suites(args, set(suites))
def _get_pyvers(args):
pyvers = [2, 3]
if args.py2:
pyvers = [2,]
elif args.py3:
pyvers = [3,]
un_versioned = False
try:
subprocess.check_call(["which", RUNNER_PY])
un_versioned = True
except subprocess.CalledProcessError:
print("No %s found no unversioned runner" % RUNNER_PY)
if 2 in pyvers:
nopy2 = False
try:
subprocess.check_call(["which", RUNNER_PY2])
except subprocess.CalledProcessError:
print("No %s found, removing python 2" % RUNNER_PY2)
del pyvers[pyvers.index(2)]
if 3 in pyvers:
nopy3 = False
try:
subprocess.check_call(["which", RUNNER_PY3])
except subprocess.CalledProcessError:
print("No %s found, removing python 3" % RUNNER_PY3)
del pyvers[pyvers.index(3)]
if not pyvers and un_versioned:
pyvers = [""]
return pyvers
def _run_test_suites(args, suites):
print("Using %d processes" % NUMPROCS)
print("Start timing")
start = time.time()
global PRINTLOCK
PRINTLOCK = threading.RLock()
global NUMREMAINING
NUMREMAINING = 0
sem = threading.BoundedSemaphore(NUMPROCS)
# Create a worker per test
workers = {}
pyvers = _get_pyvers(args)
if not pyvers:
return 1
for suite in suites:
for pyver in pyvers:
NUMREMAINING += 1
workers["py%s-%s" % (pyver, suite)] = WorkerThread(
sem, pyver, suite, args.results, args.with_coverage
)
# Start the workers
print("Starting the workers")
print()
print()
for worker in workers.values():
worker.start()
# Wait for them to terminate
for worker in workers:
workers[worker].join()
print_running()
print()
print("All work done")
# Gather results
print()
print()
if FAILED:
print("Failed tests:")
for worker in workers:
if not workers[worker].failed:
continue
print("FAILED test: %s" % (worker))
# Write failed
if FAILED:
with open(os.path.join(args.results, "newfailed"), "w") as ffile:
ffile.write(json.dumps(FAILED))
# Exit
outcode = 0
if len(FAILED) == 0:
print("ALL PASSED! CONGRATULATIONS!")
else:
outcode = 1
# Stats
end = time.time()
print()
print()
print(
"Ran %d tests in %f seconds, of which %d failed"
% (len(workers), (end - start), len(FAILED))
)
if outcode == 0 and args.with_coverage:
do_show_coverage(args)
return outcode
def do_list(args):
""" List tests that failed the last/specified run.
"""
# Some pre-flight checks
if not os.path.exists("./.git") or not os.path.exists("./pagure"):
print("Please run from a single level into the Pagure codebase")
return 1
if not os.path.exists(args.results):
print("Could not find an existing results folder at: %s" % args.results)
return 1
if not os.path.exists(os.path.join(args.results, "newfailed")):
print(
"Could not find an failed tests in the results folder at: %s" % args.results
)
return 1
print("Pre-flight checks passed")
suites = []
tmp = []
print("Loading failed tests")
try:
with open(os.path.join(args.results, "newfailed"), "r") as ffile:
suites = json.loads(ffile.read())
except json.decoder.JSONDecodeError:
print("File containing the failed tests is not JSON")
return 1
print("Failed tests")
failed_tests = len(suites)
if args.n:
suites = suites[:args.n]
print("- " + "\n- ".join(suites))
print("Total: %s test failed" % failed_tests)
if args.show:
for suite in suites:
cmd = ["less", os.path.join(args.results, suite)]
subprocess.check_call(cmd)
def do_show_coverage(args):
print()
print("Combining coverage results...")
pyvers = _get_pyvers(args)
for pyver in pyvers:
coverfiles = []
for fname in os.listdir(args.results):
if fname.endswith(".coverage") and fname.startswith("py%s-" % pyver):
coverfiles.append(os.path.join(args.results, fname))
cover = None
if pyver == 2:
cover = COVER_PY2
elif pyver == 3:
cover = COVER_PY3
else:
cover = COVER_PY
env = {"COVERAGE_FILE": os.path.join(args.results, "combined.coverage")}
cmd = [cover, "combine"] + coverfiles
subprocess.check_call(cmd, env=env)
print()
print("Python %s coverage: " % pyver)
cmd = [cover, "report", "--include=./pagure/*", "-m"]
subprocess.check_call(cmd, env=env)
def main():
""" Main function """
# Set up parser for global args
parser = setup_parser()
# Parse the commandline
try:
arg = parser.parse_args()
except argparse.ArgumentTypeError as err:
print("\nError: {0}".format(err))
return 2
logging.basicConfig()
if arg.debug:
LOG.setLevel(logging.DEBUG)
if "func" not in arg:
parser.print_help()
return 1
arg.results = os.path.abspath(arg.results)
return_code = 0
try:
return_code = arg.func(arg)
except KeyboardInterrupt:
print("\nInterrupted by user.")
return_code = 1
except Exception as err:
print("Error: {0}".format(err))
logging.exception("Generic error caught:")
return_code = 5
return return_code
if __name__ == "__main__":
sys.exit(main())