aboutsummaryrefslogtreecommitdiff
path: root/app/tests
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-28 18:44:32 -0400
committerhistoria <historiavg@proton.me>2026-08-28 18:44:32 -0400
commit03ed68bafec56ef8bb690440d796c1a742042907 (patch)
tree9f80e1848ca8c141f49875ffdbe462527ab0a9c3 /app/tests
parent3c9b7e12029f6accdaabb6f60e01bce88156db98 (diff)
downloadtts-audiobook-generator-03ed68bafec56ef8bb690440d796c1a742042907.tar.gz
feat(audio.cpp): if github binary download fails, automatically start building from source
Diffstat (limited to 'app/tests')
-rw-r--r--app/tests/test_backends_audiocpp.py251
1 files changed, 242 insertions, 9 deletions
diff --git a/app/tests/test_backends_audiocpp.py b/app/tests/test_backends_audiocpp.py
index 4c00001..ada7954 100644
--- a/app/tests/test_backends_audiocpp.py
+++ b/app/tests/test_backends_audiocpp.py
@@ -3468,7 +3468,9 @@ class PrebuiltUpdateRoutingTests(unittest.TestCase):
encoding="utf-8")
def _run(self, checkout: Path, *, marker: Optional[dict],
- release: Optional[dict]):
+ release: Optional[dict], tag: Optional[str] = None):
+ # TAG is what the quota-free redirect resolution reports; RELEASE
+ # is the API fallback (only consulted when the redirect fails).
with patch("sys.platform", "darwin"), \
patch.object(make_server.build, "find_local_checkout",
return_value=checkout), \
@@ -3478,41 +3480,49 @@ class PrebuiltUpdateRoutingTests(unittest.TestCase):
return_value={"backend": "cpu"}), \
patch.object(make_server.prebuilt, "installed_release",
return_value=marker), \
+ patch.object(make_server.prebuilt, "resolve_latest_tag",
+ return_value=tag) as mk_tag, \
patch.object(make_server.prebuilt, "fetch_latest_release",
- return_value=release), \
+ return_value=release) as mk_fetch, \
patch.object(make_server.prebuilt, "install_prebuilt",
return_value=0) as mk_install, \
patch.object(common, "git_update",
return_value=0) as mk_git:
rc = make_server.build.update()
- return rc, mk_install, mk_git
+ return rc, mk_install, mk_git, mk_tag, mk_fetch
def test_newer_release_triggers_a_redownload(self):
checkout = self._checkout()
self._mark_prebuilt(checkout, "v0.6.0")
- rc, mk_install, mk_git = self._run(
+ rc, mk_install, mk_git, _mk_tag, mk_fetch = self._run(
checkout, marker={"tag": "v0.6.0", "asset": "x"},
- release=_release([], tag="v0.7.0"))
+ release=_release([], tag="v0.7.0"), tag="v0.7.0")
self.assertEqual(rc, 0)
mk_git.assert_not_called()
mk_install.assert_called_once_with(checkout, "cpu", emit=None,
cancel=None)
+ mk_fetch.assert_not_called()
def test_current_release_is_a_noop(self):
checkout = self._checkout()
- rc, mk_install, mk_git = self._run(
+ rc, mk_install, mk_git, _mk_tag, mk_fetch = self._run(
checkout, marker={"tag": "v0.7.0", "asset": "x"},
- release=_release([], tag="v0.7.0"))
+ release=None, tag="v0.7.0")
self.assertEqual(rc, 0)
mk_install.assert_not_called()
mk_git.assert_not_called()
+ # The redirect already answered: the API (rate-limited easily)
+ # must not be touched for an "already current" check.
+ mk_fetch.assert_not_called()
def test_unreachable_github_keeps_the_install(self):
checkout = self._checkout()
- rc, mk_install, mk_git = self._run(
+ rc, mk_install, mk_git, mk_tag, mk_fetch = self._run(
checkout, marker={"tag": "v0.7.0", "asset": "x"},
- release=None)
+ release=None, tag=None)
self.assertEqual(rc, 0)
+ mk_tag.assert_called_once()
+ mk_fetch.assert_called_once() # the API fallback tried too
mk_install.assert_not_called()
mk_git.assert_not_called()
@@ -3606,3 +3616,226 @@ class PrebuiltFlagTests(unittest.TestCase):
self.assertIsNotNone(settings)
self.assertEqual(settings["build_mode"], mode)
self.assertEqual(settings["build"], expected_build)
+
+
+class PrebuiltTagResolutionTests(unittest.TestCase):
+ """resolve_latest_tag: the quota-free release-page redirect."""
+
+ def _redirect(self, url):
+ response = MagicMock()
+ response.__enter__.return_value.geturl.return_value = url
+ return response
+
+ def test_parses_the_redirect_target(self):
+ with patch("urllib.request.urlopen",
+ return_value=self._redirect(
+ "https://github.com/0xShug0/audio.cpp/releases/tag/"
+ "v0.7.0")):
+ self.assertEqual(make_server.prebuilt.resolve_latest_tag(),
+ "v0.7.0")
+
+ def test_offline_yields_none(self):
+ with patch("urllib.request.urlopen", side_effect=OSError("down")):
+ self.assertIsNone(make_server.prebuilt.resolve_latest_tag())
+
+ def test_unexpected_page_yields_none(self):
+ with patch("urllib.request.urlopen",
+ return_value=self._redirect(
+ "https://github.com/0xShug0/audio.cpp/releases")):
+ self.assertIsNone(make_server.prebuilt.resolve_latest_tag())
+
+
+class PrebuiltSyntheticAssetTests(unittest.TestCase):
+ """synthesize_assets: API-free names for the rate-limited fallback."""
+
+ def test_darwin_names_match_select_assets(self):
+ tag = "v1.2.3"
+ selected = make_server.prebuilt.select_assets(
+ [_asset(f"audio-{tag}-bin-macos-arm64-metal.tar.gz")],
+ "cpu", platform="darwin", machine="arm64")
+ synth = make_server.prebuilt.synthesize_assets(
+ "cpu", tag, platform="darwin", machine="arm64")
+ self.assertEqual(selected[0]["name"], synth[0]["name"])
+ self.assertEqual(
+ synth[0]["browser_download_url"],
+ f"https://github.com/0xShug0/audio.cpp/releases/download/"
+ f"{tag}/audio-{tag}-bin-macos-arm64-metal.tar.gz")
+ self.assertIsNone(synth[1])
+
+ def test_windows_cuda_synthesizes_both_archives(self):
+ synth = make_server.prebuilt.synthesize_assets(
+ "cuda", "v1.2.3", platform="win32", machine="AMD64",
+ cuda_variant="13.3")
+ main, extra = synth
+ self.assertEqual(main["name"],
+ "audio-v1.2.3-bin-windows-x64-cuda13.3.zip")
+ self.assertEqual(extra["name"],
+ "audio-v1.2.3-cudart-windows-x64-cuda13.3.zip")
+ self.assertIsNone(main["digest"])
+
+ def test_unsupported_backend_is_none(self):
+ self.assertIsNone(make_server.prebuilt.synthesize_assets(
+ "hip", "v1.2.3", platform="win32", machine="AMD64"))
+
+
+class PrebuiltRateLimitTests(unittest.TestCase):
+ """install_prebuilt degrades when the GitHub API is rate-limited."""
+
+ def setUp(self):
+ tmp = tempfile.TemporaryDirectory()
+ self.addCleanup(tmp.cleanup)
+ self.tmp = tmp
+
+ def _checkout(self) -> Path:
+ tmp = tempfile.TemporaryDirectory()
+ self.addCleanup(tmp.cleanup)
+ return _make_checkout(Path(tmp.name))
+
+ def test_rate_limited_api_still_installs_unverified(self):
+ checkout = self._checkout()
+ archive = _make_tar_gz(
+ Path(self.tmp.name), "audio-v9.9.9-bin-macos-x64-metal.tar.gz",
+ {"./audiocpp_server": "#!/bin/sh\n"})
+
+ def fake_download(url, dest, *, emit=None, cancel=None):
+ self.assertIn("/releases/download/v9.9.9/", url)
+ shutil.copyfile(archive, dest)
+ return 0
+
+ with patch("sys.platform", "darwin"), \
+ patch.object(make_server.prebuilt, "fetch_latest_release",
+ return_value=None), \
+ patch.object(make_server.prebuilt, "resolve_latest_tag",
+ return_value="v9.9.9"), \
+ patch.object(make_server.prebuilt, "_download",
+ side_effect=fake_download) as mk_dl, \
+ patch.object(make_server.prebuilt.common,
+ "run_console_subprocess", return_value=0), \
+ redirect_stdout(io.StringIO()) as out:
+ rc = make_server.prebuilt.install_prebuilt(checkout, "cpu")
+ self.assertEqual(rc, 0)
+ mk_dl.assert_called_once()
+ server = (checkout / "build" / "macos-metal-release" / "bin"
+ / "audiocpp_server")
+ self.assertTrue(server.exists())
+ self.assertTrue(server.stat().st_mode & 0o111)
+ marker = make_server.prebuilt.installed_release(checkout, "cpu",
+ platform="darwin")
+ self.assertEqual(marker["tag"], "v9.9.9")
+ self.assertIn("WITHOUT checksum verification", out.getvalue())
+
+ def test_total_outage_fails_the_install(self):
+ checkout = self._checkout()
+ with patch("sys.platform", "darwin"), \
+ patch.object(make_server.prebuilt, "fetch_latest_release",
+ return_value=None), \
+ patch.object(make_server.prebuilt, "resolve_latest_tag",
+ return_value=None):
+ rc = make_server.prebuilt.install_prebuilt(checkout, "cpu")
+ self.assertEqual(rc, 1)
+ self.assertIsNone(make_server.prebuilt.installed_release(
+ checkout, "cpu", platform="darwin"))
+
+ def test_reachable_api_without_a_matching_asset_is_a_hard_error(self):
+ # A reachable API with no matching asset must not be papered over
+ # with a synthesized name that would just 404.
+ checkout = self._checkout()
+ with patch("sys.platform", "darwin"), \
+ patch.object(make_server.prebuilt, "fetch_latest_release",
+ return_value=_release([], tag="v9.9.9")), \
+ patch.object(make_server.prebuilt, "resolve_latest_tag",
+ return_value="v9.9.9") as mk_tag:
+ rc = make_server.prebuilt.install_prebuilt(checkout, "cpu")
+ self.assertEqual(rc, 1)
+ mk_tag.assert_not_called()
+
+
+class PrebuiltFallbackTests(unittest.TestCase):
+ """A failed prebuilt download falls back to a source build in-lane."""
+
+ def _settings(self, checkout: Path, **overrides) -> dict:
+ settings = {
+ "audiocpp_dir": checkout, "backend": "cpu", "build": True,
+ "build_mode": "prebuilt", "prebuilt_forced": False,
+ "wav_dir": None, "include_clone": False, "plan": None,
+ "output_path": checkout / "server.json", "model_entries": [],
+ "install_guidance": [], "host": "127.0.0.1", "port": 8080,
+ "lazy_load": True, "download": False, "delete_unused": False,
+ "unused_entries": [],
+ }
+ settings.update(overrides)
+ return settings
+
+ def _build_step(self, checkout: Path, settings: dict):
+ args = argparse.Namespace(input_dir=None, whisper_model="base")
+ lanes = make_server.wizard._execute_lanes(settings, args)
+ return lanes[0].steps[0]
+
+ def _run_step(self, step, *, out):
+ with redirect_stdout(out):
+ return step.work(None, None)
+
+ def _checkout(self) -> Path:
+ tmp = tempfile.TemporaryDirectory()
+ self.addCleanup(tmp.cleanup)
+ return _make_checkout(Path(tmp.name))
+
+ def test_failed_download_falls_back_to_source_build(self):
+ checkout = self._checkout()
+ step = self._build_step(checkout, self._settings(checkout))
+ with patch.object(make_server.prebuilt, "install_prebuilt",
+ return_value=1) as mk_install, \
+ patch.object(make_server.build, "build_audiocpp",
+ return_value=0) as mk_build, \
+ redirect_stdout(io.StringIO()) as out:
+ rc = self._run_step(step, out=out)
+ self.assertEqual(rc, 0)
+ mk_install.assert_called_once()
+ mk_build.assert_called_once_with(checkout, "cpu", emit=None,
+ cancel=None)
+ self.assertIn("falling back to a source build",
+ out.getvalue())
+
+ def test_both_paths_failing_still_reports_an_error(self):
+ checkout = self._checkout()
+ step = self._build_step(checkout, self._settings(checkout))
+ with patch.object(make_server.prebuilt, "install_prebuilt",
+ return_value=1), \
+ patch.object(make_server.build, "build_audiocpp",
+ return_value=3), \
+ redirect_stdout(io.StringIO()):
+ rc = self._run_step(step, out=io.StringIO())
+ self.assertEqual(rc, 3)
+
+ def test_cancelled_download_does_not_fall_back(self):
+ checkout = self._checkout()
+ step = self._build_step(checkout, self._settings(checkout))
+ with patch.object(make_server.prebuilt, "install_prebuilt",
+ return_value=130) as _mk_install, \
+ patch.object(make_server.build, "build_audiocpp") as mk_build:
+ rc = self._run_step(step, out=io.StringIO())
+ self.assertEqual(rc, 130)
+ mk_build.assert_not_called()
+
+ def test_forced_prebuilt_fails_fast(self):
+ checkout = self._checkout()
+ step = self._build_step(
+ checkout, self._settings(checkout, prebuilt_forced=True))
+ with patch.object(make_server.prebuilt, "install_prebuilt",
+ return_value=1) as _mk_install, \
+ patch.object(make_server.build, "build_audiocpp") as mk_build:
+ rc = self._run_step(step, out=io.StringIO())
+ self.assertEqual(rc, 1)
+ mk_build.assert_not_called()
+
+ def test_source_mode_never_touches_the_download(self):
+ checkout = self._checkout()
+ step = self._build_step(
+ checkout, self._settings(checkout, build_mode="source"))
+ with patch.object(make_server.prebuilt, "install_prebuilt") as mk_i, \
+ patch.object(make_server.build, "build_audiocpp",
+ return_value=0) as mk_build:
+ rc = self._run_step(step, out=io.StringIO())
+ self.assertEqual(rc, 0)
+ mk_i.assert_not_called()
+ mk_build.assert_called_once()