|
@@ -64,7 +64,11 @@ def update_keyval_file(filename, module, returncode):
|
|
keyval["name"] = module.name
|
|
keyval["name"] = module.name
|
|
keyval["tested_dir"] = module.tested_dir
|
|
keyval["tested_dir"] = module.tested_dir
|
|
if "status" not in keyval.keys():
|
|
if "status" not in keyval.keys():
|
|
- keyval["status"] = "failed" if returncode else "passed"
|
|
|
|
|
|
+ if returncode is None or returncode:
|
|
|
|
+ status = "failed"
|
|
|
|
+ else:
|
|
|
|
+ status = "passed"
|
|
|
|
+ keyval["status"] = status
|
|
keyval["returncode"] = returncode
|
|
keyval["returncode"] = returncode
|
|
keyval["test_file_authors"] = test_file_authors
|
|
keyval["test_file_authors"] = test_file_authors
|
|
|
|
|
|
@@ -88,6 +92,7 @@ class GrassTestFilesInvoker(object):
|
|
clean_before=True,
|
|
clean_before=True,
|
|
testsuite_dir="testsuite",
|
|
testsuite_dir="testsuite",
|
|
file_anonymizer=None,
|
|
file_anonymizer=None,
|
|
|
|
+ timeout=None,
|
|
):
|
|
):
|
|
"""
|
|
"""
|
|
|
|
|
|
@@ -97,6 +102,7 @@ class GrassTestFilesInvoker(object):
|
|
:param bool clean_before: if mapsets, outputs, and results
|
|
:param bool clean_before: if mapsets, outputs, and results
|
|
should be removed before the tests start
|
|
should be removed before the tests start
|
|
(advantageous when the previous run left everything behind)
|
|
(advantageous when the previous run left everything behind)
|
|
|
|
+ :param float timeout: maximum duration of one test in seconds
|
|
"""
|
|
"""
|
|
self.start_dir = start_dir
|
|
self.start_dir = start_dir
|
|
self.clean_mapsets = clean_mapsets
|
|
self.clean_mapsets = clean_mapsets
|
|
@@ -112,6 +118,8 @@ class GrassTestFilesInvoker(object):
|
|
else:
|
|
else:
|
|
self._file_anonymizer = file_anonymizer
|
|
self._file_anonymizer = file_anonymizer
|
|
|
|
|
|
|
|
+ self.timeout = timeout
|
|
|
|
+
|
|
def _create_mapset(self, gisdbase, location, module):
|
|
def _create_mapset(self, gisdbase, location, module):
|
|
"""Create mapset according to information in module.
|
|
"""Create mapset according to information in module.
|
|
|
|
|
|
@@ -136,7 +144,7 @@ class GrassTestFilesInvoker(object):
|
|
)
|
|
)
|
|
return mapset, mapset_dir
|
|
return mapset, mapset_dir
|
|
|
|
|
|
- def _run_test_module(self, module, results_dir, gisdbase, location):
|
|
|
|
|
|
+ def _run_test_module(self, module, results_dir, gisdbase, location, timeout):
|
|
"""Run one test file."""
|
|
"""Run one test file."""
|
|
self.testsuite_dirs[module.tested_dir].append(module.name)
|
|
self.testsuite_dirs[module.tested_dir].append(module.name)
|
|
cwd = os.path.join(results_dir, module.tested_dir, module.name)
|
|
cwd = os.path.join(results_dir, module.tested_dir, module.name)
|
|
@@ -175,13 +183,7 @@ class GrassTestFilesInvoker(object):
|
|
# ignoring shebang line to use current Python
|
|
# ignoring shebang line to use current Python
|
|
# and also pass parameters to it
|
|
# and also pass parameters to it
|
|
# add also '-Qwarn'?
|
|
# add also '-Qwarn'?
|
|
- if sys.version_info.major >= 3:
|
|
|
|
- args = [sys.executable, "-tt", module.abs_file_path]
|
|
|
|
- else:
|
|
|
|
- args = [sys.executable, "-tt", "-3", module.abs_file_path]
|
|
|
|
- p = subprocess.Popen(
|
|
|
|
- args, cwd=cwd, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
|
|
|
- )
|
|
|
|
|
|
+ args = [sys.executable, "-tt", module.abs_file_path]
|
|
elif module.file_type == "sh":
|
|
elif module.file_type == "sh":
|
|
# ignoring shebang line to pass parameters to shell
|
|
# ignoring shebang line to pass parameters to shell
|
|
# expecting system to have sh or something compatible
|
|
# expecting system to have sh or something compatible
|
|
@@ -195,23 +197,39 @@ class GrassTestFilesInvoker(object):
|
|
# command is used to control an if, elif, while, or
|
|
# command is used to control an if, elif, while, or
|
|
# until; or if the command is the left hand operand
|
|
# until; or if the command is the left hand operand
|
|
# of an '&&' or '||' operator.
|
|
# of an '&&' or '||' operator.
|
|
- p = subprocess.Popen(
|
|
|
|
- ["sh", "-e", "-x", module.abs_file_path],
|
|
|
|
- cwd=cwd,
|
|
|
|
- env=env,
|
|
|
|
- stdout=subprocess.PIPE,
|
|
|
|
- stderr=subprocess.PIPE,
|
|
|
|
- )
|
|
|
|
|
|
+ args = ["sh", "-e", "-x", module.abs_file_path]
|
|
else:
|
|
else:
|
|
- p = subprocess.Popen(
|
|
|
|
- [module.abs_file_path],
|
|
|
|
|
|
+ args = [module.abs_file_path]
|
|
|
|
+ try:
|
|
|
|
+ p = subprocess.run(
|
|
|
|
+ args,
|
|
cwd=cwd,
|
|
cwd=cwd,
|
|
env=env,
|
|
env=env,
|
|
stdout=subprocess.PIPE,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
|
|
+ timeout=timeout,
|
|
|
|
+ check=False,
|
|
)
|
|
)
|
|
- stdout, stderr = p.communicate()
|
|
|
|
- returncode = p.returncode
|
|
|
|
|
|
+ stdout = p.stdout
|
|
|
|
+ stderr = p.stderr
|
|
|
|
+ returncode = p.returncode
|
|
|
|
+ # No timeout to report. Non-none time out values are used to indicate
|
|
|
|
+ # the timeout case.
|
|
|
|
+ timed_out = None
|
|
|
|
+ except subprocess.TimeoutExpired as error:
|
|
|
|
+ stdout = error.stdout
|
|
|
|
+ stderr = error.stderr
|
|
|
|
+ if stdout is None:
|
|
|
|
+ stdout = ""
|
|
|
|
+ if stderr is None:
|
|
|
|
+ stderr = f"Process has timed out in {timeout}s and produced no error output.\n"
|
|
|
|
+ # Return code is None if the process times out.
|
|
|
|
+ # Rest of the code expects success to evaluate as False.
|
|
|
|
+ # So, we assign a failing return code.
|
|
|
|
+ # In any case, we treat the timeout case as a failure.
|
|
|
|
+ returncode = 1
|
|
|
|
+ timed_out = timeout
|
|
|
|
+
|
|
encodings = [_get_encoding(), "utf8", "latin-1", "ascii"]
|
|
encodings = [_get_encoding(), "utf8", "latin-1", "ascii"]
|
|
|
|
|
|
def try_decode(data, encodings):
|
|
def try_decode(data, encodings):
|
|
@@ -256,12 +274,21 @@ class GrassTestFilesInvoker(object):
|
|
stdout=stdout_path,
|
|
stdout=stdout_path,
|
|
stderr=stderr_path,
|
|
stderr=stderr_path,
|
|
test_summary=test_summary,
|
|
test_summary=test_summary,
|
|
|
|
+ timed_out=timed_out,
|
|
)
|
|
)
|
|
# TODO: add some try-except or with for better error handling
|
|
# TODO: add some try-except or with for better error handling
|
|
os.remove(gisrc)
|
|
os.remove(gisrc)
|
|
# TODO: only if clean up
|
|
# TODO: only if clean up
|
|
if self.clean_mapsets:
|
|
if self.clean_mapsets:
|
|
- shutil.rmtree(mapset_dir)
|
|
|
|
|
|
+ try:
|
|
|
|
+ shutil.rmtree(mapset_dir)
|
|
|
|
+ except OSError:
|
|
|
|
+ # If there are still running processes (e.g., in timeout case),
|
|
|
|
+ # the cleaning may fail on non-empty directory. Although this does
|
|
|
|
+ # not guarantee removal of the directory, try it again, but this
|
|
|
|
+ # time ignore errors if something happens. (More file can appear
|
|
|
|
+ # later on if the processes are still running.)
|
|
|
|
+ shutil.rmtree(mapset_dir, ignore_errors=True)
|
|
|
|
|
|
def run_in_location(self, gisdbase, location, location_type, results_dir, exclude):
|
|
def run_in_location(self, gisdbase, location, location_type, results_dir, exclude):
|
|
"""Run tests in a given location
|
|
"""Run tests in a given location
|
|
@@ -311,6 +338,7 @@ class GrassTestFilesInvoker(object):
|
|
results_dir=results_dir,
|
|
results_dir=results_dir,
|
|
gisdbase=gisdbase,
|
|
gisdbase=gisdbase,
|
|
location=location,
|
|
location=location,
|
|
|
|
+ timeout=self.timeout,
|
|
)
|
|
)
|
|
self.reporter.finish()
|
|
self.reporter.finish()
|
|
|
|
|