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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytest-isolate
3
- Version: 0.0.8
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)+'.lock', timeout=4) as lock:
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"] == [0]
59
- assert os.environ.get("CUDA_VISIBLE_DEVICES") == "0"
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] * 2)
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] * 2)
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