| # coding=utf-8 | |
| """ | |
| Runs tox from current directory. | |
| It supports any runner, but well-known runners (py.test and unittest) are switched to our internal runners to provide | |
| better support | |
| """ | |
| import os | |
| import pluggy | |
| from tox import config as tox_config | |
| from tox.session import Session | |
| from tcmessages import TeamcityServiceMessages | |
| from tox import exception | |
| teamcity = TeamcityServiceMessages() | |
| hookimpl = pluggy.HookimplMarker("tox") | |
| helpers_dir = str(os.path.split(__file__)[0]) | |
| class JbToxHook(object): | |
| """ | |
| Hook to report test start and test end. | |
| """ | |
| def __init__(self, config): | |
| self.offsets = dict() | |
| self.current_env = None | |
| self.config = config | |
| @hookimpl | |
| def tox_runtest_pre(self, venv): | |
| """ | |
| Launched before each setup. | |
| It means prev env (if any) just finished and new is going to be created | |
| :param venv: current virtual env | |
| """ | |
| self.current_env = venv | |
| name = venv.name | |
| node_id = self.offsets[name] | |
| teamcity.testStarted(name, location="tox_env://" + str(name), parentNodeId="0", nodeId=node_id) | |
| @hookimpl | |
| def tox_runtest_post(self, venv): | |
| """ | |
| Finishes currently running env. reporting its state | |
| """ | |
| if not self.current_env: | |
| return | |
| name = venv.name | |
| node_id = self.offsets[name] | |
| status = self.current_env.status | |
| if isinstance(status, exception.InterpreterNotFound): | |
| if self.config.option.skip_missing_interpreters: | |
| self._reportFailure("SKIP", status, node_id) | |
| else: | |
| self._reportFailure("ERROR", status, node_id) | |
| elif status == "platform mismatch": | |
| self._reportFailure("SKIP", status, node_id) | |
| elif status and status == "ignored failed command": | |
| print(" %s: %s" % (self.current_env.name, str(status))) | |
| elif status and status != "skipped tests": | |
| self._reportFailure("ERROR", status, node_id) | |
| else: | |
| teamcity.testStdOut(self.current_env.name, "\n", nodeId=node_id) | |
| teamcity.testFinished(self.current_env.name, nodeId=node_id) | |
| self.current_env = None | |
| def _reportFailure(self, state, message, node_id): | |
| """ | |
| In idBased mode each test is leaf, there is no suites, so we can rerport directly to the test | |
| :param state: SKIP or ERROR (suite result) | |
| """ | |
| if state == "SKIP": | |
| teamcity.testIgnored(state, str(message), nodeId=node_id) | |
| else: | |
| teamcity.testFailed(state, str(message), nodeId=node_id) | |
| class Fixer(object): | |
| def __init__(self, runner_name): | |
| self.runner_name = runner_name | |
| def fix(self, command, bin, offset): | |
| return [bin, os.path.join(helpers_dir, self.runner_name), "--offset", str(offset), "--"] | |
| def is_parallel(self, *args, **kwargs): | |
| return False | |
| class _Unit2(Fixer): | |
| def __init__(self): | |
| super(_Unit2, self).__init__("_jb_unittest_runner.py") | |
| def fix(self, command, bin, offset): | |
| if command[0] == "unit2": | |
| return [bin, os.path.join(helpers_dir, "utrunner.py")] + command[1:] + ["true"] | |
| elif command == ["python", "-m", "unittest", "discover"]: | |
| return super(_Unit2, self).fix(command, bin, offset) + ["discover"] | |
| return None | |
| class _PyTest(Fixer): | |
| def __init__(self): | |
| super(_PyTest, self).__init__("_jb_pytest_runner.py") | |
| def is_parallel(self, config): # If xdist is used, then pytest will use parallel run | |
| deps = getattr(config, "deps", []) | |
| return bool([d for d in deps if d.name == "pytest-xdist"]) | |
| def fix(self, command, bin, offset): | |
| if command[0] not in ["pytest", "py.test"]: | |
| return None | |
| return super(_PyTest, self).fix(command, bin, offset) + command[1:] | |
| class _Nose(Fixer): | |
| def __init__(self): | |
| super(_Nose, self).__init__("_jb_nosetest_runner.py") | |
| def fix(self, command, bin, offset): | |
| if command[0] != "nosetests": | |
| return None | |
| return super(_Nose, self).fix(command, bin, offset) + command[1:] | |
| _RUNNERS = [_Unit2(), _PyTest(), _Nose()] | |
| import sys | |
| durationStrategy = "automatic" | |
| config = tox_config.parseconfig(args=sys.argv[1:]) | |
| hook = JbToxHook(config) | |
| config.pluginmanager.register(hook, "jbtoxplugin") | |
| offset = 1 | |
| for env, tmp_config in config.envconfigs.items(): | |
| hook.offsets[env] = offset | |
| if not tmp_config.setenv: | |
| tmp_config.setenv = dict() | |
| tmp_config.setenv["_jb_do_not_call_enter_matrix"] = "1" | |
| commands = tmp_config.commands | |
| if "_jb_do_not_patch_test_runners" not in os.environ and isinstance(commands, list): | |
| for fixer in _RUNNERS: | |
| _env = config.envconfigs[env] | |
| for i, command in enumerate(commands): | |
| if command: | |
| fixed_command = fixer.fix(command, str(_env.envpython), offset) | |
| if fixer.is_parallel(tmp_config): | |
| durationStrategy = "manual" | |
| if fixed_command: | |
| commands[i] = fixed_command | |
| tmp_config.commands = commands | |
| offset += 10000 | |
| session = Session(config) | |
| teamcity.testMatrixEntered(durationStrategy=durationStrategy) | |
| sys.exit(session.runcommand()) |