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

This commit is contained in:
wehub-resource-sync
2026-07-13 12:30:44 +08:00
commit bcbd1bdb22
5748 changed files with 562488 additions and 0 deletions
+13
View File
@@ -0,0 +1,13 @@
# ========= 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. =========
+45
View File
@@ -0,0 +1,45 @@
# ========= 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 time
from mirage.cache.file.entry import CacheEntry
def test_cache_entry_creation():
entry = CacheEntry(cached_at=int(time.time()), size=5)
assert entry.fingerprint is None
assert entry.size == 5
def test_cache_entry_mutable():
entry = CacheEntry(cached_at=100, size=1)
entry.cached_at -= 20
assert entry.cached_at == 80
def test_expired_with_ttl():
entry = CacheEntry(cached_at=1000, size=1, ttl=10)
assert entry.expired is True
def test_not_expired_without_ttl():
entry = CacheEntry(cached_at=int(time.time()), size=1)
assert entry.ttl is None
assert entry.expired is False
def test_not_expired_within_ttl():
entry = CacheEntry(cached_at=int(time.time()), size=1, ttl=3600)
assert entry.expired is False
+300
View File
@@ -0,0 +1,300 @@
# ========= 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 pytest
from mirage.cache.file import io as cache_io
from mirage.cache.file.ram import RAMFileCacheStore
from mirage.io import CachableAsyncIterator, IOResult
from mirage.observe.record import OpRecord
def _read_record(path: str, fingerprint: str | None) -> OpRecord:
return OpRecord(op="read",
path=path,
source="s3",
bytes=0,
timestamp=0,
duration_ms=0,
fingerprint=fingerprint)
@pytest.fixture
def cache():
return RAMFileCacheStore()
# ── cache population via apply_io ────────────────────────────────────────
@pytest.mark.asyncio
async def test_apply_io_caches_reads(cache):
"""Command reads a file → apply_io stores it in cache."""
io = IOResult(
reads={"/data/file.txt": b"hello"},
cache=["/data/file.txt"],
)
await cache_io.apply_io(cache, io)
assert await cache.get("/data/file.txt") == b"hello"
@pytest.mark.asyncio
async def test_apply_io_caches_writes(cache):
"""Command writes a file and marks it cacheable → stored in cache."""
io = IOResult(
writes={"/data/out.txt": b"output"},
cache=["/data/out.txt"],
)
await cache_io.apply_io(cache, io)
assert await cache.get("/data/out.txt") == b"output"
@pytest.mark.asyncio
async def test_apply_io_reads_preferred_over_writes(cache):
"""When both reads and writes exist for same path, read data wins."""
io = IOResult(
reads={"/f.txt": b"read-data"},
writes={"/f.txt": b"write-data"},
cache=["/f.txt"],
)
await cache_io.apply_io(cache, io)
assert await cache.get("/f.txt") == b"read-data"
@pytest.mark.asyncio
async def test_apply_io_multiple_paths(cache):
"""Multiple paths in cache list are all stored."""
io = IOResult(
reads={
"/a.txt": b"aaa",
"/b.txt": b"bbb"
},
cache=["/a.txt", "/b.txt"],
)
await cache_io.apply_io(cache, io)
assert await cache.get("/a.txt") == b"aaa"
assert await cache.get("/b.txt") == b"bbb"
# ── backend fingerprint threading ───────────────────────────────────────
@pytest.mark.asyncio
async def test_apply_io_sets_backend_fingerprint_from_records(cache):
"""A read record with a backend fingerprint (ETag, cTag, sha256)
stamps the cache entry, so ALWAYS-mode is_fresh can match it."""
io = IOResult(reads={"/s3/f.txt": b"hello"}, cache=["/s3/f.txt"])
records = [_read_record("/s3/f.txt", "etag-multipart-2")]
await cache_io.apply_io(cache, io, records=records)
assert await cache.get("/s3/f.txt") == b"hello"
assert await cache.is_fresh("/s3/f.txt", "etag-multipart-2")
@pytest.mark.asyncio
async def test_apply_io_exhausted_stream_uses_record_fingerprint(cache):
"""An exhausted stream read carries its record fingerprint too."""
stream = _make_stream(b"hello")
assert await stream.drain() == b"hello"
io = IOResult(reads={"/s3/f.txt": stream}, cache=["/s3/f.txt"])
records = [_read_record("/s3/f.txt", "etag-multipart-2")]
await cache_io.apply_io(cache, io, records=records)
assert await cache.is_fresh("/s3/f.txt", "etag-multipart-2")
@pytest.mark.asyncio
async def test_apply_io_warm_reapply_preserves_fingerprint(cache):
"""A warm read re-applies cache-served bytes with no backend read
record; the entry's backend fingerprint must survive, not be
replaced by the MD5-of-content default."""
cold = IOResult(reads={"/s3/f.txt": b"hello"}, cache=["/s3/f.txt"])
await cache_io.apply_io(cache,
cold,
records=[_read_record("/s3/f.txt", "etag-3")])
warm = IOResult(reads={"/s3/f.txt": b"hello"}, cache=["/s3/f.txt"])
await cache_io.apply_io(cache, warm, records=[])
assert await cache.is_fresh("/s3/f.txt", "etag-3")
@pytest.mark.asyncio
async def test_apply_io_changed_data_without_record_resets_entry(cache):
"""New bytes with no backend fingerprint still replace the entry."""
cold = IOResult(reads={"/s3/f.txt": b"old"}, cache=["/s3/f.txt"])
await cache_io.apply_io(cache,
cold,
records=[_read_record("/s3/f.txt", "etag-3")])
fresh = IOResult(writes={"/s3/f.txt": b"new"}, cache=["/s3/f.txt"])
await cache_io.apply_io(cache, fresh, records=[])
assert await cache.get("/s3/f.txt") == b"new"
assert not await cache.is_fresh("/s3/f.txt", "etag-3")
@pytest.mark.asyncio
async def test_apply_io_drain_uses_fingerprint_recorded_during_drain():
"""Streaming backends set the record fingerprint lazily when the GET
response arrives, i.e. during the background drain. The drained
entry must pick it up."""
cache = RAMFileCacheStore()
records: list[OpRecord] = []
async def _gen():
records.append(_read_record("/s3/f.txt", "etag-multipart-2"))
yield b"hello"
stream = CachableAsyncIterator(_gen())
io = IOResult(reads={"/s3/f.txt": stream}, cache=["/s3/f.txt"])
await cache_io.apply_io(cache, io, records=records)
await asyncio.sleep(0.05)
assert await cache.get("/s3/f.txt") == b"hello"
assert await cache.is_fresh("/s3/f.txt", "etag-multipart-2")
# ── cache invalidation ──────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_apply_io_write_without_cache_invalidates(cache):
"""Write to a path NOT in cache list → invalidate (remove from cache)."""
await cache.set("/f.txt", b"old")
io = IOResult(writes={"/f.txt": b"new"})
await cache_io.apply_io(cache, io)
assert await cache.get("/f.txt") is None
# ── edge cases ───────────────────────────────────────────────────────────
@pytest.mark.asyncio
async def test_apply_io_no_cache_no_data_skips(cache):
"""Path in cache list but no data in reads/writes → skip, don't cache."""
io = IOResult(cache=["/missing.txt"])
await cache_io.apply_io(cache, io)
assert await cache.get("/missing.txt") is None
@pytest.mark.asyncio
async def test_apply_io_empty_io(cache):
"""Empty IOResult → no-op."""
io = IOResult()
await cache_io.apply_io(cache, io)
# ── background drain ────────────────────────────────────────────────────
def _make_stream(data: bytes) -> CachableAsyncIterator:
async def _gen():
yield data
return CachableAsyncIterator(_gen())
@pytest.mark.asyncio
async def test_no_duplicate_drain():
"""If a drain is already running for a path, a second apply_io
should not start another drain. The first drain finishes and
caches the data; the second stream is ignored."""
cache = RAMFileCacheStore()
stream1 = _make_stream(b"first")
stream2 = _make_stream(b"second")
io1 = IOResult(reads={"/f.txt": stream1}, cache=["/f.txt"])
await cache_io.apply_io(cache, io1)
assert "/f.txt" in cache._drain_tasks
io2 = IOResult(reads={"/f.txt": stream2}, cache=["/f.txt"])
await cache_io.apply_io(cache, io2)
assert len([k for k in cache._drain_tasks if k == "/f.txt"]) == 1
await asyncio.sleep(0.05)
assert await cache.get("/f.txt") == b"first"
@pytest.mark.asyncio
async def test_no_drain_if_already_cached():
"""If the path is already in cache, don't start a drain even if
apply_io receives an unconsumed stream for it. Existing cached
data is preserved."""
cache = RAMFileCacheStore()
await cache.set("/f.txt", b"cached")
stream = _make_stream(b"new")
io = IOResult(reads={"/f.txt": stream}, cache=["/f.txt"])
await cache_io.apply_io(cache, io)
assert "/f.txt" not in cache._drain_tasks
assert await cache.get("/f.txt") == b"cached"
# ── max_drain_bytes (cancellable cache drain) ───────────────────────────
def _make_chunked_stream(chunks: list[bytes]) -> CachableAsyncIterator:
async def _gen():
for c in chunks:
yield c
return CachableAsyncIterator(_gen())
@pytest.mark.asyncio
async def test_drain_unbounded_when_threshold_none():
"""Default behavior: full drain into cache regardless of size."""
cache = RAMFileCacheStore() # max_drain_bytes=None
chunks = [b"a" * 100 for _ in range(10)] # 1000 bytes total
stream = _make_chunked_stream(chunks)
io = IOResult(reads={"/big.txt": stream}, cache=["/big.txt"])
await cache_io.apply_io(cache, io)
await asyncio.sleep(0.05)
cached = await cache.get("/big.txt")
assert cached is not None and len(cached) == 1000
@pytest.mark.asyncio
async def test_drain_completes_below_threshold():
"""Source is smaller than threshold → full drain, cache populated."""
cache = RAMFileCacheStore(max_drain_bytes=10000)
chunks = [b"x" * 100 for _ in range(5)] # 500 bytes total
stream = _make_chunked_stream(chunks)
io = IOResult(reads={"/small.txt": stream}, cache=["/small.txt"])
await cache_io.apply_io(cache, io)
await asyncio.sleep(0.05)
cached = await cache.get("/small.txt")
assert cached is not None and len(cached) == 500
@pytest.mark.asyncio
async def test_drain_cancelled_above_threshold():
"""Source exceeds threshold → drain stops, partial buffer NOT cached."""
cache = RAMFileCacheStore(max_drain_bytes=300)
chunks = [b"z" * 100 for _ in range(20)] # 2000 bytes total
stream = _make_chunked_stream(chunks)
io = IOResult(reads={"/huge.txt": stream}, cache=["/huge.txt"])
await cache_io.apply_io(cache, io)
await asyncio.sleep(0.05)
assert await cache.get("/huge.txt") is None
@pytest.mark.asyncio
async def test_drain_threshold_per_task_not_shared():
"""Each drain task has its own counter, not a shared workspace pool."""
cache = RAMFileCacheStore(max_drain_bytes=300)
s1 = _make_chunked_stream([b"a" * 100, b"a" * 100]) # 200 < 300
s2 = _make_chunked_stream([b"b" * 100, b"b" * 100]) # 200 < 300
io1 = IOResult(reads={"/a.txt": s1}, cache=["/a.txt"])
io2 = IOResult(reads={"/b.txt": s2}, cache=["/b.txt"])
await cache_io.apply_io(cache, io1)
await cache_io.apply_io(cache, io2)
await asyncio.sleep(0.05)
# Both fit individually under the per-task budget → both cached.
assert await cache.get("/a.txt") is not None
assert await cache.get("/b.txt") is not None
+261
View File
@@ -0,0 +1,261 @@
# ========= 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 patch
import pytest
from mirage.cache.file.ram import RAMFileCacheStore
class TestGetSet:
@pytest.mark.asyncio
async def test_set_and_get(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"hello")
result = await cache.get("/a")
assert result == b"hello"
@pytest.mark.asyncio
async def test_get_miss(self):
cache = RAMFileCacheStore(cache_limit="1MB")
assert await cache.get("/missing") is None
@pytest.mark.asyncio
async def test_set_overwrites(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"first")
await cache.set("/a", b"second")
assert await cache.get("/a") == b"second"
@pytest.mark.asyncio
async def test_default_fingerprint(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"data")
entry = cache._entries.get("/a")
assert entry is not None
assert entry.fingerprint is not None
@pytest.mark.asyncio
async def test_explicit_fingerprint(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"data", fingerprint="etag-123")
entry = cache._entries["/a"]
assert entry.fingerprint == "etag-123"
class TestAdd:
@pytest.mark.asyncio
async def test_add_new_key(self):
cache = RAMFileCacheStore(cache_limit="1MB")
result = await cache.add("/a", b"data")
assert result is True
assert await cache.get("/a") == b"data"
@pytest.mark.asyncio
async def test_add_existing_key(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"first")
result = await cache.add("/a", b"second")
assert result is False
assert await cache.get("/a") == b"first"
class TestMulti:
@pytest.mark.asyncio
async def test_multi_get(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"aaa")
await cache.set("/b", b"bbb")
results = await cache.multi_get(["/a", "/missing", "/b"])
assert results[0] == b"aaa"
assert results[1] is None
assert results[2] == b"bbb"
@pytest.mark.asyncio
async def test_multi_set(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.multi_set([("/a", b"aaa"), ("/b", b"bbb")])
assert await cache.get("/a") == b"aaa"
assert await cache.get("/b") == b"bbb"
class TestExistsRemoveClear:
@pytest.mark.asyncio
async def test_exists(self):
cache = RAMFileCacheStore(cache_limit="1MB")
assert await cache.exists("/a") is False
await cache.set("/a", b"data")
assert await cache.exists("/a") is True
@pytest.mark.asyncio
async def test_remove(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"data")
await cache.remove("/a")
assert await cache.get("/a") is None
@pytest.mark.asyncio
async def test_remove_missing(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.remove("/missing")
@pytest.mark.asyncio
async def test_remove_cancels_drain_task(self):
cache = RAMFileCacheStore(cache_limit="1MB")
cancelled = False
async def slow_drain():
nonlocal cancelled
try:
await asyncio.sleep(10)
except asyncio.CancelledError:
cancelled = True
task = asyncio.create_task(slow_drain())
cache._drain_tasks["/a"] = task
await cache.set("/a", b"data")
await asyncio.sleep(0)
await cache.remove("/a")
await asyncio.sleep(0)
assert cancelled
assert "/a" not in cache._drain_tasks
@pytest.mark.asyncio
async def test_clear(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"aaa")
await cache.set("/b", b"bbb")
await cache.clear()
assert await cache.get("/a") is None
assert await cache.get("/b") is None
assert cache.cache_size == 0
@pytest.mark.asyncio
async def test_clear_concurrent_with_set(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"aaa")
async def do_clear():
await cache.clear()
async def do_set():
await cache.set("/b", b"bbb")
await asyncio.gather(do_clear(), do_set())
actual_size = sum(e.size for e in cache._entries.values())
assert cache._cache_size == actual_size
class TestTTL:
@pytest.mark.asyncio
async def test_ttl_not_expired(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"data", ttl=60)
assert await cache.get("/a") == b"data"
@pytest.mark.asyncio
async def test_ttl_expired(self):
cache = RAMFileCacheStore(cache_limit="1MB")
with patch("mirage.cache.file.entry.time.time", return_value=1000.0):
await cache.set("/a", b"data", ttl=10)
with patch("mirage.cache.file.entry.time.time", return_value=1011.0):
result = await cache.get("/a")
assert result is None
@pytest.mark.asyncio
async def test_no_ttl_never_expires(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"data")
entry = cache._entries["/a"]
assert entry.ttl is None
assert entry.expired is False
class TestFingerprint:
@pytest.mark.asyncio
async def test_is_fresh_match(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"data", fingerprint="etag-1")
assert await cache.is_fresh("/a", "etag-1") is True
@pytest.mark.asyncio
async def test_is_fresh_mismatch(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"data", fingerprint="etag-1")
assert await cache.is_fresh("/a", "etag-2") is False
@pytest.mark.asyncio
async def test_is_fresh_missing(self):
cache = RAMFileCacheStore(cache_limit="1MB")
assert await cache.is_fresh("/missing", "etag-1") is False
class TestEviction:
@pytest.mark.asyncio
async def test_lru_evicts_oldest(self):
cache = RAMFileCacheStore(cache_limit=100)
await cache.set("/a", b"x" * 60)
await cache.set("/b", b"y" * 60)
assert await cache.get("/a") is None
assert await cache.get("/b") == b"y" * 60
@pytest.mark.asyncio
async def test_lru_access_refreshes(self):
cache = RAMFileCacheStore(cache_limit=150)
await cache.set("/a", b"x" * 50)
await cache.set("/b", b"y" * 50)
await cache.get("/a")
await cache.set("/c", b"z" * 60)
assert await cache.get("/a") is not None
assert await cache.get("/b") is None
@pytest.mark.asyncio
async def test_cache_size_tracking(self):
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"x" * 100)
assert cache.cache_size == 100
await cache.set("/b", b"y" * 200)
assert cache.cache_size == 300
await cache.remove("/a")
assert cache.cache_size == 200
@pytest.mark.asyncio
async def test_cache_limit(self):
cache = RAMFileCacheStore(cache_limit="1KB")
assert cache.cache_limit == 1024
@pytest.mark.asyncio
async def test_eviction_does_not_corrupt_size(self):
cache = RAMFileCacheStore(cache_limit=100)
await cache.set("/a", b"x" * 60)
async def concurrent_set():
await cache.set("/a", b"z" * 40)
async def trigger_eviction():
await cache.set("/b", b"y" * 60)
await asyncio.gather(concurrent_set(), trigger_eviction())
actual_size = sum(e.size for e in cache._entries.values())
assert cache._cache_size == actual_size
+101
View File
@@ -0,0 +1,101 @@
# ========= 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 pytest
from mirage.cache.file.ram import RAMFileCacheStore
@pytest.mark.asyncio
async def test_data_stored_in_store_files():
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/f.txt", b"hello")
assert cache._store.files["/f.txt"] == b"hello"
@pytest.mark.asyncio
async def test_entry_stored_in_entries():
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/f.txt", b"hello")
entry = cache._entries["/f.txt"]
assert entry.size == 5
@pytest.mark.asyncio
async def test_remove_cleans_store():
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/f.txt", b"data")
assert "/f.txt" in cache._store.files
await cache.remove("/f.txt")
assert "/f.txt" not in cache._store.files
assert "/f.txt" not in cache._entries
@pytest.mark.asyncio
async def test_clear_empties_store():
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"aaa")
await cache.set("/b", b"bbb")
await cache.clear()
assert len(cache._store.files) == 0
assert len(cache._entries) == 0
@pytest.mark.asyncio
async def test_locks_cleaned_after_remove():
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"data")
await cache.remove("/a")
assert "/a" not in cache._key_locks
@pytest.mark.asyncio
async def test_locks_cleaned_after_clear():
cache = RAMFileCacheStore(cache_limit="1MB")
await cache.set("/a", b"aaa")
await cache.set("/b", b"bbb")
await cache.clear()
assert len(cache._key_locks) == 0
@pytest.mark.asyncio
async def test_locks_cleaned_after_eviction():
cache = RAMFileCacheStore(cache_limit=100)
await cache.set("/a", b"x" * 60)
await cache.set("/b", b"y" * 60)
assert "/a" not in cache._key_locks
@pytest.mark.asyncio
async def test_drain_task_cancelled_on_remove():
cache = RAMFileCacheStore(cache_limit="1MB")
cancelled = False
async def slow():
nonlocal cancelled
try:
await asyncio.sleep(10)
except asyncio.CancelledError:
cancelled = True
task = asyncio.create_task(slow())
cache._drain_tasks["/a"] = task
await cache.set("/a", b"data")
await asyncio.sleep(0)
await cache.remove("/a")
await asyncio.sleep(0)
assert cancelled
+176
View File
@@ -0,0 +1,176 @@
# ========= 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 pytest
import pytest_asyncio
from mirage.cache.file import io as cache_io
from mirage.cache.file.redis import RedisFileCacheStore
from mirage.io import CachableAsyncIterator, IOResult
from mirage.observe.record import OpRecord
REDIS_URL = os.environ.get("REDIS_URL", "")
pytestmark = pytest.mark.skipif(not REDIS_URL, reason="REDIS_URL not set")
@pytest_asyncio.fixture()
async def cache():
c = RedisFileCacheStore(
cache_limit="1MB",
url=REDIS_URL,
key_prefix="test:cache:",
)
await c.clear()
yield c
await c.clear()
await c._store.close()
@pytest.mark.asyncio
async def test_set_and_get(cache):
await cache.set("/file.txt", b"hello")
result = await cache.get("/file.txt")
assert result == b"hello"
@pytest.mark.asyncio
async def test_get_missing(cache):
result = await cache.get("/nope")
assert result is None
@pytest.mark.asyncio
async def test_remove(cache):
await cache.set("/file.txt", b"data")
await cache.remove("/file.txt")
assert await cache.get("/file.txt") is None
@pytest.mark.asyncio
async def test_exists(cache):
assert await cache.exists("/file.txt") is False
await cache.set("/file.txt", b"data")
assert await cache.exists("/file.txt") is True
@pytest.mark.asyncio
async def test_is_fresh(cache):
await cache.set("/file.txt", b"data", fingerprint="abc123")
assert await cache.is_fresh("/file.txt", "abc123") is True
assert await cache.is_fresh("/file.txt", "different") is False
@pytest.mark.asyncio
async def test_is_fresh_missing(cache):
assert await cache.is_fresh("/nope", "abc") is False
@pytest.mark.asyncio
async def test_clear(cache):
await cache.set("/a.txt", b"a")
await cache.set("/b.txt", b"b")
await cache.clear()
assert await cache.get("/a.txt") is None
assert await cache.get("/b.txt") is None
@pytest.mark.asyncio
async def test_add_new(cache):
result = await cache.add("/file.txt", b"data")
assert result is True
assert await cache.get("/file.txt") == b"data"
@pytest.mark.asyncio
async def test_add_existing(cache):
await cache.set("/file.txt", b"first")
result = await cache.add("/file.txt", b"second")
assert result is False
assert await cache.get("/file.txt") == b"first"
@pytest.mark.asyncio
async def test_set_with_fingerprint(cache):
await cache.set("/file.txt", b"data", fingerprint="fp1")
assert await cache.is_fresh("/file.txt", "fp1") is True
@pytest.mark.asyncio
async def test_cache_limit(cache):
assert cache.cache_limit == 1 * 1024 * 1024
@pytest.mark.asyncio
async def test_key_prefix_isolation():
c1 = RedisFileCacheStore(url=REDIS_URL, key_prefix="test:cache:ns1:")
c2 = RedisFileCacheStore(url=REDIS_URL, key_prefix="test:cache:ns2:")
await c1.clear()
await c2.clear()
await c1.set("/shared", b"from-c1")
assert await c2.get("/shared") is None
assert await c1.get("/shared") == b"from-c1"
await c1.clear()
await c2.clear()
await c1._store.close()
await c2._store.close()
@pytest.mark.asyncio
async def test_apply_io_drains_stream_into_cache(cache):
"""An unexhausted stream must background-drain into the Redis cache
like the RAM store does, carrying the record fingerprint."""
async def _gen():
yield b"drained"
stream = CachableAsyncIterator(_gen())
io = IOResult(reads={"/file.txt": stream}, cache=["/file.txt"])
records = [
OpRecord(op="read",
path="/file.txt",
source="s3",
bytes=0,
timestamp=0,
duration_ms=0,
fingerprint="etag-9")
]
await cache_io.apply_io(cache, io, records=records)
tasks = list(cache._drain_tasks.values())
assert tasks, "drain task must be registered"
await asyncio.gather(*tasks)
assert await cache.get("/file.txt") == b"drained"
assert await cache.is_fresh("/file.txt", "etag-9") is True
@pytest.mark.asyncio
async def test_remove_cancels_pending_drain(cache):
started = asyncio.Event()
async def _gen():
started.set()
await asyncio.sleep(1)
yield b"slow"
stream = CachableAsyncIterator(_gen())
io = IOResult(reads={"/slow.txt": stream}, cache=["/slow.txt"])
await cache_io.apply_io(cache, io)
assert "/slow.txt" in cache._drain_tasks
await started.wait()
await cache.remove("/slow.txt")
assert "/slow.txt" not in cache._drain_tasks
await asyncio.sleep(0.05)
assert await cache.get("/slow.txt") is None
+53
View File
@@ -0,0 +1,53 @@
# ========= 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 mirage.cache.file.utils import default_fingerprint, parse_limit
def test_parse_limit_bytes():
assert parse_limit(1024) == 1024
def test_parse_limit_kb():
assert parse_limit("1KB") == 1024
def test_parse_limit_mb():
assert parse_limit("512MB") == 536870912
def test_parse_limit_gb():
assert parse_limit("1GB") == 1073741824
def test_parse_limit_lowercase():
assert parse_limit("10mb") == 10485760
def test_parse_limit_plain_string():
assert parse_limit("1024") == 1024
def test_default_fingerprint():
fp = default_fingerprint(b"hello")
assert isinstance(fp, str)
assert len(fp) == 32
def test_default_fingerprint_deterministic():
assert default_fingerprint(b"same") == default_fingerprint(b"same")
def test_default_fingerprint_different_data():
assert default_fingerprint(b"a") != default_fingerprint(b"b")