Skip to content

Commit eaeffaf

Browse files
committed
added smoke tests and todos for fixing and testing output line counting logic
1 parent 3143036 commit eaeffaf

File tree

4 files changed

+148
-0
lines changed

4 files changed

+148
-0
lines changed

src/runloop_api_client/sdk/async_execution_result.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ def _count_non_empty_lines(self, text: str) -> int:
6060

6161
def _get_last_n_lines(self, text: str, n: int) -> str:
6262
"""Extract the last N lines from text."""
63+
# TODO: Fix inconsistency - _count_non_empty_lines counts non-empty lines but
64+
# _get_last_n_lines returns N lines (may include empty ones). This means
65+
# num_lines=50 might return fewer than 50 non-empty lines. Should either:
66+
# 1. Make _get_last_n_lines return N non-empty lines, OR
67+
# 2. Make _count_non_empty_lines count all lines
68+
# This affects both Python and TypeScript SDKs - fix together.
6369
if n <= 0 or not text:
6470
return ""
6571
# Remove trailing newlines before splitting and slicing

src/runloop_api_client/sdk/execution_result.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ def _count_non_empty_lines(self, text: str) -> int:
6767

6868
def _get_last_n_lines(self, text: str, n: int) -> str:
6969
"""Extract the last N lines from text."""
70+
# TODO: Fix inconsistency - _count_non_empty_lines counts non-empty lines but
71+
# _get_last_n_lines returns N lines (may include empty ones). This means
72+
# num_lines=50 might return fewer than 50 non-empty lines. Should either:
73+
# 1. Make _get_last_n_lines return N non-empty lines, OR
74+
# 2. Make _count_non_empty_lines count all lines
75+
# This affects both Python and TypeScript SDKs - fix together.
7076
if n <= 0 or not text:
7177
return ""
7278
# Remove trailing newlines before splitting and slicing

tests/smoketests/sdk/test_async_devbox.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -614,3 +614,71 @@ async def test_snapshot_disk_async(self, async_sdk_client: AsyncRunloopSDK) -> N
614614
await snapshot.delete()
615615
finally:
616616
await devbox.shutdown()
617+
618+
619+
class TestAsyncDevboxExecutionPagination:
620+
"""Test stdout/stderr pagination and streaming functionality."""
621+
622+
@pytest.mark.timeout(TWO_MINUTE_TIMEOUT)
623+
async def test_exec_with_large_stdout_streaming(self, shared_devbox: AsyncDevbox) -> None:
624+
"""Test that large stdout output is fully captured via streaming when truncated."""
625+
# Generate 1000 lines of output
626+
result = await shared_devbox.cmd.exec(
627+
command='for i in $(seq 1 1000); do echo "Line $i with some content to make it realistic"; done',
628+
)
629+
630+
assert result.exit_code == 0
631+
stdout = await result.stdout()
632+
lines = stdout.strip().split("\n")
633+
634+
# Verify we got all 1000 lines
635+
assert len(lines) == 1000, f"Expected 1000 lines, got {len(lines)}"
636+
637+
# Verify first and last lines
638+
assert "Line 1" in lines[0]
639+
assert "Line 1000" in lines[-1]
640+
641+
@pytest.mark.timeout(TWO_MINUTE_TIMEOUT)
642+
async def test_exec_with_large_stderr_streaming(self, shared_devbox: AsyncDevbox) -> None:
643+
"""Test that large stderr output is fully captured via streaming when truncated."""
644+
# Generate 1000 lines of stderr output
645+
result = await shared_devbox.cmd.exec(
646+
command='for i in $(seq 1 1000); do echo "Error line $i" >&2; done',
647+
)
648+
649+
assert result.exit_code == 0
650+
stderr = await result.stderr()
651+
lines = stderr.strip().split("\n")
652+
653+
# Verify we got all 1000 lines
654+
assert len(lines) == 1000, f"Expected 1000 lines, got {len(lines)}"
655+
656+
# Verify first and last lines
657+
assert "Error line 1" in lines[0]
658+
assert "Error line 1000" in lines[-1]
659+
660+
@pytest.mark.timeout(TWO_MINUTE_TIMEOUT)
661+
async def test_exec_with_truncated_stdout_num_lines(self, shared_devbox: AsyncDevbox) -> None:
662+
"""Test num_lines parameter works correctly with potentially truncated output."""
663+
# Generate 2000 lines of output
664+
result = await shared_devbox.cmd.exec(
665+
command='for i in $(seq 1 2000); do echo "Line $i"; done',
666+
)
667+
668+
assert result.exit_code == 0
669+
670+
# Request last 50 lines
671+
stdout = await result.stdout(num_lines=50)
672+
lines = stdout.strip().split("\n")
673+
674+
# Verify we got exactly 50 lines
675+
assert len(lines) == 50, f"Expected 50 lines, got {len(lines)}"
676+
677+
# Verify these are the last 50 lines
678+
assert "Line 1951" in lines[0]
679+
assert "Line 2000" in lines[-1]
680+
681+
# TODO: Add test_exec_stdout_line_counting test once empty line logic is fixed.
682+
# Currently there's an inconsistency where _count_non_empty_lines counts non-empty
683+
# lines but _get_last_n_lines returns N lines (including empty ones). This affects
684+
# both Python and TypeScript SDKs and needs to be fixed together.

tests/smoketests/sdk/test_devbox.py

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,3 +609,71 @@ def test_snapshot_disk_async(self, sdk_client: RunloopSDK) -> None:
609609
snapshot.delete()
610610
finally:
611611
devbox.shutdown()
612+
613+
614+
class TestDevboxExecutionPagination:
615+
"""Test stdout/stderr pagination and streaming functionality."""
616+
617+
@pytest.mark.timeout(TWO_MINUTE_TIMEOUT)
618+
def test_exec_with_large_stdout_streaming(self, shared_devbox: Devbox) -> None:
619+
"""Test that large stdout output is fully captured via streaming when truncated."""
620+
# Generate 1000 lines of output
621+
result = shared_devbox.cmd.exec(
622+
command='for i in $(seq 1 1000); do echo "Line $i with some content to make it realistic"; done',
623+
)
624+
625+
assert result.exit_code == 0
626+
stdout = result.stdout()
627+
lines = stdout.strip().split("\n")
628+
629+
# Verify we got all 1000 lines
630+
assert len(lines) == 1000, f"Expected 1000 lines, got {len(lines)}"
631+
632+
# Verify first and last lines
633+
assert "Line 1" in lines[0]
634+
assert "Line 1000" in lines[-1]
635+
636+
@pytest.mark.timeout(TWO_MINUTE_TIMEOUT)
637+
def test_exec_with_large_stderr_streaming(self, shared_devbox: Devbox) -> None:
638+
"""Test that large stderr output is fully captured via streaming when truncated."""
639+
# Generate 1000 lines of stderr output
640+
result = shared_devbox.cmd.exec(
641+
command='for i in $(seq 1 1000); do echo "Error line $i" >&2; done',
642+
)
643+
644+
assert result.exit_code == 0
645+
stderr = result.stderr()
646+
lines = stderr.strip().split("\n")
647+
648+
# Verify we got all 1000 lines
649+
assert len(lines) == 1000, f"Expected 1000 lines, got {len(lines)}"
650+
651+
# Verify first and last lines
652+
assert "Error line 1" in lines[0]
653+
assert "Error line 1000" in lines[-1]
654+
655+
@pytest.mark.timeout(TWO_MINUTE_TIMEOUT)
656+
def test_exec_with_truncated_stdout_num_lines(self, shared_devbox: Devbox) -> None:
657+
"""Test num_lines parameter works correctly with potentially truncated output."""
658+
# Generate 2000 lines of output
659+
result = shared_devbox.cmd.exec(
660+
command='for i in $(seq 1 2000); do echo "Line $i"; done',
661+
)
662+
663+
assert result.exit_code == 0
664+
665+
# Request last 50 lines
666+
stdout = result.stdout(num_lines=50)
667+
lines = stdout.strip().split("\n")
668+
669+
# Verify we got exactly 50 lines
670+
assert len(lines) == 50, f"Expected 50 lines, got {len(lines)}"
671+
672+
# Verify these are the last 50 lines
673+
assert "Line 1951" in lines[0]
674+
assert "Line 2000" in lines[-1]
675+
676+
# TODO: Add test_exec_stdout_line_counting test once empty line logic is fixed.
677+
# Currently there's an inconsistency where _count_non_empty_lines counts non-empty
678+
# lines but _get_last_n_lines returns N lines (including empty ones). This affects
679+
# both Python and TypeScript SDKs and needs to be fixed together.

0 commit comments

Comments
 (0)