xvfbwrapper 0.2.29__tar.gz → 0.2.30__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {xvfbwrapper-0.2.29/xvfbwrapper.egg-info → xvfbwrapper-0.2.30}/PKG-INFO +1 -1
- {xvfbwrapper-0.2.29 → xvfbwrapper-0.2.30}/pyproject.toml +1 -1
- {xvfbwrapper-0.2.29 → xvfbwrapper-0.2.30}/test_xvfb.py +7 -4
- {xvfbwrapper-0.2.29 → xvfbwrapper-0.2.30}/tox.ini +0 -3
- {xvfbwrapper-0.2.29 → xvfbwrapper-0.2.30/xvfbwrapper.egg-info}/PKG-INFO +1 -1
- {xvfbwrapper-0.2.29 → xvfbwrapper-0.2.30}/xvfbwrapper.py +27 -17
- {xvfbwrapper-0.2.29 → xvfbwrapper-0.2.30}/LICENSE +0 -0
- {xvfbwrapper-0.2.29 → xvfbwrapper-0.2.30}/MANIFEST.in +0 -0
- {xvfbwrapper-0.2.29 → xvfbwrapper-0.2.30}/README.md +0 -0
- {xvfbwrapper-0.2.29 → xvfbwrapper-0.2.30}/setup.cfg +0 -0
- {xvfbwrapper-0.2.29 → xvfbwrapper-0.2.30}/xvfbwrapper.egg-info/SOURCES.txt +0 -0
- {xvfbwrapper-0.2.29 → xvfbwrapper-0.2.30}/xvfbwrapper.egg-info/dependency_links.txt +0 -0
- {xvfbwrapper-0.2.29 → xvfbwrapper-0.2.30}/xvfbwrapper.egg-info/top_level.txt +0 -0
|
@@ -216,7 +216,8 @@ class TestXvfb(XvfbCleanTestCase):
|
|
|
216
216
|
unwriteable_dir = "/etc"
|
|
217
217
|
xvfb = Xvfb(tempdir=unwriteable_dir)
|
|
218
218
|
with self.assertRaisesRegex(
|
|
219
|
-
RuntimeError,
|
|
219
|
+
RuntimeError,
|
|
220
|
+
f"Could not access writable temp directory: {unwriteable_dir}",
|
|
220
221
|
):
|
|
221
222
|
xvfb.start()
|
|
222
223
|
self.assertIsNone(xvfb.proc)
|
|
@@ -225,7 +226,8 @@ class TestXvfb(XvfbCleanTestCase):
|
|
|
225
226
|
unknown_dir = "/tmp/some_unknown_path"
|
|
226
227
|
xvfb = Xvfb(tempdir=unknown_dir)
|
|
227
228
|
with self.assertRaisesRegex(
|
|
228
|
-
RuntimeError,
|
|
229
|
+
RuntimeError,
|
|
230
|
+
f"Could not access writable temp directory: {unknown_dir}",
|
|
229
231
|
):
|
|
230
232
|
xvfb.start()
|
|
231
233
|
self.assertIsNone(xvfb.proc)
|
|
@@ -300,7 +302,7 @@ class TestXvfb(XvfbCleanTestCase):
|
|
|
300
302
|
self.assertIsNotNone(xvfb.proc)
|
|
301
303
|
|
|
302
304
|
def test_start_fails_with_unknown_kwargs(self):
|
|
303
|
-
xvfb = Xvfb(foo="bar", timeout=
|
|
305
|
+
xvfb = Xvfb(foo="bar", timeout=2)
|
|
304
306
|
expected_cmd_args = [
|
|
305
307
|
"Xvfb",
|
|
306
308
|
r":\d",
|
|
@@ -311,7 +313,8 @@ class TestXvfb(XvfbCleanTestCase):
|
|
|
311
313
|
"bar",
|
|
312
314
|
]
|
|
313
315
|
with self.assertRaisesRegex(
|
|
314
|
-
RuntimeError,
|
|
316
|
+
RuntimeError,
|
|
317
|
+
f"Xvfb display did not open: {expected_cmd_args}",
|
|
315
318
|
):
|
|
316
319
|
xvfb.start()
|
|
317
320
|
self.assertIsNone(xvfb.proc)
|
|
@@ -39,46 +39,39 @@ class Xvfb:
|
|
|
39
39
|
set_xdg_session_type: bool = False,
|
|
40
40
|
environ: MutableMapping[str, str] | None = None,
|
|
41
41
|
extra_args: Sequence[str] | None = None,
|
|
42
|
-
timeout: float = 10,
|
|
42
|
+
timeout: float | None = 10,
|
|
43
43
|
**kwargs: str,
|
|
44
44
|
) -> None:
|
|
45
45
|
self.width: int = width
|
|
46
46
|
self.height: int = height
|
|
47
47
|
self.colordepth: int = colordepth
|
|
48
48
|
self._tempdir: Path | str = tempdir or tempfile.gettempdir()
|
|
49
|
-
self._timeout: float = timeout
|
|
49
|
+
self._timeout: float | None = timeout
|
|
50
50
|
self.new_display: int | None = display
|
|
51
51
|
self.environ: MutableMapping[str, str] = environ or os.environ
|
|
52
|
-
|
|
53
52
|
if set_xdg_session_type:
|
|
54
53
|
os.environ["XDG_SESSION_TYPE"] = "x11"
|
|
55
|
-
|
|
54
|
+
self.environ["XDG_SESSION_TYPE"] = "x11"
|
|
56
55
|
if not self._xvfb_exists():
|
|
57
56
|
raise FileNotFoundError(
|
|
58
57
|
"Could not find Xvfb. Please install it and try again"
|
|
59
58
|
)
|
|
60
|
-
|
|
61
59
|
self.xvfb_cmd: list[str] = []
|
|
62
|
-
|
|
63
60
|
if not extra_args:
|
|
64
61
|
extra_args = []
|
|
65
|
-
|
|
66
62
|
self.extra_xvfb_args: list[str] = [
|
|
67
63
|
"-screen",
|
|
68
64
|
"0",
|
|
69
65
|
f"{self.width}x{self.height}x{self.colordepth}",
|
|
70
66
|
*extra_args,
|
|
71
67
|
]
|
|
72
|
-
|
|
73
68
|
for key, value in kwargs.items():
|
|
74
69
|
self.extra_xvfb_args += [f"-{key}", value]
|
|
75
|
-
|
|
76
70
|
self.orig_display_var: str | None
|
|
77
71
|
if "DISPLAY" in self.environ:
|
|
78
72
|
self.orig_display_var = self.environ["DISPLAY"]
|
|
79
73
|
else:
|
|
80
74
|
self.orig_display_var = None
|
|
81
|
-
|
|
82
75
|
self.proc: subprocess.Popen[bytes] | None = None
|
|
83
76
|
self._lock_display_file: TextIO | None = None
|
|
84
77
|
|
|
@@ -95,6 +88,8 @@ class Xvfb:
|
|
|
95
88
|
self.stop()
|
|
96
89
|
|
|
97
90
|
def start(self) -> None:
|
|
91
|
+
"""Start Xvfb."""
|
|
92
|
+
|
|
98
93
|
if not os.access(self._tempdir, os.W_OK):
|
|
99
94
|
raise RuntimeError(
|
|
100
95
|
f"Could not access writable temp directory: {self._tempdir}"
|
|
@@ -113,10 +108,11 @@ class Xvfb:
|
|
|
113
108
|
close_fds=True,
|
|
114
109
|
env=dict(self.environ),
|
|
115
110
|
)
|
|
116
|
-
start = time.
|
|
111
|
+
start = time.monotonic()
|
|
117
112
|
while not self._local_display_exists(self.new_display):
|
|
118
113
|
time.sleep(1e-3)
|
|
119
|
-
|
|
114
|
+
elapsed = time.monotonic() - start
|
|
115
|
+
if self._timeout is not None and elapsed > self._timeout:
|
|
120
116
|
self.stop()
|
|
121
117
|
raise RuntimeError(f"Xvfb display did not open: {self.xvfb_cmd}")
|
|
122
118
|
ret_code = self.proc.poll()
|
|
@@ -127,6 +123,19 @@ class Xvfb:
|
|
|
127
123
|
raise RuntimeError(f"Xvfb did not start ({ret_code}): {self.xvfb_cmd}")
|
|
128
124
|
|
|
129
125
|
def stop(self) -> None:
|
|
126
|
+
"""Stop Xvfb and clean up its resources.
|
|
127
|
+
|
|
128
|
+
Terminate the process, escalating to kill if it does not exit within the
|
|
129
|
+
timeout. If the process has already exited, termination is treated as
|
|
130
|
+
successful. If `self._timeout` is set, wait up to that duration for termination
|
|
131
|
+
and, if necessary, an additional duration of the same length after killing the
|
|
132
|
+
process. If `self._timeout` is `None`, wait indefinitely for the process to
|
|
133
|
+
exit and be reaped.
|
|
134
|
+
|
|
135
|
+
This is a one-way lifecycle transition: once called, this object is considered
|
|
136
|
+
stopped and cannot be reused, regardless of whether the process exits
|
|
137
|
+
successfully.
|
|
138
|
+
"""
|
|
130
139
|
if self.proc is None:
|
|
131
140
|
return
|
|
132
141
|
try:
|
|
@@ -134,13 +143,15 @@ class Xvfb:
|
|
|
134
143
|
self.environ.pop("DISPLAY", None)
|
|
135
144
|
else:
|
|
136
145
|
self._set_display(self.orig_display_var)
|
|
137
|
-
|
|
138
|
-
with suppress(OSError):
|
|
146
|
+
with suppress(ProcessLookupError):
|
|
139
147
|
self.proc.terminate()
|
|
148
|
+
try:
|
|
149
|
+
self.proc.wait(self._timeout)
|
|
150
|
+
except subprocess.TimeoutExpired:
|
|
151
|
+
self.proc.kill()
|
|
140
152
|
self.proc.wait(self._timeout)
|
|
141
|
-
|
|
142
|
-
self.proc = None
|
|
143
153
|
finally:
|
|
154
|
+
self.proc = None
|
|
144
155
|
self._cleanup_lock_file()
|
|
145
156
|
|
|
146
157
|
def _xvfb_exists(self) -> bool:
|
|
@@ -160,7 +171,6 @@ class Xvfb:
|
|
|
160
171
|
"""
|
|
161
172
|
if self._lock_display_file is None:
|
|
162
173
|
return
|
|
163
|
-
|
|
164
174
|
self._lock_display_file.close()
|
|
165
175
|
with suppress(OSError):
|
|
166
176
|
Path(self._lock_display_file.name).unlink()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|