Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1 | #!/usr/bin/python |
| 2 | |
| 3 | # ex:ts=4:sw=4:sts=4:et |
| 4 | # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- |
| 5 | # |
| 6 | # Copyright (C) 2015 Alexandru Damian for Intel Corp. |
| 7 | # |
| 8 | # This program is free software; you can redistribute it and/or modify |
| 9 | # it under the terms of the GNU General Public License version 2 as |
| 10 | # published by the Free Software Foundation. |
| 11 | # |
| 12 | # This program is distributed in the hope that it will be useful, |
| 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| 15 | # GNU General Public License for more details. |
| 16 | # |
| 17 | # You should have received a copy of the GNU General Public License along |
| 18 | # with this program; if not, write to the Free Software Foundation, Inc., |
| 19 | # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| 20 | |
| 21 | |
| 22 | # This is the main test execution controller. It is designed to be run |
| 23 | # manually from the command line, or to be called from a different program |
| 24 | # that schedules test execution. |
| 25 | # |
| 26 | # Execute runner.py -h for help. |
| 27 | |
| 28 | |
| 29 | |
| 30 | from __future__ import print_function |
| 31 | import sys, os |
| 32 | import unittest, importlib |
| 33 | import logging, pprint, json |
| 34 | import re |
| 35 | from shellutils import ShellCmdException, mkdirhier, run_shell_cmd |
| 36 | |
| 37 | import config |
| 38 | |
| 39 | # we also log to a file, in addition to console, because our output is important |
| 40 | __log_file_name__ = os.path.join(os.path.dirname(__file__), "log/tts_%d.log" % config.OWN_PID) |
| 41 | mkdirhier(os.path.dirname(__log_file_name__)) |
| 42 | __log_file__ = open(__log_file_name__, "w") |
| 43 | __file_handler__ = logging.StreamHandler(__log_file__) |
| 44 | __file_handler__.setFormatter(logging.Formatter("%(asctime)s %(levelname)s: %(message)s")) |
| 45 | |
| 46 | config.logger.addHandler(__file_handler__) |
| 47 | |
| 48 | # set up log directory |
| 49 | try: |
| 50 | if not os.path.exists(config.LOGDIR): |
| 51 | os.mkdir(config.LOGDIR) |
| 52 | else: |
| 53 | if not os.path.isdir(config.LOGDIR): |
| 54 | raise Exception("Expected log dir '%s' is not actually a directory." % config.LOGDIR) |
| 55 | except OSError as exc: |
| 56 | raise exc |
| 57 | |
| 58 | # creates the under-test-branch as a separate directory |
| 59 | def set_up_test_branch(settings, branch_name): |
| 60 | testdir = "%s/%s.%d" % (settings['workdir'], config.TEST_DIR_NAME, config.OWN_PID) |
| 61 | |
| 62 | # creates the host dir |
| 63 | if os.path.exists(testdir): |
| 64 | raise Exception("Test dir '%s'is already there, aborting" % testdir) |
| 65 | |
| 66 | # may raise OSError, is to be handled by the caller |
| 67 | os.makedirs(testdir) |
| 68 | |
| 69 | |
| 70 | # copies over the .git from the localclone |
| 71 | run_shell_cmd("cp -a '%s'/.git '%s'" % (settings['localclone'], testdir)) |
| 72 | |
| 73 | # add the remote if it doesn't exist |
| 74 | crt_remotes = run_shell_cmd("git remote -v", cwd=testdir) |
| 75 | remotes = [word for line in crt_remotes.split("\n") for word in line.split()] |
| 76 | if not config.CONTRIB_REPO in remotes: |
| 77 | remote_name = "tts_contrib" |
| 78 | run_shell_cmd("git remote add %s %s" % (remote_name, config.CONTRIB_REPO), cwd=testdir) |
| 79 | else: |
| 80 | remote_name = remotes[remotes.index(config.CONTRIB_REPO) - 1] |
| 81 | |
| 82 | # do the fetch |
| 83 | run_shell_cmd("git fetch %s -p" % remote_name, cwd=testdir) |
| 84 | |
| 85 | # do the checkout |
| 86 | run_shell_cmd("git checkout origin/master && git branch -D %s; git checkout %s/%s -b %s && git reset --hard" % (branch_name, remote_name, branch_name, branch_name), cwd=testdir) |
| 87 | |
| 88 | return testdir |
| 89 | |
| 90 | |
| 91 | def __search_for_tests(): |
| 92 | # we find all classes that can run, and run them |
| 93 | tests = [] |
| 94 | for _, _, files_list in os.walk(os.path.dirname(os.path.abspath(__file__))): |
| 95 | for module_file in [f[:-3] for f in files_list if f.endswith(".py") and not f.startswith("__init__")]: |
| 96 | config.logger.debug("Inspecting module %s", module_file) |
| 97 | current_module = importlib.import_module(module_file) |
| 98 | crtclass_names = vars(current_module) |
| 99 | for name in crtclass_names: |
| 100 | tested_value = crtclass_names[name] |
| 101 | if isinstance(tested_value, type(unittest.TestCase)) and issubclass(tested_value, unittest.TestCase): |
| 102 | tests.append((module_file, name)) |
| 103 | break |
| 104 | return tests |
| 105 | |
| 106 | |
| 107 | # boilerplate to self discover tests and run them |
| 108 | def execute_tests(dir_under_test, testname): |
| 109 | |
| 110 | if testname is not None and "." in testname: |
| 111 | tests = [] |
| 112 | tests.append(tuple(testname.split(".", 2))) |
| 113 | else: |
| 114 | tests = __search_for_tests() |
| 115 | |
| 116 | # let's move to the directory under test |
| 117 | crt_dir = os.getcwd() |
| 118 | os.chdir(dir_under_test) |
| 119 | |
| 120 | # execute each module |
| 121 | # pylint: disable=broad-except |
| 122 | # we disable the broad-except because we want to actually catch all possible exceptions |
| 123 | try: |
| 124 | # sorting the tests by the numeric order in the class name |
| 125 | tests = sorted(tests, key=lambda x: int(re.search(r"[0-9]+", x[1]).group(0))) |
| 126 | config.logger.debug("Discovered test clases: %s", pprint.pformat(tests)) |
| 127 | unittest.installHandler() |
| 128 | suite = unittest.TestSuite() |
| 129 | loader = unittest.TestLoader() |
| 130 | result = unittest.TestResult() |
| 131 | result.failfast = True |
| 132 | for module_file, test_name in tests: |
| 133 | suite.addTest(loader.loadTestsFromName("%s.%s" % (module_file, test_name))) |
| 134 | config.logger.info("Running %d test(s)", suite.countTestCases()) |
| 135 | suite.run(result) |
| 136 | |
| 137 | for error in result.errors: |
| 138 | config.logger.error("Exception on test: %s\n%s", error[0], |
| 139 | "\n".join(["-- %s" % x for x in error[1].split("\n")])) |
| 140 | |
| 141 | for failure in result.failures: |
| 142 | config.logger.error("Failed test: %s:\n%s\n", failure[0], |
| 143 | "\n".join(["-- %s" % x for x in failure[1].split("\n")])) |
| 144 | |
| 145 | config.logger.info("Test results: %d ran, %d errors, %d failures", result.testsRun, len(result.errors), len(result.failures)) |
| 146 | |
| 147 | except Exception as exc: |
| 148 | import traceback |
| 149 | config.logger.error("Exception while running test. Tracedump: \n%s", traceback.format_exc(exc)) |
| 150 | finally: |
| 151 | os.chdir(crt_dir) |
| 152 | return len(result.failures) |
| 153 | |
| 154 | # verify that we had a branch-under-test name as parameter |
| 155 | def validate_args(): |
| 156 | from optparse import OptionParser |
| 157 | parser = OptionParser(usage="usage: %prog [options] branch_under_test") |
| 158 | |
| 159 | parser.add_option("-t", "--test-dir", dest="testdir", default=None, help="Use specified directory to run tests, inhibits the checkout.") |
| 160 | parser.add_option("-s", "--single", dest="singletest", default=None, help="Run only the specified test") |
| 161 | |
| 162 | (options, args) = parser.parse_args() |
| 163 | if len(args) < 1: |
| 164 | raise Exception("Please specify the branch to run on. Use option '-h' when in doubt.") |
| 165 | return (options, args) |
| 166 | |
| 167 | |
| 168 | |
| 169 | |
| 170 | # load the configuration options |
| 171 | def read_settings(): |
| 172 | if not os.path.exists(config.SETTINGS_FILE) or not os.path.isfile(config.SETTINGS_FILE): |
| 173 | raise Exception("Config file '%s' cannot be openend" % config.SETTINGS_FILE) |
| 174 | return json.loads(open(config.SETTINGS_FILE, "r").read()) |
| 175 | |
| 176 | |
| 177 | # cleanup ! |
| 178 | def clean_up(testdir): |
| 179 | run_shell_cmd("rm -rf -- '%s'" % testdir) |
| 180 | |
| 181 | def dump_info(settings, options, args): |
| 182 | """ detailed information about current run configuration, for debugging purposes. |
| 183 | """ |
| 184 | config.logger.debug("Settings:\n%s\nOptions:\n%s\nArguments:\n%s\n", settings, options, args) |
| 185 | |
| 186 | def main(): |
| 187 | (options, args) = validate_args() |
| 188 | |
| 189 | settings = read_settings() |
| 190 | need_cleanup = False |
| 191 | |
| 192 | # dump debug info |
| 193 | dump_info(settings, options, args) |
| 194 | |
| 195 | testdir = None |
| 196 | no_failures = 1 |
| 197 | try: |
| 198 | if options.testdir is not None and os.path.exists(options.testdir): |
| 199 | testdir = os.path.abspath(options.testdir) |
| 200 | config.logger.info("No checkout, using %s", testdir) |
| 201 | else: |
| 202 | need_cleanup = True |
| 203 | testdir = set_up_test_branch(settings, args[0]) # we expect a branch name as first argument |
| 204 | |
| 205 | config.TESTDIR = testdir # we let tests know where to run |
| 206 | |
| 207 | # ensure that the test dir only contains no *.pyc leftovers |
| 208 | run_shell_cmd("find '%s' -type f -name *.pyc -exec rm {} \\;" % testdir) |
| 209 | |
| 210 | no_failures = execute_tests(testdir, options.singletest) |
| 211 | |
| 212 | except ShellCmdException as exc: |
| 213 | import traceback |
| 214 | config.logger.error("Error while setting up testing. Traceback: \n%s", traceback.format_exc(exc)) |
| 215 | finally: |
| 216 | if need_cleanup and testdir is not None: |
| 217 | clean_up(testdir) |
| 218 | |
| 219 | sys.exit(no_failures) |
| 220 | |
| 221 | if __name__ == "__main__": |
| 222 | main() |