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 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330
|
#!/usr/bin/env python3
# encoding: utf-8
from glob import glob
import argparse
import os
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
import time
import datetime
#Python2/3 compat code for iterating items
try:
dict.iteritems
except AttributeError:
# Python 3
def itervalues(d):
return iter(d.values())
def iteritems(d):
return iter(d.items())
def bytes_to_str(bytes):
return str(bytes, 'utf-8')
else:
# Python 2
def itervalues(d):
return d.itervalues()
def iteritems(d):
return d.iteritems()
def bytes_to_str(bytes):
return str(bytes)
has_timeout = "TimeoutExpired" in dir(subprocess)
if "monotonic" in dir(time):
get_time = time.monotonic
elif "perf_counter" in dir(time):
get_time = time.perf_counter
else:
get_time = time.time
def datadir():
return os.path.join(os.path.dirname(__file__), "data")
def datadir_for_testing():
return os.path.relpath(".", os.path.dirname(__file__))
def out(string):
sys.stdout.write(string)
sys.stdout.flush()
class WidelandsTestCase(unittest.TestCase):
do_use_random_directory = True
path_to_widelands_binary = None
keep_output_around = False
ignore_error_code = False
timeout = 600
def __init__(self, test_script, **wlargs):
unittest.TestCase.__init__(self)
self._test_script = test_script
self._wlargs = wlargs
def __str__(self):
return self._test_script
def setUp(self):
if self.do_use_random_directory:
self.run_dir = tempfile.mkdtemp(prefix="widelands_regression_test")
else:
self.run_dir = os.path.join(tempfile.gettempdir(), "widelands_regression_test", self.__class__.__name__)
if os.path.exists(self.run_dir):
if not self.keep_output_around:
shutil.rmtree(self.run_dir)
os.makedirs(self.run_dir)
else:
os.makedirs(self.run_dir)
self.widelands_returncode = 0
self.wl_timed_out = False
def run(self, result=None):
self.currentResult = result # remember result for use in tearDown
unittest.TestCase.run(self, result)
def tearDown(self):
if self.currentResult.wasSuccessful() and not self.keep_output_around:
shutil.rmtree(self.run_dir)
def run_widelands(self, wlargs, which_time):
"""Run Widelands with the given 'wlargs'. 'which_time' is an integer
defining the number of times Widelands has been run this test case
(i.e. because we might load a saved game from an earlier run. This will
impact the filenames for stdout.txt.
Returns the stdout filename."""
stdout_filename = os.path.join(self.run_dir, "stdout_{:02d}.txt".format(which_time))
if (os.path.exists(stdout_filename)):
os.unlink(stdout_filename)
with open(stdout_filename, 'a') as stdout_file:
args = [self.path_to_widelands_binary,
'--verbose',
'--datadir={}'.format(datadir()),
'--datadir_for_testing={}'.format(datadir_for_testing()),
'--homedir={}'.format(self.run_dir),
'--nosound',
'--fail-on-lua-error',
'--language=en_US' ]
args += [ "--{}={}".format(key, value) for key, value in iteritems(wlargs) ]
stdout_file.write("Running widelands binary: ")
for anarg in args:
stdout_file.write(anarg)
stdout_file.write(" ")
stdout_file.write("\n")
start_time = get_time()
widelands = subprocess.Popen(
args, shell=False, stdout=stdout_file, stderr=subprocess.STDOUT)
if has_timeout:
try:
widelands.communicate(timeout = self.timeout)
except subprocess.TimeoutExpired:
widelands.kill()
widelands.communicate()
self.wl_timed_out = True
stdout_file.write("\nTimed out.\n")
else:
widelands.communicate()
end_time = get_time()
stdout_file.flush()
self.duration = datetime.timedelta(seconds = end_time - start_time)
stdout_file.write("\nReturned from Widelands in {}, return code is {:d}\n".format(
self.duration, widelands.returncode))
self.widelands_returncode = widelands.returncode
return stdout_filename
def runTest(self):
out("\nStarting test case {}\n".format(self._test_script))
out(" Running Widelands ...\n")
stdout_filename = self.run_widelands(self._wlargs, 0)
stdout = open(stdout_filename, "r").read()
self.verify_success(stdout, stdout_filename)
find_saves = lambda stdout: re.findall("Script requests save to: (\w+)$", stdout, re.M)
savegame_done = { fn: False for fn in find_saves(stdout) }
which_time = 1
while not all(savegame_done.values()):
for savegame in sorted(savegame_done):
if not savegame_done[savegame]: break
out(" Loading savegame: {} ...\n".format(savegame))
stdout_filename = self.run_widelands({ "loadgame": os.path.join(
self.run_dir, "save", "{}.wgf".format(savegame))}, which_time)
which_time += 1
stdout = open(stdout_filename, "r").read()
for new_save in find_saves(stdout):
if new_save not in savegame_done:
savegame_done[new_save] = False
savegame_done[savegame] = True
self.verify_success(stdout, stdout_filename)
def verify_success(self, stdout, stdout_filename):
out(" Elapsed time: {}\n".format(self.duration))
# Catch instabilities with SDL in CI environment
if self.widelands_returncode == 2:
print("SDL initialization failed. TEST SKIPPED.")
with open(stdout_filename, 'r') as stdout_file:
for line in stdout_file.readlines():
print(line.strip())
out(" SKIPPED.\n")
else:
common_msg = "Analyze the files in {} to see why this test case failed. Stdout is\n {}\n\nstdout:\n{}".format(
self.run_dir, stdout_filename, stdout)
if self.wl_timed_out:
out(" TIMED OUT.\n")
self.assertTrue(False, "The test timed out. {}".format(common_msg))
if self.widelands_returncode == 1 and self.ignore_error_code:
out(" IGNORING error code 1\n")
else:
self.assertTrue(self.widelands_returncode == 0,
"Widelands exited abnormally. {}".format(common_msg)
)
self.assertTrue("All Tests passed" in stdout,
"Not all tests pass. {}.".format(common_msg)
)
self.assertFalse("lua_errors.cc" in stdout,
"Not all tests pass. {}.".format(common_msg)
)
out(" done.\n")
if self.keep_output_around:
out(" stdout: {}\n".format(stdout_filename))
def parse_args():
p = argparse.ArgumentParser(description=
"Run the regression tests suite."
)
p.add_argument("-r", "--regexp", type=str,
help = "Run only the tests from the files which filename matches."
)
p.add_argument("-n", "--nonrandom", action="store_true", default = False,
help = "Do not randomize the directories for the tests. This is useful "
"if you want to run a test more often than once and not reopen stdout.txt "
"in your editor."
)
p.add_argument("-k", "--keep-around", action="store_true", default = False,
help = "Keep the output files around even when a test terminates successfully."
)
p.add_argument("-b", "--binary", type=str,
help = "Run this binary as Widelands. Otherwise some default paths are searched."
)
p.add_argument("-i", "--ignore-error-code", action="store_true", default = False,
help = "Assume success on return code 1, to allow running the tests "
"without ASan reporting false positives."
)
if has_timeout:
p.add_argument("-t", "--timeout", type=float, default = "10",
help = "Set the timeout duration for test cases in minutes. Default is 10 minutes."
)
else:
p.epilog = "Python version does not support timeout. -t, --timeout is disabled. " \
"Python >=3.3 is required for timeout support."
args = p.parse_args()
if args.binary is None:
potential_binaries = (
glob(os.path.join(os.curdir, "widelands")) +
glob(os.path.join(os.path.dirname(__file__), "widelands")) +
glob(os.path.join("src", "widelands")) +
glob(os.path.join("..", "*", "src", "widelands"))
)
if potential_binaries:
args.binary = potential_binaries[0]
elif "which" in dir(shutil):
args.binary = shutil.which("widelands")
if args.binary is None:
p.error("No widelands binary found. Please specify with -b.")
return args
def discover_loadgame_tests(regexp, suite):
"""Add all tests using --loadgame to the 'suite'."""
# Savegames with custom scripts
for fixture in sorted(glob(os.path.join("test", "save", "*"))):
if not os.path.isdir(fixture):
continue
savegame = sorted(glob(os.path.join(fixture, "*.wgf")))[0]
for test_script in sorted(glob(os.path.join(fixture, "test*.lua"))):
if regexp is not None and not re.search(regexp, test_script):
continue
suite.addTest(
WidelandsTestCase(test_script,
loadgame=savegame, script=test_script))
# Savegames without custom script, just test loading
test_script = os.path.join("test", "scripting", "load_and_quit.lua")
for savegame in sorted(glob(os.path.join("test", "save", "*.wgf"))):
if regexp is not None and not re.search(regexp, savegame):
continue
suite.addTest(WidelandsTestCase(savegame, loadgame=savegame, script=test_script))
def discover_scenario_tests(regexp, suite):
"""Add all tests using --scenario to the 'suite'."""
for wlmap in sorted(glob(os.path.join("test", "maps", "*"))):
if not os.path.isdir(wlmap):
continue
for test_script in sorted(glob(os.path.join(wlmap, "scripting", "test*.lua"))):
if regexp is not None and not re.search(regexp, test_script):
continue
suite.addTest(
WidelandsTestCase(test_script,
scenario=wlmap, script=test_script))
def discover_game_template_tests(regexp, suite):
"""Add all tests using --new_game_from_template to the 'suite'."""
for templ in sorted(glob(os.path.join("test", "templates", "test*.wgt"))):
if not os.path.isfile(templ):
continue
test_script = templ[:-3] + 'lua'
if not os.path.isfile(test_script):
print(f"WARNING: Game template test { templ }: corresponding script { test_script } not found - Skipping.")
continue
if regexp is not None and not re.search(regexp, test_script):
continue
suite.addTest(
WidelandsTestCase(test_script,
new_game_from_template=templ, script=test_script))
def discover_editor_tests(regexp, suite):
"""Add all tests needing --editor to the 'suite'."""
for wlmap in sorted(glob(os.path.join("test", "maps", "*"))):
if not os.path.isdir(wlmap):
continue
for test_script in sorted(glob(os.path.join(wlmap, "scripting", "editor_test*.lua"))):
if regexp is not None and not re.search(regexp, test_script):
continue
suite.addTest(
WidelandsTestCase(test_script,
editor=wlmap, script=test_script))
def main():
args = parse_args()
WidelandsTestCase.path_to_widelands_binary = args.binary
print("Using '{}' binary.".format(args.binary))
WidelandsTestCase.do_use_random_directory = not args.nonrandom
WidelandsTestCase.keep_output_around = args.keep_around
WidelandsTestCase.ignore_error_code = args.ignore_error_code
if has_timeout:
WidelandsTestCase.timeout = args.timeout * 60
else:
out("Python version does not support timeout on subprocesses,\n"
"test cases may run indefinitely.\n\n")
suite = unittest.TestSuite()
discover_loadgame_tests(args.regexp, suite)
discover_scenario_tests(args.regexp, suite)
discover_game_template_tests(args.regexp, suite)
discover_editor_tests(args.regexp, suite)
return unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful()
if __name__ == '__main__':
sys.exit(0 if main() else 1)
|