Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:30:44 +08:00

433 lines
14 KiB
Python

import asyncio
from dataclasses import asdict
from datetime import datetime, timezone
from unittest.mock import AsyncMock
import pytest
from mirage.commands.builtin.find_eval import Name, Not, Or
from mirage.commands.builtin.generic.find import (FindArgs, apply_mount_prefix,
apply_mtime_filter,
parse_find_args, walk_find)
from mirage.commands.errors import FindParseError
from mirage.resource.ram import RAMResource
from mirage.types import FileStat, FileType, FindType, MountMode, PathSpec
from mirage.workspace import Workspace
def _defaults() -> dict:
return asdict(FindArgs())
def test_parse_find_args_empty_returns_defaults():
args = parse_find_args(())
assert asdict(args) == _defaults()
def test_parse_find_args_name_passthrough():
args = parse_find_args((), name="*.txt")
assert args.name == "*.txt"
assert args.or_names is None
def test_parse_find_args_iname_and_path():
args = parse_find_args((), iname="HELLO.*", path="**/sub/*")
assert args.iname == "HELLO.*"
assert args.path_pattern == "**/sub/*"
def test_parse_find_args_maxdepth_mindepth_str_to_int():
args = parse_find_args((), maxdepth="3", mindepth="1")
assert args.maxdepth == 3
assert args.mindepth == 1
def test_parse_find_args_size_plus_lower_bound():
args = parse_find_args((), size="+500c")
assert args.min_size == 501
assert args.max_size is None
def test_parse_find_args_size_minus_upper_bound():
args = parse_find_args((), size="-1k")
assert args.min_size is None
assert args.max_size == 0
def test_parse_find_args_size_exact():
args = parse_find_args((), size="1k")
assert args.min_size == 1
assert args.max_size == 1024
def test_parse_find_args_mtime_minus_recent():
"""`-mtime -1` means modified within last 1 day."""
args = parse_find_args((), mtime="-1")
assert args.mtime_min is not None
assert args.mtime_max is None
def test_parse_find_args_mtime_plus_old():
args = parse_find_args((), mtime="+7")
assert args.mtime_min is None
assert args.mtime_max is not None
def test_parse_find_args_type_canonicalized_to_findtype_enum():
"""Known POSIX `-type` values become FindType members."""
assert parse_find_args((), type="d").type is FindType.DIRECTORY
assert parse_find_args((), type="f").type is FindType.FILE
def test_parse_find_args_unknown_type_left_as_string():
"""Non-POSIX types pass through verbatim (allows custom backend types)."""
assert parse_find_args((), type="symlink").type == "symlink"
def test_parse_find_args_unknown_predicate_raises():
with pytest.raises(FindParseError,
match="find: unknown predicate '-bogus'"):
parse_find_args(("-bogus", ))
def test_parse_find_args_negation_builds_not_tree():
args = parse_find_args(("-not", "-name", "*.pyc"))
assert args.tree == Not(Name("*.pyc"))
def test_parse_find_args_or_builds_or_tree():
args = parse_find_args(("-name", "*.txt", "-o", "-name", "*.py"))
assert args.tree == Or([Name("*.txt"), Name("*.py")])
def test_parse_find_args_or_names_none_when_only_one_name():
"""If only `name` is set with no `-or -name` clauses, or_names is None."""
args = parse_find_args((), name="*.txt")
assert args.or_names is None
@pytest.mark.asyncio
async def test_apply_mtime_filter_skips_when_no_window():
out = await apply_mtime_filter(
["/a.txt"],
mtime_min=None,
mtime_max=None,
stat=_unreached_stat,
)
assert out == ["/a.txt"]
@pytest.mark.asyncio
async def test_apply_mtime_filter_keeps_within_window():
now = datetime.now(tz=timezone.utc)
iso = now.isoformat()
async def stat(_spec: PathSpec) -> FileStat:
return FileStat(name="a.txt", size=1, modified=iso, type=FileType.TEXT)
out = await apply_mtime_filter(
["/a.txt"],
mtime_min=now.timestamp() - 60,
mtime_max=now.timestamp() + 60,
stat=stat,
)
assert out == ["/a.txt"]
@pytest.mark.asyncio
async def test_apply_mtime_filter_drops_outside_window():
old = datetime(2020, 1, 1, tzinfo=timezone.utc)
async def stat(_spec: PathSpec) -> FileStat:
return FileStat(name="a.txt",
size=1,
modified=old.isoformat(),
type=FileType.TEXT)
out = await apply_mtime_filter(
["/a.txt"],
mtime_min=datetime(2025, 1, 1, tzinfo=timezone.utc).timestamp(),
mtime_max=None,
stat=stat,
)
assert out == []
@pytest.mark.asyncio
async def test_apply_mtime_filter_drops_entries_with_no_modified_time():
async def stat(_spec: PathSpec) -> FileStat:
return FileStat(name="a.txt",
size=1,
modified=None,
type=FileType.TEXT)
out = await apply_mtime_filter(
["/a.txt"],
mtime_min=1.0,
mtime_max=None,
stat=stat,
)
assert out == []
@pytest.mark.asyncio
async def test_apply_mtime_filter_silently_skips_stat_errors():
async def stat(_spec: PathSpec) -> FileStat:
raise FileNotFoundError("gone")
out = await apply_mtime_filter(
["/a.txt", "/b.txt"],
mtime_min=1.0,
mtime_max=None,
stat=stat,
)
assert out == []
def test_apply_mount_prefix_noop_when_empty():
assert apply_mount_prefix(["/a.txt"], "") == ["/a.txt"]
def test_apply_mount_prefix_prepends():
assert apply_mount_prefix(["/a.txt", "/dir/b.txt"],
"/mnt") == ["/mnt/a.txt", "/mnt/dir/b.txt"]
def test_apply_mount_prefix_strips_leading_slash_from_entries():
assert apply_mount_prefix(["a.txt"], "/mnt") == ["/mnt/a.txt"]
async def _unreached_stat(_spec: PathSpec) -> FileStat:
raise AssertionError("stat should not be called when no mtime window set")
def _root_spec() -> PathSpec:
return PathSpec(resource_path="",
virtual="/",
directory="/",
resolved=False)
@pytest.mark.asyncio
async def test_walk_find_tolerates_not_found_readdir():
readdir = AsyncMock(side_effect=FileNotFoundError("/"))
stat = AsyncMock(side_effect=FileNotFoundError("/"))
results = await walk_find(_root_spec(),
readdir=readdir,
stat=stat,
is_dir_name=lambda c: None,
index=None,
args=FindArgs())
assert results == []
@pytest.mark.asyncio
async def test_walk_find_emits_start_path_at_depth_zero():
readdir = AsyncMock(return_value=["/child.txt"])
stat = AsyncMock(return_value=FileStat(name="/", type=FileType.DIRECTORY))
results = await walk_find(_root_spec(),
readdir=readdir,
stat=stat,
is_dir_name=lambda c: False,
index=None,
args=FindArgs(maxdepth=0))
assert results == ["/"]
readdir.assert_not_awaited()
@pytest.mark.asyncio
async def test_walk_find_propagates_non_not_found_readdir_errors():
readdir = AsyncMock(side_effect=ValueError("bad page token"))
stat = AsyncMock()
with pytest.raises(ValueError, match="bad page token"):
await walk_find(_root_spec(),
readdir=readdir,
stat=stat,
is_dir_name=lambda c: None,
index=None,
args=FindArgs())
@pytest.mark.asyncio
async def test_walk_find_stat_fallback_treats_not_found_as_file():
readdir = AsyncMock(return_value=["/mystery"])
stat = AsyncMock(side_effect=FileNotFoundError("/mystery"))
results = await walk_find(_root_spec(),
readdir=readdir,
stat=stat,
is_dir_name=lambda c: None,
index=None,
args=FindArgs(type=FindType.FILE))
assert results == ["/mystery"]
@pytest.mark.asyncio
async def test_walk_find_empty_matches_empty_files_and_dirs():
async def readdir(spec: PathSpec, _index):
table = {
"/": ["/empty.txt", "/full.txt", "/empty-dir", "/full-dir"],
"/empty-dir": [],
"/full-dir": ["/full-dir/a.txt"],
}
return table[spec.virtual]
async def stat(spec: PathSpec, _index):
stats = {
"/": FileStat(name="/", type=FileType.DIRECTORY),
"/empty.txt": FileStat(name="empty.txt",
size=0,
type=FileType.TEXT),
"/full.txt": FileStat(name="full.txt", size=1, type=FileType.TEXT),
"/empty-dir": FileStat(name="empty-dir", type=FileType.DIRECTORY),
"/full-dir": FileStat(name="full-dir", type=FileType.DIRECTORY),
"/full-dir/a.txt": FileStat(name="a.txt",
size=1,
type=FileType.TEXT),
}
return stats[spec.virtual]
results = await walk_find(_root_spec(),
readdir=readdir,
stat=stat,
is_dir_name=lambda c: c.endswith("dir"),
index=None,
args=parse_find_args(("-empty", )))
assert results == ["/empty-dir", "/empty.txt"]
@pytest.mark.asyncio
async def test_walk_find_not_negates_predicate():
readdir = AsyncMock(return_value=["/a.txt", "/b.md"])
async def stat(spec: PathSpec, _index):
if spec.virtual == "/":
return FileStat(name="/", type=FileType.DIRECTORY)
return FileStat(name=spec.virtual.rsplit("/", 1)[-1],
size=1,
type=FileType.TEXT)
results = await walk_find(_root_spec(),
readdir=readdir,
stat=stat,
is_dir_name=lambda c: False,
index=None,
args=parse_find_args(("-not", "-name", "*.txt")))
assert results == ["/", "/b.md"]
@pytest.mark.asyncio
async def test_walk_find_stat_fallback_propagates_other_errors():
readdir = AsyncMock(return_value=["/mystery"])
stat = AsyncMock(side_effect=ValueError("rate limited"))
with pytest.raises(ValueError, match="rate limited"):
await walk_find(_root_spec(),
readdir=readdir,
stat=stat,
is_dir_name=lambda c: None,
index=None,
args=FindArgs())
@pytest.mark.asyncio
async def test_walk_find_size_filter_drops_not_found_entries():
readdir = AsyncMock(return_value=["/a.json"])
stat = AsyncMock(side_effect=FileNotFoundError("/a.json"))
results = await walk_find(_root_spec(),
readdir=readdir,
stat=stat,
is_dir_name=lambda c: False,
index=None,
args=FindArgs(min_size=1))
assert results == []
@pytest.mark.asyncio
async def test_walk_find_size_filter_propagates_other_stat_errors():
readdir = AsyncMock(return_value=["/a.json"])
stat = AsyncMock(side_effect=ValueError("rate limited"))
with pytest.raises(ValueError, match="rate limited"):
await walk_find(_root_spec(),
readdir=readdir,
stat=stat,
is_dir_name=lambda c: False,
index=None,
args=FindArgs(min_size=1))
@pytest.mark.parametrize("kwargs,flag,value", [
({
"maxdepth": "abc"
}, "-maxdepth", "abc"),
({
"mindepth": "xx"
}, "-mindepth", "xx"),
({
"size": ""
}, "-size", ""),
({
"size": "abc"
}, "-size", "abc"),
({
"mtime": "abc"
}, "-mtime", "abc"),
])
def test_parse_find_args_invalid_numeric_raises_find_parse_error(
kwargs, flag, value):
with pytest.raises(FindParseError) as exc:
parse_find_args((), **kwargs)
assert str(exc.value) == f"find: invalid argument '{value}' to '{flag}'"
@pytest.mark.parametrize("expr", [
"-maxdepth abc",
"-mindepth xx",
"-size ''",
"-size abc",
"-mtime abc",
])
def test_find_invalid_numeric_arg_exits_one_with_clean_stderr(expr):
async def _go() -> tuple[int, str]:
ws = Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
ws.create_session("s")
r = await ws.execute(f"find / {expr}", session_id="s")
return r.exit_code, await r.stderr_str()
code, stderr = asyncio.run(_go())
assert code == 1
assert stderr.startswith("find: invalid argument ")
assert stderr.endswith("\n")
# ── Issue #312 parse-level regression tests ────────────────
def test_parse_find_args_start_path_included():
args = parse_find_args(())
assert args.maxdepth is None
def test_parse_find_args_maxdepth_zero():
args = parse_find_args(("-maxdepth", "0"))
assert args.maxdepth == 0
def test_parse_find_args_empty_predicate():
args = parse_find_args(("-empty", ))
assert args.empty is True
def test_parse_find_args_not_negation():
args = parse_find_args(("-not", "-name", "*.txt"))
assert isinstance(args.tree, Not)
assert isinstance(args.tree.kid, Name)
assert args.tree.kid.pattern == "*.txt"
def test_parse_find_args_bogus_predicate_raises():
with pytest.raises(FindParseError, match="unknown predicate"):
parse_find_args(("-boguspredicate", ))