pytest-isolate 0.0.8__tar.gz → 0.0.9__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.
- {pytest_isolate-0.0.8 → pytest_isolate-0.0.9}/PKG-INFO +1 -1
- pytest_isolate-0.0.9/src/pytest_isolate/__about__.py +1 -0
- {pytest_isolate-0.0.8 → pytest_isolate-0.0.9}/src/pytest_isolate/plugin.py +4 -20
- {pytest_isolate-0.0.8 → pytest_isolate-0.0.9}/src/pytest_isolate/resource_management.py +83 -2
- pytest_isolate-0.0.9/src/pytest_isolate/tracing.py +19 -0
- {pytest_isolate-0.0.8 → pytest_isolate-0.0.9}/tests/test_resource_management.py +14 -5
- pytest_isolate-0.0.8/src/pytest_isolate/__about__.py +0 -1
- {pytest_isolate-0.0.8 → pytest_isolate-0.0.9}/.github/workflows/python-package.yml +0 -0
- {pytest_isolate-0.0.8 → pytest_isolate-0.0.9}/.github/workflows/python-publish.yml +0 -0
- {pytest_isolate-0.0.8 → pytest_isolate-0.0.9}/.gitignore +0 -0
- {pytest_isolate-0.0.8 → pytest_isolate-0.0.9}/LICENSE.txt +0 -0
- {pytest_isolate-0.0.8 → pytest_isolate-0.0.9}/README.md +0 -0
- {pytest_isolate-0.0.8 → pytest_isolate-0.0.9}/pyproject.toml +0 -0
- {pytest_isolate-0.0.8 → pytest_isolate-0.0.9}/src/pytest_isolate/__init__.py +0 -0
- {pytest_isolate-0.0.8 → pytest_isolate-0.0.9}/tests/conftest.py +0 -0
- {pytest_isolate-0.0.8 → pytest_isolate-0.0.9}/tests/test_isolate.py +0 -0
- {pytest_isolate-0.0.8 → pytest_isolate-0.0.9}/tox.ini +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-isolate
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.9
|
|
4
4
|
Summary: Run pytest tests in isolated subprocesses
|
|
5
5
|
Project-URL: Documentation, https://github.com/gilfree/pytest-isolate#readme
|
|
6
6
|
Project-URL: Issues, https://github.com/gilfree/pytest-isolate/issues
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.0.9"
|
|
@@ -40,9 +40,11 @@ try:
|
|
|
40
40
|
except ImportError:
|
|
41
41
|
pass
|
|
42
42
|
|
|
43
|
+
from pytest_isolate.tracing import create_event
|
|
43
44
|
from pytest_isolate.resource_management import (
|
|
44
45
|
clean_resources,
|
|
45
46
|
cleanup_resource_environment,
|
|
47
|
+
get_resource_events,
|
|
46
48
|
parse_resource_list,
|
|
47
49
|
register_resource_provider,
|
|
48
50
|
setup_resource_environment,
|
|
@@ -776,7 +778,8 @@ def pytest_terminal_summary(terminalreporter, exitstatus, config) -> None:
|
|
|
776
778
|
filename = config.getoption("timeline_file")
|
|
777
779
|
os.makedirs(os.path.dirname(os.path.abspath(filename)), exist_ok=True)
|
|
778
780
|
json.dump(events, open(filename, "w"))
|
|
779
|
-
|
|
781
|
+
filename= filename.replace('.json', '.resources.json')
|
|
782
|
+
json.dump(get_resource_events(), open(filename, "w"))
|
|
780
783
|
|
|
781
784
|
@pytest.hookimpl(tryfirst=False, hookwrapper=True)
|
|
782
785
|
def pytest_runtest_call(item: pytest.Item):
|
|
@@ -786,22 +789,3 @@ def pytest_runtest_call(item: pytest.Item):
|
|
|
786
789
|
)
|
|
787
790
|
|
|
788
791
|
|
|
789
|
-
def create_event(worker_name, test_name, category, start_time, end_time, **kwargs):
|
|
790
|
-
start_event = {
|
|
791
|
-
"name": test_name.rsplit("/")[-1],
|
|
792
|
-
"cat": category,
|
|
793
|
-
"ph": "B",
|
|
794
|
-
"pid": worker_name,
|
|
795
|
-
"tid": 0,
|
|
796
|
-
"ts": start_time * (1000**2),
|
|
797
|
-
"args": kwargs,
|
|
798
|
-
}
|
|
799
|
-
end_event = {
|
|
800
|
-
"name": test_name.rsplit("/")[-1],
|
|
801
|
-
"cat": "pipeline",
|
|
802
|
-
"ph": "E",
|
|
803
|
-
"pid": worker_name,
|
|
804
|
-
"tid": 0,
|
|
805
|
-
"ts": end_time * (1000**2),
|
|
806
|
-
}
|
|
807
|
-
return start_event, end_event
|
|
@@ -4,22 +4,26 @@ import tempfile
|
|
|
4
4
|
import time
|
|
5
5
|
import warnings
|
|
6
6
|
from contextlib import contextmanager
|
|
7
|
+
from copy import deepcopy
|
|
7
8
|
from dataclasses import asdict, dataclass
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Callable, List, Optional
|
|
10
11
|
|
|
11
12
|
import filelock
|
|
12
13
|
|
|
14
|
+
from pytest_isolate.tracing import create_event
|
|
15
|
+
|
|
13
16
|
# Constants
|
|
14
17
|
|
|
15
18
|
DEFAULT_STATE_FILE = Path(tempfile.gettempdir()) / "pytest_isolate_resources.json"
|
|
19
|
+
DEFAULT_EVENTS_FILE = Path(str(DEFAULT_STATE_FILE).replace(".json", "_events.json"))
|
|
16
20
|
DEFAULT_WAIT_TIMEOUT = 300 # 5 minutes
|
|
17
21
|
POLL_INTERVAL = 0.1 # 1 second
|
|
18
22
|
|
|
19
23
|
|
|
20
24
|
@contextmanager
|
|
21
25
|
def lock_resource_file():
|
|
22
|
-
with filelock.FileLock(str(DEFAULT_STATE_FILE)+
|
|
26
|
+
with filelock.FileLock(str(DEFAULT_STATE_FILE) + ".lock", timeout=4) as lock:
|
|
23
27
|
yield lock
|
|
24
28
|
|
|
25
29
|
|
|
@@ -74,6 +78,10 @@ def clean_resources() -> None:
|
|
|
74
78
|
"""Clean up resources by removing lock and state files."""
|
|
75
79
|
if DEFAULT_STATE_FILE.exists():
|
|
76
80
|
os.remove(DEFAULT_STATE_FILE)
|
|
81
|
+
if DEFAULT_EVENTS_FILE.exists():
|
|
82
|
+
os.remove(DEFAULT_EVENTS_FILE)
|
|
83
|
+
if DEFAULT_STATE_FILE.with_suffix(".lock").exists():
|
|
84
|
+
os.remove(DEFAULT_STATE_FILE.with_suffix(".lock"))
|
|
77
85
|
|
|
78
86
|
|
|
79
87
|
def register_resource_provider(
|
|
@@ -99,13 +107,45 @@ def parse_resource_list(env_value: Optional[str]) -> List[int]:
|
|
|
99
107
|
if not env_value:
|
|
100
108
|
return []
|
|
101
109
|
try:
|
|
102
|
-
vals= [int(x.strip()) for x in env_value.split(",") if x.strip()]
|
|
110
|
+
vals = [int(x.strip()) for x in env_value.split(",") if x.strip()]
|
|
103
111
|
vals = [x for x in vals if x >= 0]
|
|
104
112
|
return vals
|
|
105
113
|
except ValueError:
|
|
106
114
|
return []
|
|
107
115
|
|
|
108
116
|
|
|
117
|
+
def log_resource_allocation(
|
|
118
|
+
allocated,
|
|
119
|
+
resource_type: str,
|
|
120
|
+
test_id: str,
|
|
121
|
+
start_time=None,
|
|
122
|
+
end_time=None,
|
|
123
|
+
file=None,
|
|
124
|
+
) -> None:
|
|
125
|
+
file = str(file or DEFAULT_EVENTS_FILE)
|
|
126
|
+
folder = os.path.dirname(file)
|
|
127
|
+
if folder and not os.path.exists(folder):
|
|
128
|
+
os.makedirs(folder, exist_ok=True)
|
|
129
|
+
if not os.path.exists(file):
|
|
130
|
+
resource_events = {}
|
|
131
|
+
else:
|
|
132
|
+
with open(file, "r") as f:
|
|
133
|
+
resource_events = json.load(f)
|
|
134
|
+
if test_id not in resource_events:
|
|
135
|
+
resource_events[test_id] = dict(
|
|
136
|
+
allocated=allocated,
|
|
137
|
+
worker_name=resource_type,
|
|
138
|
+
test_name=test_id,
|
|
139
|
+
category="resource_allocation",
|
|
140
|
+
)
|
|
141
|
+
if start_time is not None:
|
|
142
|
+
resource_events[test_id]["start_time"] = start_time
|
|
143
|
+
if end_time is not None:
|
|
144
|
+
resource_events[test_id]["end_time"] = end_time
|
|
145
|
+
with open(file, "w") as f:
|
|
146
|
+
json.dump(resource_events, f)
|
|
147
|
+
|
|
148
|
+
|
|
109
149
|
def setup_resource_environment(
|
|
110
150
|
test_id: str,
|
|
111
151
|
count: int,
|
|
@@ -123,6 +163,12 @@ def setup_resource_environment(
|
|
|
123
163
|
if allocated:
|
|
124
164
|
os.environ[resource.env_variable] = ",".join(map(str, allocated))
|
|
125
165
|
state.save()
|
|
166
|
+
log_resource_allocation(
|
|
167
|
+
resource.allocated.get(test_id),
|
|
168
|
+
resource_type,
|
|
169
|
+
test_id,
|
|
170
|
+
start_time=time.time(),
|
|
171
|
+
)
|
|
126
172
|
return allocated
|
|
127
173
|
return []
|
|
128
174
|
|
|
@@ -143,6 +189,41 @@ def cleanup_resource_environment(test_id: str, resource_type: str) -> None:
|
|
|
143
189
|
raise ValueError(f"Resource type {resource_type} not registered.")
|
|
144
190
|
|
|
145
191
|
resource = state.resources[resource_type]
|
|
192
|
+
allocated = resource.allocated.get(test_id)
|
|
193
|
+
if allocated:
|
|
194
|
+
log_resource_allocation(
|
|
195
|
+
resource.allocated.get(test_id),
|
|
196
|
+
resource_type,
|
|
197
|
+
test_id,
|
|
198
|
+
end_time=time.time(),
|
|
199
|
+
)
|
|
146
200
|
resource.release(test_id)
|
|
201
|
+
|
|
147
202
|
os.environ.pop(resource.env_variable, None)
|
|
148
203
|
state.save()
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def get_resource_events(file=None) -> list[dict]:
|
|
207
|
+
# Convert the resource events to the format used in tracing.py,
|
|
208
|
+
# by merging the start and end events.
|
|
209
|
+
file = Path(file or str(DEFAULT_EVENTS_FILE))
|
|
210
|
+
if not file.exists():
|
|
211
|
+
return []
|
|
212
|
+
resource_events = json.load(open(file))
|
|
213
|
+
events = []
|
|
214
|
+
for test_id, event in resource_events.items():
|
|
215
|
+
event_args = dict(**event)
|
|
216
|
+
allocated = event_args.pop("allocated", None)
|
|
217
|
+
for resource_id in allocated:
|
|
218
|
+
resource_event = deepcopy(event_args)
|
|
219
|
+
resource_event["worker_name"] = (
|
|
220
|
+
f"{resource_event['worker_name']}_{resource_id}"
|
|
221
|
+
)
|
|
222
|
+
if "end_time" not in resource_event:
|
|
223
|
+
resource_event["end_time"] = time.time()
|
|
224
|
+
if "start_time" not in resource_event:
|
|
225
|
+
raise ValueError(
|
|
226
|
+
f"Missing start_time for resource event {resource_event}"
|
|
227
|
+
)
|
|
228
|
+
events.extend(create_event(**resource_event))
|
|
229
|
+
return events
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
def create_event(worker_name, test_name, category, start_time, end_time, **kwargs):
|
|
2
|
+
start_event = {
|
|
3
|
+
"name": test_name.rsplit("/")[-1],
|
|
4
|
+
"cat": category,
|
|
5
|
+
"ph": "B",
|
|
6
|
+
"pid": worker_name,
|
|
7
|
+
"tid": 0,
|
|
8
|
+
"ts": start_time * (1000**2),
|
|
9
|
+
"args": kwargs,
|
|
10
|
+
}
|
|
11
|
+
end_event = {
|
|
12
|
+
"name": test_name.rsplit("/")[-1],
|
|
13
|
+
"cat": "pipeline",
|
|
14
|
+
"ph": "E",
|
|
15
|
+
"pid": worker_name,
|
|
16
|
+
"tid": 0,
|
|
17
|
+
"ts": end_time * (1000**2),
|
|
18
|
+
}
|
|
19
|
+
return start_event, end_event
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import os
|
|
2
3
|
import random
|
|
3
4
|
import tempfile
|
|
@@ -9,8 +10,9 @@ from unittest import mock
|
|
|
9
10
|
import filelock
|
|
10
11
|
import pytest
|
|
11
12
|
|
|
12
|
-
from pytest_isolate.plugin import allocate_resources
|
|
13
|
+
from pytest_isolate.plugin import allocate_resources, get_resource_events
|
|
13
14
|
from pytest_isolate.resource_management import (
|
|
15
|
+
log_resource_allocation,
|
|
14
16
|
parse_resource_list,
|
|
15
17
|
register_resource_provider,
|
|
16
18
|
)
|
|
@@ -52,23 +54,30 @@ def test_parse_resource_list():
|
|
|
52
54
|
# Test with invalid values - no warning in new implementation
|
|
53
55
|
assert parse_resource_list("0,a,2") == []
|
|
54
56
|
|
|
57
|
+
def test_log_resource_allocation():
|
|
58
|
+
log_resource_allocation([0,1],'gpu','foo', start_time=100,file="timeline.events.test.json")
|
|
59
|
+
|
|
60
|
+
log_resource_allocation([0,1],'gpu','foo', end_time=200,file="timeline.events.test.json")
|
|
61
|
+
events = get_resource_events(file="timeline.events.test.json")
|
|
62
|
+
open("timeline.resources.test.json", "w").write(json.dumps(events))
|
|
63
|
+
|
|
55
64
|
|
|
56
65
|
def test_allocate_resources():
|
|
57
66
|
with allocate_resources({"gpu": 1}, "foo", 100) as allocated:
|
|
58
|
-
assert allocated["gpu"] ==
|
|
59
|
-
assert os.environ.get("CUDA_VISIBLE_DEVICES")
|
|
67
|
+
assert len(allocated["gpu"]) == 1
|
|
68
|
+
assert len(os.environ.get("CUDA_VISIBLE_DEVICES","").split(","))
|
|
60
69
|
|
|
61
70
|
|
|
62
71
|
|
|
63
72
|
@pytest.mark.isolate(resources={"gpu": 2}, timeout=10)
|
|
64
|
-
@pytest.mark.parametrize("dummy", [1
|
|
73
|
+
@pytest.mark.parametrize("dummy", [1, 2])
|
|
65
74
|
def test_gpu_marker_2(dummy):
|
|
66
75
|
sleep(0.3)
|
|
67
76
|
assert len(os.environ.get("CUDA_VISIBLE_DEVICES", "").split(",")) == 2
|
|
68
77
|
|
|
69
78
|
|
|
70
79
|
@pytest.mark.isolate(resources={"gpu": 1}, timeout=10)
|
|
71
|
-
@pytest.mark.parametrize("dummy", [1
|
|
80
|
+
@pytest.mark.parametrize("dummy", [1, 2])
|
|
72
81
|
def test_gpu_marker_1(dummy):
|
|
73
82
|
sleep(0.3)
|
|
74
83
|
assert len(os.environ.get("CUDA_VISIBLE_DEVICES", "").split(",")) == 1
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.0.8"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|