1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
|
# ###################################################
# Copyright (C) 2008-2017 The Unknown Horizons Team
# team@unknown-horizons.org
# This file is part of Unknown Horizons.
#
# Unknown Horizons is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the
# Free Software Foundation, Inc.,
# 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
# ###################################################
import os
import subprocess
import sys
import pytest
from tests import RANDOM_SEED
from tests.gui import TEST_FIXTURES_DIR
from tests.utils import Timer
@pytest.fixture
def gui():
# This is necessary for the function to work with pytest. It isn't actually executed by
# pytest because it's running in a subprocess, but oherwise pytest complains about unknown
# fixture `gui`.
pass
class TestFailed(Exception):
pass
def pytest_pyfunc_call(pyfuncitem):
"""
Tests marked with gui_test will, instead of executing the test function, start a new
process with the game and run the test function code inside that process.
"""
info = pyfuncitem.get_marker('gui_test')
if not info:
return
tmpdir = pyfuncitem._request.getfixturevalue('tmpdir')
use_fixture = info.kwargs.get('use_fixture', None)
use_dev_map = info.kwargs.get('use_dev_map', False)
use_scenario = info.kwargs.get('use_scenario', None)
ai_players = info.kwargs.get('ai_players', 0)
additional_cmdline = info.kwargs.get('additional_cmdline', None)
timeout = info.kwargs.get('timeout', 15 * 60)
modify_user_dir = info.kwargs.get('_modify_user_dir', lambda v: v)
test_name = '{}.{}'.format(pyfuncitem.module.__name__, pyfuncitem.name)
# when running under coverage, enable it for subprocesses too
if os.environ.get('RUNCOV'):
executable = ['coverage', 'run']
else:
executable = [sys.executable]
args = executable + ['run_uh.py', '--sp-seed', str(RANDOM_SEED), '--gui-test', test_name]
if use_fixture:
path = os.path.join(TEST_FIXTURES_DIR, use_fixture + '.sqlite')
if not os.path.exists(path):
raise Exception('Savegame {} not found'.format(path))
args.extend(['--load-game', path])
elif use_dev_map:
args.append('--start-dev-map')
elif use_scenario:
args.extend(['--start-scenario', use_scenario + '.yaml'])
if ai_players:
args.extend(['--ai-players', str(ai_players)])
if additional_cmdline:
args.extend(additional_cmdline)
user_dir = modify_user_dir(tmpdir.join('user_dir'))
env = os.environ.copy()
# Setup temporary user directory for each test
env['UH_USER_DIR'] = str(user_dir)
# Activate fail-fast, this way the game will stop running when for example the savegame
# could not be loaded (instead of showing an error popup)
env['FAIL_FAST'] = '1'
# Show additional information (e.g. about threads) when the interpreter crashes
env['PYTHONFAULTHANDLER'] = '1'
if pyfuncitem.config.option.capture == 'no':
# if pytest does not capture stdout, then most likely someone wants to
# use a debugger (he passed -s at the cmdline). In that case, we will
# redirect stdout/stderr from the gui-test process to the testrunner
# process.
stdout = sys.stdout
stderr = sys.stderr
output_captured = False
else:
# if pytest captures stdout, we can't redirect to sys.stdout, as that
# was replaced by a custom object. Instead we capture it and return the
# data at the end.
stdout = subprocess.PIPE
stderr = subprocess.PIPE
output_captured = True
# Start game
proc = subprocess.Popen(args, stdout=stdout, stderr=stderr, env=env)
def handler(signum, frame):
proc.kill()
raise TestFailed('\n\nTest run exceeded {:d}s time limit'.format(timeout))
timelimit = Timer(handler)
timelimit.start(timeout)
stdout, stderr = proc.communicate()
if proc.returncode != 0:
if output_captured:
if stdout:
print(stdout)
if b'Traceback' not in stderr:
stderr += b'\nNo usable error output received, possibly a segfault.'
raise TestFailed(stderr.decode('ascii'))
return True
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
"""
When a gui test fails, we replace the internal traceback with the one from the subprocess
stderr.
"""
report = (yield).get_result()
if report.when != 'call' or report.outcome != 'failed':
return report
if isinstance(call.excinfo.value, TestFailed):
report.longrepr = call.excinfo.value.args[0]
return report
|