diff options
| author | historia <historiavg@proton.me> | 2026-08-28 18:44:32 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-28 18:44:32 -0400 |
| commit | 03ed68bafec56ef8bb690440d796c1a742042907 (patch) | |
| tree | 9f80e1848ca8c141f49875ffdbe462527ab0a9c3 /app/tests | |
| parent | 3c9b7e12029f6accdaabb6f60e01bce88156db98 (diff) | |
| download | tts-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.py | 251 |
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() |
