Impose max running time for apptests.
Fixes domokit/devtools#11.
R=qsr@chromium.org
Review URL: https://codereview.chromium.org/1266623002 .
Cr-Mirrored-From: https://github.com/domokit/mojo
Cr-Mirrored-Commit: 2b0afa8009aeb16304edff99d41a279f748aba54
diff --git a/devtoolslib/android_shell.py b/devtoolslib/android_shell.py
index d02ea0c..7a7436f 100644
--- a/devtoolslib/android_shell.py
+++ b/devtoolslib/android_shell.py
@@ -421,20 +421,40 @@
p.wait()
return None
- def RunAndGetOutput(self, arguments):
- """Runs the shell with given arguments until shell exits.
+ def RunAndGetOutput(self, arguments, timeout=None):
+ """Runs the shell with given arguments until shell exits and returns the
+ output.
Args:
arguments: list of arguments for the shell
+ timeout: maximum running time in seconds, after which the shell will be
+ terminated
Returns:
- A tuple of (return_code, output). |return_code| is the exit code returned
- by the shell or None if the exit code cannot be retrieved. |output| is the
- stdout mingled with the stderr produced by the shell.
+ A tuple of (return_code, output, did_time_out). |return_code| is the exit
+ code returned by the shell or None if the exit code cannot be retrieved.
+ |output| is the stdout mingled with the stderr produced by the shell.
+ |did_time_out| is True iff the shell was terminated because it exceeded
+ the |timeout| and False otherwise.
"""
- (r, w) = os.pipe()
- with os.fdopen(r, "r") as rf:
- with os.fdopen(w, "w") as wf:
- self.StartShell(arguments, wf, wf.close)
- output = rf.read()
- return None, output
+ class Results:
+ """Workaround for Python scoping rules that prevent assigning to variables
+ from the outer scope.
+ """
+ output = None
+
+ def do_run():
+ (r, w) = os.pipe()
+ with os.fdopen(r, "r") as rf:
+ with os.fdopen(w, "w") as wf:
+ self.StartShell(arguments, wf, wf.close)
+ Results.output = rf.read()
+
+ run_thread = threading.Thread(target=do_run)
+ run_thread.start()
+ run_thread.join(timeout)
+
+ if run_thread.is_alive():
+ self.StopShell()
+ return None, Results.output, True
+ return None, Results.output, False
diff --git a/devtoolslib/apptest.py b/devtoolslib/apptest.py
index 463c687..44c6043 100644
--- a/devtoolslib/apptest.py
+++ b/devtoolslib/apptest.py
@@ -31,7 +31,8 @@
return result
-def run_apptest(shell, shell_args, apptest_url, apptest_args, output_test):
+def run_apptest(shell, shell_args, apptest_url, apptest_args, timeout,
+ output_test):
"""Runs shell with the given arguments, retrieves the output and applies
|output_test| to determine if the run was successful.
@@ -52,7 +53,7 @@
_logger.debug("Starting: " + command_line)
start_time = time.time()
- (exit_code, output) = shell.RunAndGetOutput(arguments)
+ (exit_code, output, did_time_out) = shell.RunAndGetOutput(arguments, timeout)
run_time = time.time() - start_time
_logger.debug("Completed: " + command_line)
@@ -60,10 +61,12 @@
if run_time >= 3:
_logger.info("Test took %.3f seconds: %s" % (run_time, command_line))
- if exit_code or not output_test(output):
+ if exit_code or did_time_out or not output_test(output):
print 'Failed test: %r' % command_line
if exit_code:
print ' due to shell exit code %d' % exit_code
+ elif did_time_out:
+ print ' due to exceeded timeout of %fs' % timeout
else:
print ' due to test results'
print 72 * '-'
diff --git a/devtoolslib/apptest_dart.py b/devtoolslib/apptest_dart.py
index 06c329e..fe6131f 100644
--- a/devtoolslib/apptest_dart.py
+++ b/devtoolslib/apptest_dart.py
@@ -14,12 +14,13 @@
SUCCESS_PATTERN = re.compile('^.+ .+: All tests passed!', re.MULTILINE)
+
def _dart_apptest_output_test(output):
- return SUCCESS_PATTERN.search(output) != None
+ return SUCCESS_PATTERN.search(output) is not None
# TODO(erg): Support android, launched services and fixture isolation.
-def run_dart_apptest(shell, shell_args, apptest_url, apptest_args):
+def run_dart_apptest(shell, shell_args, apptest_url, apptest_args, timeout):
"""Runs a dart apptest.
Args:
@@ -30,5 +31,5 @@
Returns:
True iff the test succeeded, False otherwise.
"""
- return run_apptest(shell, shell_args, apptest_url, apptest_args,
+ return run_apptest(shell, shell_args, apptest_url, apptest_args, timeout,
_dart_apptest_output_test)
diff --git a/devtoolslib/apptest_gtest.py b/devtoolslib/apptest_gtest.py
index 5308f09..4515e89 100644
--- a/devtoolslib/apptest_gtest.py
+++ b/devtoolslib/apptest_gtest.py
@@ -25,7 +25,8 @@
return True
-def run_gtest_apptest(shell, shell_args, apptest_url, apptest_args, isolate):
+def run_gtest_apptest(shell, shell_args, apptest_url, apptest_args, timeout,
+ isolate):
"""Runs a gtest apptest.
Args:
@@ -41,7 +42,7 @@
"""
if not isolate:
- return run_apptest(shell, shell_args, apptest_url, apptest_args,
+ return run_apptest(shell, shell_args, apptest_url, apptest_args, timeout,
_gtest_apptest_output_test)
# List the apptest fixtures so they can be run independently for isolation.
@@ -54,7 +55,7 @@
for fixture in fixtures:
isolated_apptest_args = apptest_args + ["--gtest_filter=%s" % fixture]
success = run_apptest(shell, shell_args, apptest_url, isolated_apptest_args,
- _gtest_apptest_output_test)
+ timeout, _gtest_apptest_output_test)
if not success:
apptest_result = False
@@ -78,8 +79,8 @@
arguments.append("--args-for=%s %s" % (apptest, "--gtest_list_tests"))
arguments.append(apptest)
- (exit_code, output) = shell.RunAndGetOutput(arguments)
- if exit_code:
+ (exit_code, output, did_time_out) = shell.RunAndGetOutput(arguments)
+ if exit_code or did_time_out:
command_line = "mojo_shell " + " ".join(["%r" % x for x in arguments])
print "Failed to get test fixtures: %r" % command_line
print 72 * '-'
diff --git a/devtoolslib/linux_shell.py b/devtoolslib/linux_shell.py
index 6e03e6f..a8c25df 100644
--- a/devtoolslib/linux_shell.py
+++ b/devtoolslib/linux_shell.py
@@ -3,6 +3,7 @@
# found in the LICENSE file.
import subprocess
+import threading
from devtoolslib.shell import Shell
from devtoolslib import http_server
@@ -61,19 +62,40 @@
command = self.command_prefix + [self.executable_path] + arguments
return subprocess.call(command, stderr=subprocess.STDOUT)
- def RunAndGetOutput(self, arguments):
- """Runs the shell with given arguments until shell exits.
+ def RunAndGetOutput(self, arguments, timeout=None):
+ """Runs the shell with given arguments until shell exits and returns the
+ output.
Args:
arguments: list of arguments for the shell
+ timeout: maximum running time in seconds, after which the shell will be
+ terminated
Returns:
- A tuple of (return_code, output). |return_code| is the exit code returned
- by the shell or None if the exit code cannot be retrieved. |output| is the
- stdout mingled with the stderr produced by the shell.
+ A tuple of (return_code, output, did_time_out). |return_code| is the exit
+ code returned by the shell or None if the exit code cannot be retrieved.
+ |output| is the stdout mingled with the stderr produced by the shell.
+ |did_time_out| is True iff the shell was terminated because it exceeded
+ the |timeout| and False otherwise.
"""
command = self.command_prefix + [self.executable_path] + arguments
p = subprocess.Popen(command, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT)
- (output, _) = p.communicate()
- return p.returncode, output
+
+ class Results:
+ """Workaround for Python scoping rules that prevent assigning to variables
+ from the outer scope.
+ """
+ output = None
+
+ def do_run():
+ (Results.output, _) = p.communicate()
+
+ run_thread = threading.Thread(target=do_run)
+ run_thread.start()
+ run_thread.join(timeout)
+
+ if run_thread.is_alive():
+ p.terminate()
+ return p.returncode, Results.output, True
+ return p.returncode, Results.output, False
diff --git a/devtoolslib/shell.py b/devtoolslib/shell.py
index bb22ea4..16f4669 100644
--- a/devtoolslib/shell.py
+++ b/devtoolslib/shell.py
@@ -44,16 +44,20 @@
"""
raise NotImplementedError()
- def RunAndGetOutput(self, arguments):
+ def RunAndGetOutput(self, arguments, timeout=None):
"""Runs the shell with given arguments until shell exits and returns the
output.
Args:
arguments: list of arguments for the shell
+ timeout: maximum running time in seconds, after which the shell will be
+ terminated
Returns:
- A tuple of (return_code, output). |return_code| is the exit code returned
- by the shell or None if the exit code cannot be retrieved. |output| is the
- stdout mingled with the stderr produced by the shell.
+ A tuple of (return_code, output, did_time_out). |return_code| is the exit
+ code returned by the shell or None if the exit code cannot be retrieved.
+ |output| is the stdout mingled with the stderr produced by the shell.
+ |did_time_out| is True iff the shell was terminated because it exceeded
+ the |timeout| and False otherwise.
"""
raise NotImplementedError()
diff --git a/mojo_test b/mojo_test
index c824f10..d59bacc 100755
--- a/mojo_test
+++ b/mojo_test
@@ -36,6 +36,8 @@
"test-args": ["--an_arg", "another_arg"],
# Optional shell arguments.
"shell-args": ["--some-flag-for-the-shell", "--another-flag"],
+ # Optional timeout in seconds, 60 by default.
+ "timeout": 120,
}
|test_list_file| may reference the |target_os| global that will be any of
@@ -77,6 +79,7 @@
test_type = test_dict.get("type", "gtest")
test_args = test_dict.get("test-args", [])
shell_args = test_dict.get("shell-args", []) + common_shell_args
+ timeout = test_dict.get("timeout", 60)
_logger.info("Will start: %s" % test_name)
print "Running %s...." % test_name,
@@ -84,13 +87,15 @@
if test_type == "dart":
apptest_result = apptest_dart.run_dart_apptest(shell, shell_args, test,
- test_args)
+ test_args, timeout)
elif test_type == "gtest":
apptest_result = apptest_gtest.run_gtest_apptest(shell, shell_args, test,
- test_args, False)
+ test_args, timeout,
+ False)
elif test_type == "gtest_isolated":
apptest_result = apptest_gtest.run_gtest_apptest(shell, shell_args, test,
- test_args, True)
+ test_args, timeout,
+ True)
else:
apptest_result = False
print "Unrecognized test type in %r" % test_dict