Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
0.19.0 (UNRELEASED)
-------------------

- reorganize internals. ``pytest-xprocess`` is now a package and all resources
used by running processes are kept as instances of :class:``XProcessResources``.


0.18.1 (2021-07-27)
-------------------

Expand Down
13 changes: 7 additions & 6 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,23 @@ classifiers=
Topic :: Utilities

[options]
packages = find:
setup_requires=
setuptools_scm
py_modules=
pytest_xprocess
xprocess
python_requires = >= 3.5

[options.packages.find]
exclude = docs, tests

[options.entry_points]
pytest11 =
xprocess = pytest_xprocess
xprocess = xprocess.pytest_xprocess

[coverage:run]
branch = true
include =
xprocess.py
pytest_xprocess.py
xprocess/xprocess.py
xprocess/pytest_xprocess.py

[flake8]
# B = bugbear
Expand Down
2 changes: 1 addition & 1 deletion tests/test_process_initialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ class Starter(ProcessStarter):
xprocess.ensure(proc_name, Starter)

info = xprocess.getinfo(proc_name)
proc = xprocess._popen_instances[-1]
proc = xprocess.resources[-1].popen

if sys.version_info < (3, 7):
text_mode = proc.universal_newlines
Expand Down
11 changes: 11 additions & 0 deletions xprocess/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from .xprocess import ProcessStarter
from .xprocess import XProcess
from .xprocess import XProcessInfo
from .xprocess import XProcessResources

__all__ = [
"ProcessStarter",
"XProcess",
"XProcessResources",
"XProcessInfo",
]
12 changes: 2 additions & 10 deletions pytest_xprocess.py → xprocess/pytest_xprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,6 @@ def pytest_runtest_makereport(item, call):


def pytest_unconfigure(config):
try:
xprocess = config._xprocess
except AttributeError:
# xprocess fixture was not used
pass
else:
xprocess._clean_up_resources()

print(
"pytest-xprocess reminder::Be sure to terminate the started process by running "
"'pytest --xkill' if you have not explicitly done so in your fixture with "
Expand All @@ -89,7 +81,7 @@ def pytest_configure(self, config):
self.config = config

def info_objects(self):
return self.config._xprocess._info_objects
return [xrsc.info for xrsc in self.config._xprocess.resources]

def interruption_clean_up(self):
try:
Expand All @@ -100,7 +92,7 @@ def interruption_clean_up(self):
for info, terminate_on_interrupt in self.info_objects():
if terminate_on_interrupt:
info.terminate()
xprocess._clean_up_resources()
xprocess._force_clean_up()

def pytest_keyboard_interrupt(self, excinfo):
self.interruption_clean_up()
Expand Down
88 changes: 63 additions & 25 deletions xprocess.py → xprocess/xprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,47 @@ def isrunning(self, ignore_zombies=True):
)


class XProcessResources:
"""Resources used by a running process.
Each time XProcess.ensure is called a single XProcessResources
instance will be created and all resources used by the started
process will be held by it. Namely: file handle, XProcessInfo
and Popen instance.
"""

def __init__(self, timeout):
self.timeout = timeout
# handle to the process logfile
self.fhandle = None
# XProcessInfo holding information on XProcess instance
self.info = None
# Each XProcess will have a related Popen instance
# used for process management through python's
# subprocess API
self.popen = None

def __del__(self):
self.release()

def __repr__(self):
return "<XProcessResources {}, {}, {}>".format(
self.fhandle, self.info, self.popen
)

def release(self):
# file handles should always be closed
# in order to avoid ResourceWarnings
self.fhandle.close()

# We should wait on procs exit status if
# termination signal has been issued
try:
if self.info[0]._termination_signal:
self.popen.wait(self.timeout)
except TypeError:
pass


class XProcess:
"""Main xprocess class. Represents a running process instance for which
a set of actions is offered, such as process startup, command line actions
Expand All @@ -130,11 +171,9 @@ def __init__(self, config, rootdir, log=None, proc_wait_timeout=60):
self.rootdir = rootdir
self.proc_wait_timeout = proc_wait_timeout

# these will be used to keep all necessary
# references for proper cleanup before exiting
self._info_objects = []
self._file_handles = []
self._popen_instances = []
# used to keep all necessary references
# for proper cleanup before exiting
self.resources = []

class Log:
def debug(self, msg, *args):
Expand Down Expand Up @@ -175,6 +214,8 @@ def ensure(self, name, preparefunc, restart=False):

from subprocess import Popen, STDOUT

xresource = XProcessResources(self.proc_wait_timeout)

info = self.getinfo(name)
if not restart and not info.isrunning():
restart = True
Expand Down Expand Up @@ -213,22 +254,29 @@ def ensure(self, name, preparefunc, restart=False):

# keep references of all popen
# and info objects for cleanup
self._info_objects.append((info, starter.terminate_on_interrupt))
self._popen_instances.append(Popen(args, **popen_kwargs, **kwargs))
xresource.info = (info, starter.terminate_on_interrupt)
xresource.popen = Popen(args, **popen_kwargs, **kwargs)

info.pid = pid = self._popen_instances[-1].pid
info.pid = pid = xresource.popen.pid
info.pidpath.write(str(pid))
self.log.debug("process %r started pid=%s", name, pid)
stdout.close()

# keep track of all file handles so we can
# cleanup later during teardown phase
self._file_handles.append(info.logpath.open())
xresource.fhandle = info.logpath.open()

self.resources.append(xresource)
print(
"self.resources at end of ensure function: ",
self.resources,
file=sys.stderr,
)

if not restart:
self._file_handles[-1].seek(0, 2)
xresource.fhandle.seek(0, 2)
else:
if not starter.wait(self._file_handles[-1]):
if not starter.wait(xresource.fhandle):
raise RuntimeError(
"Could not start process {}, the specified "
"log pattern was not found within {} lines.".format(
Expand All @@ -238,7 +286,7 @@ def ensure(self, name, preparefunc, restart=False):
self.log.debug("%s process startup detected", name)

pytest_extlogfiles = self.config.__dict__.setdefault("_extlogfiles", {})
pytest_extlogfiles[name] = self._file_handles[-1]
pytest_extlogfiles[name] = xresource.fhandle
self.getinfo(name)

return info.pid, info.logpath
Expand Down Expand Up @@ -267,19 +315,9 @@ def _xshow(self, tw):
tw.line(tmpl.format(**locals()))
return 0

def _clean_up_resources(self):
# file handles should always be closed
# in order to avoid ResourceWarnings
for f in self._file_handles:
f.close()
# XProcessInfo objects and Popen objects have
# a one to one relation, so we should wait on
# procs exit status if termination signal has
# been isued for that particular XProcessInfo
# Object (subprocess requirement)
for (info, _), proc in zip(self._info_objects, self._popen_instances):
if info._termination_signal:
proc.wait(self.proc_wait_timeout)
def _force_clean_up(self):
for xresource in self.resources:
xresource.release()


class ProcessStarter(ABC):
Expand Down