bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
343 lines
9.3 KiB
Python
343 lines
9.3 KiB
Python
import pytest
|
|
|
|
from mirage.commands.builtin.generic.wc import (WCCounts, format_multi,
|
|
format_wc, wc, wc_lines)
|
|
from mirage.types import PathSpec
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_default_counts_bytes():
|
|
counts = await wc(b"hello world\nfoo bar\n")
|
|
assert counts.lines == 2
|
|
assert counts.words == 4
|
|
assert counts.bytes_ == 20
|
|
assert counts.chars == 20
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_empty_input():
|
|
counts = await wc(b"")
|
|
assert counts == WCCounts(lines=0,
|
|
words=0,
|
|
bytes_=0,
|
|
chars=0,
|
|
max_line_length=0)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_lines_with_trailing_newline():
|
|
counts = await wc(b"a\nb\nc\n")
|
|
assert counts.lines == 3
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_lines_without_trailing_newline():
|
|
"""POSIX: lines = number of \\n bytes. `a\\nb\\nc` has 2 newlines."""
|
|
counts = await wc(b"a\nb\nc")
|
|
assert counts.lines == 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_words_single_line():
|
|
counts = await wc(b"one two three")
|
|
assert counts.words == 3
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_words_multiline():
|
|
counts = await wc(b"one two\nthree four five\nsix\n")
|
|
assert counts.words == 6
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_words_leading_trailing_whitespace():
|
|
"""POSIX: leading/trailing whitespace doesn't add words."""
|
|
counts = await wc(b" hello world ")
|
|
assert counts.words == 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_words_only_whitespace():
|
|
counts = await wc(b" \t \n ")
|
|
assert counts.words == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_bytes_ascii():
|
|
counts = await wc(b"hello")
|
|
assert counts.bytes_ == 5
|
|
assert counts.chars == 5
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_chars_vs_bytes_multibyte_utf8():
|
|
"""`café` is 4 chars / 5 bytes (é is 2 bytes in UTF-8)."""
|
|
data = "café".encode()
|
|
counts = await wc(data)
|
|
assert counts.bytes_ == 5
|
|
assert counts.chars == 4
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_chars_vs_bytes_pure_multibyte():
|
|
data = "ééé".encode()
|
|
counts = await wc(data)
|
|
assert counts.bytes_ == 6
|
|
assert counts.chars == 3
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_max_line_length_empty():
|
|
counts = await wc(b"")
|
|
assert counts.max_line_length == 0
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_max_line_length_single_line_with_newline():
|
|
counts = await wc(b"hello\n")
|
|
assert counts.max_line_length == 5
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_max_line_length_picks_longest():
|
|
counts = await wc(b"short\na much longer line\nmed\n")
|
|
assert counts.max_line_length == len(b"a much longer line")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_max_line_length_no_trailing_newline():
|
|
counts = await wc(b"hello world")
|
|
assert counts.max_line_length == 11
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_streams_chunked():
|
|
"""Streaming through chunk boundaries gives same counts as buffered."""
|
|
|
|
async def src():
|
|
yield b"hello "
|
|
yield b"world\n"
|
|
yield b"foo bar\n"
|
|
|
|
counts = await wc(src())
|
|
assert counts.lines == 2
|
|
assert counts.words == 4
|
|
assert counts.bytes_ == 20
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_word_split_across_chunks():
|
|
"""A word straddling a chunk boundary still counts as one word."""
|
|
|
|
async def src():
|
|
yield b"hel"
|
|
yield b"lo"
|
|
yield b" world\n"
|
|
|
|
counts = await wc(src())
|
|
assert counts.words == 2
|
|
assert counts.lines == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_utf8_split_across_chunks_does_not_lose_chars():
|
|
"""A multibyte UTF-8 sequence split across chunks must still decode."""
|
|
|
|
async def src():
|
|
# é = b"\xc3\xa9" — split between the two bytes
|
|
yield b"caf\xc3"
|
|
yield b"\xa9"
|
|
|
|
counts = await wc(src())
|
|
assert counts.bytes_ == 5
|
|
assert counts.chars == 4
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_byte_by_byte_chunking():
|
|
"""Worst-case chunking: every byte its own chunk."""
|
|
|
|
async def src():
|
|
for byte in b"hello world\nfoo bar\n":
|
|
yield bytes([byte])
|
|
|
|
counts = await wc(src())
|
|
assert counts.lines == 2
|
|
assert counts.words == 4
|
|
assert counts.bytes_ == 20
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_binary_input_does_not_crash():
|
|
"""`errors='replace'` must let arbitrary bytes pass — count bytes
|
|
accurately even when UTF-8 decoding produces replacement chars."""
|
|
data = bytes(range(256))
|
|
counts = await wc(data)
|
|
assert counts.bytes_ == 256
|
|
assert counts.lines == 1 # one \n at byte 0x0a
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_lines_fast_path_matches_full():
|
|
"""`wc_lines` fast path must agree with the full counter."""
|
|
data = b"alpha\nbeta\ngamma\ndelta\n"
|
|
fast = await wc_lines(data)
|
|
full = await wc(data)
|
|
assert fast == full.lines == 4
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_lines_fast_path_no_trailing_newline():
|
|
assert await wc_lines(b"a\nb\nc") == 2
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_wc_lines_fast_path_empty():
|
|
assert await wc_lines(b"") == 0
|
|
|
|
|
|
def test_format_wc_default_no_label():
|
|
counts = WCCounts(lines=2, words=4, bytes_=20)
|
|
assert format_wc(counts) == " 2 4 20"
|
|
|
|
|
|
def test_format_wc_default_with_label():
|
|
counts = WCCounts(lines=2, words=4, bytes_=20)
|
|
assert format_wc(counts, label="/f.txt") == " 2 4 20 /f.txt"
|
|
|
|
|
|
def test_format_wc_args_l():
|
|
counts = WCCounts(lines=2, words=4, bytes_=20)
|
|
assert format_wc(counts, args_l=True) == "2"
|
|
assert format_wc(counts, args_l=True, label="/f.txt") == "2 /f.txt"
|
|
|
|
|
|
def test_format_wc_w_c_m():
|
|
counts = WCCounts(lines=2, words=4, bytes_=20, chars=18)
|
|
assert format_wc(counts, w=True) == "4"
|
|
assert format_wc(counts, c=True) == "20"
|
|
assert format_wc(counts, m=True) == "18"
|
|
|
|
|
|
def test_format_wc_L_wins_when_both_set():
|
|
"""L has highest precedence (matching wrapper behavior)."""
|
|
counts = WCCounts(lines=2, max_line_length=11)
|
|
assert format_wc(counts, args_l=True, L=True) == "11"
|
|
|
|
|
|
def test_format_wc_precedence_l_over_w_c_m():
|
|
"""args_l > w > c > m precedence."""
|
|
counts = WCCounts(lines=2, words=4, bytes_=20, chars=18)
|
|
assert format_wc(counts, args_l=True, w=True) == "2"
|
|
|
|
|
|
def test_wc_counts_merge():
|
|
a = WCCounts(lines=2, words=4, bytes_=20, chars=18, max_line_length=11)
|
|
b = WCCounts(lines=1, words=2, bytes_=8, chars=8, max_line_length=20)
|
|
a.merge(b)
|
|
assert a.lines == 3
|
|
assert a.words == 6
|
|
assert a.bytes_ == 28
|
|
assert a.chars == 26
|
|
assert a.max_line_length == 20
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_format_multi_single_path_emits_trailing_newline():
|
|
paths = [PathSpec.from_str_path("/a.txt")]
|
|
|
|
async def fake_read(_accessor, _path):
|
|
return b"hello\n"
|
|
|
|
out, err = await format_multi(paths, read=fake_read, args_l=True)
|
|
assert out == b"1 /a.txt\n"
|
|
assert err == b""
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_format_multi_multi_path_emits_total_and_trailing_newline():
|
|
paths = [
|
|
PathSpec.from_str_path("/a.txt"),
|
|
PathSpec.from_str_path("/b.txt"),
|
|
]
|
|
data = {"/a.txt": b"hello\n", "/b.txt": b"world\nworld\n"}
|
|
|
|
async def fake_read(_accessor, path):
|
|
return data[path.virtual]
|
|
|
|
out, err = await format_multi(paths, read=fake_read, args_l=True)
|
|
assert err == b""
|
|
assert out.endswith(b"\n")
|
|
lines = out.decode().rstrip("\n").split("\n")
|
|
assert lines == ["1 /a.txt", "2 /b.txt", "3 total"]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_format_multi_accepts_sync_read_returning_bytes():
|
|
paths = [PathSpec.from_str_path("/a.txt")]
|
|
|
|
def sync_read(_accessor, _path):
|
|
return b"x\n"
|
|
|
|
out, err = await format_multi(paths, read=sync_read, args_l=True)
|
|
assert out == b"1 /a.txt\n"
|
|
assert err == b""
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_format_multi_empty_paths_returns_empty():
|
|
|
|
async def fake_read(_accessor, _path):
|
|
return b""
|
|
|
|
out, err = await format_multi([], read=fake_read, args_l=True)
|
|
assert out == b""
|
|
assert err == b""
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_format_multi_missing_operand_reports_and_totals():
|
|
paths = [
|
|
PathSpec.from_str_path("/a.txt"),
|
|
PathSpec.from_str_path("/m.txt"),
|
|
]
|
|
|
|
async def fake_read(_accessor, path):
|
|
if path.virtual == "/m.txt":
|
|
raise FileNotFoundError(path.virtual)
|
|
return b"hello\n"
|
|
|
|
out, err = await format_multi(paths, read=fake_read, args_l=True)
|
|
assert out == b"1 /a.txt\n1 total\n"
|
|
assert err == b"wc: /m.txt: No such file or directory\n"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_format_multi_all_missing_zero_total():
|
|
paths = [
|
|
PathSpec.from_str_path("/m1.txt"),
|
|
PathSpec.from_str_path("/m2.txt"),
|
|
]
|
|
|
|
async def fake_read(_accessor, path):
|
|
raise FileNotFoundError(path.virtual)
|
|
|
|
out, err = await format_multi(paths, read=fake_read, args_l=True)
|
|
assert out == b"0 total\n"
|
|
assert err == (b"wc: /m1.txt: No such file or directory\n"
|
|
b"wc: /m2.txt: No such file or directory\n")
|
|
|
|
|
|
async def _async_byte_read(_accessor, _path):
|
|
yield b"hello "
|
|
yield b"world\n"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_format_multi_accepts_async_iterator_read():
|
|
paths = [PathSpec.from_str_path("/a.txt")]
|
|
|
|
out, err = await format_multi(paths, read=_async_byte_read, args_l=True)
|
|
assert out == b"1 /a.txt\n"
|
|
assert err == b""
|