chore: import upstream snapshot with attribution
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
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
This commit is contained in:
@@ -0,0 +1,323 @@
|
||||
# ========= 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. =========
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.cache.index import IndexEntry
|
||||
from mirage.fuse.fs import MirageFS
|
||||
from mirage.ops import Ops
|
||||
from mirage.ops.config import OpsMount
|
||||
from mirage.resource.discord import DiscordConfig, DiscordResource
|
||||
from mirage.types import FileType, MountMode
|
||||
|
||||
GUILD = "TestGuild"
|
||||
CHANNEL = "general"
|
||||
DATE = "2024-01-15"
|
||||
FILE = "chat.jsonl"
|
||||
GUILD_PATH = f"{GUILD}"
|
||||
CHANNEL_PATH = f"{GUILD}/channels/{CHANNEL}"
|
||||
DATE_DIR_PATH = f"{GUILD}/channels/{CHANNEL}/{DATE}"
|
||||
FILE_PATH = f"{GUILD}/channels/{CHANNEL}/{DATE}/{FILE}"
|
||||
MEMBER_PATH = f"{GUILD}/members/alice.json"
|
||||
|
||||
PREFIX = "/discord"
|
||||
|
||||
FAKE_JSONL = (b'{"id":"1","content":"hello","author":{"username":"alice"}}\n'
|
||||
b'{"id":"2","content":"world","author":{"username":"bob"}}\n')
|
||||
|
||||
FAKE_MEMBER = b'{"user":{"id":"U1","username":"alice"},"roles":[]}'
|
||||
|
||||
|
||||
def _run(coro):
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
def _make_resource() -> DiscordResource:
|
||||
config = DiscordConfig(token="test-token")
|
||||
resource = DiscordResource(config=config)
|
||||
index = resource.index
|
||||
_run(
|
||||
index.put(
|
||||
f"{PREFIX}/{GUILD}",
|
||||
IndexEntry(id="G1",
|
||||
name=GUILD,
|
||||
resource_type="discord/guild",
|
||||
vfs_name=GUILD)))
|
||||
_run(
|
||||
index.put(
|
||||
f"{PREFIX}/{CHANNEL_PATH}",
|
||||
IndexEntry(id="C1",
|
||||
name=CHANNEL,
|
||||
resource_type="discord/channel",
|
||||
vfs_name=CHANNEL)))
|
||||
_run(
|
||||
index.put(
|
||||
f"{PREFIX}/{DATE_DIR_PATH}",
|
||||
IndexEntry(id="C1:2024-01-15",
|
||||
name="2024-01-15",
|
||||
resource_type="discord/history",
|
||||
vfs_name=DATE)))
|
||||
_run(
|
||||
index.put(
|
||||
f"{PREFIX}/{FILE_PATH}",
|
||||
IndexEntry(id="C1:2024-01-15:chat",
|
||||
name="chat.jsonl",
|
||||
resource_type="discord/chat_jsonl",
|
||||
vfs_name=FILE)))
|
||||
_run(
|
||||
index.put(
|
||||
f"{PREFIX}/{MEMBER_PATH}",
|
||||
IndexEntry(id="U1",
|
||||
name="alice",
|
||||
resource_type="discord/member",
|
||||
vfs_name="alice.json")))
|
||||
_run(
|
||||
index.put(
|
||||
f"{PREFIX}/{GUILD}/members",
|
||||
IndexEntry(id="G1:members",
|
||||
name="members",
|
||||
resource_type="discord/virtual_dir")))
|
||||
_run(
|
||||
index.set_dir(f"{PREFIX}/{GUILD}/channels", [
|
||||
(CHANNEL,
|
||||
IndexEntry(id="C1",
|
||||
name=CHANNEL,
|
||||
resource_type="discord/channel",
|
||||
vfs_name=CHANNEL)),
|
||||
]))
|
||||
_run(
|
||||
index.set_dir(f"{PREFIX}/{CHANNEL_PATH}", [
|
||||
(DATE,
|
||||
IndexEntry(id="C1:2024-01-15",
|
||||
name="2024-01-15",
|
||||
resource_type="discord/history",
|
||||
vfs_name=DATE)),
|
||||
]))
|
||||
_run(
|
||||
index.set_dir(f"{PREFIX}/{DATE_DIR_PATH}", [
|
||||
(FILE,
|
||||
IndexEntry(id="C1:2024-01-15:chat",
|
||||
name="chat.jsonl",
|
||||
resource_type="discord/chat_jsonl",
|
||||
vfs_name=FILE)),
|
||||
]))
|
||||
_run(
|
||||
index.set_dir(f"{PREFIX}/{GUILD}/members", [
|
||||
("alice.json",
|
||||
IndexEntry(id="U1",
|
||||
name="alice",
|
||||
resource_type="discord/member",
|
||||
vfs_name="alice.json")),
|
||||
]))
|
||||
return resource
|
||||
|
||||
|
||||
def _make_ops(resource: DiscordResource) -> Ops:
|
||||
mount = OpsMount(
|
||||
prefix=f"{PREFIX}/",
|
||||
resource_type=resource.name,
|
||||
accessor=resource.accessor,
|
||||
index=resource.index,
|
||||
mode=MountMode.READ,
|
||||
ops=resource.ops_list(),
|
||||
)
|
||||
return Ops([mount])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def resource():
|
||||
return _make_resource()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ops(resource):
|
||||
return _make_ops(resource)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fs(ops):
|
||||
return MirageFS(ops)
|
||||
|
||||
|
||||
# ── ops.readdir ──────────────────────────────────
|
||||
|
||||
|
||||
def test_ops_readdir_guild(ops):
|
||||
entries = _run(ops.readdir(f"{PREFIX}/{GUILD}/"))
|
||||
names = [e.rsplit("/", 1)[-1] for e in entries]
|
||||
assert "channels" in names
|
||||
assert "members" in names
|
||||
|
||||
|
||||
def test_ops_readdir_channels(ops):
|
||||
entries = _run(ops.readdir(f"{PREFIX}/{GUILD}/channels/"))
|
||||
names = [e.rsplit("/", 1)[-1] for e in entries]
|
||||
assert CHANNEL in names
|
||||
|
||||
|
||||
def test_ops_readdir_dates(ops):
|
||||
entries = _run(ops.readdir(f"{PREFIX}/{CHANNEL_PATH}/"))
|
||||
names = [e.rsplit("/", 1)[-1] for e in entries]
|
||||
assert DATE in names
|
||||
|
||||
|
||||
def test_ops_readdir_members(ops):
|
||||
entries = _run(ops.readdir(f"{PREFIX}/{GUILD}/members/"))
|
||||
names = [e.rsplit("/", 1)[-1] for e in entries]
|
||||
assert "alice.json" in names
|
||||
|
||||
|
||||
# ── ops.stat ─────────────────────────────────────
|
||||
|
||||
|
||||
def test_ops_stat_guild(ops):
|
||||
s = _run(ops.stat(f"{PREFIX}/{GUILD}"))
|
||||
assert s.type == FileType.DIRECTORY
|
||||
|
||||
|
||||
def test_ops_stat_channel(ops):
|
||||
s = _run(ops.stat(f"{PREFIX}/{CHANNEL_PATH}"))
|
||||
assert s.type == FileType.DIRECTORY
|
||||
|
||||
|
||||
def test_ops_stat_file(ops):
|
||||
s = _run(ops.stat(f"{PREFIX}/{FILE_PATH}"))
|
||||
assert s.type == FileType.TEXT
|
||||
assert s.name == FILE
|
||||
|
||||
|
||||
# ── ops.read ─────────────────────────────────────
|
||||
|
||||
|
||||
def test_ops_read_jsonl(ops):
|
||||
with patch("mirage.core.discord.read.get_history_jsonl",
|
||||
new_callable=AsyncMock,
|
||||
return_value=FAKE_JSONL):
|
||||
data = _run(ops.read(f"{PREFIX}/{FILE_PATH}"))
|
||||
assert data == FAKE_JSONL
|
||||
assert b"hello" in data
|
||||
|
||||
|
||||
def test_ops_read_member(ops):
|
||||
with patch("mirage.core.discord.read.list_members",
|
||||
new_callable=AsyncMock,
|
||||
return_value=[{
|
||||
"user": {
|
||||
"id": "U1",
|
||||
"username": "alice"
|
||||
}
|
||||
}]):
|
||||
data = _run(ops.read(f"{PREFIX}/{MEMBER_PATH}"))
|
||||
assert b"alice" in data
|
||||
|
||||
|
||||
# ── ops.read prefix in index keys ────────────────
|
||||
|
||||
|
||||
def test_ops_read_uses_prefix_for_index(ops):
|
||||
with patch("mirage.core.discord.read.get_history_jsonl",
|
||||
new_callable=AsyncMock,
|
||||
return_value=b"data") as mock_get:
|
||||
data = _run(ops.read(f"{PREFIX}/{FILE_PATH}"))
|
||||
assert data == b"data"
|
||||
mock_get.assert_called_once()
|
||||
|
||||
|
||||
# ── FUSE getattr ─────────────────────────────────
|
||||
|
||||
|
||||
def test_fuse_getattr_root(fs):
|
||||
attr = fs.getattr("/")
|
||||
assert attr["st_mode"] & 0o170000 == 0o040000
|
||||
|
||||
|
||||
def test_fuse_getattr_mount_prefix(fs):
|
||||
attr = fs.getattr("/discord")
|
||||
assert attr["st_mode"] & 0o170000 == 0o040000
|
||||
|
||||
|
||||
def test_fuse_getattr_guild(fs):
|
||||
attr = fs.getattr(f"{PREFIX}/{GUILD}")
|
||||
assert attr["st_mode"] & 0o170000 == 0o040000
|
||||
|
||||
|
||||
def test_fuse_getattr_file(fs):
|
||||
attr = fs.getattr(f"{PREFIX}/{FILE_PATH}")
|
||||
assert attr["st_mode"] & 0o170000 == 0o100000
|
||||
|
||||
|
||||
def test_fuse_getattr_unknown_size(fs):
|
||||
# Unknown size before open() — direct_io ensures read() still works
|
||||
attr = fs.getattr(f"{PREFIX}/{FILE_PATH}")
|
||||
assert attr["st_size"] == 0
|
||||
|
||||
|
||||
# ── FUSE readdir ─────────────────────────────────
|
||||
|
||||
|
||||
def test_fuse_readdir_root(fs):
|
||||
entries = fs.readdir("/", None)
|
||||
assert "discord" in entries
|
||||
|
||||
|
||||
def test_fuse_readdir_guild(fs):
|
||||
entries = fs.readdir(f"{PREFIX}/{GUILD}", None)
|
||||
assert "channels" in entries
|
||||
assert "members" in entries
|
||||
|
||||
|
||||
def test_fuse_readdir_channels(fs):
|
||||
entries = fs.readdir(f"{PREFIX}/{GUILD}/channels", None)
|
||||
assert CHANNEL in entries
|
||||
|
||||
|
||||
def test_fuse_readdir_dates(fs):
|
||||
entries = fs.readdir(f"{PREFIX}/{CHANNEL_PATH}", None)
|
||||
assert DATE in entries
|
||||
|
||||
|
||||
# ── FUSE open + read ─────────────────────────────
|
||||
|
||||
|
||||
def test_fuse_read_file(fs):
|
||||
with patch("mirage.core.discord.read.get_history_jsonl",
|
||||
new_callable=AsyncMock,
|
||||
return_value=FAKE_JSONL):
|
||||
fh = fs.open(f"{PREFIX}/{FILE_PATH}", 0)
|
||||
data = fs.read(f"{PREFIX}/{FILE_PATH}", 4096, 0, fh)
|
||||
fs.release(f"{PREFIX}/{FILE_PATH}", fh)
|
||||
assert data == FAKE_JSONL
|
||||
|
||||
|
||||
def test_fuse_read_offset(fs):
|
||||
with patch("mirage.core.discord.read.get_history_jsonl",
|
||||
new_callable=AsyncMock,
|
||||
return_value=FAKE_JSONL):
|
||||
fh = fs.open(f"{PREFIX}/{FILE_PATH}", 0)
|
||||
data = fs.read(f"{PREFIX}/{FILE_PATH}", 10, 0, fh)
|
||||
fs.release(f"{PREFIX}/{FILE_PATH}", fh)
|
||||
assert data == FAKE_JSONL[:10]
|
||||
|
||||
|
||||
def test_fuse_read_beyond_eof(fs):
|
||||
with patch("mirage.core.discord.read.get_history_jsonl",
|
||||
new_callable=AsyncMock,
|
||||
return_value=FAKE_JSONL):
|
||||
fh = fs.open(f"{PREFIX}/{FILE_PATH}", 0)
|
||||
data = fs.read(f"{PREFIX}/{FILE_PATH}", 4096, len(FAKE_JSONL), fh)
|
||||
fs.release(f"{PREFIX}/{FILE_PATH}", fh)
|
||||
assert data == b""
|
||||
@@ -0,0 +1,564 @@
|
||||
# ========= 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. =========
|
||||
|
||||
import errno
|
||||
import os
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
|
||||
from mirage.fuse.fs import MirageFS
|
||||
from mirage.resource.ram import RAMResource
|
||||
from mirage.types import MountMode
|
||||
from mirage.workspace import Workspace
|
||||
|
||||
_fuse_available = sys.platform in ("linux", "darwin")
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def seed_ws():
|
||||
ws = Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
|
||||
await ws.execute("tee /a.txt", stdin=b"hello world")
|
||||
await ws.execute("mkdir /sub")
|
||||
await ws.execute("tee /sub/b.txt", stdin=b"nested")
|
||||
return ws
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def rw_ws():
|
||||
return Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_getattr_root(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
attrs = fs.getattr("/")
|
||||
assert attrs["st_mode"] & stat.S_IFDIR
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_getattr_file(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
attrs = fs.getattr("/a.txt")
|
||||
assert attrs["st_mode"] & stat.S_IFREG
|
||||
assert attrs["st_size"] == len(b"hello world")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_getattr_dir(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
attrs = fs.getattr("/sub")
|
||||
assert attrs["st_mode"] & stat.S_IFDIR
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_getattr_missing(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
with pytest.raises(OSError) as exc:
|
||||
fs.getattr("/no_such_file.txt")
|
||||
assert exc.value.errno == errno.ENOENT
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_getattr_empty_readdir_not_ghost_dir():
|
||||
ws = Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
|
||||
await ws.execute("mkdir /emptydir")
|
||||
fs = MirageFS(ws.ops)
|
||||
with pytest.raises(OSError) as exc:
|
||||
fs.getattr("/typo_command")
|
||||
assert exc.value.errno == errno.ENOENT
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_root(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
entries = fs.readdir("/", None)
|
||||
assert "." in entries
|
||||
assert ".." in entries
|
||||
assert "a.txt" in entries
|
||||
assert "sub" in entries
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_subdir(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
entries = fs.readdir("/sub", None)
|
||||
assert "b.txt" in entries
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_missing(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
with pytest.raises(OSError) as exc:
|
||||
fs.readdir("/nope", None)
|
||||
assert exc.value.errno == errno.ENOENT
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_full(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
fh = fs.open("/a.txt", os.O_RDONLY)
|
||||
data = fs.read("/a.txt", 1024, 0, fh)
|
||||
assert data == b"hello world"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_read_offset(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
fh = fs.open("/a.txt", os.O_RDONLY)
|
||||
data = fs.read("/a.txt", 5, 6, fh)
|
||||
assert data == b"world"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_open_missing(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
with pytest.raises(OSError) as exc:
|
||||
fs.open("/missing.txt", os.O_RDONLY)
|
||||
assert exc.value.errno == errno.ENOENT
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_and_write(rw_ws):
|
||||
fs = MirageFS(rw_ws.ops)
|
||||
fh = fs.create("/new.txt", 0o644)
|
||||
fs.write("/new.txt", b"data", 0, fh)
|
||||
fs.flush("/new.txt", fh)
|
||||
result = await rw_ws.execute("cat /new.txt")
|
||||
assert result.stdout == b"data"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_mkdir(rw_ws):
|
||||
fs = MirageFS(rw_ws.ops)
|
||||
fs.mkdir("/newdir", 0o755)
|
||||
entries = fs.readdir("/newdir", None)
|
||||
assert "." in entries
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unlink(rw_ws):
|
||||
await rw_ws.execute("tee /todel.txt", stdin=b"bye")
|
||||
fs = MirageFS(rw_ws.ops)
|
||||
fs.unlink("/todel.txt")
|
||||
with pytest.raises(OSError) as exc:
|
||||
fs.getattr("/todel.txt")
|
||||
assert exc.value.errno == errno.ENOENT
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rename(rw_ws):
|
||||
await rw_ws.execute("tee /old.txt", stdin=b"content")
|
||||
fs = MirageFS(rw_ws.ops)
|
||||
fs.rename("/old.txt", "/new.txt")
|
||||
result = await rw_ws.execute("cat /new.txt")
|
||||
assert result.stdout == b"content"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rmdir_empty(rw_ws):
|
||||
await rw_ws.execute("mkdir /emptydir")
|
||||
fs = MirageFS(rw_ws.ops)
|
||||
fs.rmdir("/emptydir")
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rmdir_nonempty(rw_ws):
|
||||
await rw_ws.execute("mkdir /nonempty")
|
||||
await rw_ws.execute("tee /nonempty/file.txt", stdin=b"x")
|
||||
fs = MirageFS(rw_ws.ops)
|
||||
with pytest.raises(OSError) as exc:
|
||||
fs.rmdir("/nonempty")
|
||||
assert exc.value.errno == errno.ENOTEMPTY
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_truncate(rw_ws):
|
||||
await rw_ws.execute("tee /f.txt", stdin=b"hello world")
|
||||
fs = MirageFS(rw_ws.ops)
|
||||
fs.truncate("/f.txt", 5)
|
||||
result = await rw_ws.execute("cat /f.txt")
|
||||
assert result.stdout == b"hello"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_truncate_extend(rw_ws):
|
||||
await rw_ws.execute("tee /f.txt", stdin=b"hi")
|
||||
fs = MirageFS(rw_ws.ops)
|
||||
fs.truncate("/f.txt", 5)
|
||||
result = await rw_ws.execute("cat /f.txt")
|
||||
assert result.stdout == b"hi\x00\x00\x00"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_write_at_offset(rw_ws):
|
||||
await rw_ws.execute("tee /f.txt", stdin=b"hello world")
|
||||
fs = MirageFS(rw_ws.ops)
|
||||
fh = fs.open("/f.txt", os.O_RDWR)
|
||||
fs.write("/f.txt", b"WORLD", 6, fh)
|
||||
fs.flush("/f.txt", fh)
|
||||
result = await rw_ws.execute("cat /f.txt")
|
||||
assert result.stdout == b"hello WORLD"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_statfs(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
result = fs.statfs("/")
|
||||
assert "f_bsize" in result
|
||||
assert "f_blocks" in result
|
||||
assert result["f_bsize"] > 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chmod_does_not_raise(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
fs.chmod("/a.txt", 0o644)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_chown_does_not_raise(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
fs.chown("/a.txt", os.getuid(), os.getgid())
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_utimens_does_not_raise(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
fs.utimens("/a.txt", None)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_access_does_not_raise(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
fs.access("/a.txt", os.R_OK)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fsync_delegates_to_flush(rw_ws):
|
||||
await rw_ws.execute("tee /f.txt", stdin=b"before")
|
||||
fs = MirageFS(rw_ws.ops)
|
||||
fh = fs.open("/f.txt", os.O_RDWR)
|
||||
fs.write("/f.txt", b"after!", 0, fh)
|
||||
fs.fsync("/f.txt", 0, fh)
|
||||
result = await rw_ws.execute("cat /f.txt")
|
||||
assert result.stdout == b"after!"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_open_returns_unique_handles(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
fh1 = fs.open("/a.txt", os.O_RDONLY)
|
||||
fh2 = fs.open("/a.txt", os.O_RDONLY)
|
||||
assert fh1 != fh2
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_release_cleans_handles(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
fh = fs.open("/a.txt", os.O_RDONLY)
|
||||
assert fh in fs._handles
|
||||
fs.release("/a.txt", fh)
|
||||
assert fh not in fs._handles
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_drain_ops_returns_and_clears(rw_ws):
|
||||
await rw_ws.execute("tee /track.txt", stdin=b"x")
|
||||
fs = MirageFS(rw_ws.ops)
|
||||
fh = fs.create("/new.txt", 0o644)
|
||||
fs.write("/new.txt", b"y", 0, fh)
|
||||
fs.flush("/new.txt", fh)
|
||||
ops = fs.drain_ops()
|
||||
assert any(o["op"] == "create" for o in ops)
|
||||
assert any(o["op"] == "write" for o in ops)
|
||||
assert len(fs.drain_ops()) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_drain_ops_read_deduplication(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
fh = fs.open("/a.txt", os.O_RDONLY)
|
||||
fs.read("/a.txt", 1024, 0, fh)
|
||||
fs.read("/a.txt", 1024, 0, fh)
|
||||
ops = fs.drain_ops()
|
||||
read_ops = [o for o in ops if o["op"] == "read" and o["path"] == "/a.txt"]
|
||||
assert len(read_ops) >= 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fuse_read_uses_cache_when_populated():
|
||||
mem = RAMResource()
|
||||
mem.caches_reads = True
|
||||
mem._store.files["/a.txt"] = b"hello world"
|
||||
ws = Workspace({"/": mem}, mode=MountMode.WRITE)
|
||||
await ws.execute("cat /a.txt")
|
||||
fs = MirageFS(ws.ops)
|
||||
fh = fs.open("/a.txt", os.O_RDONLY)
|
||||
data = fs.read("/a.txt", 5, 0, fh)
|
||||
assert data == b"hello"
|
||||
assert (await ws._cache.get("/a.txt")) is not None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_readdir_logs_ls_op(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
fs.readdir("/", None)
|
||||
ops = fs.drain_ops()
|
||||
assert any(o["op"] == "readdir" and o["path"] == "/" for o in ops)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_total_ops_persists_across_drains(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
fs.readdir("/", None)
|
||||
first = fs.drain_ops()
|
||||
fs.readdir("/sub", None)
|
||||
second = fs.drain_ops()
|
||||
assert len(first) >= 1
|
||||
assert len(second) >= 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_total_ops_counts_reads_and_writes(rw_ws):
|
||||
await rw_ws.execute("tee /f.txt", stdin=b"x")
|
||||
fs = MirageFS(rw_ws.ops)
|
||||
fs._ops.records.clear()
|
||||
fh = fs.open("/f.txt", os.O_RDONLY)
|
||||
fs.read("/f.txt", 1024, 0, fh)
|
||||
fh2 = fs.create("/g.txt", 0o644)
|
||||
fs.write("/g.txt", b"y", 0, fh2)
|
||||
fs.flush("/g.txt", fh2)
|
||||
ops = fs.drain_ops()
|
||||
assert len(ops) >= 3
|
||||
|
||||
|
||||
def test_permission_error_logged_on_create():
|
||||
ro_ws = Workspace({"/": RAMResource()}, mode=MountMode.READ)
|
||||
fs = MirageFS(ro_ws.ops)
|
||||
with pytest.raises(Exception):
|
||||
fs.create("/new.txt", 0o644)
|
||||
|
||||
|
||||
def test_permission_error_not_counted_as_op():
|
||||
ro_ws = Workspace({"/": RAMResource()}, mode=MountMode.READ)
|
||||
fs = MirageFS(ro_ws.ops)
|
||||
fs._ops.records.clear()
|
||||
with pytest.raises(Exception):
|
||||
fs.create("/new.txt", 0o644)
|
||||
ops = fs.drain_ops()
|
||||
assert len(ops) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fuse_dispatches_to_backend_hooks(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
fh = fs.open("/a.txt", os.O_RDONLY)
|
||||
data = fs.read("/a.txt", 1024, 0, fh)
|
||||
assert data == b"hello world"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fuse_write_buffered_flush(rw_ws):
|
||||
fs = MirageFS(rw_ws.ops)
|
||||
await rw_ws.execute("tee /f.txt", stdin=b"hello world")
|
||||
fh = fs.open("/f.txt", os.O_RDWR)
|
||||
fs.write("/f.txt", b"HELLO", 0, fh)
|
||||
fs.flush("/f.txt", fh)
|
||||
result = await rw_ws.execute("cat /f.txt")
|
||||
assert result.stdout == b"HELLO world"
|
||||
|
||||
|
||||
@pytest.mark.skipif(not _fuse_available,
|
||||
reason="FUSE not available on this platform")
|
||||
@pytest.mark.asyncio
|
||||
async def test_mount_background_readable():
|
||||
from mirage.fuse.mount import mount_background
|
||||
ws = Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
|
||||
await ws.execute("tee /hello.txt", stdin=b"hi from memory")
|
||||
with tempfile.TemporaryDirectory() as mountpoint:
|
||||
t = mount_background(ws.ops, mountpoint, agent_id="test-agent")
|
||||
try:
|
||||
import time
|
||||
time.sleep(1)
|
||||
path = os.path.join(mountpoint, "hello.txt")
|
||||
assert os.path.exists(path)
|
||||
with open(path, "rb") as f:
|
||||
assert f.read() == b"hi from memory"
|
||||
finally:
|
||||
if sys.platform == "darwin":
|
||||
subprocess.run(["diskutil", "unmount", "force", mountpoint],
|
||||
capture_output=True)
|
||||
else:
|
||||
subprocess.run(["fusermount", "-u", mountpoint],
|
||||
capture_output=True)
|
||||
t.join(timeout=3)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_xattr_set_get_roundtrip(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
fs.setxattr("/a.txt", "user.test", b"value", 0)
|
||||
assert fs.getxattr("/a.txt", "user.test") == b"value"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_xattr_get_missing_raises(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
with pytest.raises(OSError) as exc:
|
||||
fs.getxattr("/a.txt", "user.absent")
|
||||
assert exc.value.errno in (errno.ENODATA,
|
||||
getattr(errno, "ENOATTR", errno.ENODATA))
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_xattr_list_and_remove(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
fs.setxattr("/a.txt", "user.one", b"1", 0)
|
||||
fs.setxattr("/a.txt", "user.two", b"2", 0)
|
||||
assert sorted(fs.listxattr("/a.txt")) == ["user.one", "user.two"]
|
||||
fs.removexattr("/a.txt", "user.one")
|
||||
assert fs.listxattr("/a.txt") == ["user.two"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_xattr_probe_succeeds(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
assert fs.setxattr("/a.txt", "user.containers._probe", b"x", 0) == 0
|
||||
assert fs.removexattr("/a.txt", "user.containers._probe") == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_xattr_cleared_on_unlink(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
fs.setxattr("/a.txt", "user.keep", b"v", 0)
|
||||
fs.unlink("/a.txt")
|
||||
assert fs.listxattr("/sub") == []
|
||||
assert "/a.txt" not in fs._xattrs
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_xattr_follows_rename(seed_ws):
|
||||
fs = MirageFS(seed_ws.ops)
|
||||
fs.setxattr("/a.txt", "user.keep", b"v", 0)
|
||||
fs.rename("/a.txt", "/renamed.txt")
|
||||
assert fs.getxattr("/renamed.txt", "user.keep") == b"v"
|
||||
assert "/a.txt" not in fs._xattrs
|
||||
|
||||
|
||||
class _SizelessOps:
|
||||
|
||||
def __init__(self, ops):
|
||||
self._inner = ops
|
||||
self.read_calls = 0
|
||||
|
||||
def __getattr__(self, name):
|
||||
return getattr(self._inner, name)
|
||||
|
||||
async def stat(self, path):
|
||||
s = await self._inner.stat(path)
|
||||
return s.model_copy(update={"size": None})
|
||||
|
||||
async def read(self, path):
|
||||
self.read_calls += 1
|
||||
return await self._inner.read(path)
|
||||
|
||||
|
||||
_PAYLOAD = b"payload-bytes"
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def sizeless_fs():
|
||||
ws = Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
|
||||
await ws.execute("tee /u.json", stdin=_PAYLOAD)
|
||||
ops = _SizelessOps(ws.ops)
|
||||
return MirageFS(ops), ops
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unknown_size_preopen_stats_zero(sizeless_fs):
|
||||
fs, ops = sizeless_fs
|
||||
attrs = fs.getattr("/u.json")
|
||||
assert attrs["st_size"] == 0
|
||||
assert ops.read_calls == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unknown_size_fh_stat_returns_real_size(sizeless_fs):
|
||||
fs, _ = sizeless_fs
|
||||
fh = fs.open("/u.json", os.O_RDONLY)
|
||||
attrs = fs.getattr("/u.json", fh)
|
||||
assert attrs["st_size"] == len(_PAYLOAD)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unknown_size_path_stat_uses_open_handle(sizeless_fs):
|
||||
fs, _ = sizeless_fs
|
||||
fs.open("/u.json", os.O_RDONLY)
|
||||
attrs = fs.getattr("/u.json")
|
||||
assert attrs["st_size"] == len(_PAYLOAD)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prefetch_survives_release_within_ttl(sizeless_fs):
|
||||
fs, ops = sizeless_fs
|
||||
fh = fs.open("/u.json", os.O_RDONLY)
|
||||
fs.release("/u.json", fh)
|
||||
attrs = fs.getattr("/u.json")
|
||||
assert attrs["st_size"] == len(_PAYLOAD)
|
||||
assert ops.read_calls == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_prefetch_expires_after_ttl(sizeless_fs):
|
||||
fs, _ = sizeless_fs
|
||||
fh = fs.open("/u.json", os.O_RDONLY)
|
||||
fs.release("/u.json", fh)
|
||||
data, _ = fs._prefetch["/u.json"]
|
||||
fs._prefetch["/u.json"] = (data, 0.0)
|
||||
attrs = fs.getattr("/u.json")
|
||||
assert attrs["st_size"] == 0
|
||||
assert "/u.json" not in fs._prefetch
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_open_then_read_does_not_refetch(sizeless_fs):
|
||||
fs, ops = sizeless_fs
|
||||
fh = fs.open("/u.json", os.O_RDONLY)
|
||||
assert fs.read("/u.json", 1024, 0, fh) == _PAYLOAD
|
||||
assert fs.read("/u.json", 7, 0, fh) == _PAYLOAD[:7]
|
||||
assert ops.read_calls == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_flush_drops_prefetch(sizeless_fs):
|
||||
fs, _ = sizeless_fs
|
||||
fh = fs.open("/u.json", os.O_RDWR)
|
||||
assert "/u.json" in fs._prefetch
|
||||
fs.write("/u.json", b"NEW", 0, fh)
|
||||
fs.flush("/u.json", fh)
|
||||
assert "/u.json" not in fs._prefetch
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unlink_drops_prefetch(sizeless_fs):
|
||||
fs, _ = sizeless_fs
|
||||
fh = fs.open("/u.json", os.O_RDONLY)
|
||||
fs.release("/u.json", fh)
|
||||
fs.unlink("/u.json")
|
||||
assert "/u.json" not in fs._prefetch
|
||||
@@ -0,0 +1,50 @@
|
||||
# ========= 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. =========
|
||||
|
||||
import pytest
|
||||
|
||||
from mirage.fuse.fs import MirageFS
|
||||
from mirage.fuse.mount import _run_fuse
|
||||
from mirage.resource.ram import RAMResource
|
||||
from mirage.types import MountMode
|
||||
from mirage.workspace import Workspace
|
||||
|
||||
|
||||
class _CaptureFuse:
|
||||
|
||||
kwargs: dict = {}
|
||||
args: tuple = ()
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
_CaptureFuse.args = args
|
||||
_CaptureFuse.kwargs = kwargs
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fs():
|
||||
ws = Workspace({"/": RAMResource()}, mode=MountMode.WRITE)
|
||||
return MirageFS(ws.ops)
|
||||
|
||||
|
||||
def test_run_fuse_mount_options(monkeypatch, fs):
|
||||
monkeypatch.setattr("mirage.fuse.mount.fuse.FUSE", _CaptureFuse)
|
||||
_run_fuse(fs, "/tmp/mp", foreground=True)
|
||||
assert _CaptureFuse.args == (fs, "/tmp/mp")
|
||||
assert _CaptureFuse.kwargs["nothreads"] is True
|
||||
assert _CaptureFuse.kwargs["foreground"] is True
|
||||
# direct_io keeps reads correct for tools that never fstat; attr_timeout=0
|
||||
# keeps fstat-based tools (wc -c, BSD cp, tail -c) from clamping at the
|
||||
# stale pre-open size.
|
||||
assert _CaptureFuse.kwargs["direct_io"] is True
|
||||
assert _CaptureFuse.kwargs["attr_timeout"] == 0
|
||||
Reference in New Issue
Block a user