xvfbwrapper 0.2.28__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.28/xvfbwrapper.egg-info → xvfbwrapper-0.2.30}/PKG-INFO +4 -5
- {xvfbwrapper-0.2.28 → xvfbwrapper-0.2.30}/README.md +3 -4
- {xvfbwrapper-0.2.28 → xvfbwrapper-0.2.30}/pyproject.toml +2 -4
- {xvfbwrapper-0.2.28 → xvfbwrapper-0.2.30}/test_xvfb.py +19 -14
- {xvfbwrapper-0.2.28 → xvfbwrapper-0.2.30}/tox.ini +0 -3
- {xvfbwrapper-0.2.28 → xvfbwrapper-0.2.30/xvfbwrapper.egg-info}/PKG-INFO +4 -5
- {xvfbwrapper-0.2.28 → xvfbwrapper-0.2.30}/xvfbwrapper.py +27 -17
- {xvfbwrapper-0.2.28 → xvfbwrapper-0.2.30}/LICENSE +0 -0
- {xvfbwrapper-0.2.28 → xvfbwrapper-0.2.30}/MANIFEST.in +0 -0
- {xvfbwrapper-0.2.28 → xvfbwrapper-0.2.30}/setup.cfg +0 -0
- {xvfbwrapper-0.2.28 → xvfbwrapper-0.2.30}/xvfbwrapper.egg-info/SOURCES.txt +0 -0
- {xvfbwrapper-0.2.28 → xvfbwrapper-0.2.30}/xvfbwrapper.egg-info/dependency_links.txt +0 -0
- {xvfbwrapper-0.2.28 → xvfbwrapper-0.2.30}/xvfbwrapper.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xvfbwrapper
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.30
|
|
4
4
|
Summary: Manage headless displays with Xvfb (X virtual framebuffer)
|
|
5
5
|
Author: Corey Goldberg
|
|
6
6
|
Maintainer: Corey Goldberg
|
|
@@ -164,7 +164,7 @@ xvfb.start()
|
|
|
164
164
|
from xvfbwrapper import Xvfb
|
|
165
165
|
|
|
166
166
|
xvfb = Xvfb(display=23)
|
|
167
|
-
xvfb.start()
|
|
167
|
+
xvfb.start() # Xvfb will start on display :23
|
|
168
168
|
```
|
|
169
169
|
|
|
170
170
|
#### Setting XDG_SESSION_TYPE:
|
|
@@ -194,7 +194,7 @@ creating an `xvfbwrapper.Xvfb` instance. For example:
|
|
|
194
194
|
from xvfbwrapper import Xvfb
|
|
195
195
|
|
|
196
196
|
xvfb = Xvfb(nolisten="tcp")
|
|
197
|
-
xvfb.start()
|
|
197
|
+
xvfb.start() # Xvfb will be called with the `-nolisten tcp` argument
|
|
198
198
|
```
|
|
199
199
|
|
|
200
200
|
However, there are other possible types of arguments:
|
|
@@ -212,7 +212,7 @@ an `xvfbwrapper.Xvfb` instance. For example:
|
|
|
212
212
|
from xvfbwrapper import Xvfb
|
|
213
213
|
|
|
214
214
|
xvfb = Xvfb(extra_args=("ttyxx", "-nocursor", "+extension", "RANDR"))
|
|
215
|
-
xvfb.start()
|
|
215
|
+
xvfb.start() # Xvfb will be called with the `ttyxx -nocursor +extension RANDR` arguments
|
|
216
216
|
```
|
|
217
217
|
|
|
218
218
|
#### Multithreaded execution:
|
|
@@ -260,7 +260,6 @@ from xvfbwrapper import Xvfb
|
|
|
260
260
|
|
|
261
261
|
|
|
262
262
|
class TestPage(unittest.TestCase):
|
|
263
|
-
|
|
264
263
|
def setUp(self):
|
|
265
264
|
xvfb = Xvfb(set_xdg_session_type=True)
|
|
266
265
|
xvfb.start()
|
|
@@ -133,7 +133,7 @@ xvfb.start()
|
|
|
133
133
|
from xvfbwrapper import Xvfb
|
|
134
134
|
|
|
135
135
|
xvfb = Xvfb(display=23)
|
|
136
|
-
xvfb.start()
|
|
136
|
+
xvfb.start() # Xvfb will start on display :23
|
|
137
137
|
```
|
|
138
138
|
|
|
139
139
|
#### Setting XDG_SESSION_TYPE:
|
|
@@ -163,7 +163,7 @@ creating an `xvfbwrapper.Xvfb` instance. For example:
|
|
|
163
163
|
from xvfbwrapper import Xvfb
|
|
164
164
|
|
|
165
165
|
xvfb = Xvfb(nolisten="tcp")
|
|
166
|
-
xvfb.start()
|
|
166
|
+
xvfb.start() # Xvfb will be called with the `-nolisten tcp` argument
|
|
167
167
|
```
|
|
168
168
|
|
|
169
169
|
However, there are other possible types of arguments:
|
|
@@ -181,7 +181,7 @@ an `xvfbwrapper.Xvfb` instance. For example:
|
|
|
181
181
|
from xvfbwrapper import Xvfb
|
|
182
182
|
|
|
183
183
|
xvfb = Xvfb(extra_args=("ttyxx", "-nocursor", "+extension", "RANDR"))
|
|
184
|
-
xvfb.start()
|
|
184
|
+
xvfb.start() # Xvfb will be called with the `ttyxx -nocursor +extension RANDR` arguments
|
|
185
185
|
```
|
|
186
186
|
|
|
187
187
|
#### Multithreaded execution:
|
|
@@ -229,7 +229,6 @@ from xvfbwrapper import Xvfb
|
|
|
229
229
|
|
|
230
230
|
|
|
231
231
|
class TestPage(unittest.TestCase):
|
|
232
|
-
|
|
233
232
|
def setUp(self):
|
|
234
233
|
xvfb = Xvfb(set_xdg_session_type=True)
|
|
235
234
|
xvfb.start()
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "xvfbwrapper"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.30"
|
|
8
8
|
description = "Manage headless displays with Xvfb (X virtual framebuffer)"
|
|
9
9
|
license = "MIT"
|
|
10
10
|
license-files = ["LICENSE"]
|
|
@@ -52,9 +52,7 @@ type = [
|
|
|
52
52
|
"types-psutil",
|
|
53
53
|
]
|
|
54
54
|
validate = [
|
|
55
|
-
"
|
|
56
|
-
"tomli",
|
|
57
|
-
"validate-pyproject",
|
|
55
|
+
"validate-pyproject[all]",
|
|
58
56
|
]
|
|
59
57
|
|
|
60
58
|
[tool.setuptools]
|
|
@@ -21,9 +21,6 @@ class XvfbCleanTestCase(unittest.TestCase):
|
|
|
21
21
|
|
|
22
22
|
- Records existing Xvfb processes at class start.
|
|
23
23
|
- Kills newly spawned Xvfb processes at class end.
|
|
24
|
-
|
|
25
|
-
- Records existing Xvfb processes at test start.
|
|
26
|
-
- Kills newly spawned Xvfb processes at test end.
|
|
27
24
|
"""
|
|
28
25
|
|
|
29
26
|
# ---------- Utility ----------
|
|
@@ -219,7 +216,8 @@ class TestXvfb(XvfbCleanTestCase):
|
|
|
219
216
|
unwriteable_dir = "/etc"
|
|
220
217
|
xvfb = Xvfb(tempdir=unwriteable_dir)
|
|
221
218
|
with self.assertRaisesRegex(
|
|
222
|
-
RuntimeError,
|
|
219
|
+
RuntimeError,
|
|
220
|
+
f"Could not access writable temp directory: {unwriteable_dir}",
|
|
223
221
|
):
|
|
224
222
|
xvfb.start()
|
|
225
223
|
self.assertIsNone(xvfb.proc)
|
|
@@ -228,7 +226,8 @@ class TestXvfb(XvfbCleanTestCase):
|
|
|
228
226
|
unknown_dir = "/tmp/some_unknown_path"
|
|
229
227
|
xvfb = Xvfb(tempdir=unknown_dir)
|
|
230
228
|
with self.assertRaisesRegex(
|
|
231
|
-
RuntimeError,
|
|
229
|
+
RuntimeError,
|
|
230
|
+
f"Could not access writable temp directory: {unknown_dir}",
|
|
232
231
|
):
|
|
233
232
|
xvfb.start()
|
|
234
233
|
self.assertIsNone(xvfb.proc)
|
|
@@ -269,7 +268,8 @@ class TestXvfb(XvfbCleanTestCase):
|
|
|
269
268
|
self.assertEqual(xvfb.new_display, display_num)
|
|
270
269
|
self.assertIsNotNone(xvfb.proc)
|
|
271
270
|
with self.assertRaisesRegex(
|
|
272
|
-
RuntimeError,
|
|
271
|
+
RuntimeError,
|
|
272
|
+
f"Could not lock display :{display_num}",
|
|
273
273
|
):
|
|
274
274
|
xvfb2.start()
|
|
275
275
|
|
|
@@ -302,7 +302,7 @@ class TestXvfb(XvfbCleanTestCase):
|
|
|
302
302
|
self.assertIsNotNone(xvfb.proc)
|
|
303
303
|
|
|
304
304
|
def test_start_fails_with_unknown_kwargs(self):
|
|
305
|
-
xvfb = Xvfb(foo="bar", timeout=
|
|
305
|
+
xvfb = Xvfb(foo="bar", timeout=2)
|
|
306
306
|
expected_cmd_args = [
|
|
307
307
|
"Xvfb",
|
|
308
308
|
r":\d",
|
|
@@ -313,7 +313,8 @@ class TestXvfb(XvfbCleanTestCase):
|
|
|
313
313
|
"bar",
|
|
314
314
|
]
|
|
315
315
|
with self.assertRaisesRegex(
|
|
316
|
-
RuntimeError,
|
|
316
|
+
RuntimeError,
|
|
317
|
+
f"Xvfb display did not open: {expected_cmd_args}",
|
|
317
318
|
):
|
|
318
319
|
xvfb.start()
|
|
319
320
|
self.assertIsNone(xvfb.proc)
|
|
@@ -348,15 +349,15 @@ class TestXvfb(XvfbCleanTestCase):
|
|
|
348
349
|
def test_environ_keyword_isolates_environment_modification(self):
|
|
349
350
|
# Check that start and stop methods modified the environ dict if
|
|
350
351
|
# passed and does not modify os.environ
|
|
351
|
-
|
|
352
|
-
xvfb = Xvfb(environ=
|
|
352
|
+
custom_env = os.environ.copy()
|
|
353
|
+
xvfb = Xvfb(environ=custom_env)
|
|
353
354
|
xvfb.start()
|
|
354
355
|
new_display = f":{xvfb.new_display}"
|
|
355
356
|
self.assertEqual(":0", os.environ["DISPLAY"])
|
|
356
|
-
self.assertEqual(new_display,
|
|
357
|
+
self.assertEqual(new_display, custom_env["DISPLAY"])
|
|
357
358
|
xvfb.stop()
|
|
358
359
|
self.assertEqual(":0", os.environ["DISPLAY"])
|
|
359
|
-
self.assertEqual(":0",
|
|
360
|
+
self.assertEqual(":0", custom_env["DISPLAY"])
|
|
360
361
|
self.assertIsNone(xvfb.proc)
|
|
361
362
|
|
|
362
363
|
def test_environ_is_passed_to_xvfb_subprocess(self):
|
|
@@ -367,7 +368,10 @@ class TestXvfb(XvfbCleanTestCase):
|
|
|
367
368
|
self.addCleanup(xvfb.stop)
|
|
368
369
|
xvfb.start()
|
|
369
370
|
proc = psutil.Process(xvfb.proc.pid)
|
|
370
|
-
self.assertEqual(
|
|
371
|
+
self.assertEqual(
|
|
372
|
+
proc.environ().get("XVFBWRAPPER_TEST_MARKER"),
|
|
373
|
+
marker_value,
|
|
374
|
+
)
|
|
371
375
|
|
|
372
376
|
def test_start_failure_without_initial_display_env(self):
|
|
373
377
|
# Provide a custom env *without* DISPLAY so orig_display_var == None
|
|
@@ -388,7 +392,8 @@ class TestXvfb(XvfbCleanTestCase):
|
|
|
388
392
|
with (
|
|
389
393
|
patch.object(xvfb, "_local_display_exists", return_value=False),
|
|
390
394
|
self.assertRaisesRegex(
|
|
391
|
-
RuntimeError,
|
|
395
|
+
RuntimeError,
|
|
396
|
+
f"Xvfb display did not open: {expected_cmd_args}",
|
|
392
397
|
),
|
|
393
398
|
):
|
|
394
399
|
xvfb.start()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xvfbwrapper
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.30
|
|
4
4
|
Summary: Manage headless displays with Xvfb (X virtual framebuffer)
|
|
5
5
|
Author: Corey Goldberg
|
|
6
6
|
Maintainer: Corey Goldberg
|
|
@@ -164,7 +164,7 @@ xvfb.start()
|
|
|
164
164
|
from xvfbwrapper import Xvfb
|
|
165
165
|
|
|
166
166
|
xvfb = Xvfb(display=23)
|
|
167
|
-
xvfb.start()
|
|
167
|
+
xvfb.start() # Xvfb will start on display :23
|
|
168
168
|
```
|
|
169
169
|
|
|
170
170
|
#### Setting XDG_SESSION_TYPE:
|
|
@@ -194,7 +194,7 @@ creating an `xvfbwrapper.Xvfb` instance. For example:
|
|
|
194
194
|
from xvfbwrapper import Xvfb
|
|
195
195
|
|
|
196
196
|
xvfb = Xvfb(nolisten="tcp")
|
|
197
|
-
xvfb.start()
|
|
197
|
+
xvfb.start() # Xvfb will be called with the `-nolisten tcp` argument
|
|
198
198
|
```
|
|
199
199
|
|
|
200
200
|
However, there are other possible types of arguments:
|
|
@@ -212,7 +212,7 @@ an `xvfbwrapper.Xvfb` instance. For example:
|
|
|
212
212
|
from xvfbwrapper import Xvfb
|
|
213
213
|
|
|
214
214
|
xvfb = Xvfb(extra_args=("ttyxx", "-nocursor", "+extension", "RANDR"))
|
|
215
|
-
xvfb.start()
|
|
215
|
+
xvfb.start() # Xvfb will be called with the `ttyxx -nocursor +extension RANDR` arguments
|
|
216
216
|
```
|
|
217
217
|
|
|
218
218
|
#### Multithreaded execution:
|
|
@@ -260,7 +260,6 @@ from xvfbwrapper import Xvfb
|
|
|
260
260
|
|
|
261
261
|
|
|
262
262
|
class TestPage(unittest.TestCase):
|
|
263
|
-
|
|
264
263
|
def setUp(self):
|
|
265
264
|
xvfb = Xvfb(set_xdg_session_type=True)
|
|
266
265
|
xvfb.start()
|
|
@@ -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
|