mirror of
https://github.com/baketnk/frame-yap.git
synced 2026-10-06 02:00:06 +02:00
Allow configured local inference in native-only installs
This commit is contained in:
1 parent
1ba0f26e0e
commit
a19d95a353
8 files changed
+129
-26
No files matched your search
+2
-1
@@ -26,7 +26,8 @@ are deliberately generic.
|
|||||||
The private pipes use unsigned LE32 payload lengths (1..65536), a one-byte
|
The private pipes use unsigned LE32 payload lengths (1..65536), a one-byte
|
||||||
message type and, for requests/replies, unsigned LE64 request ID. `T` + ID
|
message type and, for requests/replies, unsigned LE64 request ID. `T` + ID
|
||||||
requests reading the fixed clip; `Y` means ready; `F` means load failure
|
requests reading the fixed clip; `Y` means ready; `F` means load failure
|
||||||
(optional `M` for missing model, `D` for runtime failure); `R` + ID + UTF-8
|
(`M` for missing/mismatched pinned model or private clip directory, `I` for a
|
||||||
|
missing Python dependency, `D` for runtime/model load failure); `R` + ID + UTF-8
|
||||||
text and `E` + ID + generic UTF-8 error are replies. Text is at most 4096
|
text and `E` + ID + generic UTF-8 error are replies. Text is at most 4096
|
||||||
bytes. An unexpected or duplicate reply, wrong ID, extra frame, closed pipe
|
bytes. An unexpected or duplicate reply, wrong ID, extra frame, closed pipe
|
||||||
or oversized frame stops the worker. Warmup deadline is 120 s, transcription
|
or oversized frame stops the worker. Warmup deadline is 120 s, transcription
|
||||||
|
|||||||
+26
-7
@@ -200,7 +200,7 @@ def select(root, link, target):
|
|||||||
temp.unlink(missing_ok=True)
|
temp.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def desired_launcher(root):
|
def desired_launcher(root, legacy=False):
|
||||||
import shlex
|
import shlex
|
||||||
q = lambda path: shlex.quote(str(path))
|
q = lambda path: shlex.quote(str(path))
|
||||||
font = root / "current/fonts/font.ttf"
|
font = root / "current/fonts/font.ttf"
|
||||||
@@ -208,11 +208,29 @@ def desired_launcher(root):
|
|||||||
flags = ["--assets", base / "assets", "--font", font,
|
flags = ["--assets", base / "assets", "--font", font,
|
||||||
"--worker", base / "python/frameyap/worker.py"]
|
"--worker", base / "python/frameyap/worker.py"]
|
||||||
args = " ".join(q(item) for item in flags)
|
args = " ".join(q(item) for item in flags)
|
||||||
|
# Parse two literal absolute paths, never source/eval this user-owned file.
|
||||||
|
# Environment overrides remain useful for a one-off explicit launch.
|
||||||
|
config = ('' if legacy else
|
||||||
|
'CONFIG=${XDG_CONFIG_HOME:-$HOME/.config}/frameyap/paths.conf\n'
|
||||||
|
'CONFIG_PYTHON= CONFIG_MODEL=\n'
|
||||||
|
'if [ -f "$CONFIG" ] && [ ! -L "$CONFIG" ]; then\n'
|
||||||
|
' while IFS= read -r line || [ -n "$line" ]; do\n'
|
||||||
|
' case "$line" in\n'
|
||||||
|
' python=/*) CONFIG_PYTHON=${line#python=};;\n'
|
||||||
|
' model=/*) CONFIG_MODEL=${line#model=};;\n'
|
||||||
|
' esac\n'
|
||||||
|
' done < "$CONFIG"\n'
|
||||||
|
'fi\n')
|
||||||
|
python_default = (q(base / "runtime/bin/python3") if legacy else
|
||||||
|
'${CONFIG_PYTHON:-' + q(base / "runtime/bin/python3") + '}')
|
||||||
|
model_default = (q(base / "model") if legacy else
|
||||||
|
'${CONFIG_MODEL:-' + q(base / "model") + '}')
|
||||||
return ("#!/bin/sh\n" + MARKER + 'export PYTHONDONTWRITEBYTECODE=1\n'
|
return ("#!/bin/sh\n" + MARKER + 'export PYTHONDONTWRITEBYTECODE=1\n'
|
||||||
+ f'export FRAMEYAP_INSTALL_ROOT={q(root)}\n'
|
+ f'export FRAMEYAP_INSTALL_ROOT={q(root)}\n'
|
||||||
+ f'export LD_LIBRARY_PATH={q(base / "lib")}${{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}}\n'
|
+ f'export LD_LIBRARY_PATH={q(base / "lib")}${{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}}\n'
|
||||||
+ f'PYTHON=${{FRAMEYAP_PYTHON:-{q(base / "runtime/bin/python3")}}}\n'
|
+ config
|
||||||
+ f'MODEL=${{FRAMEYAP_MODEL:-{q(base / "model")}}}\n'
|
+ f'PYTHON=${{FRAMEYAP_PYTHON:-{python_default}}}\n'
|
||||||
|
+ f'MODEL=${{FRAMEYAP_MODEL:-{model_default}}}\n'
|
||||||
+ 'if [ "$#" -eq 0 ]; then set -- --run; fi\n'
|
+ 'if [ "$#" -eq 0 ]; then set -- --run; fi\n'
|
||||||
+ 'case "$1" in\n'
|
+ 'case "$1" in\n'
|
||||||
+ ' --run)\n'
|
+ ' --run)\n'
|
||||||
@@ -242,21 +260,22 @@ def desired_desktop(launcher):
|
|||||||
f'Exec="{executable}"\nTerminal=false\nCategories=Utility;\n').encode()
|
f'Exec="{executable}"\nTerminal=false\nCategories=Utility;\n').encode()
|
||||||
|
|
||||||
|
|
||||||
def check_owned_file(path, expected):
|
def check_owned_file(path, expected, alternatives=()):
|
||||||
if path.is_symlink():
|
if path.is_symlink():
|
||||||
fail(f"refusing foreign symlink: {path}")
|
fail(f"refusing foreign symlink: {path}")
|
||||||
if path.exists():
|
if path.exists():
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
fail(f"refusing foreign path: {path}")
|
fail(f"refusing foreign path: {path}")
|
||||||
data = path.read_bytes()
|
data = path.read_bytes()
|
||||||
if data != expected:
|
if data != expected and data not in alternatives:
|
||||||
fail(f"refusing to replace modified/foreign file: {path}")
|
fail(f"refusing to replace modified/foreign file: {path}")
|
||||||
|
|
||||||
|
|
||||||
def check_wrappers(root, launcher):
|
def check_wrappers(root, launcher):
|
||||||
manifest = root / "frameyap.vrmanifest"
|
manifest = root / "frameyap.vrmanifest"
|
||||||
desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode()
|
desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode()
|
||||||
check_owned_file(launcher, desired_launcher(root))
|
# Accept only the exact earlier managed script for migration.
|
||||||
|
check_owned_file(launcher, desired_launcher(root), (desired_launcher(root, legacy=True),))
|
||||||
check_owned_file(manifest, desired)
|
check_owned_file(manifest, desired)
|
||||||
check_owned_file(desktop_path(root), desired_desktop(launcher))
|
check_owned_file(desktop_path(root), desired_desktop(launcher))
|
||||||
return manifest, desired
|
return manifest, desired
|
||||||
@@ -387,7 +406,7 @@ def uninstall(root, launcher):
|
|||||||
manifest = root / "frameyap.vrmanifest"
|
manifest = root / "frameyap.vrmanifest"
|
||||||
desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode()
|
desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode()
|
||||||
check_owned_file(manifest, desired)
|
check_owned_file(manifest, desired)
|
||||||
check_owned_file(launcher, desired_launcher(root))
|
check_owned_file(launcher, desired_launcher(root), (desired_launcher(root, legacy=True),))
|
||||||
desktop = desktop_path(root)
|
desktop = desktop_path(root)
|
||||||
check_owned_file(desktop, desired_desktop(launcher))
|
check_owned_file(desktop, desired_desktop(launcher))
|
||||||
versions = root / "versions"
|
versions = root / "versions"
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ except ImportError: # direct executable script
|
|||||||
from model_files import REVISION, FILES
|
from model_files import REVISION, FILES
|
||||||
|
|
||||||
|
|
||||||
|
class LocalModelError(ValueError):
|
||||||
|
"""Missing, incomplete or wrong pinned local weights (never a download cue)."""
|
||||||
|
|
||||||
|
|
||||||
def read_exact(fd, count):
|
def read_exact(fd, count):
|
||||||
parts = bytearray()
|
parts = bytearray()
|
||||||
while len(parts) < count:
|
while len(parts) < count:
|
||||||
@@ -72,17 +76,17 @@ def local_model(path):
|
|||||||
"""Require a real local directory with weight file(s), never a hub identifier."""
|
"""Require a real local directory with weight file(s), never a hub identifier."""
|
||||||
root = Path(path)
|
root = Path(path)
|
||||||
if not root.is_absolute() or not root.is_dir() or root.is_symlink():
|
if not root.is_absolute() or not root.is_dir() or root.is_symlink():
|
||||||
raise ValueError("absolute local model directory required")
|
raise LocalModelError("absolute local model directory required")
|
||||||
for name, (size, expected) in FILES.items():
|
for name, (size, expected) in FILES.items():
|
||||||
file = root / name
|
file = root / name
|
||||||
if not file.is_file() or file.is_symlink() or file.stat().st_size != size:
|
if not file.is_file() or file.is_symlink() or file.stat().st_size != size:
|
||||||
raise ValueError("pinned local Redux model weights missing or incomplete")
|
raise LocalModelError("pinned local Redux model weights missing or incomplete")
|
||||||
digest = hashlib.sha256()
|
digest = hashlib.sha256()
|
||||||
with file.open("rb") as stream:
|
with file.open("rb") as stream:
|
||||||
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
||||||
digest.update(chunk)
|
digest.update(chunk)
|
||||||
if digest.hexdigest() != expected:
|
if digest.hexdigest() != expected:
|
||||||
raise ValueError("local Redux model does not match pinned revision")
|
raise LocalModelError("local Redux model does not match pinned revision")
|
||||||
return str(root)
|
return str(root)
|
||||||
|
|
||||||
|
|
||||||
@@ -170,14 +174,17 @@ def main(argv=None):
|
|||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
private_dir(args.clip_dir)
|
private_dir(args.clip_dir)
|
||||||
# The full hash validation occurs exactly once in load_model.
|
|
||||||
if not Path(args.model).is_dir():
|
|
||||||
raise ValueError("missing model")
|
|
||||||
except Exception:
|
except Exception:
|
||||||
send_frame(protocol_fd, b"F", b"M")
|
send_frame(protocol_fd, b"F", b"M")
|
||||||
return 1
|
return 1
|
||||||
try:
|
try:
|
||||||
model = load_model(args.model, args.threads)
|
model = load_model(args.model, args.threads)
|
||||||
|
except LocalModelError:
|
||||||
|
send_frame(protocol_fd, b"F", b"M")
|
||||||
|
return 1
|
||||||
|
except ImportError:
|
||||||
|
send_frame(protocol_fd, b"F", b"I")
|
||||||
|
return 1
|
||||||
except Exception:
|
except Exception:
|
||||||
send_frame(protocol_fd, b"F", b"D")
|
send_frame(protocol_fd, b"F", b"D")
|
||||||
return 1
|
return 1
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ def select(root, link, target):
|
|||||||
temp.unlink(missing_ok=True)
|
temp.unlink(missing_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def desired_launcher(root):
|
def desired_launcher(root, legacy=False):
|
||||||
import shlex
|
import shlex
|
||||||
q = lambda path: shlex.quote(str(path))
|
q = lambda path: shlex.quote(str(path))
|
||||||
font = root / "current/fonts/font.ttf"
|
font = root / "current/fonts/font.ttf"
|
||||||
@@ -197,11 +197,29 @@ def desired_launcher(root):
|
|||||||
flags = ["--assets", base / "assets", "--font", font,
|
flags = ["--assets", base / "assets", "--font", font,
|
||||||
"--worker", base / "python/frameyap/worker.py"]
|
"--worker", base / "python/frameyap/worker.py"]
|
||||||
args = " ".join(q(item) for item in flags)
|
args = " ".join(q(item) for item in flags)
|
||||||
|
# Parse two literal absolute paths, never source/eval this user-owned file.
|
||||||
|
# Environment overrides remain useful for a one-off explicit launch.
|
||||||
|
config = ('' if legacy else
|
||||||
|
'CONFIG=${XDG_CONFIG_HOME:-$HOME/.config}/frameyap/paths.conf\n'
|
||||||
|
'CONFIG_PYTHON= CONFIG_MODEL=\n'
|
||||||
|
'if [ -f "$CONFIG" ] && [ ! -L "$CONFIG" ]; then\n'
|
||||||
|
' while IFS= read -r line || [ -n "$line" ]; do\n'
|
||||||
|
' case "$line" in\n'
|
||||||
|
' python=/*) CONFIG_PYTHON=${line#python=};;\n'
|
||||||
|
' model=/*) CONFIG_MODEL=${line#model=};;\n'
|
||||||
|
' esac\n'
|
||||||
|
' done < "$CONFIG"\n'
|
||||||
|
'fi\n')
|
||||||
|
python_default = (q(base / "runtime/bin/python3") if legacy else
|
||||||
|
'${CONFIG_PYTHON:-' + q(base / "runtime/bin/python3") + '}')
|
||||||
|
model_default = (q(base / "model") if legacy else
|
||||||
|
'${CONFIG_MODEL:-' + q(base / "model") + '}')
|
||||||
return ("#!/bin/sh\n" + MARKER + 'export PYTHONDONTWRITEBYTECODE=1\n'
|
return ("#!/bin/sh\n" + MARKER + 'export PYTHONDONTWRITEBYTECODE=1\n'
|
||||||
+ f'export FRAMEYAP_INSTALL_ROOT={q(root)}\n'
|
+ f'export FRAMEYAP_INSTALL_ROOT={q(root)}\n'
|
||||||
+ f'export LD_LIBRARY_PATH={q(base / "lib")}${{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}}\n'
|
+ f'export LD_LIBRARY_PATH={q(base / "lib")}${{LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}}\n'
|
||||||
+ f'PYTHON=${{FRAMEYAP_PYTHON:-{q(base / "runtime/bin/python3")}}}\n'
|
+ config
|
||||||
+ f'MODEL=${{FRAMEYAP_MODEL:-{q(base / "model")}}}\n'
|
+ f'PYTHON=${{FRAMEYAP_PYTHON:-{python_default}}}\n'
|
||||||
|
+ f'MODEL=${{FRAMEYAP_MODEL:-{model_default}}}\n'
|
||||||
+ 'if [ "$#" -eq 0 ]; then set -- --run; fi\n'
|
+ 'if [ "$#" -eq 0 ]; then set -- --run; fi\n'
|
||||||
+ 'case "$1" in\n'
|
+ 'case "$1" in\n'
|
||||||
+ ' --run)\n'
|
+ ' --run)\n'
|
||||||
@@ -231,21 +249,22 @@ def desired_desktop(launcher):
|
|||||||
f'Exec="{executable}"\nTerminal=false\nCategories=Utility;\n').encode()
|
f'Exec="{executable}"\nTerminal=false\nCategories=Utility;\n').encode()
|
||||||
|
|
||||||
|
|
||||||
def check_owned_file(path, expected):
|
def check_owned_file(path, expected, alternatives=()):
|
||||||
if path.is_symlink():
|
if path.is_symlink():
|
||||||
fail(f"refusing foreign symlink: {path}")
|
fail(f"refusing foreign symlink: {path}")
|
||||||
if path.exists():
|
if path.exists():
|
||||||
if not path.is_file():
|
if not path.is_file():
|
||||||
fail(f"refusing foreign path: {path}")
|
fail(f"refusing foreign path: {path}")
|
||||||
data = path.read_bytes()
|
data = path.read_bytes()
|
||||||
if data != expected:
|
if data != expected and data not in alternatives:
|
||||||
fail(f"refusing to replace modified/foreign file: {path}")
|
fail(f"refusing to replace modified/foreign file: {path}")
|
||||||
|
|
||||||
|
|
||||||
def check_wrappers(root, launcher):
|
def check_wrappers(root, launcher):
|
||||||
manifest = root / "frameyap.vrmanifest"
|
manifest = root / "frameyap.vrmanifest"
|
||||||
desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode()
|
desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode()
|
||||||
check_owned_file(launcher, desired_launcher(root))
|
# Accept only the exact earlier managed script for migration.
|
||||||
|
check_owned_file(launcher, desired_launcher(root), (desired_launcher(root, legacy=True),))
|
||||||
check_owned_file(manifest, desired)
|
check_owned_file(manifest, desired)
|
||||||
check_owned_file(desktop_path(root), desired_desktop(launcher))
|
check_owned_file(desktop_path(root), desired_desktop(launcher))
|
||||||
return manifest, desired
|
return manifest, desired
|
||||||
@@ -376,7 +395,7 @@ def uninstall(root, launcher):
|
|||||||
manifest = root / "frameyap.vrmanifest"
|
manifest = root / "frameyap.vrmanifest"
|
||||||
desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode()
|
desired = (json.dumps(desired_manifest(launcher), sort_keys=True, indent=2) + "\n").encode()
|
||||||
check_owned_file(manifest, desired)
|
check_owned_file(manifest, desired)
|
||||||
check_owned_file(launcher, desired_launcher(root))
|
check_owned_file(launcher, desired_launcher(root), (desired_launcher(root, legacy=True),))
|
||||||
desktop = desktop_path(root)
|
desktop = desktop_path(root)
|
||||||
check_owned_file(desktop, desired_desktop(launcher))
|
check_owned_file(desktop, desired_desktop(launcher))
|
||||||
versions = root / "versions"
|
versions = root / "versions"
|
||||||
|
|||||||
+4
-2
@@ -268,8 +268,10 @@ std::optional<WorkerReply> Worker::poll() {
|
|||||||
}
|
}
|
||||||
if (type == 'F' && !s.loaded) {
|
if (type == 'F' && !s.loaded) {
|
||||||
if (size == 2 && s.input[5] == 'M')
|
if (size == 2 && s.input[5] == 'M')
|
||||||
throw std::runtime_error("worker missing local model weights or private clip directory");
|
throw std::runtime_error("Missing/mismatched pinned local model weights or private clip directory; check --model");
|
||||||
throw std::runtime_error("worker failed to load local CPU model/runtime");
|
if (size == 2 && s.input[5] == 'I')
|
||||||
|
throw std::runtime_error("Authorized Python lacks compatible CPU moondream/torch dependencies; check --python");
|
||||||
|
throw std::runtime_error("Local Redux model failed to load; check authorized CPU runtime and weights");
|
||||||
}
|
}
|
||||||
if ((type != 'R' && type != 'E') || size < 9 || !s.loaded || !s.pending ||
|
if ((type != 'R' && type != 'E') || size < 9 || !s.loaded || !s.pending ||
|
||||||
get64(s.input.data() + 5) != *s.pending || size - 9 > max_text)
|
get64(s.input.data() + 5) != *s.pending || size - 9 > max_text)
|
||||||
|
|||||||
@@ -290,6 +290,40 @@ class InstallTests(unittest.TestCase):
|
|||||||
self.assertTrue(probe.stdout.startswith("--check-controls\n--assets\n"))
|
self.assertTrue(probe.stdout.startswith("--check-controls\n--assets\n"))
|
||||||
self.assertNotIn("--python", probe.stdout)
|
self.assertNotIn("--python", probe.stdout)
|
||||||
|
|
||||||
|
def test_external_runtime_paths_config_and_legacy_launcher_migration(self):
|
||||||
|
import shutil
|
||||||
|
shutil.rmtree(self.stage / "runtime")
|
||||||
|
native = self.stage / "bin/frameyap"
|
||||||
|
native.write_text('#!/bin/sh\nprintf "%s\\n" "$@"\n')
|
||||||
|
native.chmod(0o755)
|
||||||
|
a, h = self.package("external", "--external-runtime")
|
||||||
|
self.install("external", a, h)
|
||||||
|
root = self.data / "frameyap"
|
||||||
|
launcher = self.home / ".local/bin/frameyap"
|
||||||
|
# A pre-config release launcher may be upgraded only when its exact
|
||||||
|
# bytes match the managed legacy template, not just its marker.
|
||||||
|
launcher.write_bytes(installer.desired_launcher(root, legacy=True))
|
||||||
|
self.install("external", a, h)
|
||||||
|
self.assertEqual(launcher.read_bytes(), installer.desired_launcher(root))
|
||||||
|
config = self.home / ".config/frameyap/paths.conf"
|
||||||
|
config.parent.mkdir(parents=True)
|
||||||
|
config.write_text("# Literal paths, not shell code\npython=/opt/approved python/bin/python3\n"
|
||||||
|
"model=/opt/$(printf not-executed)/local model\n")
|
||||||
|
run = subprocess.run([str(launcher)], capture_output=True, text=True, check=True)
|
||||||
|
self.assertIn("--python\n/opt/approved python/bin/python3\n"
|
||||||
|
"--model\n/opt/$(printf not-executed)/local model\n", run.stdout)
|
||||||
|
override = subprocess.run([str(launcher)], capture_output=True, text=True, check=True,
|
||||||
|
env={**os.environ, "FRAMEYAP_PYTHON": "/other/python"})
|
||||||
|
self.assertIn("--python\n/other/python\n--model\n/opt/$(printf not-executed)/local model\n", override.stdout)
|
||||||
|
config.unlink()
|
||||||
|
config.symlink_to(self.stage / "model/weights.bin")
|
||||||
|
symlinked = subprocess.run([str(launcher)], capture_output=True, text=True, check=True)
|
||||||
|
self.assertIn(f"--model\n{root}/current/model\n", symlinked.stdout)
|
||||||
|
# A user-modified launcher must remain protected, even with the marker.
|
||||||
|
launcher.write_bytes(installer.desired_launcher(root, legacy=True) + b"# changed\n")
|
||||||
|
with self.assertRaisesRegex(ValueError, "foreign file"):
|
||||||
|
self.install("external", a, h)
|
||||||
|
|
||||||
def test_package_rejects_symlink(self):
|
def test_package_rejects_symlink(self):
|
||||||
(self.stage / "lib/link.so").symlink_to("libtest.so")
|
(self.stage / "lib/link.so").symlink_to("libtest.so")
|
||||||
result = subprocess.run([sys.executable, str(REPO / "scripts/package-release.py"), "--stage", str(self.stage),
|
result = subprocess.run([sys.executable, str(REPO / "scripts/package-release.py"), "--stage", str(self.stage),
|
||||||
@@ -298,6 +332,15 @@ class InstallTests(unittest.TestCase):
|
|||||||
self.assertNotEqual(result.returncode, 0)
|
self.assertNotEqual(result.returncode, 0)
|
||||||
self.assertIn("links and special files forbidden", result.stderr)
|
self.assertIn("links and special files forbidden", result.stderr)
|
||||||
|
|
||||||
|
def test_utc_timestamp_release_tag_roundtrip(self):
|
||||||
|
version = "2026-09-24T162712Z-g417f81c-dirty"
|
||||||
|
archive, digest = self.package(version)
|
||||||
|
self.assertEqual(archive.name, f"frameyap-{version}-linux-aarch64.tar.gz")
|
||||||
|
self.install(version, archive, digest)
|
||||||
|
root = self.data / "frameyap"
|
||||||
|
self.assertEqual(os.readlink(root / "current"), f"versions/{version}")
|
||||||
|
self.assertEqual(json.loads((root / "current/release.json").read_text())["version"], version)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
@@ -32,8 +32,9 @@ def fake_child():
|
|||||||
parser.add_argument("--threads", required=True)
|
parser.add_argument("--threads", required=True)
|
||||||
parser.add_argument("--clip-dir", required=True)
|
parser.add_argument("--clip-dir", required=True)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
if args.model == "fail":
|
if args.model in ("fail", "missing-model", "missing-import"):
|
||||||
worker.send_frame(1, b"F")
|
worker.send_frame(1, b"F", {"fail": b"", "missing-model": b"M",
|
||||||
|
"missing-import": b"I"}[args.model])
|
||||||
return
|
return
|
||||||
if args.model == "crash":
|
if args.model == "crash":
|
||||||
return
|
return
|
||||||
@@ -106,7 +107,7 @@ class WorkerTests(unittest.TestCase):
|
|||||||
|
|
||||||
def test_missing_weights_no_dependency_import(self):
|
def test_missing_weights_no_dependency_import(self):
|
||||||
with tempfile.TemporaryDirectory() as path:
|
with tempfile.TemporaryDirectory() as path:
|
||||||
with self.assertRaisesRegex(ValueError, "weights missing"):
|
with self.assertRaisesRegex(worker.LocalModelError, "weights missing"):
|
||||||
worker.local_model(path)
|
worker.local_model(path)
|
||||||
with patch.dict(sys.modules, {"moondream": None}):
|
with patch.dict(sys.modules, {"moondream": None}):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(ValueError):
|
||||||
|
|||||||
@@ -63,6 +63,17 @@ int main(int argc, char** argv) {
|
|||||||
return failed;
|
return failed;
|
||||||
});
|
});
|
||||||
assert(!worker.ready());
|
assert(!worker.ready());
|
||||||
|
for (const auto& [mode, message] : std::vector<std::pair<const char*, const char*>>{
|
||||||
|
{"missing-model", "check --model"}, {"missing-import", "check --python"}}) {
|
||||||
|
worker.start(argv[1], argv[2], mode, 2);
|
||||||
|
failed = false;
|
||||||
|
until([&] {
|
||||||
|
try { worker.poll(); }
|
||||||
|
catch (const std::runtime_error& e) { failed = std::string(e.what()).find(message) != std::string::npos; }
|
||||||
|
return failed;
|
||||||
|
});
|
||||||
|
assert(!worker.ready());
|
||||||
|
}
|
||||||
worker.start(argv[1], argv[2], "stale", 2);
|
worker.start(argv[1], argv[2], "stale", 2);
|
||||||
until([&] { worker.poll(); return worker.ready(); });
|
until([&] { worker.poll(); return worker.ready(); });
|
||||||
worker.submit(4, clip);
|
worker.submit(4, clip);
|
||||||
|
|||||||
Reference in new issue
Block a user