jw-pkg/test/unit/python/jw/pkg/lib/FcCache/test.py

198 lines
6.1 KiB
Python
Raw Normal View History

from __future__ import annotations
import asyncio
import os
import tempfile
from typing import TYPE_CHECKING, override
if TYPE_CHECKING:
from jw.pkg.lib.file_types import FileType
from jw.pkg.lib.base import StatResult
from jw.pkg.lib.ec.Local import Local
from jw.pkg.lib.ExecContext import ExecContext
from jw.pkg.lib.FcCache import FcCache
from jw.pkg.lib.file_types import FT_DIR, FT_REG
from jw.pkg.lib.FileContext import FileContext
from jw.pkg.lib.Result import Result
class FakeCtx(FileContext):
"""An in-memory FileContext that counts its primitive calls"""
def __init__(self, uri: str, chroot: bool = False) -> None:
super().__init__(uri, chroot = chroot)
self.files: dict[str, bytes] = {}
self.dirs: set[str] = set()
self.counts: dict[str, int] = {}
def count(self, op: str) -> None:
self.counts[op] = self.counts.get(op, 0) + 1
def _stat_of(self, path: str) -> StatResult:
if path in self.dirs:
return StatResult(0o40755, '0', '0', 40, 0.0, 0.0, 0.0)
if path in self.files:
return StatResult(0o100644, '0', '0', len(self.files[path]), 0.0, 0.0, 0.0)
raise FileNotFoundError(path)
@override
async def _get(
self, path: str, wd: str | None, throw: bool, verbose: bool | None, title: str
) -> Result:
self.count('get')
if wd is not None:
path = wd + '/' + path
if path not in self.files:
raise FileNotFoundError(path)
return Result(stdout = self.files[path])
@override
async def _put(
self,
path: str,
content: bytes,
wd: str | None,
throw: bool,
verbose: bool | None,
title: str,
owner: str | None,
group: str | None,
mode: str | None,
atomic: bool,
ftype: FileType,
) -> Result:
self.count('put')
if wd is not None:
path = wd + '/' + path
self.files[path] = content
return Result(stdout = b'')
@override
async def _mkdir(self, path: str, mode: int, ftype: FileType) -> None:
self.count('mkdir')
self.dirs.add(path)
@override
async def _unlink(self, path: str) -> None:
self.count('unlink')
self.files.pop(path, None)
self.dirs.discard(path)
@override
async def _stat(self, path: str, follow_symlinks: bool) -> StatResult:
self.count('stat')
return self._stat_of(path)
class CountingLocal(Local):
"""A Local context that counts its executed commands"""
def __init__(self) -> None:
super().__init__('local')
self.runs = 0
@override
async def _run(
self,
cmd: list[str],
wd: str | None,
verbose: bool,
cmd_input: bytes | None,
mod_env: dict[str, str] | None,
interactive: bool,
log_prefix: str,
) -> Result:
self.runs += 1
return await super()._run(
cmd, wd, verbose, cmd_input, mod_env, interactive, log_prefix
)
async def main() -> None:
fake = FakeCtx('local:///tmp/fc-cache-fake', chroot = True)
fc = FcCache(fake)
await fc.mkdir('d', 0o755, FT_DIR)
await fc.put('d/a', b'x', ftype = FT_REG)
# -- Successful stats are cached
st = await fc.stat('d/a')
assert st.size == 1
assert (await fc.stat('d/a')).size == 1
assert fake.counts['stat'] == 1
# -- Absences are cached, too
assert await fc.file_exists('d/missing') is False
assert await fc.file_exists('d/missing') is False
assert fake.counts['stat'] == 2
# -- Gets are cached per (path, wd, throw)
ret = await fc.get('d/a')
assert (await fc.get('d/a')).stdout == b'x'
assert fake.counts['get'] == 1
ret = await fc._get('a', '/tmp/fc-cache-fake/d', True, None, 'get')
assert fake.counts['get'] == 2
assert ret.stdout == b'x'
# -- Writes drop the cache entries of their paths
await fc.put('d/a', b'xy', ftype = FT_REG)
assert (await fc.get('d/a')).stdout == b'xy'
assert fake.counts['get'] == 3
assert (await fc.stat('d/a')).size == 2
assert fake.counts['stat'] == 3
# -- Unlink drops them as well
await fc.unlink('d/a')
assert await fc.file_exists('d/a') is False
assert fake.counts['stat'] == 4
assert '/tmp/fc-cache-fake/d/a' not in fake.files
# -- The wrapper itself is not an exec context
assert not isinstance(fc, ExecContext)
# -- warm() on a non-exec context: one stat per path, then cached
fake2 = FakeCtx('local:///tmp/fc-cache-warm', chroot = True)
fc2 = FcCache(fake2)
await fc2.put('p/q', b'z', ftype = FT_REG)
await fc2.warm(['p', 'p/q', 'p/r'])
assert fake2.counts['stat'] == 3
await fc2.warm(['p', 'p/q', 'p/r'])
assert fake2.counts['stat'] == 3
assert await fc2.file_exists('p/r') is False
assert fake2.counts['stat'] == 3
# -- warm() on an exec context: one host command for all paths
with tempfile.TemporaryDirectory() as td:
f = os.path.join(td, 'there')
with open(f, 'w') as fh:
fh.write('hello\n')
cl = CountingLocal()
fc3 = FcCache(cl)
missing = os.path.join(td, 'not-there')
await fc3.warm([f, missing])
assert cl.runs == 1
st = await fc3.stat(f)
assert cl.runs == 1
assert st.size == 6
assert await fc3.file_exists(missing) is False
assert cl.runs == 1
try:
await fc3.stat(missing)
assert False, 'stat of a warmed absence must raise'
except FileNotFoundError:
pass
assert cl.runs == 1
# -- A put through the cache lands in the wrapped context and
# drops its cache entries, so the next stat is a fresh one.
# Local stats without a command, so file_exists adds no run
g = os.path.join(td, 'new')
await fc3.put(g, b'fresh\n', ftype = FT_REG)
assert cl.runs == 2
assert (await fc3.get(g)).stdout == b'fresh\n'
assert cl.runs == 3
assert await fc3.file_exists(g) is True
assert cl.runs == 3
assert await fc3.file_exists(g) is True
assert cl.runs == 3
asyncio.run(main())