policyengine-us 1.381.0__py3-none-any.whl → 1.381.1__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.

@@ -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()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: policyengine-us
3
- Version: 1.381.0
3
+ Version: 1.381.1
4
4
  Summary: Add your description here.
5
5
  Author-email: PolicyEngine <hello@policyengine.org>
6
6
  License-File: LICENSE
@@ -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.0.dist-info/METADATA,sha256=oMC1pQBIqRWLHmU5Urfd3UAtQDOtO-S32j3slpZGH-g,1649
8300
- policyengine_us-1.381.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8301
- policyengine_us-1.381.0.dist-info/entry_points.txt,sha256=MLaqNyNTbReALyKNkde85VkuFFpdPWAcy8VRG1mjczc,57
8302
- policyengine_us-1.381.0.dist-info/licenses/LICENSE,sha256=2N5ReRelkdqkR9a-KP-y-shmcD5P62XoYiG-miLTAzo,34519
8303
- policyengine_us-1.381.0.dist-info/RECORD,,
8300
+ policyengine_us-1.381.1.dist-info/METADATA,sha256=rdqgMfC3fFzbBQJZYNPa9836TqJdb9SlqZN-tnLM8EI,1649
8301
+ policyengine_us-1.381.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8302
+ policyengine_us-1.381.1.dist-info/entry_points.txt,sha256=MLaqNyNTbReALyKNkde85VkuFFpdPWAcy8VRG1mjczc,57
8303
+ policyengine_us-1.381.1.dist-info/licenses/LICENSE,sha256=2N5ReRelkdqkR9a-KP-y-shmcD5P62XoYiG-miLTAzo,34519
8304
+ policyengine_us-1.381.1.dist-info/RECORD,,