Changeset 25:0281d2f4f77f in pyenvjasmine


Ignore:
Timestamp:
Jan 18, 2018, 12:20:34 PM (3 years ago)
Author:
Borja Lopez <borja@…>
Branch:
default
Phase:
public
Message:

Added a method to run the js tests external process in a timeout-monitored way,
so if the java process running the tests takes longer than expected, it gets
killed.

Removed the capture_output option when running the runner. Runner.run() now
returns success (True/False) depending on the tests run status:

  • False if the process running the tests timed out
  • False if the process did not time out but any js tests failed
  • True if the process did not time out and no js tests failed
File:
1 edited

Legend:

Unmodified
Added
Removed
  • pyenvjasmine/runner.py

    r22 r25  
    11import os
     2import sys
    23import subprocess
     4import threading
     5import signal
    36
    47
     
    1215        environment = '--environment=UNIX'
    1316    return environment
     17
     18
     19def print_no_newline(string):
     20    sys.stdout.write(str(string))
     21    sys.stdout.flush()
     22
     23
     24def run_popen_with_timeout(
     25        command, timeout, input_data, stdin, stdout, stderr, env=None):
     26    """
     27    Run a sub-program in subprocess.Popen, pass it the input_data,
     28    kill it if the specified timeout has passed.
     29    returns a tuple of success, stdout, stderr
     30
     31    sample usage:
     32
     33    timeout = 60  # seconds
     34    path = '/path/to/event.log'
     35    command = ['/usr/bin/tail', '-30', path]
     36    input_data = ''
     37    success, stdout, stderr = run_popen_with_timeout(command, timeout,
     38                                                     input_data)
     39    if not success:
     40        print('timeout on tail event.log output')
     41    tail_output = stdout
     42    """
     43    kill_check = threading.Event()
     44
     45    def _kill_process_after_a_timeout(pid):
     46        try:
     47            os.kill(pid, signal.SIGTERM)
     48        except OSError:
     49            # catch a possible race condition, the process terminated normally
     50            # between the timer firing and our kill
     51            return
     52        kill_check.set()  # tell the main routine that we had to kill
     53        # use SIGKILL if hard to kill...
     54        return
     55
     56    stdout_l = []
     57
     58    # don't use shell if command/options come in as list
     59    use_shell = not isinstance(command, list)
     60    try:
     61        p = subprocess.Popen(command, bufsize=1, shell=use_shell,
     62                             stdin=stdin, stdout=stdout,
     63                             stderr=stderr, env=env)
     64    except OSError as error_message:
     65        stderr = 'OSError: ' + str(error_message)
     66        return (False, '', stderr)
     67    pid = p.pid
     68
     69    watchdog = threading.Timer(timeout, _kill_process_after_a_timeout,
     70                               args=(pid, ))
     71    watchdog.start()
     72
     73    while True:
     74        output = p.stdout.readline(1)
     75        if output == '' and p.poll() is not None:
     76            break
     77        if output == '\n':
     78            print(output)
     79        else:
     80            print_no_newline(output)
     81        stdout_l.append(output)
     82
     83    try:
     84        (stdout, stderr) = p.communicate(input_data)
     85    except OSError as error_message:
     86        stdout = ''
     87        stderr = 'OSError: ' + str(error_message)
     88        p.returncode = -666
     89
     90    watchdog.cancel()  # if it's still waiting to run
     91
     92    # if it timed out, success is False
     93    success = (not kill_check.isSet()) and p.returncode >= 0
     94    kill_check.clear()
     95    return (success, ''.join(stdout_l), stderr)
    1496
    1597
     
    47129        self.runner_html = os.path.join(here, 'runner.html')
    48130
    49     def run(self, spec=None, capture_output=True):
    50         """
    51         Run the js tests with envjasmine.
     131    def run(self, spec=None, timeout=None):
     132        """
     133        Run the js tests with envjasmine, return success (true/false) and
     134        the captured stdout data
     135
    52136        spec: (relative) path to a spec file (run only that spec)
    53         Returns the output
     137        timeout: Set it to a given number of seconds and the process running
     138                 the js tests will be killed passed that time
    54139        """
    55140        environment = get_environment()
     
    85170
    86171        shell = False
    87         stdout = None
    88         stderr = None
    89         if capture_output:
    90             # override if we want to capture the output of the test run
    91             stdout = subprocess.PIPE
    92             stderr = subprocess.PIPE
    93 
    94         p = subprocess.Popen(command, shell=shell, stdout=stdout,
    95                              stderr=stderr)
    96         (res, stderr) = p.communicate()
    97         return res
     172        stdin = None
     173        stdout = subprocess.PIPE
     174        stderr = subprocess.PIPE
     175        input_data = ''
     176
     177        success, stdout, stderr = run_popen_with_timeout(
     178            command, timeout, input_data, stdin, stdout, stderr
     179        )
     180
     181        # success will be true if the subprocess did not timeout, now look
     182        # for actual failures if there was not a timeout
     183        if success:
     184            success = self.did_test_pass(stdout)
     185        return success, stdout
     186
     187    def did_test_pass(self, stdout):
     188        for line in stdout.splitlines():
     189            if 'Failed' in line:
     190                failed = line.split(':')[1].strip()
     191                return failed == '0'
     192        return False
    98193
    99194    def write_browser_htmlfile(self):
Note: See TracChangeset for help on using the changeset viewer.