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

361 lines
11 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. =========
import asyncio
import os
import subprocess
import uuid
from collections.abc import AsyncIterator
from contextlib import ExitStack
from datetime import datetime, timezone
from unittest.mock import patch
import pytest
from mirage.core.ram.mkdir import mkdir
from mirage.core.ram.write import write_bytes as mem_write
from mirage.io.types import ByteSource
from mirage.resource.disk import DiskResource
from mirage.resource.ram import RAMResource
from mirage.resource.redis import RedisResource
from mirage.resource.s3 import S3Config, S3Resource
from mirage.types import MountMode
from mirage.workspace import Workspace
BUCKET = "test-bucket"
REGION = "us-east-1"
LAST_MODIFIED = datetime(2026, 3, 31, tzinfo=timezone.utc)
_CORE_MODULES = [
"mirage.core.s3.read",
"mirage.core.s3.write",
"mirage.core.s3.stat",
"mirage.core.s3.readdir",
"mirage.core.s3.find",
"mirage.core.s3.du",
"mirage.core.s3.stream",
"mirage.core.s3.copy",
"mirage.core.s3.rename",
"mirage.core.s3.unlink",
"mirage.core.s3.rmdir",
"mirage.core.s3.rm",
"mirage.core.s3.mkdir",
"mirage.core.s3.create",
"mirage.core.s3.truncate",
]
class AsyncMockBody:
def __init__(self, data: bytes) -> None:
self._data = data
async def read(self) -> bytes:
return self._data
async def iter_chunks(self, chunk_size: int = 8192):
for i in range(0, len(self._data), chunk_size):
yield self._data[i:i + chunk_size]
class AsyncMockPaginator:
def __init__(self, objects: dict[str, bytes]) -> None:
self.objects = objects
async def paginate(self,
Bucket: str,
Prefix: str = "",
Delimiter: str | None = None):
del Bucket
if Delimiter == "/":
yield _paginate_directory(self.objects, Prefix)
else:
yield _paginate_flat(self.objects, Prefix)
class AsyncMockS3Client:
def __init__(self, objects: dict[str, bytes]) -> None:
self.objects = objects
async def get_object(self,
Bucket: str,
Key: str,
Range: str | None = None) -> dict:
del Bucket
if Key not in self.objects:
raise _mock_s3_error("NoSuchKey")
data = self.objects[Key]
if Range is not None:
data = _slice_range(data, Range)
return {"Body": AsyncMockBody(data)}
async def head_object(self, Bucket: str, Key: str) -> dict:
del Bucket
if Key not in self.objects:
raise _mock_s3_error("NoSuchKey")
return {
"ContentLength": len(self.objects[Key]),
"LastModified": LAST_MODIFIED,
"ETag": f'"{Key}"',
}
def get_paginator(self, name: str) -> AsyncMockPaginator:
assert name == "list_objects_v2"
return AsyncMockPaginator(self.objects)
async def put_object(self, Bucket: str, Key: str, Body: bytes) -> None:
self.objects[Key] = Body
async def delete_object(self, Bucket: str, Key: str) -> None:
self.objects.pop(Key, None)
async def copy_object(self, Bucket: str, CopySource: dict,
Key: str) -> None:
src_key = CopySource["Key"]
if src_key in self.objects:
self.objects[Key] = self.objects[src_key]
async def delete_objects(self, Bucket: str, Delete: dict) -> None:
for obj in Delete.get("Objects", []):
self.objects.pop(obj["Key"], None)
async def list_objects_v2(self,
Bucket: str,
Prefix: str = "",
Delimiter: str = "",
MaxKeys: int = 1000,
**kwargs) -> dict:
if Delimiter == "/":
return _paginate_directory(self.objects, Prefix)
return _paginate_flat(self.objects, Prefix)
async def __aenter__(self):
return self
async def __aexit__(self, *args):
pass
class MockAsyncSession:
def __init__(self, objects: dict[str, bytes]) -> None:
self._client = AsyncMockS3Client(objects)
def client(self, **kwargs):
return self._client
def _mock_s3_error(code: str) -> Exception:
exc = Exception(code)
exc.response = {"Error": {"Code": code}}
return exc
def _paginate_directory(objects, prefix):
common_prefixes: set[str] = set()
contents: list[dict[str, object]] = []
for key, data in sorted(objects.items()):
if not key.startswith(prefix):
continue
relative = key[len(prefix):]
if not relative:
contents.append({"Key": key, "Size": len(data)})
continue
if "/" in relative:
child = relative.split("/", 1)[0]
common_prefixes.add(prefix + child + "/")
continue
contents.append({"Key": key, "Size": len(data)})
return {
"CommonPrefixes": [{
"Prefix": v
} for v in sorted(common_prefixes)],
"Contents": contents,
}
def _paginate_flat(objects, prefix):
return {
"Contents": [{
"Key": k,
"Size": len(v)
} for k, v in sorted(objects.items()) if k.startswith(prefix)]
}
def _slice_range(data: bytes, range_spec: str) -> bytes:
if not range_spec.startswith("bytes="):
return data
bounds = range_spec.removeprefix("bytes=").split("-", 1)
start = int(bounds[0]) if bounds[0] else 0
end = int(bounds[1]) if bounds[1] else len(data) - 1
return data[start:end + 1]
def _patch_async_session(objects):
mock_session = MockAsyncSession(objects)
stack = ExitStack()
for mod in _CORE_MODULES:
stack.enter_context(
patch(f"{mod}.async_session", return_value=mock_session))
return stack
def _make_s3_env(tmp_path):
objects: dict[str, bytes] = {}
config = S3Config(
bucket=BUCKET,
region=REGION,
aws_access_key_id="testing",
aws_secret_access_key="testing",
)
resource = S3Resource(config)
return NativeTestEnv(tmp_path, resource, "s3", objects=objects)
def _make_memory_env(tmp_path):
resource = RAMResource()
return NativeTestEnv(tmp_path, resource, "ram")
def _make_disk_env(tmp_path):
disk_root = tmp_path / "disk_root"
disk_root.mkdir()
resource = DiskResource(root=str(disk_root))
return NativeTestEnv(tmp_path, resource, "disk", disk_root=disk_root)
def _make_redis_env(tmp_path):
url = os.environ["REDIS_URL"]
prefix = f"mirage:test:{uuid.uuid4().hex[:8]}:"
resource = RedisResource(url=url, key_prefix=prefix)
return NativeTestEnv(tmp_path, resource, "redis")
def _redis_available():
return "REDIS_URL" in os.environ
_PARAMS = ["ram", "s3", "disk"]
if _redis_available():
_PARAMS.append("redis")
@pytest.fixture(params=_PARAMS)
def env(request, tmp_path):
if request.param == "s3":
test_env = _make_s3_env(tmp_path)
with _patch_async_session(test_env.objects):
yield test_env
elif request.param == "disk":
yield _make_disk_env(tmp_path)
elif request.param == "redis":
test_env = _make_redis_env(tmp_path)
yield test_env
asyncio.run(test_env.resource._store.clear())
else:
yield _make_memory_env(tmp_path)
async def _drain(ait: AsyncIterator[bytes]) -> list[bytes]:
return [chunk async for chunk in ait]
def _collect(stdout: ByteSource | None) -> bytes:
if stdout is None:
return b""
if isinstance(stdout, bytes):
return stdout
return b"".join(asyncio.run(_drain(stdout)))
class NativeTestEnv:
def __init__(self,
tmp_path,
resource,
resource_type,
objects=None,
disk_root=None):
self.tmp_path = tmp_path
self.resource = resource
self.resource_type = resource_type
self.objects = objects
self.disk_root = disk_root
self.ws = Workspace(
{"/data": (resource, MountMode.WRITE)},
mode=MountMode.WRITE,
)
def create_file(self, name: str, content: bytes):
local_path = self.tmp_path / name
local_path.parent.mkdir(parents=True, exist_ok=True)
local_path.write_bytes(content)
remote_path = "/" + name
if self.resource_type == "disk":
resource_path = self.disk_root / name
resource_path.parent.mkdir(parents=True, exist_ok=True)
resource_path.write_bytes(content)
elif self.resource_type == "s3":
key = name
self.objects[key] = content
elif self.resource_type == "redis":
self._redis_write(remote_path, content)
else:
self._memory_write(remote_path, content)
def _redis_write(self, path: str, content: bytes):
store = self.resource._store
parts = path.strip("/").split("/")
for i in range(1, len(parts)):
d = "/" + "/".join(parts[:i])
asyncio.run(store.add_dir(d))
asyncio.run(store.set_file(path, content))
def _memory_write(self, path: str, content: bytes):
accessor = self.resource.accessor
parts = path.strip("/").split("/")
for i in range(1, len(parts)):
d = "/" + "/".join(parts[:i])
if d not in accessor.store.dirs:
try:
asyncio.run(mkdir(accessor, d))
except (FileExistsError, ValueError):
pass
asyncio.run(mem_write(accessor, path, content))
def native(self, cmd: str, stdin: bytes | None = None) -> str:
native_cwd = self.disk_root if self.disk_root else self.tmp_path
result = subprocess.run(
["/bin/sh", "-c", cmd],
cwd=str(native_cwd),
capture_output=True,
input=stdin,
)
return result.stdout.decode(errors="replace")
def mirage(self, cmd: str, stdin: bytes | None = None) -> str:
self.ws._cwd = "/data"
io = asyncio.run(self.ws.execute(cmd, stdin=stdin))
if io.exit_code:
import sys
err = _collect(io.stderr).decode(errors="replace")
print(
f"\nMIRAGE-DEBUG exit={io.exit_code} cmd={cmd!r} "
f"stderr={err!r}",
file=sys.stderr)
return _collect(io.stdout).decode(errors="replace")