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
309 lines
9.8 KiB
Python
309 lines
9.8 KiB
Python
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
|
|
|
|
from functools import partial
|
|
|
|
import pytest
|
|
|
|
from mirage.commands.builtin.grep_helper import (compile_pattern,
|
|
get_extension,
|
|
grep_files_only, grep_lines,
|
|
grep_recursive)
|
|
from mirage.core.ram.mkdir import mkdir
|
|
from mirage.core.ram.read import read_bytes
|
|
from mirage.core.ram.readdir import readdir
|
|
from mirage.core.ram.stat import stat
|
|
from mirage.core.ram.write import write_bytes as _async_write_bytes
|
|
|
|
|
|
async def _write(backend, path, content):
|
|
accessor = backend.accessor
|
|
await _async_write_bytes(accessor, path, content.encode())
|
|
|
|
|
|
async def _mkdir(backend, path):
|
|
accessor = backend.accessor
|
|
await mkdir(accessor, path, parents=True)
|
|
|
|
|
|
def _bind(backend):
|
|
accessor = backend.accessor
|
|
index = backend.index
|
|
|
|
async def _readdir(path):
|
|
return await readdir(accessor, path, index)
|
|
|
|
return (
|
|
_readdir,
|
|
partial(stat, accessor),
|
|
partial(read_bytes, accessor),
|
|
)
|
|
|
|
|
|
async def grep(backend, path, pattern, **kwargs):
|
|
rd, st, rb = _bind(backend)
|
|
recursive = kwargs.pop("recursive", False)
|
|
ignore_case = kwargs.pop("ignore_case", False)
|
|
invert = kwargs.pop("invert", False)
|
|
line_numbers = kwargs.pop("line_numbers", False)
|
|
count_only = kwargs.pop("count_only", False)
|
|
files_only = kwargs.pop("files_only", False)
|
|
fixed_string = kwargs.pop("fixed_string", False)
|
|
only_matching = kwargs.pop("only_matching", False)
|
|
max_count = kwargs.pop("max_count", None)
|
|
whole_word = kwargs.pop("whole_word", False)
|
|
show_filename = kwargs.pop("show_filename", None)
|
|
warnings = kwargs.pop("warnings", None)
|
|
|
|
compiled = compile_pattern(pattern, ignore_case, fixed_string, whole_word)
|
|
|
|
if recursive:
|
|
results = await grep_recursive(
|
|
rd,
|
|
st,
|
|
rb,
|
|
path,
|
|
compiled,
|
|
invert,
|
|
line_numbers,
|
|
count_only,
|
|
files_only,
|
|
only_matching,
|
|
max_count,
|
|
warnings,
|
|
)
|
|
if show_filename is False and not count_only and not files_only:
|
|
stripped = []
|
|
for r in results:
|
|
colon_idx = r.find(":")
|
|
stripped.append(r[colon_idx + 1:] if colon_idx != -1 else r)
|
|
return stripped
|
|
return results
|
|
|
|
return await grep_files_only(
|
|
rd,
|
|
st,
|
|
rb,
|
|
path,
|
|
pattern,
|
|
recursive=False,
|
|
ignore_case=ignore_case,
|
|
invert=invert,
|
|
line_numbers=line_numbers,
|
|
count_only=count_only,
|
|
fixed_string=fixed_string,
|
|
only_matching=only_matching,
|
|
max_count=max_count,
|
|
whole_word=whole_word,
|
|
warnings=warnings,
|
|
)
|
|
|
|
|
|
class TestCompilePattern:
|
|
|
|
def test_basic(self):
|
|
pat = compile_pattern("hello")
|
|
assert pat.search("hello world")
|
|
|
|
def test_ignore_case(self):
|
|
pat = compile_pattern("hello", ignore_case=True)
|
|
assert pat.search("HELLO")
|
|
|
|
def test_fixed_string(self):
|
|
pat = compile_pattern("a.b", fixed_string=True)
|
|
assert not pat.search("axb")
|
|
assert pat.search("a.b")
|
|
|
|
def test_whole_word(self):
|
|
pat = compile_pattern("foo", whole_word=True)
|
|
assert not pat.search("foobar")
|
|
assert pat.search("foo bar")
|
|
|
|
|
|
class TestGetExtension:
|
|
|
|
def test_normal(self):
|
|
assert get_extension("file.txt") == ".txt"
|
|
|
|
def test_no_ext(self):
|
|
assert get_extension("file") is None
|
|
|
|
def test_directory_dot(self):
|
|
assert get_extension("dir.d/file") is None
|
|
|
|
|
|
class TestGrepLines:
|
|
|
|
def test_basic(self):
|
|
compiled = compile_pattern("hello")
|
|
result = grep_lines("/f.txt", ["hello world", "foo"], compiled, False,
|
|
False, False, False, False, None)
|
|
assert result == ["hello world"]
|
|
|
|
|
|
class TestBasicMatching:
|
|
|
|
@pytest.mark.anyio
|
|
async def test_match_found(self, backend):
|
|
await _write(backend, "/tmp/a.txt",
|
|
"hello world\nfoo bar\nhello again")
|
|
result = await grep(backend, "/tmp/a.txt", "hello")
|
|
assert result == ["/tmp/a.txt"]
|
|
|
|
@pytest.mark.anyio
|
|
async def test_no_match(self, backend):
|
|
await _write(backend, "/tmp/a.txt", "hello world\nfoo bar")
|
|
result = await grep(backend, "/tmp/a.txt", "xyz")
|
|
assert result == []
|
|
|
|
@pytest.mark.anyio
|
|
async def test_empty_file(self, backend):
|
|
await _write(backend, "/tmp/a.txt", "")
|
|
result = await grep(backend, "/tmp/a.txt", "hello")
|
|
assert result == []
|
|
|
|
|
|
class TestIgnoreCase:
|
|
|
|
@pytest.mark.anyio
|
|
async def test_ignore_case_matches(self, backend):
|
|
await _write(backend, "/tmp/a.txt", "Hello World\nhello world\nHELLO")
|
|
result = await grep(backend,
|
|
"/tmp/a.txt",
|
|
"hello",
|
|
ignore_case=True,
|
|
files_only=True)
|
|
assert result == ["/tmp/a.txt"]
|
|
|
|
|
|
class TestInvert:
|
|
|
|
@pytest.mark.anyio
|
|
async def test_invert_match(self, backend):
|
|
await _write(backend, "/tmp/a.txt", "hello\nworld\nhello again")
|
|
result = await grep(backend,
|
|
"/tmp/a.txt",
|
|
"hello",
|
|
invert=True,
|
|
files_only=True)
|
|
assert result == ["/tmp/a.txt"]
|
|
|
|
|
|
class TestCountOnly:
|
|
|
|
@pytest.mark.anyio
|
|
async def test_count_only(self, backend):
|
|
await _write(backend, "/tmp/a.txt", "foo\nbar\nfoo baz")
|
|
result = await grep(backend, "/tmp/a.txt", "foo", count_only=True)
|
|
assert result == ["2"]
|
|
|
|
|
|
class TestRecursive:
|
|
|
|
@pytest.mark.anyio
|
|
async def test_recursive_basic(self, backend):
|
|
await _mkdir(backend, "/tmp/sub")
|
|
await _write(backend, "/tmp/a.txt", "hello")
|
|
await _write(backend, "/tmp/sub/b.txt", "hello world")
|
|
result = await grep(backend, "/tmp", "hello", recursive=True)
|
|
assert "/tmp/a.txt:hello" in result
|
|
assert "/tmp/sub/b.txt:hello world" in result
|
|
|
|
@pytest.mark.anyio
|
|
async def test_recursive_with_line_numbers(self, backend):
|
|
await _mkdir(backend, "/tmp/sub")
|
|
await _write(backend, "/tmp/sub/b.txt", "x\nhello\ny")
|
|
result = await grep(backend,
|
|
"/tmp",
|
|
"hello",
|
|
recursive=True,
|
|
line_numbers=True)
|
|
assert "/tmp/sub/b.txt:2:hello" in result
|
|
|
|
@pytest.mark.anyio
|
|
async def test_recursive_with_files_only(self, backend):
|
|
await _mkdir(backend, "/tmp/sub")
|
|
await _write(backend, "/tmp/a.txt", "hello")
|
|
await _write(backend, "/tmp/sub/b.txt", "world")
|
|
result = await grep(backend,
|
|
"/tmp",
|
|
"hello",
|
|
recursive=True,
|
|
files_only=True)
|
|
assert "/tmp/a.txt" in result
|
|
assert "/tmp/sub/b.txt" not in result
|
|
|
|
@pytest.mark.anyio
|
|
async def test_recursive_with_count_only(self, backend):
|
|
await _mkdir(backend, "/tmp/sub")
|
|
await _write(backend, "/tmp/a.txt", "hello\nhello")
|
|
await _write(backend, "/tmp/sub/b.txt", "hello")
|
|
result = await grep(backend,
|
|
"/tmp",
|
|
"hello",
|
|
recursive=True,
|
|
count_only=True)
|
|
assert len(result) > 0
|
|
|
|
|
|
class TestMixedFlags:
|
|
|
|
@pytest.mark.anyio
|
|
async def test_recursive_ignore_case_line_numbers(self, backend):
|
|
await _mkdir(backend, "/tmp/sub")
|
|
await _write(backend, "/tmp/sub/a.txt", "Hello\nworld")
|
|
result = await grep(backend,
|
|
"/tmp",
|
|
"hello",
|
|
recursive=True,
|
|
ignore_case=True,
|
|
line_numbers=True)
|
|
assert "/tmp/sub/a.txt:1:Hello" in result
|
|
|
|
|
|
class TestShowFilename:
|
|
|
|
@pytest.mark.anyio
|
|
async def test_grep_hide_filename_recursive(self, backend):
|
|
await _mkdir(backend, "/tmp/sub")
|
|
await _write(backend, "/tmp/sub/a.txt", "needle")
|
|
result = await grep(backend,
|
|
"/tmp/sub/",
|
|
"needle",
|
|
recursive=True,
|
|
show_filename=False)
|
|
assert result == ["needle"]
|
|
|
|
|
|
class TestWarnings:
|
|
|
|
@pytest.mark.anyio
|
|
async def test_warnings_on_missing_file(self, backend):
|
|
warnings = []
|
|
result = await grep(backend,
|
|
"/tmp/nonexistent.txt",
|
|
"foo",
|
|
warnings=warnings)
|
|
assert result == []
|
|
assert len(warnings) > 0
|
|
assert "nonexistent" in warnings[0]
|
|
|
|
@pytest.mark.anyio
|
|
async def test_warnings_none_does_not_error(self, backend):
|
|
result = await grep(backend,
|
|
"/tmp/nonexistent.txt",
|
|
"foo",
|
|
warnings=None)
|
|
assert result == []
|