aboutsummaryrefslogtreecommitdiff
path: root/app/tests/test_backends_audiocpp.py
diff options
context:
space:
mode:
Diffstat (limited to 'app/tests/test_backends_audiocpp.py')
-rw-r--r--app/tests/test_backends_audiocpp.py219
1 files changed, 207 insertions, 12 deletions
diff --git a/app/tests/test_backends_audiocpp.py b/app/tests/test_backends_audiocpp.py
index b724c0d..8f79085 100644
--- a/app/tests/test_backends_audiocpp.py
+++ b/app/tests/test_backends_audiocpp.py
@@ -5,6 +5,7 @@ import io
import json
import sys
import tempfile
+import threading
import unittest
from contextlib import redirect_stdout
from pathlib import Path
@@ -792,6 +793,46 @@ class FindAudiocppServerBinTests(unittest.TestCase):
self.assertIsNone(make_server.find_audiocpp_server_bin(self.checkout))
+class BuiltServerBinaryTests(unittest.TestCase):
+ """built_server_binary: locating a specific backend's build."""
+
+ def setUp(self):
+ self._td = tempfile.TemporaryDirectory()
+ self.checkout = Path(self._td.name) / "audio.cpp"
+ self.checkout.mkdir()
+
+ def tearDown(self):
+ self._td.cleanup()
+
+ def _build(self, name, binary="audiocpp_server"):
+ bin_dir = self.checkout / "build" / name / "bin"
+ bin_dir.mkdir(parents=True)
+ (bin_dir / binary).write_bytes(b"x")
+
+ def test_returns_the_matching_backend_binary(self):
+ self._build("linux-cuda-release")
+ self._build("linux-cpu-release")
+ self.assertEqual(
+ make_server.built_server_binary(self.checkout, "cpu"),
+ self.checkout / "build" / "linux-cpu-release" / "bin"
+ / "audiocpp_server")
+
+ def test_returns_none_for_unbuilt_backend(self):
+ self._build("linux-cuda-release")
+ self.assertIsNone(
+ make_server.built_server_binary(self.checkout, "vulkan"))
+
+ def test_metal_counts_as_cpu(self):
+ self._build("macos-metal-release")
+ self.assertEqual(
+ make_server.built_server_binary(self.checkout, "cpu"),
+ self.checkout / "build" / "macos-metal-release" / "bin"
+ / "audiocpp_server")
+
+ def test_no_build_dir_returns_none(self):
+ self.assertIsNone(make_server.built_server_binary(self.checkout, "cpu"))
+
+
class BuildAudiocppTests(unittest.TestCase):
"""Running the audio.cpp build helper script."""
@@ -803,10 +844,23 @@ class BuildAudiocppTests(unittest.TestCase):
self.scripts.mkdir()
(self.scripts / "build_linux.sh").write_text("#!/bin/sh\n",
encoding="utf-8")
+ self.log_dir = Path(self._td.name) / "logs"
+ self.addCleanup(make_server.common.drain_post_tui_notices)
def tearDown(self):
self._td.cleanup()
+ def _emit(self):
+ lines = []
+
+ def emit(line):
+ lines.append(line)
+
+ return lines, emit
+
+ def _log_files(self):
+ return sorted(self.log_dir.glob("audiocpp_build_*.log"))
+
def test_runs_build_script_with_backend_and_target(self):
with patch.object(make_server.common, "run_console_subprocess",
return_value=0) as run:
@@ -826,6 +880,81 @@ class BuildAudiocppTests(unittest.TestCase):
rc = make_server.build_audiocpp(self.checkout, "cuda")
self.assertNotEqual(rc, 0)
+ def test_console_path_writes_no_log_and_no_notice(self):
+ with patch.object(make_server.common, "LOG_DIR", self.log_dir), \
+ patch.object(make_server.common, "run_console_subprocess",
+ return_value=0):
+ rc = make_server.build_audiocpp(self.checkout, "cuda")
+ self.assertEqual(rc, 0)
+ self.assertEqual(self._log_files(), [])
+ self.assertEqual(make_server.common.drain_post_tui_notices(), [])
+
+ def test_tui_success_writes_log_and_no_notice(self):
+ emitted, emit = self._emit()
+ with patch.object(make_server.common, "LOG_DIR", self.log_dir), \
+ patch.object(make_server.common, "run_console_subprocess",
+ return_value=0):
+ rc = make_server.build_audiocpp(self.checkout, "cuda",
+ emit=emit)
+ self.assertEqual(rc, 0)
+ self.assertEqual(len(self._log_files()), 1)
+ log_text = self._log_files()[0].read_text(encoding="utf-8")
+ self.assertIn("[INFO] Building audiocpp_server", log_text)
+ self.assertIn("--backend cuda", log_text)
+ self.assertTrue(emitted)
+ self.assertEqual(make_server.common.drain_post_tui_notices(), [])
+
+ def test_tui_failure_writes_log_and_records_notice(self):
+ emitted, emit = self._emit()
+ with patch.object(make_server.common, "LOG_DIR", self.log_dir), \
+ patch.object(make_server.common, "run_console_subprocess",
+ return_value=3):
+ rc = make_server.build_audiocpp(self.checkout, "cuda",
+ emit=emit)
+ self.assertEqual(rc, 3)
+ logs = self._log_files()
+ self.assertEqual(len(logs), 1)
+ log_text = logs[0].read_text(encoding="utf-8")
+ self.assertIn("failed (exit code 3)", log_text)
+ notices = make_server.common.drain_post_tui_notices()
+ self.assertEqual(len(notices), 1)
+ notice = notices[0]
+ self.assertIn("failed (exit code 3)", notice)
+ self.assertIn(f"Build log: {logs[0]}", notice)
+ command = (f"cd {self.checkout} && sh "
+ f"{self.scripts / 'build_linux.sh'} --backend cuda "
+ "--target audiocpp_server")
+ self.assertIn(command, notice)
+ self.assertIn("Troubleshoot by re-running this command", notice)
+ self.assertTrue(any("failed (exit code 3)" in line
+ for line in emitted))
+
+ def test_tui_cancel_suppresses_notice_but_writes_log(self):
+ emitted, emit = self._emit()
+ cancel = threading.Event()
+ cancel.set()
+ with patch.object(make_server.common, "LOG_DIR", self.log_dir), \
+ patch.object(make_server.common, "run_console_subprocess",
+ return_value=130):
+ rc = make_server.build_audiocpp(self.checkout, "cuda",
+ emit=emit, cancel=cancel)
+ self.assertEqual(rc, 130)
+ self.assertEqual(len(self._log_files()), 1)
+ self.assertEqual(make_server.common.drain_post_tui_notices(), [])
+
+ def test_tui_missing_script_records_guidance_notice(self):
+ for f in self.scripts.iterdir():
+ f.unlink()
+ emitted, emit = self._emit()
+ with patch.object(make_server.common, "LOG_DIR", self.log_dir):
+ rc = make_server.build_audiocpp(self.checkout, "cuda",
+ emit=emit)
+ self.assertNotEqual(rc, 0)
+ self.assertEqual(self._log_files(), [])
+ notices = make_server.common.drain_post_tui_notices()
+ self.assertEqual(len(notices), 1)
+ self.assertIn("No build script found", notices[0])
+
class AudiocppDetectTests(unittest.TestCase):
"""backends.audiocpp.detect() status reporting."""
@@ -854,6 +983,7 @@ class AudiocppDetectTests(unittest.TestCase):
self.assertFalse(status.installed)
self.assertFalse(status.configured)
self.assertEqual(status.launch_hint, "")
+ self.assertEqual(status.partial, "downloaded (not built)")
def test_built_and_configured_ready(self):
binary = self.checkout / "build" / "linux-cuda-release" / "bin" \
@@ -869,6 +999,19 @@ class AudiocppDetectTests(unittest.TestCase):
self.assertTrue(status.configured)
self.assertIn(str(binary), status.launch_hint)
self.assertIn(str(server_json), status.launch_hint)
+ self.assertEqual(status.partial, "")
+
+ def test_built_not_configured(self):
+ binary = self.checkout / "build" / "linux-cuda-release" / "bin" \
+ / "audiocpp_server"
+ binary.parent.mkdir(parents=True)
+ binary.write_bytes(b"x")
+ with patch.object(make_server, "find_local_checkout",
+ return_value=self.checkout):
+ status = make_server.detect()
+ self.assertTrue(status.installed)
+ self.assertFalse(status.configured)
+ self.assertEqual(status.partial, "built (not configured)")
class NonInteractiveMainTests(unittest.TestCase):
@@ -918,7 +1061,7 @@ class NonInteractiveMainTests(unittest.TestCase):
self.assertEqual(data["host"], "127.0.0.1")
self.assertEqual(data["port"], make_server.config_port())
self.assertEqual(data["backend"], "cuda")
- self.assertFalse(data["lazy_load"])
+ self.assertTrue(data["lazy_load"])
self.assertEqual([m["id"] for m in data["models"]], ["higgs"])
self.assertNotIn("voice_dir", data)
@@ -1490,7 +1633,8 @@ class InstallModelsTests(unittest.TestCase):
guidance = [("qwen", "qwen3_tts_0_6b_base_q8_0")]
with patch.object(make_server, "_install_models") as mk:
make_server.install_models(checkout, guidance)
- mk.assert_called_once_with(checkout, guidance, download=True)
+ mk.assert_called_once_with(checkout, guidance, download=True,
+ emit=None, cancel=None)
class HandInstallGuidanceTests(unittest.TestCase):
@@ -1533,6 +1677,53 @@ class WizardNavigationTests(unittest.TestCase):
make_server.build_parser())
self.assertIsNone(settings)
+ def test_modify_flow_offers_build_when_not_built(self):
+ # A server.json recording "vulkan" exists, but nothing is built: the
+ # wizard must still reach the backend menu (pre-selecting vulkan) and
+ # offer the build — instead of silently skipping it because the
+ # existing server.json already records a backend.
+ checkout = self._checkout()
+ (checkout / "server.json").write_text(
+ json.dumps({"models": [], "backend": "vulkan"}),
+ encoding="utf-8")
+ catalog = make_server.load_model_catalog(checkout)
+ supertonic = next(i for i, entry in enumerate(catalog)
+ if entry["family"] == "supertonic")
+ confirm_questions = []
+
+ def fake_tree(*args, **kwargs):
+ return [(supertonic, "Supertonic-GGUF")]
+
+ def fake_line_edit(stdscr, title, default, **kwargs):
+ if title == "Bind host":
+ return "127.0.0.1"
+ if title == "Port":
+ return "8080"
+ return default
+
+ def fake_menu(stdscr, title, options, **kwargs):
+ return "vulkan"
+
+ def fake_confirm(stdscr, question, **kwargs):
+ confirm_questions.append(question)
+ return False # decline the build
+
+ with patch.object(make_server, "find_local_checkout",
+ return_value=checkout), \
+ patch.object(tui, "checkbox_tree", side_effect=fake_tree), \
+ patch.object(tui, "line_edit", side_effect=fake_line_edit), \
+ patch.object(tui, "menu", side_effect=fake_menu), \
+ patch.object(tui, "confirm", side_effect=fake_confirm):
+ settings = make_server._wizard(None, self._args(),
+ make_server.build_parser())
+ self.assertIsNotNone(settings)
+ self.assertEqual(settings["backend"], "vulkan")
+ self.assertFalse(settings["build"])
+ # The build offer was shown (and declined); the old modify flow
+ # skipped it entirely.
+ self.assertTrue(any("not built for vulkan" in q
+ for q in confirm_questions))
+
def test_bind_host_esc_returns_to_families_tree(self):
# Esc on "Bind host" must fall back to the model-family tree, then
# re-selecting proceeds through the rest of the wizard.
@@ -1601,24 +1792,28 @@ if __name__ == "__main__":
class SetupScreenTests(unittest.TestCase):
- """setup_screen: the wizard run on the hub's screen, console tail via
- suspend."""
+ """setup_screen: the wizard run on the hub's screen, setup tail via the
+ in-TUI task view."""
def test_abort_returns_one_without_executing(self):
with patch.object(make_server, "_wizard", return_value=None) as mk_wizard, \
- patch.object(make_server, "_execute") as mk_execute:
+ patch.object(make_server, "_execute_steps") as mk_steps:
rc = make_server.setup_screen(None)
self.assertEqual(rc, 1)
mk_wizard.assert_called_once()
- mk_execute.assert_not_called()
+ mk_steps.assert_not_called()
- def test_success_executes_the_tail_under_suspend(self):
+ def test_success_runs_the_tail_in_the_task_view(self):
settings = {"audiocpp_dir": Path("/x")}
+ steps = [make_server.taskview.TaskStep("t", lambda emit, cancel: 0)]
with patch.object(make_server, "_wizard", return_value=settings), \
- patch.object(make_server, "_execute",
- return_value=0) as mk_execute, \
- patch.object(tui, "suspend", contextlib.nullcontext):
+ patch.object(make_server, "_execute_steps",
+ return_value=steps) as mk_steps, \
+ patch.object(make_server.taskview, "run_steps",
+ return_value=0) as mk_run:
rc = make_server.setup_screen(None)
self.assertEqual(rc, 0)
- mk_execute.assert_called_once()
- self.assertIs(mk_execute.call_args[0][0], settings)
+ mk_steps.assert_called_once()
+ self.assertIs(mk_steps.call_args[0][0], settings)
+ mk_run.assert_called_once()
+ self.assertEqual(mk_run.call_args[0][2], steps)