aboutsummaryrefslogtreecommitdiffstats
path: root/testrunner.py
diff options
context:
space:
mode:
Diffstat (limited to 'testrunner.py')
-rw-r--r--testrunner.py791
1 files changed, 4 insertions, 787 deletions
diff --git a/testrunner.py b/testrunner.py
index acf4e407b..e3a30a07e 100644
--- a/testrunner.py
+++ b/testrunner.py
@@ -37,793 +37,10 @@
##
#############################################################################
-from __future__ import print_function
+from __future__ import print_function, absolute_import
-"""
-testrunner
-
-Provide an interface to the pyside tests.
-
-- find the latest build dir.
- This is found by the build_history in setup.py,
- near the end of pyside_build.run()
-
-- run 'make test' and record the output
- (not ready)
-
-- compare the result list with the current blacklist
-
-- return the correct error status
- (zero if expected includes observed, else 1)
-
-Recommended build process:
-There is no need to install the project.
-Building the project with something like
-
- python setup.py build --build-tests --qmake=<qmakepath> --ignore-git --debug
-
-is sufficient. The tests are run by changing into the latest build dir and there
-into pyside2, then 'make test'.
-
-"""
-
-import os
import sys
-import re
-import subprocess
-import zipfile
-import argparse
-
-PY3 = sys.version_info[0] == 3 # from the six module
-from subprocess import PIPE
-if PY3:
- from subprocess import TimeoutExpired
- from io import StringIO
-else:
- class SubprocessError(Exception): pass
- # this is a fake, just to keep the source compatible.
- # timeout support is in python 3.3 and above.
- class TimeoutExpired(SubprocessError): pass
- from StringIO import StringIO
-from collections import namedtuple
-
-# Change the cwd to our source dir
-try:
- this_file = __file__
-except NameError:
- this_file = sys.argv[0]
-this_file = os.path.abspath(this_file)
-if os.path.dirname(this_file):
- os.chdir(os.path.dirname(this_file))
-script_dir = os.getcwd()
-
-LogEntry = namedtuple("LogEntry", ["log_dir", "build_dir"])
-
-
-class BuildLog(object):
- """
- This class is a convenience wrapper around a list of log entries.
-
- The list of entries is sorted by date and checked for consistency.
- For simplicity and readability, the log entries are named tuples.
-
- """
- def __init__(self, script_dir=script_dir):
- history_dir = os.path.join(script_dir, 'build_history')
- build_history = []
- for timestamp in os.listdir(history_dir):
- log_dir = os.path.join(history_dir, timestamp)
- if not os.path.isdir(log_dir):
- continue
- fpath = os.path.join(log_dir, 'build_dir.txt')
- if not os.path.exists(fpath):
- print("Warning: %s not found, skipped" % fpath)
- continue
- with open(fpath) as f:
- build_dir = f.read().strip()
- if not os.path.exists(build_dir):
- rel_dir, low_part = os.path.split(build_dir)
- rel_dir, two_part = os.path.split(rel_dir)
- if two_part.startswith("pyside") and two_part.endswith("build"):
- build_dir = os.path.abspath(os.path.join(two_part, low_part))
- if os.path.exists(build_dir):
- print("Note: build_dir was probably moved.")
- else:
- print("Warning: missing build dir %s" % build_dir)
- continue
- entry = LogEntry(log_dir, build_dir)
- build_history.append(entry)
- # we take the latest build for now.
- build_history.sort()
- self.history = build_history
- self._buildno = None
-
- def set_buildno(self, buildno):
- self.history[buildno] # test
- self._buildno = buildno
-
- @property
- def selected(self):
- if self._buildno is None:
- return None
- if self.history is None:
- return None
- return self.history[self._buildno]
-
- @property
- def classifiers(self):
- if not self.selected:
- raise ValueError('+++ No build with the configuration found!')
- # Python2 legacy: Correct 'linux2' to 'linux', recommended way.
- platform = 'linux' if sys.platform.startswith('linux') else sys.platform
- res = [platform]
- # the rest must be guessed from the given filename
- path = self.selected.build_dir
- base = os.path.basename(path)
- res.extend(base.split('-'))
- # add all the python and qt subkeys
- for entry in res:
- parts = entry.split(".")
- for idx in range(len(parts)):
- key = ".".join(parts[:idx])
- if key not in res:
- res.append(key)
- return res
-
-
-class TestRunner(object):
- def __init__(self, log_entry, project):
- self.log_entry = log_entry
- built_path = log_entry.build_dir
- self.test_dir = os.path.join(built_path, project)
- log_dir = log_entry.log_dir
- self.logfile = os.path.join(log_dir, project + ".log")
- os.environ['CTEST_OUTPUT_ON_FAILURE'] = '1'
- self._setup()
-
- def _setup(self):
- if sys.platform == 'win32':
- # Windows: Helper implementing 'which' command using 'where.exe'
- def winWhich(binary):
- cmd = ['where.exe', binary]
- stdOut = subprocess.Popen(cmd, stdout=subprocess.PIPE).stdout
- result = stdOut.readlines()
- stdOut.close()
- if len(result) > 0:
- return re.compile('\\s+').sub(' ', result[0].decode('utf-8'))
- return None
-
- self.makeCommand = 'nmake'
- qmakeSpec = os.environ.get('QMAKESPEC')
- if qmakeSpec is not None and 'g++' in qmakeSpec:
- self.makeCommand = 'mingw32-make'
- # Can 'tee' be found in the environment (MSYS-git installation with usr/bin in path)?
- self.teeCommand = winWhich('tee.exe')
- if self.teeCommand is None:
- git = winWhich('git.exe')
- if not git:
- # In COIN we have only git.cmd in path
- git = winWhich('git.cmd')
- if 'cmd' in git:
- # Check for a MSYS-git installation with 'cmd' in the path and grab 'tee' from usr/bin
- index = git.index('cmd')
- self.teeCommand = git[0:index] + 'bin\\tee.exe'
- if not os.path.exists(self.teeCommand):
- self.teeCommand = git[0:index] + 'usr\\bin\\tee.exe' # git V2.8.X
- if not os.path.exists(self.teeCommand):
- raise "Cannot locate 'tee' command"
-
- else:
- self.makeCommand = 'make'
- self.teeCommand = 'tee'
-
- def run(self, timeout = 300):
- """
- perform a test run in a given build. The build can be stopped by a
- keyboard interrupt for testing this script. Also, a timeout can
- be used.
- """
-
- if sys.platform == "win32":
- cmd = (self.makeCommand, 'test')
- tee_cmd = (self.teeCommand, self.logfile)
- print("running", cmd, 'in', self.test_dir, ',\n logging to', self.logfile, 'using ', tee_cmd)
- make = subprocess.Popen(cmd, cwd=self.test_dir, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
- tee = subprocess.Popen(tee_cmd, cwd=self.test_dir, stdin=make.stdout, shell=True)
- else:
- cmd = (self.makeCommand, 'test')
- tee_cmd = (self.teeCommand, self.logfile)
- print("running", cmd, 'in', self.test_dir, ',\n logging to', self.logfile, 'using ', tee_cmd)
- make = subprocess.Popen(cmd, cwd=self.test_dir, stdout=subprocess.PIPE)
- tee = subprocess.Popen(tee_cmd, cwd=self.test_dir, stdin=make.stdout)
- make.stdout.close()
- try:
- if PY3:
- output = tee.communicate(timeout=timeout)[0]
- else:
- output = tee.communicate()[0]
- except (TimeoutExpired, KeyboardInterrupt):
- print()
- print("aborted")
- tee.kill()
- make.kill()
- outs, errs = tee.communicate()
- finally:
- print("End of the test run")
- tee.wait()
-
-
-_EXAMPLE = """
-Example output:
-
-ip1 n sharp mod_name code tim
------------------------------------------------------------------------------------------
-114/391 Test #114: QtCore_qfileinfo_test-42 ........................ Passed 0.10 sec
- Start 115: QtCore_qfile_test
-115/391 Test #115: QtCore_qfile_test ...............................***Failed 0.11 sec
- Start 116: QtCore_qflags_test
-
-We will only look for the dotted lines and calculate everything from that.
-The summary statistics at the end will be ignored. That allows us to test
-this functionality with short timeout values.
-
-Note the field "mod_name". I had split this before, but it is necessary
-to use the combination as the key, because the test names are not unique.
-"""
-
-# validation of our pattern:
-
-_TEST_PAT = r"""
- ^ # start
- \s* # any whitespace ==: WS
- ([0-9]+)/([0-9]+) # ip1 "/" n
- \s+ # some WS
- Test # "Test"
- \s+ # some WS
- \# # sharp symbol "#"
- ([0-9]+) # sharp
- : # colon symbol ':'
- \s+ # some WS
- ([\w-]+) # mod_name
- .*? # whatever (non greedy)
- ( #
- (Passed) # either "Passed", None
- | #
- \*\*\*(\w+.*?) # or None, "Something"
- ) # code
- \s+ # some WS
- ([0-9]+\.[0-9]+) # tim
- \s+ # some WS
- sec # "sec"
- \s* # any WS
- $ # end
- """
-assert re.match(_TEST_PAT, _EXAMPLE.splitlines()[5], re.VERBOSE)
-assert len(re.match(_TEST_PAT, _EXAMPLE.splitlines()[5], re.VERBOSE).groups()) == 8
-assert len(re.match(_TEST_PAT, _EXAMPLE.splitlines()[7], re.VERBOSE).groups()) == 8
-
-TestResult = namedtuple("TestResult", ["idx", "mod_name", "passed",
- "code", "time"])
-
-
-class TestParser(object):
- def __init__(self, test_log):
- self._result = _parse_tests(test_log)
-
- @property
- def result(self):
- return self._result
-
- def __len__(self):
- return len(self._result)
-
- def iter_blacklist(self, blacklist):
- bl = blacklist
- for line in self._result:
- mod_name = line.mod_name
- passed = line.passed
- match = bl.find_matching_line(line)
- if not passed:
- if match:
- res = "BFAIL"
- else:
- res = "FAIL"
- else:
- if match:
- res = "BPASS"
- else:
- res = "PASS"
- yield mod_name, res
-
-
-class BlackList(object):
- def __init__(self, blname):
- if blname == None:
- f = StringIO()
- self.raw_data = []
- else:
- with open(blname) as f:
- self.raw_data = f.readlines()
- # keep all lines, but see what is not relevant
- lines = self.raw_data[:]
-
- def filtered_line(line):
- if '#' in line:
- line = line[0:line.index('#')]
- return line.split()
-
- # now put every bracketed line in a test
- # and use subsequent identifiers for a match
- def is_test(fline):
- return fline and fline[0].startswith("[")
-
- self.tests = {}
-
- if not lines:
- # nothing supplied
- return
-
- self.index = {}
- for idx, line in enumerate(lines):
- fline = filtered_line(line)
- if not fline:
- continue
- if is_test(fline):
- break
- # we have a global section
- name = ''
- self.tests[name] = []
- for idx, line in enumerate(lines):
- fline = filtered_line(line)
- if is_test(fline):
- # a new name
- name = decorate(fline[0][1:-1])
- self.tests[name] = []
- self.index[name] = idx
- elif fline:
- # a known name with a new entry
- self.tests[name].append(fline)
-
- def find_matching_line(self, test):
- """
- Take a test result.
- Find a line in the according blacklist file where all keys of the line are found.
- If line not found, do nothing.
- if line found and test passed, it is a BPASS.
- If line found and test failed, it is a BFAIL.
- """
- passed = test.passed
- classifiers = set(builds.classifiers)
-
- if "" in self.tests:
- # this is a global section
- for line in self.tests[""]:
- keys = set(line)
- if keys <= classifiers:
- # found a match!
- return line
- mod_name = test.mod_name
- if mod_name not in self.tests and decorate(mod_name) not in self.tests:
- return None
- if mod_name in self.tests:
- thing = mod_name
- else:
- thing = decorate(mod_name)
- for line in self.tests[thing]:
- keys = set(line)
- if keys <= classifiers:
- # found a match!
- return line
- else:
- return None # noting found
-
-
-"""
-Simplified blacklist file
--------------------------
-
-A comment reaches from '#' to the end of line.
-The file starts with an optional global section.
-A test is started with a [square-bracketed] section name.
-A line matches if all keys in the line are found.
-If a line matches, the corresponding test is marked BFAIL or BPASS depending if the test passed or
-not.
-
-Known keys are:
-
-darwin
-win32
-linux
-...
-
-qt5.6.1
-qt5.6.2
-...
-
-py3
-py2
-
-32bit
-64bit
-
-debug
-release
-"""
-
-"""
-Data Folding v2
-===============
-
-In the first layout of data folding, we distinguished complete domains
-like "debug/release" and incomplete domains like "ubuntu/win32" which
-can be extended to any number.
-
-This version is simpler. We do a first pass over all data and collect
-all data. Therefore, incomplete domains do not exist. The definition
-of the current members of the domain goes into a special comment at
-the beginning of the file.
-
-
-Compressing a blacklist
------------------------
-
-When we have many samples of data, it is very likely to get very similar
-entries. The redundancy is quite high, and we would like to compress
-data without loosing information.
-
-Consider the following data set:
-
-[some::sample_test]
- darwin qt5.6.1 py3 64bit debug
- darwin qt5.6.1 py3 64bit release
- darwin qt5.6.1 py2 64bit debug
- darwin qt5.6.1 py2 64bit release
- win32 qt5.6.1 py3 64bit debug
- win32 qt5.6.1 py3 64bit release
- win32 qt5.6.1 py2 64bit debug
- win32 qt5.6.1 py2 64bit release
-
-The keys "debug" and "release" build the complete set of keys in their
-domain. When sorting the lines, we can identify all similar entries which
-are only different by the keys "debug" and "release".
-
-[some::sample_test]
- darwin qt5.6.1 py3 64bit
- darwin qt5.6.1 py2 64bit
- win32 qt5.6.1 py3 64bit
- win32 qt5.6.1 py2 64bit
-
-We can do the same for "py3" and "py2", because we have again the complete
-set of possible keys available:
-
-[some::sample_test]
- darwin qt5.6.1 64bit
- win32 qt5.6.1 64bit
-
-The operating system has the current keys "darwin" and "win32".
-They are kept in a special commend, and we get:
-
-# COMPRESSION: darwin win32
-[some::sample_test]
- qt5.6.1 64bit
-
-
-Expanding a blacklist
----------------------
-
-All of the above steps are completely reversible.
-
-
-Alternate implementation
-------------------------
-
-Instead of using a special comment, I am currently in favor of
-the following:
-
-The global section gets the complete set of variables, like so
-
-# Globals
- darwin win32 linux
- qt5.6.1 qt5.6.2
- py3 py2
- 32bit 64bit
- debug release
-[some::sample_test]
- qt5.6.1 64bit
-
-This approach has the advantage that it does not depend on comments.
-The lines in the global section can always added without any conflict,
-because these test results are impossible. Therefore, we list all our
-keys without adding anything that could influence a test.
-It makes also sense to have everything explicitly listed here.
-"""
-
-def _parse_tests(test_log):
- """
- Create a TestResult object for every entry.
- """
- result = []
- if isinstance(test_log, StringIO):
- lines = test_log.readlines()
- elif test_log is not None and os.path.exists(test_log):
- with open(test_log) as f:
- lines = f.readlines()
- else:
- lines = []
- pat = _TEST_PAT
- for line in lines:
- match = re.match(pat, line, re.VERBOSE)
- if match:
- idx, n, sharp, mod_name, much_stuff, code1, code2, tim = tup = match.groups()
- # either code1 or code2 is None
- code = code1 or code2
- if idx != sharp:
- raise ValueError("inconsistent log lines or program error: %s" % tup)
- idx, n, code, tim = int(idx), int(n), code.lower(), float(tim)
- res = TestResult(idx, mod_name, code == "passed", code, tim)
- result.append(res)
- return result
-
-def decorate(mod_name):
- """
- Write the combination of "modulename_funcname"
- in the Qt-like form "modulename::funcname"
- """
- if "_" not in mod_name:
- return mod_name
- if "::" in mod_name:
- return mod_name
- name, rest = mod_name.split("_", 1)
- return name + "::" + rest
-
-def create_read_write(filename):
- if os.path.isfile(filename):
- # existing file, open for read and write
- return open(filename, 'r+')
- elif os.path.exists(filename):
- # a directory?
- raise argparse.ArgumentTypeError(None, "invalid file argument: %s" % filename)
- else:
- try:
- return open(filename, 'w')
- except IOError:
- raise argparse.ArgumentError(None, "cannot create file: %s" % filename)
-
-def learn_blacklist(fname, result, selected):
- with open(fname, "r+") as f:
- _remove_from_blacklist(f.name)
- _add_to_blacklist(f.name, result)
- _update_header(f.name, selected)
-
-def _remove_from_blacklist(old_blname):
- # get rid of existing classifiers
- classifierset = set(builds.classifiers)
-
- # for every line, remove comments and see if the current set if an exact
- # match. We will touch only exact matches.
- def filtered_line(line):
- if '#' in line:
- line = line[0:line.index('#')]
- return line.split()
-
- with open(old_blname, "r") as f:
- lines = f.readlines()
- deletions = []
- for idx, line in enumerate(lines):
- fline = filtered_line(line)
- if not fline:
- continue
- if '[' in fline[0]:
- # a heading line
- continue
- if set(fline) == classifierset:
- deletions.append(idx)
- while deletions:
- delete = deletions.pop()
- del lines[delete]
- # remove all blank lines, but keep comments
- for idx, line in reversed(list(enumerate(lines))):
- if not line.split():
- del lines[idx]
- # remove all consecutive sections, but keep comments
- for idx, line in reversed(list(enumerate(lines))):
- fline = line.split()
- if fline and fline[0].startswith("["):
- if idx+1 == len(lines):
- # remove section at the end
- del lines[idx]
- continue
- gline = lines[idx+1].split()
- if gline and gline[0].startswith("["):
- # next section right after this, remove this
- del lines[idx]
- with open(old_blname, "w") as f:
- f.writelines(lines)
-
-def _add_to_blacklist(old_blname, result):
- # insert new classifiers
- classifiers = " " + " ".join(builds.classifiers) + "\n"
- insertions = []
- additions = []
- old_bl = BlackList(old_blname)
- lines = old_bl.raw_data[:]
- if lines and not lines[-1].endswith("\n"):
- lines[-1] += "\n"
- for test in result:
- if test.passed:
- continue
- if test.mod_name in old_bl.tests:
- # the test is already there, add to the first line
- idx = old_bl.index[test.mod_name]
- insertions.append(idx)
- if decorate(test.mod_name) in old_bl.tests:
- # the same, but the name was decorated
- idx = old_bl.index[decorate(test.mod_name)]
- insertions.append(idx)
- else:
- # the test is new, append it to the end
- additions.append("[" + decorate(test.mod_name) + "]\n")
- while insertions:
- this = insertions.pop()
- lines[this] += classifiers
- for line in additions:
- lines.append(line)
- lines.append(classifiers)
- # now write the data out
- with open(old_blname, "r+") as f:
- f.writelines(lines)
-
-def _update_header(old_blname, selected):
- with open(old_blname) as f:
- lines = f.readlines()
- classifierset = set(builds.classifiers)
- for idx, line in reversed(list(enumerate(lines))):
- fline = line.split()
- if fline and fline[0].startswith('#'):
- if set(fline) >= classifierset:
- del lines[idx]
-
- classifiers = " ".join(builds.classifiers)
- path = selected.log_dir
- base = os.path.basename(path)
- test = '### test date = %s classifiers = %s\n' % (base, classifiers)
- lines.insert(0, test)
- with open(old_blname, "w") as f:
- f.writelines(lines)
-
-
-if __name__ == '__main__':
- # create the top-level parser
- parser = argparse.ArgumentParser()
- subparsers = parser.add_subparsers(dest="subparser_name")
-
- # create the parser for the "test" command
- parser_test = subparsers.add_parser("test")
- group = parser_test.add_mutually_exclusive_group(required=False)
- blacklist_default = os.path.join(script_dir, 'build_history', 'blacklist.txt')
- group.add_argument("--blacklist", "-b", type=argparse.FileType('r'),
- default=blacklist_default,
- help='a Qt blacklist file (default: {})'.format(blacklist_default))
- group.add_argument("--learn", "-l", type=create_read_write,
- help="add new entries to a blacklist file")
- parser_test.add_argument("--skip", action='store_true',
- help="skip the tests if they were run before")
- parser_test.add_argument("--environ", nargs='+',
- help="use name=value ... to set environment variables")
- parser_test.add_argument("--buildno", default=-1, type=int,
- help="use build number n (0-based), latest = -1 (default)")
- all_projects = "shiboken2 pyside2 pyside2-tools".split()
- tested_projects = "shiboken2 pyside2".split()
- parser_test.add_argument("--projects", nargs='+', type=str,
- default=tested_projects,
- choices=all_projects,
- help="use 'pyside2' (default) or other projects")
- parser_getcwd = subparsers.add_parser("getcwd")
- parser_getcwd.add_argument("filename", type=argparse.FileType('w'),
- help="write the build dir name into a file")
- parser_getcwd.add_argument("--buildno", default=-1, type=int,
- help="use build number n (0-based), latest = -1 (default)")
- parser_list = subparsers.add_parser("list")
- args = parser.parse_args()
-
- print("System:\n Platform=%s\n Executable=%s\n Version=%s\n API version=%s\n\nEnvironment:" %
- (sys.platform, sys.executable, sys.version.replace("\n", " "), sys.api_version))
- for v in sorted(os.environ.keys()):
- print(" %s=%s" % (v, os.environ[v]))
-
- builds = BuildLog(script_dir)
- if hasattr(args, "buildno"):
- try:
- builds.set_buildno(args.buildno)
- except IndexError:
- print("history out of range. Try '%s list'" % __file__)
- sys.exit(1)
-
- if args.subparser_name == "getcwd":
- print(builds.selected.build_dir, file=args.filename)
- print(builds.selected.build_dir, "written to file", args.filename.name)
- sys.exit(0)
- elif args.subparser_name == "test":
- pass # we do it afterwards
- elif args.subparser_name == "list":
- rp = os.path.relpath
- print()
- print("History")
- print("-------")
- for idx, build in enumerate(builds.history):
- print(idx, rp(build.log_dir), rp(build.build_dir))
- print()
- print("Note: only the last history entry of a folder is valid!")
- sys.exit(0)
- else:
- parser.print_help()
- sys.exit(1)
-
- if args.blacklist:
- args.blacklist.close()
- bl = BlackList(args.blacklist.name)
- elif args.learn:
- args.learn.close()
- learn_blacklist(args.learn.name, result.result, builds.selected)
- bl = BlackList(args.learn.name)
- else:
- bl = BlackList(None)
- if args.environ:
- for line in args.environ:
- things = line.split("=")
- if len(things) != 2:
- raise ValueError("you need to pass one or more name=value pairs.")
- key, value = things
- os.environ[key] = value
-
- q = 5 * [0]
-
- # now loop over the projects and accumulate
- for project in args.projects:
- runner = TestRunner(builds.selected, project)
- if os.path.exists(runner.logfile) and args.skip:
- print("Parsing existing log file:", runner.logfile)
- else:
- runner.run(10 * 60)
- result = TestParser(runner.logfile)
- r = 5 * [0]
- print("********* Start testing of %s *********" % project)
- print("Config: Using", " ".join(builds.classifiers))
- for test, res in result.iter_blacklist(bl):
- print("%-6s" % res, ":", decorate(test) + "()")
- r[0] += 1 if res == "PASS" else 0
- r[1] += 1 if res == "FAIL" else 0
- r[2] += 1 if res == "SKIPPED" else 0 # not yet supported
- r[3] += 1 if res == "BFAIL" else 0
- r[4] += 1 if res == "BPASS" else 0
- print("Totals:", sum(r), "tests.",
- "{} passed, {} failed, {} skipped, {} blacklisted, {} bpassed."
- .format(*r))
- print("********* Finished testing of %s *********" % project)
- print()
- q = list(map(lambda x, y: x+y, r, q))
-
- if len(args.projects) > 1:
- print("All above projects:", sum(q), "tests.",
- "{} passed, {} failed, {} skipped, {} blacklisted, {} bpassed."
- .format(*q))
- print()
-
- # nag us about unsupported projects
- ap, tp = set(all_projects), set(tested_projects)
- if ap != tp:
- print("+++++ Note: please support", " ".join(ap-tp), "+++++")
- print()
+import testing
+import testing.blacklist # just to be sure it's us...
- for project in args.projects:
- runner = TestRunner(builds.selected, project)
- result = TestParser(runner.logfile)
- for test, res in result.iter_blacklist(bl):
- if res == "FAIL":
- raise ValueError("At least one failure was not blacklisted")
- # the makefile does run, although it does not find any tests.
- # We simply check if any tests were found.
- if len(result) == 0:
- path = builds.selected.build_dir
- project = os.path.join(path, args.project)
- raise ValueError("there are no tests in %s" % project)
+testing.main()