Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor subprocess mixin to control setting up and tearing down chil… (
#894) The Subprocess mixin can be used to control setting up and tearing down child process. The `TestServerBase` will leverage this and it will be re-used by a xDS configuration generator. Signed-off-by: Kevin Baichoo <kbaichoo@google.com>
- Loading branch information
Showing
5 changed files
with
153 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
"""Mixin for managing child subprocess with an additional thread.""" | ||
|
||
from threading import Thread, Condition | ||
import subprocess | ||
import logging | ||
from abc import ABC, abstractmethod | ||
|
||
|
||
class SubprocessMixin(ABC): | ||
"""Mixin used to manage launching subprocess using a separate Python thread. | ||
See test_subprocess_captures_stdout and test_subprocess_stop to see usage | ||
of the mixin. | ||
""" | ||
|
||
def __init__(self): | ||
"""Create SubprocessMixin.""" | ||
self._server_thread = Thread(target=self._serverThreadRunner) | ||
self._server_process = None | ||
self._has_launched = False | ||
self._has_launched_cv = Condition() | ||
|
||
@abstractmethod | ||
def _argsForSubprocess(self) -> list[str]: | ||
"""Return the args to launch the subprocess.""" | ||
pass | ||
|
||
def _serverThreadRunner(self): | ||
"""Routine executed by the python thread that launches the child subprocess. | ||
Derived classes may wish to extend this to use the stdout, stderr of | ||
the child process. | ||
""" | ||
args = self._argsForSubprocess() | ||
logging.info("Test server popen() args: %s" % str.join(" ", args)) | ||
self._server_process = subprocess.Popen(args, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | ||
with self._has_launched_cv: | ||
self._has_launched = True | ||
self._has_launched_cv.notify_all() | ||
stdout, stderr = self._server_process.communicate() | ||
logging.info("Process stdout: %s", stdout.decode("utf-8")) | ||
logging.info("Process stderr: %s", stderr.decode("utf-8")) | ||
return stdout, stderr | ||
|
||
def launchSubprocess(self): | ||
"""Start the subprocess.""" | ||
self._server_thread.daemon = True | ||
self._server_thread.start() | ||
|
||
def waitUntilSubprocessLaunched(self): | ||
"""Blocks until the subprocess has launched at least once.""" | ||
|
||
def hasLaunched(): | ||
return self._has_launched | ||
|
||
with self._has_launched_cv: | ||
self._has_launched_cv.wait_for(hasLaunched) | ||
assert self._has_launched | ||
|
||
def waitForSubprocessNotRunning(self): | ||
"""Wait for the subprocess to not be running assuming it exits.""" | ||
if not self._has_launched or not self._server_thread.is_alive(): | ||
return | ||
self._server_thread.join() | ||
|
||
def hasLaunched(): | ||
return self._has_launched | ||
|
||
with self._has_launched_cv: | ||
self._has_launched_cv.wait_for(hasLaunched) | ||
assert self._has_launched | ||
|
||
def stopSubprocess(self) -> int: | ||
"""Stop the subprocess. | ||
Returns: | ||
Int: exit code of the server process. | ||
""" | ||
self._server_process.terminate() | ||
self._server_thread.join() | ||
return self._server_process.returncode |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
"""Contains unit tests for subprocess_mixin.py.""" | ||
|
||
import pytest | ||
|
||
from test.integration.subprocess_mixin import SubprocessMixin | ||
|
||
|
||
class TestSubprocessMixin(SubprocessMixin): | ||
"""Helper class for testing the SubprocessMixin.""" | ||
|
||
def __init__(self, args): | ||
"""Create the TestSubprocessMixin.""" | ||
super().__init__() | ||
self.args = args | ||
self.stdout = b'' | ||
self.stderr = b'' | ||
|
||
def _argsForSubprocess(self) -> list[str]: | ||
return self.args | ||
|
||
def _serverThreadRunner(self): | ||
self.stdout, self.stderr = super()._serverThreadRunner() | ||
|
||
|
||
def test_subprocess_captures_stdout(): | ||
"""Test the subprocess captures stdout.""" | ||
child_process = TestSubprocessMixin(['echo', 'stdout']) | ||
child_process.launchSubprocess() | ||
child_process.waitUntilSubprocessLaunched() | ||
child_process.waitForSubprocessNotRunning() | ||
assert b'stdout' in child_process.stdout | ||
|
||
|
||
def test_subprocess_captures_stderr(): | ||
"""Test the subprocess captures stderr.""" | ||
child_process = TestSubprocessMixin(['logger', '--no-act', '-s', 'stderr']) | ||
child_process.launchSubprocess() | ||
child_process.waitUntilSubprocessLaunched() | ||
child_process.waitForSubprocessNotRunning() | ||
assert child_process.stderr != b'' | ||
|
||
|
||
def test_subprocess_stop(): | ||
"""Test the subprocess can be stopped.""" | ||
child_process = TestSubprocessMixin(['sleep', '120']) | ||
child_process.launchSubprocess() | ||
child_process.waitUntilSubprocessLaunched() | ||
ret_code = child_process.stopSubprocess() | ||
# Non-zero exit is expected as the subprocess should be killed. | ||
assert ret_code != 0 | ||
|
||
|
||
if __name__ == '__main__': | ||
raise SystemExit(pytest.main([__file__, '--assert=plain'])) |