policyengine-us 1.381.0__py3-none-any.whl → 1.381.2__py3-none-any.whl
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.
Potentially problematic release.
This version of policyengine-us might be problematic. Click here for more details.
- policyengine_us/tests/test_batched.py +365 -0
- {policyengine_us-1.381.0.dist-info → policyengine_us-1.381.2.dist-info}/METADATA +1 -1
- {policyengine_us-1.381.0.dist-info → policyengine_us-1.381.2.dist-info}/RECORD +6 -5
- {policyengine_us-1.381.0.dist-info → policyengine_us-1.381.2.dist-info}/WHEEL +0 -0
- {policyengine_us-1.381.0.dist-info → policyengine_us-1.381.2.dist-info}/entry_points.txt +0 -0
- {policyengine_us-1.381.0.dist-info → policyengine_us-1.381.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
"""
|
|
3
|
+
Generic test runner that can split tests into batches for memory management.
|
|
4
|
+
Can be used for any test folder with configurable batch count.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
import os
|
|
10
|
+
import gc
|
|
11
|
+
import time
|
|
12
|
+
import argparse
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import List, Dict
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def count_yaml_files(directory: Path) -> int:
|
|
18
|
+
"""Count YAML files in a directory recursively."""
|
|
19
|
+
if not directory.exists():
|
|
20
|
+
return 0
|
|
21
|
+
return len(list(directory.rglob("*.yaml")))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_test_directories(base_path: Path) -> Dict[str, int]:
|
|
25
|
+
"""Get all subdirectories and their test counts."""
|
|
26
|
+
dir_counts = {}
|
|
27
|
+
|
|
28
|
+
# Check for yaml files directly in base directory
|
|
29
|
+
root_count = len(list(base_path.glob("*.yaml")))
|
|
30
|
+
if root_count > 0:
|
|
31
|
+
dir_counts["."] = root_count
|
|
32
|
+
|
|
33
|
+
# Get all subdirectories with their test counts
|
|
34
|
+
for item in base_path.iterdir():
|
|
35
|
+
if item.is_dir():
|
|
36
|
+
yaml_count = count_yaml_files(item)
|
|
37
|
+
if yaml_count > 0:
|
|
38
|
+
dir_counts[item.name] = yaml_count
|
|
39
|
+
|
|
40
|
+
return dir_counts
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def split_into_batches(base_path: Path, num_batches: int) -> List[List[str]]:
|
|
44
|
+
"""
|
|
45
|
+
Split test directories into specified number of batches.
|
|
46
|
+
Special handling for baseline tests to separate states.
|
|
47
|
+
Special handling for contrib tests to divide by folder count.
|
|
48
|
+
"""
|
|
49
|
+
# Special handling for contrib tests - each folder is its own batch
|
|
50
|
+
if "contrib" in str(base_path):
|
|
51
|
+
# Get all subdirectories and sort them alphabetically
|
|
52
|
+
subdirs = sorted(
|
|
53
|
+
[item for item in base_path.iterdir() if item.is_dir()]
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Get root level YAML files and sort them
|
|
57
|
+
root_files = sorted(list(base_path.glob("*.yaml")))
|
|
58
|
+
|
|
59
|
+
# Create one batch per subdirectory
|
|
60
|
+
batches = []
|
|
61
|
+
for subdir in subdirs:
|
|
62
|
+
batches.append([str(subdir)])
|
|
63
|
+
|
|
64
|
+
# If there are root files, group them together in their own batch
|
|
65
|
+
if root_files:
|
|
66
|
+
root_batch = [str(file) for file in root_files]
|
|
67
|
+
batches.append(root_batch)
|
|
68
|
+
|
|
69
|
+
return batches
|
|
70
|
+
|
|
71
|
+
# Special handling for reform tests - run all together in one batch
|
|
72
|
+
if "reform" in str(base_path):
|
|
73
|
+
return [[str(base_path)]]
|
|
74
|
+
|
|
75
|
+
# Special handling for baseline tests with 2 batches
|
|
76
|
+
if "baseline" in str(base_path) and num_batches == 2:
|
|
77
|
+
states_path = base_path / "gov" / "states"
|
|
78
|
+
if states_path.exists() and count_yaml_files(states_path) > 0:
|
|
79
|
+
# Batch 1: Only states
|
|
80
|
+
batch1 = [str(states_path)]
|
|
81
|
+
|
|
82
|
+
# Batch 2: Everything else (excluding states)
|
|
83
|
+
batch2 = []
|
|
84
|
+
|
|
85
|
+
# Add root level files if any
|
|
86
|
+
for yaml_file in base_path.glob("*.yaml"):
|
|
87
|
+
batch2.append(str(yaml_file))
|
|
88
|
+
|
|
89
|
+
# Add all directories except gov/states
|
|
90
|
+
for item in base_path.iterdir():
|
|
91
|
+
if item.is_dir():
|
|
92
|
+
if item.name == "gov":
|
|
93
|
+
# Add gov subdirectories except states
|
|
94
|
+
for gov_item in item.iterdir():
|
|
95
|
+
if gov_item.is_dir() and gov_item.name != "states":
|
|
96
|
+
batch2.append(str(gov_item))
|
|
97
|
+
elif gov_item.suffix == ".yaml":
|
|
98
|
+
batch2.append(str(gov_item))
|
|
99
|
+
else:
|
|
100
|
+
# Non-gov directories
|
|
101
|
+
batch2.append(str(item))
|
|
102
|
+
|
|
103
|
+
return [batch1, batch2] if batch2 else [batch1]
|
|
104
|
+
|
|
105
|
+
# Default behavior for non-baseline or different batch counts
|
|
106
|
+
dir_counts = get_test_directories(base_path)
|
|
107
|
+
|
|
108
|
+
if num_batches <= 0:
|
|
109
|
+
num_batches = 1
|
|
110
|
+
|
|
111
|
+
# If only 1 batch, return everything
|
|
112
|
+
if num_batches == 1:
|
|
113
|
+
return [[str(base_path)]]
|
|
114
|
+
|
|
115
|
+
# Sort directories by test count (largest first)
|
|
116
|
+
sorted_dirs = sorted(dir_counts.items(), key=lambda x: x[1], reverse=True)
|
|
117
|
+
|
|
118
|
+
# Initialize batches
|
|
119
|
+
batches = [[] for _ in range(num_batches)]
|
|
120
|
+
batch_counts = [0] * num_batches
|
|
121
|
+
|
|
122
|
+
# Distribute directories to batches (greedy algorithm - add to smallest batch)
|
|
123
|
+
for dir_name, count in sorted_dirs:
|
|
124
|
+
# Find batch with fewest tests
|
|
125
|
+
min_batch_idx = batch_counts.index(min(batch_counts))
|
|
126
|
+
|
|
127
|
+
# Add directory to that batch
|
|
128
|
+
if dir_name == ".":
|
|
129
|
+
# Root level files - add individually
|
|
130
|
+
for yaml_file in base_path.glob("*.yaml"):
|
|
131
|
+
batches[min_batch_idx].append(str(yaml_file))
|
|
132
|
+
else:
|
|
133
|
+
batches[min_batch_idx].append(str(base_path / dir_name))
|
|
134
|
+
|
|
135
|
+
batch_counts[min_batch_idx] += count
|
|
136
|
+
|
|
137
|
+
# Filter out empty batches
|
|
138
|
+
return [batch for batch in batches if batch]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def run_batch(test_paths: List[str], batch_name: str) -> Dict:
|
|
142
|
+
"""Run a batch of tests in an isolated subprocess."""
|
|
143
|
+
|
|
144
|
+
python_exe = sys.executable
|
|
145
|
+
|
|
146
|
+
start_time = time.time()
|
|
147
|
+
|
|
148
|
+
# Build command - direct policyengine-core with timeout protection
|
|
149
|
+
cmd = (
|
|
150
|
+
[
|
|
151
|
+
python_exe,
|
|
152
|
+
"-m",
|
|
153
|
+
"policyengine_core.scripts.policyengine_command",
|
|
154
|
+
"test",
|
|
155
|
+
]
|
|
156
|
+
+ test_paths
|
|
157
|
+
+ ["-c", "policyengine_us"]
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
print(f" Running {batch_name}...")
|
|
161
|
+
print(f" Paths: {len(test_paths)} items")
|
|
162
|
+
print()
|
|
163
|
+
|
|
164
|
+
# Use Popen for more control over process lifecycle
|
|
165
|
+
process = subprocess.Popen(
|
|
166
|
+
cmd,
|
|
167
|
+
stdout=subprocess.PIPE,
|
|
168
|
+
stderr=subprocess.STDOUT,
|
|
169
|
+
text=True,
|
|
170
|
+
bufsize=1, # Line buffered
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
test_completed = False
|
|
175
|
+
test_passed = False
|
|
176
|
+
output_lines = []
|
|
177
|
+
|
|
178
|
+
# Monitor output line by line
|
|
179
|
+
while True:
|
|
180
|
+
line = process.stdout.readline()
|
|
181
|
+
if not line:
|
|
182
|
+
# No more output, check if process is done
|
|
183
|
+
poll_result = process.poll()
|
|
184
|
+
if poll_result is not None:
|
|
185
|
+
# Process terminated
|
|
186
|
+
break
|
|
187
|
+
# Process still running but no output
|
|
188
|
+
time.sleep(0.1)
|
|
189
|
+
continue
|
|
190
|
+
|
|
191
|
+
# Print line in real-time
|
|
192
|
+
print(line, end="")
|
|
193
|
+
output_lines.append(line)
|
|
194
|
+
|
|
195
|
+
# Detect pytest completion
|
|
196
|
+
# Look for patterns like "====== 5638 passed in 491.24s ======"
|
|
197
|
+
# or "====== 2 failed, 5636 passed in 500s ======"
|
|
198
|
+
import re
|
|
199
|
+
|
|
200
|
+
if re.search(r"=+.*\d+\s+(passed|failed).*in\s+[\d.]+s.*=+", line):
|
|
201
|
+
test_completed = True
|
|
202
|
+
# Check if tests passed (no failures mentioned or 0 failed)
|
|
203
|
+
if "failed" not in line or "0 failed" in line:
|
|
204
|
+
test_passed = True
|
|
205
|
+
else:
|
|
206
|
+
test_passed = False
|
|
207
|
+
|
|
208
|
+
print(f"\n Tests completed, terminating process...")
|
|
209
|
+
|
|
210
|
+
# Give 5 seconds grace period for cleanup
|
|
211
|
+
time.sleep(5)
|
|
212
|
+
|
|
213
|
+
# Terminate the process
|
|
214
|
+
process.terminate()
|
|
215
|
+
try:
|
|
216
|
+
process.wait(timeout=5)
|
|
217
|
+
except subprocess.TimeoutExpired:
|
|
218
|
+
# Force kill if it won't terminate
|
|
219
|
+
print(f" Force killing process...")
|
|
220
|
+
process.kill()
|
|
221
|
+
process.wait()
|
|
222
|
+
break
|
|
223
|
+
|
|
224
|
+
# If we didn't detect completion, wait for process with timeout
|
|
225
|
+
if not test_completed:
|
|
226
|
+
try:
|
|
227
|
+
# Wait up to 30 minutes total
|
|
228
|
+
elapsed = time.time() - start_time
|
|
229
|
+
remaining_timeout = max(1800 - elapsed, 1)
|
|
230
|
+
process.wait(timeout=remaining_timeout)
|
|
231
|
+
except subprocess.TimeoutExpired:
|
|
232
|
+
print(f"\n ⏱️ Timeout - terminating process...")
|
|
233
|
+
process.terminate()
|
|
234
|
+
try:
|
|
235
|
+
process.wait(timeout=5)
|
|
236
|
+
except subprocess.TimeoutExpired:
|
|
237
|
+
process.kill()
|
|
238
|
+
process.wait()
|
|
239
|
+
|
|
240
|
+
elapsed = time.time() - start_time
|
|
241
|
+
return {
|
|
242
|
+
"elapsed": elapsed,
|
|
243
|
+
"status": "timeout",
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
elapsed = time.time() - start_time
|
|
247
|
+
|
|
248
|
+
if test_completed:
|
|
249
|
+
print(f"\n Batch completed in {elapsed:.1f}s")
|
|
250
|
+
return {
|
|
251
|
+
"elapsed": elapsed,
|
|
252
|
+
"status": "passed" if test_passed else "failed",
|
|
253
|
+
}
|
|
254
|
+
else:
|
|
255
|
+
# Process ended without detecting test completion
|
|
256
|
+
returncode = process.poll()
|
|
257
|
+
print(f"\n Batch completed in {elapsed:.1f}s")
|
|
258
|
+
return {
|
|
259
|
+
"elapsed": elapsed,
|
|
260
|
+
"status": "passed" if returncode == 0 else "failed",
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
except Exception as e:
|
|
264
|
+
elapsed = time.time() - start_time
|
|
265
|
+
print(f"\n ❌ Error: {str(e)[:100]}")
|
|
266
|
+
|
|
267
|
+
# Clean up process if still running
|
|
268
|
+
try:
|
|
269
|
+
process.terminate()
|
|
270
|
+
process.wait(timeout=5)
|
|
271
|
+
except:
|
|
272
|
+
try:
|
|
273
|
+
process.kill()
|
|
274
|
+
process.wait()
|
|
275
|
+
except:
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
"elapsed": elapsed,
|
|
280
|
+
"status": "error",
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def main():
|
|
285
|
+
"""Main entry point for the generic test runner."""
|
|
286
|
+
|
|
287
|
+
parser = argparse.ArgumentParser(
|
|
288
|
+
description="Run tests in batches with memory cleanup between batches"
|
|
289
|
+
)
|
|
290
|
+
parser.add_argument("test_path", help="Path to the test directory")
|
|
291
|
+
parser.add_argument(
|
|
292
|
+
"--batches",
|
|
293
|
+
type=int,
|
|
294
|
+
default=2,
|
|
295
|
+
help="Number of batches to split tests into (default: 2)",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
args = parser.parse_args()
|
|
299
|
+
|
|
300
|
+
if not os.path.exists("policyengine_us"):
|
|
301
|
+
print("Error: Must run from PolicyEngine US root directory")
|
|
302
|
+
sys.exit(1)
|
|
303
|
+
|
|
304
|
+
test_path = Path(args.test_path)
|
|
305
|
+
if not test_path.exists():
|
|
306
|
+
print(f"Error: Test path does not exist: {args.test_path}")
|
|
307
|
+
sys.exit(1)
|
|
308
|
+
|
|
309
|
+
print(f"PolicyEngine Test Runner")
|
|
310
|
+
print("=" * 60)
|
|
311
|
+
print(f"Test path: {test_path}")
|
|
312
|
+
print(f"Requested batches: {args.batches}")
|
|
313
|
+
|
|
314
|
+
# Count total tests
|
|
315
|
+
total_tests = count_yaml_files(test_path)
|
|
316
|
+
print(f"Total test files: {total_tests}")
|
|
317
|
+
|
|
318
|
+
# Split into batches
|
|
319
|
+
batches = split_into_batches(test_path, args.batches)
|
|
320
|
+
if len(batches) != args.batches:
|
|
321
|
+
print(
|
|
322
|
+
f"Actual batches: {len(batches)} (optimized for {total_tests} files)"
|
|
323
|
+
)
|
|
324
|
+
else:
|
|
325
|
+
print(f"Actual batches: {len(batches)}")
|
|
326
|
+
print("=" * 60)
|
|
327
|
+
|
|
328
|
+
# Run batches
|
|
329
|
+
all_failed = False
|
|
330
|
+
total_elapsed = 0
|
|
331
|
+
|
|
332
|
+
for i, batch_paths in enumerate(batches, 1):
|
|
333
|
+
print(f"\n[Batch {i}/{len(batches)}]")
|
|
334
|
+
|
|
335
|
+
# Show what's in this batch
|
|
336
|
+
batch_test_count = sum(
|
|
337
|
+
count_yaml_files(Path(p)) if Path(p).is_dir() else 1
|
|
338
|
+
for p in batch_paths
|
|
339
|
+
)
|
|
340
|
+
print(f" Test files: ~{batch_test_count}")
|
|
341
|
+
print("-" * 60)
|
|
342
|
+
|
|
343
|
+
result = run_batch(batch_paths, f"Batch {i}")
|
|
344
|
+
total_elapsed += result["elapsed"]
|
|
345
|
+
|
|
346
|
+
if result["status"] != "passed":
|
|
347
|
+
all_failed = True
|
|
348
|
+
|
|
349
|
+
# Memory cleanup after each batch
|
|
350
|
+
print(" Cleaning up memory...")
|
|
351
|
+
gc.collect()
|
|
352
|
+
|
|
353
|
+
# Final summary
|
|
354
|
+
print("\n" + "=" * 60)
|
|
355
|
+
print("TEST SUMMARY")
|
|
356
|
+
print("=" * 60)
|
|
357
|
+
print(f"Total test files: {total_tests}")
|
|
358
|
+
print(f"Total time: {total_elapsed:.1f}s")
|
|
359
|
+
|
|
360
|
+
# Exit code
|
|
361
|
+
sys.exit(1 if all_failed else 0)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
if __name__ == "__main__":
|
|
365
|
+
main()
|
|
@@ -3161,6 +3161,7 @@ policyengine_us/reforms/tax_exempt/tax_exempt_reform.py,sha256=bkW91XMJ-jd23nhq4
|
|
|
3161
3161
|
policyengine_us/reforms/treasury/__init__.py,sha256=406jIbu32B57tUKhVLflWxlXf3S4SWjclVqlEtMeMwg,92
|
|
3162
3162
|
policyengine_us/reforms/treasury/repeal_dependent_exemptions.py,sha256=-xyMCtUK_dIBn2aAmhPW8pJEZjxcgOZzWKEUSI3w5e8,1354
|
|
3163
3163
|
policyengine_us/tests/run_selective_tests.py,sha256=AYvT1yM4bkOy4_2RtJEfpiV-l9lrlj86cl11zs8TsrE,18931
|
|
3164
|
+
policyengine_us/tests/test_batched.py,sha256=K1LN3IIbWIioXWpFXZ6Hlch-BeglV3QPc1zxAbUeP-M,11491
|
|
3164
3165
|
policyengine_us/tests/code_health/parameters.py,sha256=e9VKC8dmmB_dTafOO90WJuVu-PAogoLAaa5QZd2rY-s,1126
|
|
3165
3166
|
policyengine_us/tests/code_health/variable_names.py,sha256=hY4ucqPwBD7v_fvnBpzexJDf0yCGpF4Sueff4z4rQ24,554
|
|
3166
3167
|
policyengine_us/tests/microsimulation/test_microsim.py,sha256=_yT0ljW8aWjAOgSCWPH0vBv6jUJ4H_XM4clcg01mH1k,1181
|
|
@@ -8296,8 +8297,8 @@ policyengine_us/variables/input/farm_income.py,sha256=BEKxYmHNNnWJAAvULl5qZJigy5
|
|
|
8296
8297
|
policyengine_us/variables/input/geography.py,sha256=XmBlgXhzBoLRKk6R8taVZHqUw1eU8MbNeGS9iJ7_l44,4506
|
|
8297
8298
|
policyengine_us/variables/input/self_employment_income.py,sha256=PwsGz8R4lRikKWUYOhsC0qosNNLXq4f5SQmfw4S3mk8,511
|
|
8298
8299
|
policyengine_us/variables/input/self_employment_income_before_lsr.py,sha256=E8fcX9Nlyqz8dziHhQv_euutdmoIwFMMWePUwbbwv_w,379
|
|
8299
|
-
policyengine_us-1.381.
|
|
8300
|
-
policyengine_us-1.381.
|
|
8301
|
-
policyengine_us-1.381.
|
|
8302
|
-
policyengine_us-1.381.
|
|
8303
|
-
policyengine_us-1.381.
|
|
8300
|
+
policyengine_us-1.381.2.dist-info/METADATA,sha256=8Dz62ELY1SeOarI6KgWiIDIt6CBR04RifwNoS7qNV9U,1649
|
|
8301
|
+
policyengine_us-1.381.2.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
8302
|
+
policyengine_us-1.381.2.dist-info/entry_points.txt,sha256=MLaqNyNTbReALyKNkde85VkuFFpdPWAcy8VRG1mjczc,57
|
|
8303
|
+
policyengine_us-1.381.2.dist-info/licenses/LICENSE,sha256=2N5ReRelkdqkR9a-KP-y-shmcD5P62XoYiG-miLTAzo,34519
|
|
8304
|
+
policyengine_us-1.381.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|