shrinkray 25.12.27.2__py3-none-any.whl → 25.12.27.3__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.
shrinkray/__main__.py CHANGED
@@ -17,18 +17,19 @@ from shrinkray.cli import (
17
17
  validate_command,
18
18
  validate_ui,
19
19
  )
20
+ from shrinkray.formatting import determine_formatter_command
20
21
  from shrinkray.passes.clangdelta import (
21
22
  C_FILE_EXTENSIONS,
22
23
  ClangDelta,
23
24
  find_clang_delta,
24
25
  )
25
- from shrinkray.problem import InvalidInitialExample
26
26
  from shrinkray.state import (
27
27
  ShrinkRayDirectoryState,
28
28
  ShrinkRayState,
29
29
  ShrinkRayStateSingleFile,
30
30
  )
31
31
  from shrinkray.ui import BasicUI, ShrinkRayUI
32
+ from shrinkray.validation import run_validation
32
33
  from shrinkray.work import Volume
33
34
 
34
35
 
@@ -39,12 +40,9 @@ async def run_shrink_ray(
39
40
  """Run the shrink ray reduction process."""
40
41
  async with trio.open_nursery() as nursery:
41
42
  problem = state.problem
42
- try:
43
- await problem.setup()
44
- except* InvalidInitialExample as excs:
45
- assert len(excs.exceptions) == 1
46
- (e,) = excs.exceptions
47
- await state.report_error(e)
43
+ # Validation runs before run_shrink_ray is called, so setup() should
44
+ # always succeed. If it doesn't, there's a bug and we want it to propagate.
45
+ await problem.setup()
48
46
 
49
47
  reducer = state.reducer
50
48
 
@@ -273,6 +271,26 @@ def main(
273
271
  if not backup:
274
272
  backup = filename + os.extsep + "bak"
275
273
 
274
+ # Run initial validation before any state setup
275
+ # This validates the interestingness test and formatter with proper output streaming
276
+ formatter_command = None
277
+ if not os.path.isdir(filename) and formatter.lower() != "none":
278
+ formatter_command = determine_formatter_command(formatter, filename)
279
+
280
+ validation_result = run_validation(
281
+ file_path=filename,
282
+ test=test,
283
+ input_type=input_type,
284
+ in_place=in_place,
285
+ formatter_command=formatter_command,
286
+ )
287
+
288
+ if not validation_result.success:
289
+ print(f"\nError: {validation_result.error_message}", file=sys.stderr)
290
+ sys.exit(1)
291
+
292
+ print("\nStarting reduction...", file=sys.stderr, flush=True)
293
+
276
294
  state_kwargs: dict[str, Any] = {
277
295
  "input_type": input_type,
278
296
  "in_place": in_place,
@@ -307,8 +325,6 @@ def main(
307
325
 
308
326
  state = ShrinkRayDirectoryState(initial=initial, **state_kwargs)
309
327
 
310
- trio.run(state.check_formatter)
311
-
312
328
  else:
313
329
  try:
314
330
  os.remove(backup)
@@ -323,8 +339,6 @@ def main(
323
339
 
324
340
  state = ShrinkRayStateSingleFile(initial=initial, **state_kwargs)
325
341
 
326
- trio.run(state.check_formatter)
327
-
328
342
  if ui_type == UIType.textual:
329
343
  from shrinkray.tui import run_textual_ui
330
344
 
shrinkray/state.py CHANGED
@@ -8,6 +8,7 @@ import subprocess
8
8
  import sys
9
9
  import time
10
10
  from abc import ABC, abstractmethod
11
+ from collections import deque
11
12
  from datetime import timedelta
12
13
  from tempfile import TemporaryDirectory
13
14
  from typing import Any, TypeVar
@@ -58,6 +59,125 @@ def compute_dynamic_timeout(runtime: float) -> float:
58
59
  )
59
60
 
60
61
 
62
+ @define
63
+ class TestOutputManager:
64
+ """Manages temporary files for test output capture.
65
+
66
+ Allocates unique files for each test's stdout/stderr output,
67
+ tracks active and completed tests, and cleans up old files.
68
+ """
69
+
70
+ output_dir: str
71
+ max_files: int = 50
72
+ max_age_seconds: float = 60.0
73
+ min_display_seconds: float = 1.0 # Minimum time to show completed output
74
+ grace_period_seconds: float = (
75
+ 0.5 # Extra time to wait for new test after min_display
76
+ )
77
+
78
+ _sequence: int = 0
79
+ _active_outputs: dict[int, str] = {}
80
+ _completed_outputs: deque[tuple[int, str, float]] = deque()
81
+
82
+ def __attrs_post_init__(self) -> None:
83
+ # Initialize mutable defaults
84
+ self._active_outputs = {}
85
+ self._completed_outputs = deque()
86
+
87
+ def allocate_output_file(self) -> tuple[int, str]:
88
+ """Allocate a new output file for a test. Returns (test_id, file_path)."""
89
+ test_id = self._sequence
90
+ self._sequence += 1
91
+ file_path = os.path.join(self.output_dir, f"test_{test_id}.log")
92
+ self._active_outputs[test_id] = file_path
93
+ return test_id, file_path
94
+
95
+ def mark_completed(self, test_id: int) -> None:
96
+ """Mark a test as completed and move to completed queue."""
97
+ if test_id in self._active_outputs:
98
+ file_path = self._active_outputs.pop(test_id)
99
+ self._completed_outputs.append((test_id, file_path, time.time()))
100
+ self._cleanup_old_files()
101
+
102
+ def _cleanup_old_files(self) -> None:
103
+ """Remove old output files based on count and age limits."""
104
+ now = time.time()
105
+ # Remove files older than max_age_seconds
106
+ while (
107
+ self._completed_outputs
108
+ and now - self._completed_outputs[0][2] > self.max_age_seconds
109
+ ):
110
+ _, file_path, _ = self._completed_outputs.popleft()
111
+ self._safe_delete(file_path)
112
+ # Remove excess files beyond max_files
113
+ while len(self._completed_outputs) > self.max_files:
114
+ _, file_path, _ = self._completed_outputs.popleft()
115
+ self._safe_delete(file_path)
116
+
117
+ def _should_show_completed(self) -> tuple[int, str] | None:
118
+ """Check if we should show a completed test's output.
119
+
120
+ Note: This method is only called when there are no active tests.
121
+ It returns the completed test info if within the display window
122
+ (min_display_seconds + grace_period).
123
+ """
124
+ if not self._completed_outputs:
125
+ return None
126
+ test_id, file_path, completion_time = self._completed_outputs[-1]
127
+ elapsed = time.time() - completion_time
128
+
129
+ # Show completed test during the full display window
130
+ if elapsed < self.min_display_seconds + self.grace_period_seconds:
131
+ return test_id, file_path
132
+
133
+ return None
134
+
135
+ def get_current_output_path(self) -> str | None:
136
+ """Get the most relevant output file path.
137
+
138
+ Active tests always take priority. If no active test, shows
139
+ recently completed test output for min_display_seconds, plus
140
+ an additional grace_period if no new test has started.
141
+ """
142
+ # Active tests always take priority
143
+ if self._active_outputs:
144
+ max_id = max(self._active_outputs.keys())
145
+ return self._active_outputs[max_id]
146
+ # Then check for recently completed test that should stay visible
147
+ recent = self._should_show_completed()
148
+ if recent is not None:
149
+ return recent[1]
150
+ # Fall back to most recent completed (even if past display window)
151
+ if self._completed_outputs:
152
+ return self._completed_outputs[-1][1]
153
+ return None
154
+
155
+ def get_active_test_id(self) -> int | None:
156
+ """Get the currently running test ID, if any.
157
+
158
+ Returns the active test ID if one is running, None otherwise.
159
+ """
160
+ if self._active_outputs:
161
+ return max(self._active_outputs.keys())
162
+ return None
163
+
164
+ def cleanup_all(self) -> None:
165
+ """Clean up all output files (called on shutdown)."""
166
+ for file_path in self._active_outputs.values():
167
+ self._safe_delete(file_path)
168
+ for _, file_path, _ in self._completed_outputs:
169
+ self._safe_delete(file_path)
170
+ self._active_outputs.clear()
171
+ self._completed_outputs.clear()
172
+
173
+ @staticmethod
174
+ def _safe_delete(path: str) -> None:
175
+ try:
176
+ os.unlink(path)
177
+ except OSError:
178
+ pass
179
+
180
+
61
181
  @define(slots=False)
62
182
  class ShrinkRayState[TestCase](ABC):
63
183
  input_type: Any # InputType from __main__
@@ -91,6 +211,9 @@ class ShrinkRayState[TestCase](ABC):
91
211
  # Stores the output from the last debug run
92
212
  _last_debug_output: str = ""
93
213
 
214
+ # Optional output manager for capturing test output (TUI mode)
215
+ output_manager: TestOutputManager | None = None
216
+
94
217
  def __attrs_post_init__(self):
95
218
  self.is_interesting_limiter = trio.CapacityLimiter(max(self.parallelism, 1))
96
219
  self.setup_formatter()
@@ -175,8 +298,17 @@ class ShrinkRayState[TestCase](ABC):
175
298
 
176
299
  return completed.returncode
177
300
 
178
- # Check if we should stream output to stderr (volume=debug)
179
- if self.volume == Volume.debug:
301
+ # Determine output handling
302
+ test_id: int | None = None
303
+ output_file_handle = None
304
+
305
+ if self.output_manager is not None:
306
+ # Capture output to a file for TUI display
307
+ test_id, output_path = self.output_manager.allocate_output_file()
308
+ output_file_handle = open(output_path, "wb")
309
+ kwargs["stdout"] = output_file_handle.fileno()
310
+ kwargs["stderr"] = subprocess.STDOUT # Combine stderr into stdout
311
+ elif self.volume == Volume.debug:
180
312
  # Inherit stderr from parent process to stream output in real-time
181
313
  kwargs["stderr"] = None # None means inherit
182
314
  kwargs["stdout"] = subprocess.DEVNULL
@@ -185,59 +317,66 @@ class ShrinkRayState[TestCase](ABC):
185
317
  kwargs["stdout"] = subprocess.DEVNULL
186
318
  kwargs["stderr"] = subprocess.DEVNULL
187
319
 
188
- async with trio.open_nursery() as nursery:
320
+ try:
321
+ async with trio.open_nursery() as nursery:
189
322
 
190
- def call_with_kwargs(task_status=trio.TASK_STATUS_IGNORED): # type: ignore
191
- return trio.run_process(command, **kwargs, task_status=task_status)
323
+ def call_with_kwargs(task_status=trio.TASK_STATUS_IGNORED): # type: ignore
324
+ return trio.run_process(command, **kwargs, task_status=task_status)
192
325
 
193
- start_time = time.time()
194
- sp = await nursery.start(call_with_kwargs)
195
-
196
- try:
197
- # Determine effective timeout for this call
198
- if self.first_call:
199
- # For first call: use calibration timeout if dynamic, otherwise 10x explicit timeout
200
- if self.timeout is None:
201
- effective_timeout = DYNAMIC_TIMEOUT_CALIBRATION_TIMEOUT
202
- else:
203
- effective_timeout = self.timeout * 10
204
- else:
205
- # For subsequent calls, timeout must be set (either explicit or computed)
206
- assert self.timeout is not None
207
- effective_timeout = self.timeout
208
-
209
- with trio.move_on_after(effective_timeout):
210
- await sp.wait()
211
-
212
- runtime = time.time() - start_time
213
-
214
- if sp.returncode is None:
215
- # Process didn't terminate before timeout - kill it
216
- await self._interrupt_wait_and_kill(sp)
217
-
218
- # Check for timeout violation (only when timeout is explicitly set)
219
- if (
220
- self.timeout is not None
221
- and runtime >= self.timeout
222
- and self.first_call
223
- ):
224
- raise TimeoutExceededOnInitial(
225
- timeout=self.timeout,
226
- runtime=runtime,
227
- )
228
- finally:
229
- if self.first_call:
230
- self.initial_exit_code = sp.returncode
231
- # Set dynamic timeout if not explicitly specified
232
- if self.timeout is None:
233
- runtime = time.time() - start_time
234
- self.timeout = compute_dynamic_timeout(runtime)
235
- self.first_call = False
326
+ start_time = time.time()
327
+ sp = await nursery.start(call_with_kwargs)
236
328
 
237
- result: int | None = sp.returncode
238
- assert result is not None
239
-
240
- return result
329
+ try:
330
+ # Determine effective timeout for this call
331
+ if self.first_call:
332
+ # For first call: use calibration timeout if dynamic, otherwise 10x explicit timeout
333
+ if self.timeout is None:
334
+ effective_timeout = DYNAMIC_TIMEOUT_CALIBRATION_TIMEOUT
335
+ else:
336
+ effective_timeout = self.timeout * 10
337
+ else:
338
+ # For subsequent calls, timeout must be set (either explicit or computed)
339
+ assert self.timeout is not None
340
+ effective_timeout = self.timeout
341
+
342
+ with trio.move_on_after(effective_timeout):
343
+ await sp.wait()
344
+
345
+ runtime = time.time() - start_time
346
+
347
+ if sp.returncode is None:
348
+ # Process didn't terminate before timeout - kill it
349
+ await self._interrupt_wait_and_kill(sp)
350
+
351
+ # Check for timeout violation (only when timeout is explicitly set)
352
+ if (
353
+ self.timeout is not None
354
+ and runtime >= self.timeout
355
+ and self.first_call
356
+ ):
357
+ raise TimeoutExceededOnInitial(
358
+ timeout=self.timeout,
359
+ runtime=runtime,
360
+ )
361
+ finally:
362
+ if self.first_call:
363
+ self.initial_exit_code = sp.returncode
364
+ # Set dynamic timeout if not explicitly specified
365
+ if self.timeout is None:
366
+ runtime = time.time() - start_time
367
+ self.timeout = compute_dynamic_timeout(runtime)
368
+ self.first_call = False
369
+
370
+ result: int | None = sp.returncode
371
+ assert result is not None
372
+
373
+ return result
374
+ finally:
375
+ # Clean up output file handle and mark test as completed
376
+ if output_file_handle is not None:
377
+ output_file_handle.close()
378
+ if test_id is not None and self.output_manager is not None:
379
+ self.output_manager.mark_completed(test_id)
241
380
 
242
381
  async def run_for_exit_code(self, test_case: TestCase, debug: bool = False) -> int:
243
382
  # Lazy import
@@ -136,6 +136,7 @@ class SubprocessClient:
136
136
  no_clang_delta: bool = False,
137
137
  clang_delta: str = "",
138
138
  trivial_is_error: bool = True,
139
+ skip_validation: bool = False,
139
140
  ) -> Response:
140
141
  """Start the reduction process."""
141
142
  params: dict[str, Any] = {
@@ -149,6 +150,7 @@ class SubprocessClient:
149
150
  "no_clang_delta": no_clang_delta,
150
151
  "clang_delta": clang_delta,
151
152
  "trivial_is_error": trivial_is_error,
153
+ "skip_validation": skip_validation,
152
154
  }
153
155
  if parallelism is not None:
154
156
  params["parallelism"] = parallelism
@@ -56,6 +56,10 @@ class ProgressUpdate:
56
56
  current_pass_name: str = ""
57
57
  # List of disabled pass names
58
58
  disabled_passes: list[str] = field(default_factory=list)
59
+ # Test output preview (last 4KB of current/recent test output)
60
+ test_output_preview: str = ""
61
+ # Currently running test ID (None if no test running)
62
+ active_test_id: int | None = None
59
63
 
60
64
 
61
65
  @dataclass
@@ -115,6 +119,8 @@ def serialize(msg: Request | Response | ProgressUpdate) -> str:
115
119
  ],
116
120
  "current_pass_name": msg.current_pass_name,
117
121
  "disabled_passes": msg.disabled_passes,
122
+ "test_output_preview": msg.test_output_preview,
123
+ "active_test_id": msg.active_test_id,
118
124
  },
119
125
  }
120
126
  else:
@@ -162,6 +168,8 @@ def deserialize(line: str) -> Request | Response | ProgressUpdate:
162
168
  pass_stats=pass_stats_data,
163
169
  current_pass_name=d.get("current_pass_name", ""),
164
170
  disabled_passes=d.get("disabled_passes", []),
171
+ test_output_preview=d.get("test_output_preview", ""),
172
+ active_test_id=d.get("active_test_id"),
165
173
  )
166
174
 
167
175
  # Check for response (has "result" or "error" field)
@@ -1,7 +1,9 @@
1
1
  """Worker subprocess that runs the reducer with trio and communicates via JSON protocol."""
2
2
 
3
3
  import os
4
+ import shutil
4
5
  import sys
6
+ import tempfile
5
7
  import time
6
8
  import traceback
7
9
  from contextlib import aclosing
@@ -62,6 +64,8 @@ class ReducerWorker:
62
64
  # I/O streams - None means use stdin/stdout
63
65
  self._input_stream = input_stream
64
66
  self._output_stream = output_stream
67
+ # Output directory for test output capture (cleaned up on shutdown)
68
+ self._output_dir: str | None = None
65
69
 
66
70
  async def emit(self, msg: Response | ProgressUpdate) -> None:
67
71
  """Write a message to the output stream."""
@@ -163,6 +167,7 @@ class ReducerWorker:
163
167
  from shrinkray.state import (
164
168
  ShrinkRayDirectoryState,
165
169
  ShrinkRayStateSingleFile,
170
+ TestOutputManager,
166
171
  )
167
172
  from shrinkray.work import Volume
168
173
 
@@ -178,6 +183,7 @@ class ReducerWorker:
178
183
  no_clang_delta = params.get("no_clang_delta", False)
179
184
  clang_delta_path = params.get("clang_delta", "")
180
185
  trivial_is_error = params.get("trivial_is_error", True)
186
+ skip_validation = params.get("skip_validation", False)
181
187
 
182
188
  clang_delta_executable = None
183
189
  if os.path.splitext(filename)[1] in C_FILE_EXTENSIONS and not no_clang_delta:
@@ -213,12 +219,18 @@ class ReducerWorker:
213
219
  initial = reader.read()
214
220
  self.state = ShrinkRayStateSingleFile(initial=initial, **state_kwargs)
215
221
 
222
+ # Create output manager for test output capture (always enabled for TUI)
223
+ self._output_dir = tempfile.mkdtemp(prefix="shrinkray-output-")
224
+ self.state.output_manager = TestOutputManager(output_dir=self._output_dir)
225
+
216
226
  self.problem = self.state.problem
217
227
  self.reducer = self.state.reducer
218
228
 
219
229
  # Validate initial example before starting - this will raise
220
- # InvalidInitialExample if the initial test case fails
221
- await self.problem.setup()
230
+ # InvalidInitialExample if the initial test case fails.
231
+ # Skip if validation was already done by the caller (e.g., main()).
232
+ if not skip_validation:
233
+ await self.problem.setup()
222
234
 
223
235
  self.running = True
224
236
 
@@ -300,6 +312,32 @@ class ReducerWorker:
300
312
  return Response(id=request_id, result={"status": "skipped"})
301
313
  return Response(id=request_id, error="Reducer does not support pass control")
302
314
 
315
+ def _get_test_output_preview(self) -> tuple[str, int | None]:
316
+ """Get preview of current test output and active test ID."""
317
+ if self.state is None or self.state.output_manager is None:
318
+ return "", None
319
+
320
+ manager = self.state.output_manager
321
+ active_test_id = manager.get_active_test_id()
322
+ output_path = manager.get_current_output_path()
323
+
324
+ if output_path is None:
325
+ return "", active_test_id
326
+
327
+ # Read last 4KB of file
328
+ try:
329
+ with open(output_path, "rb") as f:
330
+ f.seek(0, 2) # Seek to end
331
+ size = f.tell()
332
+ if size > 4096:
333
+ f.seek(-4096, 2)
334
+ else:
335
+ f.seek(0)
336
+ data = f.read()
337
+ return data.decode("utf-8", errors="replace"), active_test_id
338
+ except OSError:
339
+ return "", active_test_id
340
+
303
341
  def _get_content_preview(self) -> tuple[str, bool]:
304
342
  """Get a preview of the current test case content."""
305
343
  if self.problem is None:
@@ -402,6 +440,9 @@ class ReducerWorker:
402
440
  else:
403
441
  disabled_passes = []
404
442
 
443
+ # Get test output preview
444
+ test_output_preview, active_test_id = self._get_test_output_preview()
445
+
405
446
  return ProgressUpdate(
406
447
  status=self.reducer.status if self.reducer else "",
407
448
  size=stats.current_test_case_size,
@@ -420,6 +461,8 @@ class ReducerWorker:
420
461
  pass_stats=pass_stats_list,
421
462
  current_pass_name=current_pass_name,
422
463
  disabled_passes=disabled_passes,
464
+ test_output_preview=test_output_preview,
465
+ active_test_id=active_test_id,
423
466
  )
424
467
 
425
468
  async def emit_progress_updates(self) -> None:
@@ -469,25 +512,32 @@ class ReducerWorker:
469
512
 
470
513
  async def run(self) -> None:
471
514
  """Main entry point for the worker."""
472
- async with trio.open_nursery() as nursery:
473
- await nursery.start(self.read_commands)
515
+ try:
516
+ async with trio.open_nursery() as nursery:
517
+ await nursery.start(self.read_commands)
474
518
 
475
- # Wait for start command
476
- while not self.running:
477
- await trio.sleep(0.01)
519
+ # Wait for start command
520
+ while not self.running:
521
+ await trio.sleep(0.01)
478
522
 
479
- # Start progress updates and reducer
480
- nursery.start_soon(self.emit_progress_updates)
481
- await self.run_reducer()
523
+ # Start progress updates and reducer
524
+ nursery.start_soon(self.emit_progress_updates)
525
+ await self.run_reducer()
482
526
 
483
- # Emit final progress update before completion
484
- final_update = await self._build_progress_update()
485
- if final_update is not None:
486
- await self.emit(final_update)
527
+ # Emit final progress update before completion
528
+ final_update = await self._build_progress_update()
529
+ if final_update is not None:
530
+ await self.emit(final_update)
487
531
 
488
- # Signal completion
489
- await self.emit(Response(id="", result={"status": "completed"}))
490
- nursery.cancel_scope.cancel()
532
+ # Signal completion
533
+ await self.emit(Response(id="", result={"status": "completed"}))
534
+ nursery.cancel_scope.cancel()
535
+ finally:
536
+ # Clean up test output files and temp directory
537
+ if self.state is not None and self.state.output_manager is not None:
538
+ self.state.output_manager.cleanup_all()
539
+ if self._output_dir is not None and os.path.isdir(self._output_dir):
540
+ shutil.rmtree(self._output_dir, ignore_errors=True)
491
541
 
492
542
 
493
543
  def main() -> None:
shrinkray/tui.py CHANGED
@@ -1,6 +1,7 @@
1
1
  """Textual-based TUI for Shrink Ray."""
2
2
 
3
3
  import os
4
+ import time
4
5
  import traceback
5
6
  from collections.abc import AsyncGenerator
6
7
  from contextlib import aclosing
@@ -11,14 +12,18 @@ import humanize
11
12
  from rich.text import Text
12
13
  from textual import work
13
14
  from textual.app import App, ComposeResult
14
- from textual.containers import Vertical, VerticalScroll
15
+ from textual.containers import Horizontal, Vertical, VerticalScroll
15
16
  from textual.reactive import reactive
16
17
  from textual.screen import ModalScreen
17
18
  from textual.theme import Theme
18
19
  from textual.widgets import DataTable, Footer, Header, Label, Static
19
20
 
20
21
  from shrinkray.subprocess.client import SubprocessClient
21
- from shrinkray.subprocess.protocol import PassStatsData, ProgressUpdate, Response
22
+ from shrinkray.subprocess.protocol import (
23
+ PassStatsData,
24
+ ProgressUpdate,
25
+ Response,
26
+ )
22
27
 
23
28
 
24
29
  ThemeMode = Literal["auto", "dark", "light"]
@@ -229,8 +234,6 @@ class ContentPreview(Static):
229
234
  _pending_hex_mode: bool = False
230
235
 
231
236
  def update_content(self, content: str, hex_mode: bool) -> None:
232
- import time
233
-
234
237
  # Store the pending content
235
238
  self._pending_content = content
236
239
  self._pending_hex_mode = hex_mode
@@ -305,6 +308,67 @@ class ContentPreview(Static):
305
308
  )
306
309
 
307
310
 
311
+ class OutputPreview(Static):
312
+ """Widget to display test output preview."""
313
+
314
+ output_content = reactive("")
315
+ active_test_id: reactive[int | None] = reactive(None)
316
+ _last_update_time: float = 0.0
317
+ _last_seen_test_id: int | None = None # Track last test ID for "completed" message
318
+
319
+ def update_output(self, content: str, test_id: int | None) -> None:
320
+ # Throttle updates to every 200ms
321
+ now = time.time()
322
+ if now - self._last_update_time < 0.2:
323
+ return
324
+
325
+ self._last_update_time = now
326
+ self.output_content = content
327
+ # Track the last test ID we've seen (for showing in "completed" message)
328
+ if test_id is not None:
329
+ self._last_seen_test_id = test_id
330
+ self.active_test_id = test_id
331
+ self.refresh(layout=True)
332
+
333
+ def _get_available_lines(self) -> int:
334
+ """Get the number of lines available for display based on container size."""
335
+ try:
336
+ parent = self.parent
337
+ if parent and hasattr(parent, "size"):
338
+ parent_size = parent.size # type: ignore[union-attr]
339
+ if parent_size.height > 0:
340
+ return max(10, parent_size.height - 3)
341
+ if self.app and self.app.size.height > 0:
342
+ return max(10, self.app.size.height - 15)
343
+ except Exception:
344
+ pass
345
+ return 30
346
+
347
+ def render(self) -> str:
348
+ # Header line
349
+ if self.active_test_id is not None:
350
+ header = f"[green]Test #{self.active_test_id} running...[/green]"
351
+ elif self.output_content and self._last_seen_test_id is not None:
352
+ header = f"[dim]Test #{self._last_seen_test_id} completed[/dim]"
353
+ else:
354
+ header = "[dim]No test output yet...[/dim]"
355
+
356
+ if not self.output_content:
357
+ return header
358
+
359
+ available_lines = self._get_available_lines()
360
+ lines = self.output_content.split("\n")
361
+
362
+ # Show tail of output (most recent lines)
363
+ if len(lines) <= available_lines:
364
+ return f"{header}\n{self.output_content}"
365
+
366
+ # Truncate from the beginning
367
+ truncated_lines = lines[-(available_lines):]
368
+ skipped = len(lines) - available_lines
369
+ return f"{header}\n... ({skipped} earlier lines)\n" + "\n".join(truncated_lines)
370
+
371
+
308
372
  class HelpScreen(ModalScreen[None]):
309
373
  """Modal screen showing keyboard shortcuts help."""
310
374
 
@@ -595,10 +659,32 @@ class ShrinkRayApp(App[None]):
595
659
  margin: 0 1;
596
660
  }
597
661
 
662
+ #content-area {
663
+ height: 1fr;
664
+ }
665
+
598
666
  #content-container {
599
667
  border: solid blue;
600
668
  margin: 1;
601
- height: 1fr;
669
+ padding: 1;
670
+ width: 1fr;
671
+ height: 100%;
672
+ }
673
+
674
+ #content-container:dark {
675
+ border: solid lightskyblue;
676
+ }
677
+
678
+ #output-container {
679
+ border: solid blue;
680
+ margin: 1;
681
+ padding: 1;
682
+ width: 1fr;
683
+ height: 100%;
684
+ }
685
+
686
+ #output-container:dark {
687
+ border: solid lightskyblue;
602
688
  }
603
689
  """
604
690
 
@@ -661,8 +747,13 @@ class ShrinkRayApp(App[None]):
661
747
  )
662
748
  with Vertical(id="stats-container"):
663
749
  yield StatsDisplay(id="stats-display")
664
- with VerticalScroll(id="content-container"):
665
- yield ContentPreview(id="content-preview")
750
+ with Horizontal(id="content-area"):
751
+ with VerticalScroll(id="content-container") as content_scroll:
752
+ content_scroll.border_title = "Recent Reductions"
753
+ yield ContentPreview(id="content-preview")
754
+ with VerticalScroll(id="output-container") as output_scroll:
755
+ output_scroll.border_title = "Test Output"
756
+ yield OutputPreview(id="output-preview")
666
757
  yield Footer()
667
758
 
668
759
  async def on_mount(self) -> None:
@@ -695,7 +786,7 @@ class ShrinkRayApp(App[None]):
695
786
 
696
787
  await self._client.start()
697
788
 
698
- # Start the reduction
789
+ # Start the reduction - validation was already done by main()
699
790
  response = await self._client.start_reduction(
700
791
  file_path=self._file_path,
701
792
  test=self._test,
@@ -709,6 +800,7 @@ class ShrinkRayApp(App[None]):
709
800
  no_clang_delta=self._no_clang_delta,
710
801
  clang_delta=self._clang_delta,
711
802
  trivial_is_error=self._trivial_is_error,
803
+ skip_validation=True,
712
804
  )
713
805
 
714
806
  if response.error:
@@ -719,6 +811,7 @@ class ShrinkRayApp(App[None]):
719
811
  # Monitor progress (client is already started and reduction is running)
720
812
  stats_display = self.query_one("#stats-display", StatsDisplay)
721
813
  content_preview = self.query_one("#content-preview", ContentPreview)
814
+ output_preview = self.query_one("#output-preview", OutputPreview)
722
815
 
723
816
  async with aclosing(self._client.get_progress_updates()) as updates:
724
817
  async for update in updates:
@@ -726,6 +819,9 @@ class ShrinkRayApp(App[None]):
726
819
  content_preview.update_content(
727
820
  update.content_preview, update.hex_mode
728
821
  )
822
+ output_preview.update_output(
823
+ update.test_output_preview, update.active_test_id
824
+ )
729
825
  self._latest_pass_stats = update.pass_stats
730
826
  self._current_pass_name = update.current_pass_name
731
827
  self._disabled_passes = update.disabled_passes
@@ -741,7 +837,10 @@ class ShrinkRayApp(App[None]):
741
837
  # Check if there was an error from the worker
742
838
  if self._client.error_message:
743
839
  # Exit immediately on error, printing the error message
744
- self.exit(return_code=1, message=f"Error: {self._client.error_message}")
840
+ self.exit(
841
+ return_code=1,
842
+ message=f"Error: {self._client.error_message}",
843
+ )
745
844
  return
746
845
  elif self._exit_on_completion:
747
846
  self.exit()
@@ -804,54 +903,6 @@ class ShrinkRayApp(App[None]):
804
903
  return self._completed
805
904
 
806
905
 
807
- async def _validate_initial_example(
808
- file_path: str,
809
- test: list[str],
810
- parallelism: int | None,
811
- timeout: float | None,
812
- seed: int,
813
- input_type: str,
814
- in_place: bool,
815
- formatter: str,
816
- volume: str,
817
- no_clang_delta: bool,
818
- clang_delta: str,
819
- trivial_is_error: bool,
820
- ) -> str | None:
821
- """Validate initial example before showing TUI.
822
-
823
- Returns error_message if validation failed, None if it passed.
824
- """
825
- debug_mode = volume == "debug"
826
- client = SubprocessClient(debug_mode=debug_mode)
827
- try:
828
- await client.start()
829
-
830
- response = await client.start_reduction(
831
- file_path=file_path,
832
- test=test,
833
- parallelism=parallelism,
834
- timeout=timeout,
835
- seed=seed,
836
- input_type=input_type,
837
- in_place=in_place,
838
- formatter=formatter,
839
- volume=volume,
840
- no_clang_delta=no_clang_delta,
841
- clang_delta=clang_delta,
842
- trivial_is_error=trivial_is_error,
843
- )
844
-
845
- if response.error:
846
- return response.error
847
-
848
- # Validation passed - cancel this reduction since TUI will start fresh
849
- await client.cancel()
850
- return None
851
- finally:
852
- await client.close()
853
-
854
-
855
906
  def run_textual_ui(
856
907
  file_path: str,
857
908
  test: list[str],
@@ -868,43 +919,14 @@ def run_textual_ui(
868
919
  exit_on_completion: bool = True,
869
920
  theme: ThemeMode = "auto",
870
921
  ) -> None:
871
- """Run the textual TUI."""
872
- import asyncio
873
- import sys
874
-
875
- print("Validating initial example...", flush=True)
876
-
877
- # Validate initial example before showing TUI
878
- async def validate():
879
- return await _validate_initial_example(
880
- file_path=file_path,
881
- test=test,
882
- parallelism=parallelism,
883
- timeout=timeout,
884
- seed=seed,
885
- input_type=input_type,
886
- in_place=in_place,
887
- formatter=formatter,
888
- volume=volume,
889
- no_clang_delta=no_clang_delta,
890
- clang_delta=clang_delta,
891
- trivial_is_error=trivial_is_error,
892
- )
922
+ """Run the textual TUI.
893
923
 
894
- try:
895
- error = asyncio.run(validate())
896
- except Exception as e:
897
- import traceback
898
-
899
- traceback.print_exc()
900
- print(f"Error: {e}", file=sys.stderr)
901
- sys.exit(1)
902
-
903
- if error:
904
- print(f"Error: {error}", file=sys.stderr)
905
- sys.exit(1)
924
+ Note: Validation must be done before calling this function.
925
+ The caller (main()) is responsible for running run_validation() first.
926
+ """
927
+ import sys
906
928
 
907
- # Validation passed - now show the TUI which will start a fresh client
929
+ # Start the TUI app - validation has already been done by main()
908
930
  app = ShrinkRayApp(
909
931
  file_path=file_path,
910
932
  test=test,
@@ -0,0 +1,403 @@
1
+ """Initial validation of interestingness tests before reduction.
2
+
3
+ This module provides validation that runs in the main process using trio,
4
+ before the TUI is launched. It prints commands and temporary directories
5
+ to stderr so users can understand what's happening with slow tests, and
6
+ preserves temporary directories on failure for debugging.
7
+ """
8
+
9
+ import io
10
+ import os
11
+ import shlex
12
+ import shutil
13
+ import subprocess
14
+ import sys
15
+ import tempfile
16
+ import traceback
17
+ from dataclasses import dataclass
18
+
19
+ import trio
20
+
21
+ from shrinkray.cli import InputType
22
+
23
+
24
+ @dataclass
25
+ class ValidationResult:
26
+ """Result of initial validation."""
27
+
28
+ success: bool
29
+ error_message: str | None = None
30
+ exit_code: int | None = None
31
+ # Temp directories to clean up only on success
32
+ temp_dirs: list[str] | None = None
33
+ # Whether formatter is usable (None if no formatter specified)
34
+ formatter_works: bool | None = None
35
+
36
+
37
+ def _build_command(
38
+ test: list[str],
39
+ working_file: str,
40
+ input_type: InputType,
41
+ ) -> list[str]:
42
+ """Build the command to run, adding test file path if needed."""
43
+ if input_type.enabled(InputType.arg):
44
+ return test + [working_file]
45
+ return list(test)
46
+
47
+
48
+ def _format_command_for_display(command: list[str], cwd: str) -> str:
49
+ """Format a command for display, with cd on its own line and relative paths.
50
+
51
+ Returns a multi-line string with:
52
+ - cd <directory>
53
+ - <command with relative paths for files in cwd>
54
+ """
55
+ # Convert absolute paths within cwd to relative paths for readability
56
+ display_parts = []
57
+ for part in command:
58
+ if part.startswith(cwd + os.sep):
59
+ # Convert to relative path
60
+ display_parts.append(os.path.relpath(part, cwd))
61
+ else:
62
+ display_parts.append(part)
63
+
64
+ quoted = " ".join(shlex.quote(part) for part in display_parts)
65
+ return f"cd {shlex.quote(cwd)}\n{quoted}"
66
+
67
+
68
+ async def _run_validation_test(
69
+ test: list[str],
70
+ initial_content: bytes,
71
+ base: str,
72
+ input_type: InputType,
73
+ in_place: bool,
74
+ filename: str,
75
+ ) -> ValidationResult:
76
+ """Run the interestingness test once and check if it passes.
77
+
78
+ Returns ValidationResult with success=True if the test passed (exit code 0),
79
+ or success=False with error details if it failed.
80
+ """
81
+ temp_dirs: list[str] = []
82
+
83
+ try:
84
+ # Determine working directory and file path
85
+ if in_place:
86
+ if input_type == InputType.basename:
87
+ working = filename
88
+ cwd = os.getcwd()
89
+ # Write directly to original file
90
+ async with await trio.open_file(working, "wb") as f:
91
+ await f.write(initial_content)
92
+ else:
93
+ # Create a temp file in same directory with random suffix
94
+ base_name, ext = os.path.splitext(filename)
95
+ working = base_name + "-" + os.urandom(16).hex() + ext
96
+ cwd = os.getcwd()
97
+ async with await trio.open_file(working, "wb") as f:
98
+ await f.write(initial_content)
99
+ temp_dirs.append(working) # Track for cleanup
100
+ else:
101
+ # Create a temporary directory
102
+ temp_dir = tempfile.mkdtemp(prefix="shrinkray-validate-")
103
+ temp_dirs.append(temp_dir)
104
+ working = os.path.join(temp_dir, base)
105
+ cwd = temp_dir
106
+ async with await trio.open_file(working, "wb") as f:
107
+ await f.write(initial_content)
108
+
109
+ # Build command
110
+ command = _build_command(test, working, input_type)
111
+
112
+ # Print what we're doing to stderr
113
+ print(
114
+ "\nRunning interestingness test:",
115
+ file=sys.stderr,
116
+ flush=True,
117
+ )
118
+ print(
119
+ _format_command_for_display(command, cwd),
120
+ file=sys.stderr,
121
+ flush=True,
122
+ )
123
+ print(file=sys.stderr, flush=True)
124
+
125
+ # Handle stdin if needed
126
+ stdin_data: bytes | None = None
127
+ if input_type.enabled(InputType.stdin) and not os.path.isdir(working):
128
+ with open(working, "rb") as f:
129
+ stdin_data = f.read()
130
+
131
+ # Run subprocess with real-time output streaming
132
+ # We use subprocess.run in a thread because trio.run_process doesn't
133
+ # properly support file descriptor inheritance for streaming output.
134
+ def run_subprocess() -> subprocess.CompletedProcess[bytes]:
135
+ # Try to stream output directly to stderr if possible
136
+ # This allows real-time output visibility for slow tests
137
+ try:
138
+ stderr_fd = sys.stderr.fileno()
139
+ return subprocess.run(
140
+ command,
141
+ cwd=cwd,
142
+ stdin=subprocess.DEVNULL if stdin_data is None else None,
143
+ stdout=stderr_fd,
144
+ stderr=stderr_fd,
145
+ input=stdin_data,
146
+ check=False,
147
+ )
148
+ except (io.UnsupportedOperation, OSError):
149
+ # Falls back to capturing if stderr doesn't have a real file
150
+ # descriptor (e.g., when running under pytest with capture)
151
+ return subprocess.run(
152
+ command,
153
+ cwd=cwd,
154
+ stdin=subprocess.DEVNULL if stdin_data is None else None,
155
+ stdout=subprocess.PIPE,
156
+ stderr=subprocess.PIPE,
157
+ input=stdin_data,
158
+ check=False,
159
+ )
160
+
161
+ result = await trio.to_thread.run_sync(run_subprocess)
162
+
163
+ # If we captured output (fallback mode), print it now
164
+ if result.stdout:
165
+ sys.stderr.buffer.write(result.stdout)
166
+ sys.stderr.flush()
167
+ if result.stderr:
168
+ sys.stderr.buffer.write(result.stderr)
169
+ sys.stderr.flush()
170
+
171
+ print(file=sys.stderr, flush=True)
172
+ print(
173
+ f"Exit code: {result.returncode}",
174
+ file=sys.stderr,
175
+ flush=True,
176
+ )
177
+
178
+ if result.returncode != 0:
179
+ return ValidationResult(
180
+ success=False,
181
+ error_message=(
182
+ f"Interestingness test exited with code {result.returncode}, "
183
+ f"but should return 0 for interesting test cases.\n\n"
184
+ f"To reproduce:\n{_format_command_for_display(command, cwd)}"
185
+ ),
186
+ exit_code=result.returncode,
187
+ temp_dirs=temp_dirs,
188
+ )
189
+
190
+ return ValidationResult(
191
+ success=True,
192
+ exit_code=0,
193
+ temp_dirs=temp_dirs,
194
+ )
195
+
196
+ except Exception as e:
197
+ traceback.print_exc()
198
+ return ValidationResult(
199
+ success=False,
200
+ error_message=f"Error running interestingness test: {e}",
201
+ temp_dirs=temp_dirs,
202
+ )
203
+
204
+
205
+ async def _run_formatter(
206
+ formatter_command: list[str],
207
+ content: bytes,
208
+ ) -> subprocess.CompletedProcess[bytes]:
209
+ """Run the formatter command on content, streaming output to stderr."""
210
+
211
+ print("\nRunning formatter:", file=sys.stderr, flush=True)
212
+ print(
213
+ " ".join(shlex.quote(part) for part in formatter_command),
214
+ file=sys.stderr,
215
+ flush=True,
216
+ )
217
+
218
+ def run_subprocess() -> subprocess.CompletedProcess[bytes]:
219
+ return subprocess.run(
220
+ formatter_command,
221
+ input=content,
222
+ capture_output=True,
223
+ check=False,
224
+ )
225
+
226
+ result = await trio.to_thread.run_sync(run_subprocess)
227
+
228
+ # Show stderr from formatter if any
229
+ if result.stderr:
230
+ sys.stderr.buffer.write(result.stderr)
231
+ sys.stderr.flush()
232
+
233
+ print(
234
+ f"Formatter exit code: {result.returncode}",
235
+ file=sys.stderr,
236
+ flush=True,
237
+ )
238
+
239
+ return result
240
+
241
+
242
+ async def validate_initial_example(
243
+ file_path: str,
244
+ test: list[str],
245
+ input_type: InputType,
246
+ in_place: bool,
247
+ formatter_command: list[str] | None = None,
248
+ ) -> ValidationResult:
249
+ """Validate that the initial example passes the interestingness test.
250
+
251
+ This runs directly in the main process using trio, streaming output
252
+ to stderr so users can see progress for slow tests. Also checks the
253
+ formatter if one is specified.
254
+
255
+ Args:
256
+ file_path: Path to the file to reduce
257
+ test: The interestingness test command
258
+ input_type: How to pass input to the test
259
+ in_place: Whether to run in the current directory
260
+ formatter_command: Optional formatter command to validate
261
+
262
+ Returns:
263
+ ValidationResult indicating success or failure with details.
264
+ On failure, temp_dirs are preserved for debugging.
265
+ """
266
+ # Read the initial content
267
+ if os.path.isdir(file_path):
268
+ # For directories, we need different handling
269
+ # For now, just validate that it's a valid directory
270
+ return ValidationResult(success=True)
271
+
272
+ with open(file_path, "rb") as f:
273
+ initial_content = f.read()
274
+
275
+ base = os.path.basename(file_path)
276
+
277
+ print("Validating interestingness test...", file=sys.stderr, flush=True)
278
+
279
+ result = await _run_validation_test(
280
+ test=test,
281
+ initial_content=initial_content,
282
+ base=base,
283
+ input_type=input_type,
284
+ in_place=in_place,
285
+ filename=file_path,
286
+ )
287
+
288
+ if not result.success:
289
+ # On failure, keep temp directories and tell user
290
+ if result.temp_dirs:
291
+ print(
292
+ "\nTemporary files preserved for debugging:",
293
+ file=sys.stderr,
294
+ flush=True,
295
+ )
296
+ for path in result.temp_dirs:
297
+ print(f" {path}", file=sys.stderr, flush=True)
298
+ return result
299
+
300
+ # Clean up temp directories from initial test
301
+ if result.temp_dirs:
302
+ for path in result.temp_dirs:
303
+ try:
304
+ if os.path.isdir(path):
305
+ shutil.rmtree(path)
306
+ elif os.path.exists(path):
307
+ os.unlink(path)
308
+ except Exception:
309
+ pass # Best effort cleanup
310
+
311
+ print("Initial validation passed.", file=sys.stderr, flush=True)
312
+
313
+ # Now check formatter if specified
314
+ formatter_works: bool | None = None
315
+ if formatter_command is not None:
316
+ formatter_result = await _run_formatter(formatter_command, initial_content)
317
+
318
+ if formatter_result.returncode != 0:
319
+ return ValidationResult(
320
+ success=False,
321
+ error_message=(
322
+ "Formatter exited unexpectedly on initial test case. "
323
+ "If this is expected, please run with --formatter=none.\n\n"
324
+ f"Formatter stderr:\n{formatter_result.stderr.decode('utf-8', errors='replace').strip()}"
325
+ ),
326
+ exit_code=formatter_result.returncode,
327
+ )
328
+
329
+ reformatted = formatter_result.stdout
330
+
331
+ # If formatter changed the content, verify it's still interesting
332
+ if reformatted != initial_content:
333
+ print(
334
+ "\nChecking if formatted version is still interesting...",
335
+ file=sys.stderr,
336
+ flush=True,
337
+ )
338
+ formatted_result = await _run_validation_test(
339
+ test=test,
340
+ initial_content=reformatted,
341
+ base=base,
342
+ input_type=input_type,
343
+ in_place=in_place,
344
+ filename=file_path,
345
+ )
346
+
347
+ # Clean up temp dirs from formatted test
348
+ if formatted_result.temp_dirs:
349
+ for path in formatted_result.temp_dirs:
350
+ try:
351
+ if os.path.isdir(path):
352
+ shutil.rmtree(path)
353
+ elif os.path.exists(path):
354
+ os.unlink(path)
355
+ except Exception:
356
+ pass
357
+
358
+ if not formatted_result.success:
359
+ return ValidationResult(
360
+ success=False,
361
+ error_message=(
362
+ "Formatting initial test case made it uninteresting. "
363
+ "If this is expected, please run with --formatter=none.\n\n"
364
+ f"Formatter stderr:\n{formatter_result.stderr.decode('utf-8', errors='replace').strip()}"
365
+ ),
366
+ exit_code=formatted_result.exit_code,
367
+ )
368
+
369
+ print("Formatted version is also interesting.", file=sys.stderr, flush=True)
370
+
371
+ formatter_works = True
372
+
373
+ return ValidationResult(
374
+ success=True,
375
+ exit_code=0,
376
+ formatter_works=formatter_works,
377
+ )
378
+
379
+
380
+ def run_validation(
381
+ file_path: str,
382
+ test: list[str],
383
+ input_type: InputType,
384
+ in_place: bool,
385
+ formatter_command: list[str] | None = None,
386
+ ) -> ValidationResult:
387
+ """Run initial validation synchronously using trio.run().
388
+
389
+ This is the main entry point for validation from the CLI/TUI.
390
+ It runs validation directly in the main process before any asyncio
391
+ event loop is started.
392
+ """
393
+
394
+ async def _run() -> ValidationResult:
395
+ return await validate_initial_example(
396
+ file_path,
397
+ test,
398
+ input_type,
399
+ in_place,
400
+ formatter_command,
401
+ )
402
+
403
+ return trio.run(_run)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shrinkray
3
- Version: 25.12.27.2
3
+ Version: 25.12.27.3
4
4
  Summary: Shrink Ray
5
5
  Author-email: "David R. MacIver" <david@drmaciver.com>
6
6
  License: MIT
@@ -1,5 +1,5 @@
1
1
  shrinkray/__init__.py,sha256=b5MvcvhsEGYya3GRXNbCJAlAL5IZHSsETLK_vtfmXRY,18
2
- shrinkray/__main__.py,sha256=K3_s96Tyoi7SxNOyoZXkfiEoSxVBL__TJ3o2Cefadmg,11093
2
+ shrinkray/__main__.py,sha256=HuDZFXgD9LH3XPBK2QBvP4YkyTFJvduK9-PVtz9fKpk,11762
3
3
  shrinkray/cli.py,sha256=1-qjaIchyCDd-YCdGWtK7q9j9qr6uX6AqtwW8m5QCQg,1697
4
4
  shrinkray/display.py,sha256=WYN05uqmUVpZhwi2pxr1U-wLHWZ9KiL0RUlTCBJ1N3E,2430
5
5
  shrinkray/formatting.py,sha256=tXCGnhJn-WJGpHMaLHRCAXK8aKJBbrOdiW9QGERrQEk,3121
@@ -7,9 +7,10 @@ shrinkray/problem.py,sha256=_edENYk8OC5o_2ng0WZrhIfilhlY5IuOrqt0qWBZAuM,25979
7
7
  shrinkray/process.py,sha256=-eP8h5X0ESbkcTic8FFEzkd4-vwaZ0YI5tLxUR25L8U,1599
8
8
  shrinkray/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  shrinkray/reducer.py,sha256=t2BBjq-EZ0qBbsez4ep0tFYCkDT8RwKYnjJ2fRsXubg,20096
10
- shrinkray/state.py,sha256=owyDNiSeHKnCsgOQczSz796KEANem-4YWTmbmZBRE0w,24040
11
- shrinkray/tui.py,sha256=3RskLo6JvKdUQIHi40R5ka-F_1GkBXyA_d_SkYbLlCw,31601
10
+ shrinkray/state.py,sha256=a8Xq7IQwLz7CyZ58zU0Z91uoglgPJmhQUvbcdgsIoKk,29593
11
+ shrinkray/tui.py,sha256=Ovd6Kbk_cmOYJlwnjEyl6ka_2g1qJAtwxojh5CWEzNA,33001
12
12
  shrinkray/ui.py,sha256=xuDUwU-MM3AetvwUB7bfzav0P_drUsBrKFPhON_Nr-k,2251
13
+ shrinkray/validation.py,sha256=piBCO-k9he_id6TWC4EHMK3GfuyPqRcNfkNJPVjxEaU,13366
13
14
  shrinkray/work.py,sha256=GEZ14Kk3bvwUxAnACvY-wom2lVWaGrELMNxrDjv03dk,8110
14
15
  shrinkray/passes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
16
  shrinkray/passes/bytes.py,sha256=U1sNAuqUlsaLpRSJuS9X4H7S2jOiilTUIIE9-WjSd4k,23967
@@ -22,12 +23,12 @@ shrinkray/passes/python.py,sha256=3WN1lZTf5oVL8FCTGomhrCuE04wIX9ocKcmFV86NMZA,68
22
23
  shrinkray/passes/sat.py,sha256=OboY6jsKf6lph3pAFh535plvhNOVzEF8HJ66WEqsNm4,19483
23
24
  shrinkray/passes/sequences.py,sha256=jCK1fWBxCz79u7JWSps9wf7Yru7W_FAsJwdgg--CLxU,3040
24
25
  shrinkray/subprocess/__init__.py,sha256=FyV2y05uwQ1RTZGwREI0aAVaLX1jiwRcWsdsksFmdbM,451
25
- shrinkray/subprocess/client.py,sha256=erqnPglPO0YNdwEKlmhB3yDo6Mfc00Lxh4T85lZhsDo,9341
26
- shrinkray/subprocess/protocol.py,sha256=LuHl0IkKpDzYhAGZz_EiTHNqDNq_v1ozg5aUSl7UzE4,6203
27
- shrinkray/subprocess/worker.py,sha256=ke-9DYFH117EpJEntkucTrn7ep7pygzmV-VXkRe1o-E,19294
28
- shrinkray-25.12.27.2.dist-info/licenses/LICENSE,sha256=iMKX79AuokJfIZUnGUARdUp30vVAoIPOJ7ek8TY63kk,1072
29
- shrinkray-25.12.27.2.dist-info/METADATA,sha256=-rrEVZeXrERITn0adTTOOi6Ui7fK6Dz-GvyFEQcHvtw,7600
30
- shrinkray-25.12.27.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
31
- shrinkray-25.12.27.2.dist-info/entry_points.txt,sha256=wIZvnGyOdVeaLTiv2klnSyTe-EKkkwn4SwHh9bmJ7qk,104
32
- shrinkray-25.12.27.2.dist-info/top_level.txt,sha256=fLif8-rFoFOnf5h8-vs3ECkKNWQopTQh3xvl1s7pchQ,10
33
- shrinkray-25.12.27.2.dist-info/RECORD,,
26
+ shrinkray/subprocess/client.py,sha256=abBkrXaJcA6cd3l_avPuteO_kYnjU5IRG7VtlmiAJgE,9428
27
+ shrinkray/subprocess/protocol.py,sha256=fbY29q-j98KnAuZJ1Y_ARoi7BskkUu37SoybwVWb-pA,6636
28
+ shrinkray/subprocess/worker.py,sha256=IBAXZq3E27bTuN9kHGgU5xhU6E8m_5-Ioz1dubVTuN8,21449
29
+ shrinkray-25.12.27.3.dist-info/licenses/LICENSE,sha256=iMKX79AuokJfIZUnGUARdUp30vVAoIPOJ7ek8TY63kk,1072
30
+ shrinkray-25.12.27.3.dist-info/METADATA,sha256=BUSNccYFT8TdtojoQHdt-YR9DEVtjsG8fNvlybnRsB0,7600
31
+ shrinkray-25.12.27.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
32
+ shrinkray-25.12.27.3.dist-info/entry_points.txt,sha256=wIZvnGyOdVeaLTiv2klnSyTe-EKkkwn4SwHh9bmJ7qk,104
33
+ shrinkray-25.12.27.3.dist-info/top_level.txt,sha256=fLif8-rFoFOnf5h8-vs3ECkKNWQopTQh3xvl1s7pchQ,10
34
+ shrinkray-25.12.27.3.dist-info/RECORD,,