shrinkray 26.1.1.0__tar.gz → 26.2.4.0__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.
- {shrinkray-26.1.1.0/src/shrinkray.egg-info → shrinkray-26.2.4.0}/PKG-INFO +1 -1
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/pyproject.toml +1 -1
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/tui.py +66 -53
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0/src/shrinkray.egg-info}/PKG-INFO +1 -1
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_tui.py +119 -25
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/LICENSE +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/README.md +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/setup.cfg +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/__init__.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/__main__.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/cli.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/formatting.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/history.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/passes/__init__.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/passes/bytes.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/passes/clangdelta.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/passes/definitions.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/passes/genericlanguages.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/passes/json.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/passes/patching.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/passes/python.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/passes/sat.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/passes/sequences.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/problem.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/process.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/py.typed +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/reducer.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/state.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/subprocess/__init__.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/subprocess/client.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/subprocess/protocol.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/subprocess/worker.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/ui.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/validation.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray/work.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray.egg-info/SOURCES.txt +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray.egg-info/dependency_links.txt +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray.egg-info/entry_points.txt +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray.egg-info/requires.txt +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/src/shrinkray.egg-info/top_level.txt +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_byte_reduction_passes.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_clang_delta.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_cli.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_definitions.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_dimacs_cnf.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_formatting.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_generic_language.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_generic_shrinking_properties.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_history.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_json_passes.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_main.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_misc_reduction_performance.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_natural_sort_orders.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_patching.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_problem.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_process.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_python_reducers.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_reducer.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_reduction_passes.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_sat.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_state.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_subprocess_client.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_subprocess_integration.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_subprocess_protocol.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_subprocess_worker.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_tui_snapshots.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_ui.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_validation.py +0 -0
- {shrinkray-26.1.1.0 → shrinkray-26.2.4.0}/tests/test_work.py +0 -0
|
@@ -13,7 +13,6 @@ from difflib import unified_diff
|
|
|
13
13
|
from typing import Literal, Protocol, cast
|
|
14
14
|
|
|
15
15
|
import humanize
|
|
16
|
-
from rich.markup import escape as escape_markup
|
|
17
16
|
from rich.text import Text
|
|
18
17
|
from textual import work
|
|
19
18
|
from textual.app import App, ComposeResult
|
|
@@ -46,6 +45,7 @@ from shrinkray.subprocess.protocol import (
|
|
|
46
45
|
|
|
47
46
|
ThemeMode = Literal["auto", "dark", "light"]
|
|
48
47
|
|
|
48
|
+
|
|
49
49
|
# Custom themes with true white/black backgrounds
|
|
50
50
|
SHRINKRAY_LIGHT_THEME = Theme(
|
|
51
51
|
name="shrinkray-light",
|
|
@@ -505,20 +505,20 @@ class ContentPreview(Static):
|
|
|
505
505
|
# Fallback based on common terminal height
|
|
506
506
|
return 30
|
|
507
507
|
|
|
508
|
-
def render(self) ->
|
|
508
|
+
def render(self) -> Text:
|
|
509
509
|
if not self.preview_content:
|
|
510
|
-
return "Loading..."
|
|
510
|
+
return Text("Loading...")
|
|
511
511
|
|
|
512
512
|
available_lines = self._get_available_lines()
|
|
513
513
|
|
|
514
514
|
if self.hex_mode:
|
|
515
|
-
return f"[Hex mode]\n{self.preview_content}"
|
|
515
|
+
return Text(f"[Hex mode]\n{self.preview_content}")
|
|
516
516
|
|
|
517
517
|
lines = self.preview_content.split("\n")
|
|
518
518
|
|
|
519
519
|
# For small files that fit, show full content
|
|
520
520
|
if len(lines) <= available_lines:
|
|
521
|
-
return self.preview_content
|
|
521
|
+
return Text(self.preview_content)
|
|
522
522
|
|
|
523
523
|
# For larger files, show diff if we have previous displayed content
|
|
524
524
|
if (
|
|
@@ -530,10 +530,10 @@ class ContentPreview(Static):
|
|
|
530
530
|
diff = list(unified_diff(prev_lines, curr_lines, lineterm=""))
|
|
531
531
|
if diff:
|
|
532
532
|
# Show as much diff as fits
|
|
533
|
-
return "\n".join(diff[:available_lines])
|
|
533
|
+
return Text("\n".join(diff[:available_lines]))
|
|
534
534
|
|
|
535
535
|
# No diff available, show truncated content
|
|
536
|
-
return (
|
|
536
|
+
return Text(
|
|
537
537
|
"\n".join(lines[:available_lines])
|
|
538
538
|
+ f"\n\n... ({len(lines) - available_lines} more lines)"
|
|
539
539
|
)
|
|
@@ -591,39 +591,46 @@ class OutputPreview(Static):
|
|
|
591
591
|
pass
|
|
592
592
|
return 30
|
|
593
593
|
|
|
594
|
-
def render(self) ->
|
|
594
|
+
def render(self) -> Text:
|
|
595
595
|
# Header line - use return_code to determine if test is running
|
|
596
596
|
# (return_code is None means still running, has value means completed)
|
|
597
597
|
if self.active_test_id is not None and self.last_return_code is None:
|
|
598
|
-
|
|
598
|
+
header_text = f"Test #{self.active_test_id} running..."
|
|
599
|
+
header_style = "green"
|
|
599
600
|
elif self.active_test_id is not None:
|
|
600
|
-
|
|
601
|
+
header_text = (
|
|
602
|
+
f"Test #{self.active_test_id} exited with code {self.last_return_code}"
|
|
603
|
+
)
|
|
604
|
+
header_style = "dim"
|
|
601
605
|
elif self._has_seen_output or self.output_content:
|
|
602
606
|
# Have seen output before - show without header
|
|
603
|
-
|
|
607
|
+
header_text = ""
|
|
608
|
+
header_style = ""
|
|
604
609
|
else:
|
|
605
|
-
|
|
610
|
+
header_text = "No test output yet..."
|
|
611
|
+
header_style = "dim"
|
|
606
612
|
|
|
607
613
|
if not self.output_content:
|
|
608
|
-
return
|
|
614
|
+
return Text(header_text, style=header_style)
|
|
609
615
|
|
|
610
616
|
available_lines = self._get_available_lines()
|
|
611
|
-
|
|
612
|
-
# (test output may contain characters like ^ that have special meaning)
|
|
613
|
-
escaped_content = escape_markup(self.output_content)
|
|
614
|
-
lines = escaped_content.split("\n")
|
|
615
|
-
|
|
616
|
-
# Build prefix (header + newline, or empty if no header)
|
|
617
|
-
prefix = f"{header}\n" if header else ""
|
|
617
|
+
lines = self.output_content.split("\n")
|
|
618
618
|
|
|
619
619
|
# Show tail of output (most recent lines)
|
|
620
|
-
if len(lines)
|
|
621
|
-
|
|
620
|
+
if len(lines) > available_lines:
|
|
621
|
+
truncated_lines = lines[-(available_lines):]
|
|
622
|
+
skipped = len(lines) - available_lines
|
|
623
|
+
content = f"... ({skipped} earlier lines)\n" + "\n".join(truncated_lines)
|
|
624
|
+
else:
|
|
625
|
+
content = self.output_content
|
|
622
626
|
|
|
623
|
-
#
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
+
# Build result with styled header and unstyled content
|
|
628
|
+
result = Text()
|
|
629
|
+
if header_text:
|
|
630
|
+
result.append(header_text, style=header_style)
|
|
631
|
+
result.append("\n")
|
|
632
|
+
result.append(content)
|
|
633
|
+
return result
|
|
627
634
|
|
|
628
635
|
|
|
629
636
|
class HelpScreen(ModalScreen[None]):
|
|
@@ -734,21 +741,20 @@ class ExpandedBoxModal(ModalScreen[None]):
|
|
|
734
741
|
self._content_widget_id = content_widget_id
|
|
735
742
|
self._file_path = file_path
|
|
736
743
|
|
|
737
|
-
def _read_file(self, file_path: str) ->
|
|
744
|
+
def _read_file(self, file_path: str) -> Text:
|
|
738
745
|
"""Read file content, decoding as text if possible."""
|
|
739
746
|
if not os.path.isfile(file_path):
|
|
740
|
-
return "
|
|
747
|
+
return Text("File not found", style="dim")
|
|
741
748
|
try:
|
|
742
749
|
with open(file_path, "rb") as f:
|
|
743
750
|
raw_content = f.read()
|
|
744
751
|
# Try to decode as text, fall back to hex display if binary
|
|
745
752
|
encoding, text = try_decode(raw_content)
|
|
746
753
|
if encoding is not None:
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
return "[Binary content - hex display]\n\n" + raw_content.hex()
|
|
754
|
+
return Text(text)
|
|
755
|
+
return Text("Binary content - hex display\n\n" + raw_content.hex())
|
|
750
756
|
except OSError:
|
|
751
|
-
return "
|
|
757
|
+
return Text("Error reading file", style="red")
|
|
752
758
|
|
|
753
759
|
def compose(self) -> ComposeResult:
|
|
754
760
|
with Vertical():
|
|
@@ -781,16 +787,16 @@ class ExpandedBoxModal(ModalScreen[None]):
|
|
|
781
787
|
return "Statistics not available"
|
|
782
788
|
return stats_displays[0].render()
|
|
783
789
|
|
|
784
|
-
def _get_file_content(self, app: "ShrinkRayApp") -> str:
|
|
790
|
+
def _get_file_content(self, app: "ShrinkRayApp") -> str | Text:
|
|
785
791
|
"""Get content from file or preview widget."""
|
|
786
792
|
if self._file_path:
|
|
787
793
|
return self._read_file(self._file_path)
|
|
788
794
|
content_previews = list(app.query("#content-preview").results(ContentPreview))
|
|
789
795
|
if not content_previews:
|
|
790
796
|
return "Content preview not available"
|
|
791
|
-
return content_previews[0].preview_content
|
|
797
|
+
return Text(content_previews[0].preview_content)
|
|
792
798
|
|
|
793
|
-
def _get_output_content(self, app: "ShrinkRayApp") -> str:
|
|
799
|
+
def _get_output_content(self, app: "ShrinkRayApp") -> str | Text:
|
|
794
800
|
"""Get output content from the output preview widget."""
|
|
795
801
|
output_previews = list(app.query("#output-preview").results(OutputPreview))
|
|
796
802
|
if not output_previews:
|
|
@@ -811,21 +817,29 @@ class ExpandedBoxModal(ModalScreen[None]):
|
|
|
811
817
|
)
|
|
812
818
|
has_seen_output = output_preview._has_seen_output
|
|
813
819
|
|
|
814
|
-
# Build header
|
|
820
|
+
# Build header with styling
|
|
815
821
|
if test_id is not None and return_code is None:
|
|
816
|
-
|
|
822
|
+
header_text = f"Test #{test_id} running..."
|
|
823
|
+
header_style = "green"
|
|
817
824
|
elif test_id is not None:
|
|
818
|
-
|
|
825
|
+
header_text = f"Test #{test_id} exited with code {return_code}"
|
|
826
|
+
header_style = "dim"
|
|
819
827
|
else:
|
|
820
|
-
|
|
828
|
+
header_text = ""
|
|
829
|
+
header_style = ""
|
|
821
830
|
|
|
822
831
|
if raw_content:
|
|
823
|
-
|
|
832
|
+
result = Text()
|
|
833
|
+
if header_text:
|
|
834
|
+
result.append(header_text, style=header_style)
|
|
835
|
+
result.append("\n\n")
|
|
836
|
+
result.append(raw_content)
|
|
837
|
+
return result
|
|
824
838
|
elif has_seen_output or test_id is not None:
|
|
825
839
|
# We've seen output before - show header only (no "No test output" message)
|
|
826
|
-
return
|
|
840
|
+
return Text(header_text, style=header_style)
|
|
827
841
|
else:
|
|
828
|
-
return "
|
|
842
|
+
return Text("No test output yet...", style="dim")
|
|
829
843
|
|
|
830
844
|
def on_mount(self) -> None:
|
|
831
845
|
"""Populate content from the source widget."""
|
|
@@ -1447,13 +1461,13 @@ class HistoryExplorerModal(ModalScreen[None]):
|
|
|
1447
1461
|
if os.path.isfile(output_path):
|
|
1448
1462
|
output_content = self._read_file(output_path)
|
|
1449
1463
|
else:
|
|
1450
|
-
output_content = "
|
|
1464
|
+
output_content = Text("No output captured", style="dim")
|
|
1451
1465
|
self.query_one(f"#{output_preview_id}", Static).update(output_content)
|
|
1452
1466
|
|
|
1453
|
-
def _read_file(self, file_path: str) ->
|
|
1467
|
+
def _read_file(self, file_path: str) -> Text:
|
|
1454
1468
|
"""Read file content, decoding as text if possible."""
|
|
1455
1469
|
if not os.path.isfile(file_path):
|
|
1456
|
-
return "
|
|
1470
|
+
return Text("File not found", style="dim")
|
|
1457
1471
|
try:
|
|
1458
1472
|
with open(file_path, "rb") as f:
|
|
1459
1473
|
raw_content = f.read()
|
|
@@ -1465,18 +1479,17 @@ class HistoryExplorerModal(ModalScreen[None]):
|
|
|
1465
1479
|
# Try to decode as text
|
|
1466
1480
|
encoding, text = try_decode(raw_content)
|
|
1467
1481
|
if encoding is not None:
|
|
1468
|
-
|
|
1469
|
-
text = escape_markup(text)
|
|
1482
|
+
result = Text(text)
|
|
1470
1483
|
if truncated:
|
|
1471
|
-
|
|
1472
|
-
return
|
|
1484
|
+
result.append("\n\n... (truncated)", style="dim")
|
|
1485
|
+
return result
|
|
1473
1486
|
# Binary content - hex display
|
|
1474
|
-
|
|
1487
|
+
result = Text("Binary content - hex display\n\n" + raw_content.hex())
|
|
1475
1488
|
if truncated:
|
|
1476
|
-
|
|
1477
|
-
return
|
|
1489
|
+
result.append("\n\n... (truncated)", style="dim")
|
|
1490
|
+
return result
|
|
1478
1491
|
except OSError:
|
|
1479
|
-
return "
|
|
1492
|
+
return Text("Error reading file", style="red")
|
|
1480
1493
|
|
|
1481
1494
|
def action_restart_from_here(self) -> None:
|
|
1482
1495
|
"""Restart reduction from the currently selected history point."""
|
|
@@ -9,6 +9,7 @@ from collections.abc import AsyncGenerator
|
|
|
9
9
|
from unittest.mock import AsyncMock, MagicMock, Mock, PropertyMock, patch
|
|
10
10
|
|
|
11
11
|
import pytest
|
|
12
|
+
from rich.text import Text
|
|
12
13
|
from textual.app import App
|
|
13
14
|
from textual.widgets import DataTable, Label, ListView, Static, TabbedContent
|
|
14
15
|
|
|
@@ -715,6 +716,40 @@ def test_output_preview_get_available_lines_app_zero_height():
|
|
|
715
716
|
assert lines == 30
|
|
716
717
|
|
|
717
718
|
|
|
719
|
+
def test_output_preview_render_returns_text():
|
|
720
|
+
"""Test that OutputPreview.render() returns a Text object, not a markup string.
|
|
721
|
+
|
|
722
|
+
Returning a Text object ensures Textual never parses user content as markup.
|
|
723
|
+
Previously, render() returned a markup string and used Rich's escape() which
|
|
724
|
+
failed on patterns like [key=; causing MarkupError.
|
|
725
|
+
|
|
726
|
+
This fixes the crash from: shrinkray --parallelism 60 interest2.sh bf_simple_yk.c
|
|
727
|
+
"""
|
|
728
|
+
widget = OutputPreview()
|
|
729
|
+
# Content that would crash if parsed as markup
|
|
730
|
+
widget.output_content = "some output\n[key=;value"
|
|
731
|
+
widget.active_test_id = 1
|
|
732
|
+
|
|
733
|
+
rendered = widget.render()
|
|
734
|
+
assert isinstance(rendered, Text)
|
|
735
|
+
assert "[key=;value" in rendered
|
|
736
|
+
|
|
737
|
+
|
|
738
|
+
def test_content_preview_render_returns_text():
|
|
739
|
+
"""Test that ContentPreview.render() returns a Text object, not a markup string.
|
|
740
|
+
|
|
741
|
+
Returning a Text object ensures Textual never parses user content as markup.
|
|
742
|
+
Previously, render() returned raw strings that would crash on patterns like
|
|
743
|
+
[Key=; when Textual parsed them as markup.
|
|
744
|
+
"""
|
|
745
|
+
widget = ContentPreview()
|
|
746
|
+
widget.update_content("[Key=;value", False)
|
|
747
|
+
|
|
748
|
+
rendered = widget.render()
|
|
749
|
+
assert isinstance(rendered, Text)
|
|
750
|
+
assert "[Key=;value" in rendered
|
|
751
|
+
|
|
752
|
+
|
|
718
753
|
# === ShrinkRayApp with fake client tests ===
|
|
719
754
|
|
|
720
755
|
|
|
@@ -1303,21 +1338,22 @@ def test_expanded_modal_read_file_success(tmp_path):
|
|
|
1303
1338
|
|
|
1304
1339
|
modal = ExpandedBoxModal("Test", "content-container")
|
|
1305
1340
|
content = modal._read_file(str(test_file))
|
|
1306
|
-
assert content
|
|
1341
|
+
assert isinstance(content, Text)
|
|
1342
|
+
assert content.plain == "Hello World"
|
|
1307
1343
|
|
|
1308
1344
|
|
|
1309
|
-
def
|
|
1310
|
-
"""Test _read_file
|
|
1345
|
+
def test_expanded_modal_read_file_preserves_brackets(tmp_path):
|
|
1346
|
+
"""Test _read_file preserves bracket characters in file content."""
|
|
1311
1347
|
test_file = tmp_path / "test.txt"
|
|
1312
1348
|
# Content with brackets that could be interpreted as Rich markup
|
|
1313
1349
|
test_file.write_text("expected [bold] and [red]text[/red]")
|
|
1314
1350
|
|
|
1315
1351
|
modal = ExpandedBoxModal("Test", "content-container")
|
|
1316
1352
|
content = modal._read_file(str(test_file))
|
|
1317
|
-
#
|
|
1318
|
-
|
|
1319
|
-
assert "[bold]"
|
|
1320
|
-
assert "[red]
|
|
1353
|
+
# Returns a Text object so brackets are preserved literally
|
|
1354
|
+
assert isinstance(content, Text)
|
|
1355
|
+
assert "[bold]" in content.plain
|
|
1356
|
+
assert "[red]text[/red]" in content.plain
|
|
1321
1357
|
|
|
1322
1358
|
|
|
1323
1359
|
def test_expanded_modal_read_file_binary(tmp_path):
|
|
@@ -1328,26 +1364,29 @@ def test_expanded_modal_read_file_binary(tmp_path):
|
|
|
1328
1364
|
|
|
1329
1365
|
modal = ExpandedBoxModal("Test", "content-container")
|
|
1330
1366
|
content = modal._read_file(str(test_file))
|
|
1367
|
+
assert isinstance(content, Text)
|
|
1331
1368
|
assert "Binary content" in content
|
|
1332
1369
|
assert "80818283" in content
|
|
1333
1370
|
|
|
1334
1371
|
|
|
1335
1372
|
def test_expanded_modal_read_file_missing(tmp_path):
|
|
1336
|
-
"""Test _read_file returns
|
|
1373
|
+
"""Test _read_file returns styled message for missing file."""
|
|
1337
1374
|
modal = ExpandedBoxModal("Test", "content-container")
|
|
1338
1375
|
result = modal._read_file(str(tmp_path / "nonexistent.txt"))
|
|
1339
|
-
assert
|
|
1376
|
+
assert isinstance(result, Text)
|
|
1377
|
+
assert "File not found" in result
|
|
1340
1378
|
|
|
1341
1379
|
|
|
1342
1380
|
def test_expanded_modal_read_file_oserror(tmp_path):
|
|
1343
|
-
"""Test _read_file returns
|
|
1381
|
+
"""Test _read_file returns styled message on OSError."""
|
|
1344
1382
|
modal = ExpandedBoxModal("Test", "content-container")
|
|
1345
1383
|
# Create a file that exists but can't be read
|
|
1346
1384
|
test_file = tmp_path / "unreadable.txt"
|
|
1347
1385
|
test_file.write_text("content")
|
|
1348
1386
|
with patch("builtins.open", side_effect=OSError("Permission denied")):
|
|
1349
1387
|
result = modal._read_file(str(test_file))
|
|
1350
|
-
assert
|
|
1388
|
+
assert isinstance(result, Text)
|
|
1389
|
+
assert "Error reading file" in result
|
|
1351
1390
|
|
|
1352
1391
|
|
|
1353
1392
|
# === ExpandedBoxModal integration tests ===
|
|
@@ -5367,6 +5406,55 @@ def test_expanded_modal_output_with_empty_content_and_header():
|
|
|
5367
5406
|
asyncio.run(run())
|
|
5368
5407
|
|
|
5369
5408
|
|
|
5409
|
+
def test_expanded_modal_output_content_without_test_id():
|
|
5410
|
+
"""Test output modal with content but no test ID (no header)."""
|
|
5411
|
+
|
|
5412
|
+
async def run():
|
|
5413
|
+
fake_client = FakeReductionClient(updates=[], wait_indefinitely=True)
|
|
5414
|
+
|
|
5415
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".txt", delete=False) as f:
|
|
5416
|
+
f.write("test")
|
|
5417
|
+
temp_file = f.name
|
|
5418
|
+
|
|
5419
|
+
try:
|
|
5420
|
+
app = ShrinkRayApp(
|
|
5421
|
+
file_path=temp_file,
|
|
5422
|
+
test=["true"],
|
|
5423
|
+
exit_on_completion=False,
|
|
5424
|
+
client=fake_client,
|
|
5425
|
+
)
|
|
5426
|
+
|
|
5427
|
+
async with app.run_test() as pilot:
|
|
5428
|
+
await pilot.pause()
|
|
5429
|
+
|
|
5430
|
+
# Set output preview with content but no test ID
|
|
5431
|
+
output_preview = app.query_one("#output-preview", OutputPreview)
|
|
5432
|
+
output_preview.active_test_id = None
|
|
5433
|
+
output_preview._has_seen_output = True
|
|
5434
|
+
output_preview.output_content = "some raw output"
|
|
5435
|
+
output_preview._pending_content = "some raw output"
|
|
5436
|
+
output_preview._pending_test_id = None
|
|
5437
|
+
|
|
5438
|
+
# Create modal for output-container
|
|
5439
|
+
modal = ExpandedBoxModal("Output", "output-container", None)
|
|
5440
|
+
await app.push_screen(modal)
|
|
5441
|
+
await pilot.pause()
|
|
5442
|
+
|
|
5443
|
+
# Should show content without header
|
|
5444
|
+
content = modal.query_one("#expanded-content", Static)
|
|
5445
|
+
content_str = get_static_content(content)
|
|
5446
|
+
assert "some raw output" in content_str
|
|
5447
|
+
assert "Test #" not in content_str
|
|
5448
|
+
|
|
5449
|
+
await pilot.press("escape")
|
|
5450
|
+
await pilot.press("q")
|
|
5451
|
+
finally:
|
|
5452
|
+
if os.path.exists(temp_file):
|
|
5453
|
+
os.unlink(temp_file)
|
|
5454
|
+
|
|
5455
|
+
asyncio.run(run())
|
|
5456
|
+
|
|
5457
|
+
|
|
5370
5458
|
# === Action method tests ===
|
|
5371
5459
|
|
|
5372
5460
|
|
|
@@ -6125,11 +6213,12 @@ def test_history_modal_read_file_success(tmp_path):
|
|
|
6125
6213
|
modal = HistoryExplorerModal(str(tmp_path), "test.txt")
|
|
6126
6214
|
content = modal._read_file(str(test_file))
|
|
6127
6215
|
|
|
6128
|
-
assert content
|
|
6216
|
+
assert isinstance(content, Text)
|
|
6217
|
+
assert content.plain == "Hello World\nLine 2"
|
|
6129
6218
|
|
|
6130
6219
|
|
|
6131
|
-
def
|
|
6132
|
-
"""Test _read_file
|
|
6220
|
+
def test_history_modal_read_file_preserves_brackets(tmp_path):
|
|
6221
|
+
"""Test _read_file preserves bracket characters in file content."""
|
|
6133
6222
|
test_file = tmp_path / "test.txt"
|
|
6134
6223
|
# Content with brackets that look like Rich markup tags
|
|
6135
6224
|
test_file.write_text("error: [bold]text[/bold] failed")
|
|
@@ -6137,9 +6226,9 @@ def test_history_modal_read_file_escapes_markup(tmp_path):
|
|
|
6137
6226
|
modal = HistoryExplorerModal(str(tmp_path), "test.txt")
|
|
6138
6227
|
content = modal._read_file(str(test_file))
|
|
6139
6228
|
|
|
6140
|
-
#
|
|
6141
|
-
assert
|
|
6142
|
-
|
|
6229
|
+
# Returns a Text object so brackets are preserved literally
|
|
6230
|
+
assert isinstance(content, Text)
|
|
6231
|
+
assert "[bold]" in content.plain
|
|
6143
6232
|
assert "error:" in content
|
|
6144
6233
|
assert "failed" in content
|
|
6145
6234
|
|
|
@@ -6153,15 +6242,17 @@ def test_history_modal_read_file_binary(tmp_path):
|
|
|
6153
6242
|
content = modal._read_file(str(test_file))
|
|
6154
6243
|
|
|
6155
6244
|
# Should fall back to hex display
|
|
6245
|
+
assert isinstance(content, Text)
|
|
6156
6246
|
assert "Binary content" in content
|
|
6157
6247
|
|
|
6158
6248
|
|
|
6159
6249
|
def test_history_modal_read_file_missing(tmp_path):
|
|
6160
|
-
"""Test _read_file returns
|
|
6250
|
+
"""Test _read_file returns styled message for missing file."""
|
|
6161
6251
|
modal = HistoryExplorerModal(str(tmp_path), "test.txt")
|
|
6162
6252
|
content = modal._read_file(str(tmp_path / "nonexistent.txt"))
|
|
6163
6253
|
|
|
6164
|
-
assert
|
|
6254
|
+
assert isinstance(content, Text)
|
|
6255
|
+
assert "File not found" in content
|
|
6165
6256
|
|
|
6166
6257
|
|
|
6167
6258
|
def test_history_modal_read_file_truncated_text(tmp_path):
|
|
@@ -6174,9 +6265,10 @@ def test_history_modal_read_file_truncated_text(tmp_path):
|
|
|
6174
6265
|
content = modal._read_file(str(test_file))
|
|
6175
6266
|
|
|
6176
6267
|
# Should be truncated
|
|
6177
|
-
assert
|
|
6178
|
-
|
|
6179
|
-
|
|
6268
|
+
assert isinstance(content, Text)
|
|
6269
|
+
assert "truncated" in content
|
|
6270
|
+
# Content should be limited (50000 chars + truncation message)
|
|
6271
|
+
assert len(content.plain) < 60000
|
|
6180
6272
|
|
|
6181
6273
|
|
|
6182
6274
|
def test_history_modal_read_file_truncated_binary(tmp_path):
|
|
@@ -6189,8 +6281,9 @@ def test_history_modal_read_file_truncated_binary(tmp_path):
|
|
|
6189
6281
|
content = modal._read_file(str(test_file))
|
|
6190
6282
|
|
|
6191
6283
|
# Should be truncated and shown as binary
|
|
6192
|
-
assert
|
|
6193
|
-
assert "
|
|
6284
|
+
assert isinstance(content, Text)
|
|
6285
|
+
assert "Binary content" in content
|
|
6286
|
+
assert "truncated" in content
|
|
6194
6287
|
|
|
6195
6288
|
|
|
6196
6289
|
def test_history_modal_read_file_oserror(tmp_path):
|
|
@@ -6204,7 +6297,8 @@ def test_history_modal_read_file_oserror(tmp_path):
|
|
|
6204
6297
|
with patch("builtins.open", side_effect=OSError("Permission denied")):
|
|
6205
6298
|
content = modal._read_file(str(test_file))
|
|
6206
6299
|
|
|
6207
|
-
assert
|
|
6300
|
+
assert isinstance(content, Text)
|
|
6301
|
+
assert "Error reading file" in content
|
|
6208
6302
|
|
|
6209
6303
|
|
|
6210
6304
|
def test_history_modal_on_list_view_highlighted_no_entries(tmp_path):
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|