mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-21 12:21:49 +00:00
625 lines
25 KiB
Python
625 lines
25 KiB
Python
"""Tests for lib/audio.py command resolution and decode failure reporting.
|
|
|
|
Some tests below create POSIX shell-script stubs as fake `vgmstream-cli`
|
|
binaries. On Windows, `subprocess.run([path])` won't honor the shebang
|
|
and `shutil.which` won't pick up an extensionless script as executable,
|
|
so those fixtures don't translate cleanly — they get marked
|
|
`requires_posix_subprocess` and skipped on Windows. The pure-Python
|
|
unit tests (`_scrub_paths`, `_truncate_detail`) deliberately do *not*
|
|
carry that marker because the redaction logic they pin is platform-
|
|
independent — in particular it has to work on Windows, since that's
|
|
where most of the Windows-path inputs come from in real deployments."""
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from audio import _scrub_paths, _truncate_detail, _vgmstream_cmd, convert_wem
|
|
|
|
requires_posix_subprocess = pytest.mark.skipif(
|
|
sys.platform == "win32",
|
|
reason="POSIX shell-script fixtures are not executable on Windows",
|
|
)
|
|
|
|
|
|
# --- _scrub_paths unit tests --------------------------------------------------
|
|
#
|
|
# These exercise the generic redaction in isolation, so the contract is
|
|
# pinned even on platforms where the full convert_wem flow can't run —
|
|
# *including* Windows, since the Windows-path branches of the regex are
|
|
# specifically meant to handle inputs that originate there. Deliberately
|
|
# no `@requires_posix_subprocess` marker on these.
|
|
|
|
def test_scrub_paths_redacts_posix_path():
|
|
out = _scrub_paths("could not open /etc/secret/file.cfg today")
|
|
assert "/etc/secret" not in out
|
|
assert "file.cfg" in out
|
|
|
|
|
|
def test_scrub_paths_redacts_windows_drive_path():
|
|
out = _scrub_paths(r"could not open C:\Users\Foo\config.txt today")
|
|
assert r"C:\Users" not in out
|
|
assert "config.txt" in out
|
|
|
|
|
|
def test_scrub_paths_redacts_windows_unc_path():
|
|
out = _scrub_paths(r"failed at \\server\share\file.cfg now")
|
|
assert r"\\server" not in out
|
|
assert "file.cfg" in out
|
|
|
|
|
|
def test_scrub_paths_redacts_windows_forward_slash_drive_path():
|
|
"""Native Windows APIs and many cross-platform tools (PowerShell,
|
|
.NET, ffmpeg `-i C:/...`) emit drive-qualified paths with forward
|
|
slashes, not backslashes. The regex must catch that shape too."""
|
|
out = _scrub_paths("ffmpeg: could not open C:/Users/Alice/song.wem now")
|
|
assert "C:/Users" not in out
|
|
assert "song.wem" in out
|
|
|
|
|
|
def test_scrub_paths_redacts_quoted_windows_forward_slash_path():
|
|
out = _scrub_paths('failed at "C:/Program Files/vgmstream-cli.exe" again')
|
|
assert "C:/Program" not in out
|
|
assert "Program Files" not in out
|
|
assert "vgmstream-cli.exe" in out
|
|
|
|
|
|
def test_scrub_paths_redacts_quoted_path_with_spaces():
|
|
"""Decoders quote paths that contain spaces. The previous regex
|
|
stopped at any whitespace, so `'C:\\Program Files\\foo.exe'` only
|
|
matched the `C:\\Program` prefix and the rest leaked through. The
|
|
quoted-path branch should now match the full quoted span."""
|
|
out = _scrub_paths('failed at "C:\\Program Files\\vgmstream-cli.exe" today')
|
|
assert "C:\\Program" not in out
|
|
assert "Program Files" not in out
|
|
assert "vgmstream-cli.exe" in out
|
|
|
|
|
|
def test_scrub_paths_redacts_quoted_posix_path_with_spaces():
|
|
out = _scrub_paths("could not open '/Users/Alice/My Secrets/file.cfg'")
|
|
assert "My Secrets" not in out
|
|
assert "/Users/Alice" not in out
|
|
assert "file.cfg" in out
|
|
|
|
|
|
def test_scrub_paths_handles_trailing_separator():
|
|
# os.path.basename("/a/b/") == "" — the older replacement function fell
|
|
# back to the full match when basename was empty, leaving the directory
|
|
# intact. The current helper strips trailing separators before taking the
|
|
# basename so a search-dir path like this gets collapsed to its final
|
|
# segment ("secrets") rather than slipping through verbatim.
|
|
out = _scrub_paths("scanning search dir /etc/secrets/ now")
|
|
assert "/etc/secrets" not in out
|
|
assert "secrets" in out
|
|
|
|
|
|
# --- _truncate_detail: skip ffmpeg banner ------------------------------------
|
|
|
|
|
|
def test_truncate_detail_skips_ffmpeg_banner():
|
|
"""ffmpeg starts its stderr with a multi-line version / build /
|
|
configuration banner. The actionable error is somewhere after. The
|
|
truncate helper should skip the banner lines and pick the first
|
|
actionable one."""
|
|
stderr = (
|
|
"ffmpeg version 4.4.2-0ubuntu0.22.04.1 Copyright (c) 2000-2021\n"
|
|
" built with gcc 11 (Ubuntu 11.2.0-19ubuntu1)\n"
|
|
" configuration: --prefix=/usr --extra-version=0ubuntu0.22.04.1\n"
|
|
" libavutil 56. 70.100\n"
|
|
" libavcodec 58.134.100\n"
|
|
"Input #0, wav, from 'song.wav':\n"
|
|
"song.wem: Invalid data found when processing input\n"
|
|
)
|
|
out = _truncate_detail(stderr)
|
|
assert "ffmpeg version" not in out
|
|
assert "libavutil" not in out
|
|
# Picks the actionable line.
|
|
assert "Invalid data found" in out
|
|
|
|
|
|
def test_truncate_detail_caps_long_lines():
|
|
long_line = "boom: " + ("x" * 1000)
|
|
out = _truncate_detail(long_line, limit=80)
|
|
assert len(out) <= 80
|
|
assert out.startswith("boom:")
|
|
assert out.endswith("…")
|
|
|
|
|
|
# --- _vgmstream_cmd / convert_wem flow ---------------------------------------
|
|
|
|
|
|
def _make_executable(path: Path, body: str) -> None:
|
|
path.write_text(body, encoding="utf-8")
|
|
path.chmod(0o755)
|
|
|
|
|
|
@requires_posix_subprocess
|
|
def test_vgmstream_cmd_uses_explicit_env_var(tmp_path, monkeypatch):
|
|
exe = tmp_path / "custom-vgmstream"
|
|
_make_executable(exe, "#!/bin/sh\nexit 0\n")
|
|
|
|
monkeypatch.setenv("VGMSTREAM_CLI", str(exe))
|
|
monkeypatch.setenv("PATH", "")
|
|
|
|
assert _vgmstream_cmd() == str(exe.resolve())
|
|
|
|
|
|
@requires_posix_subprocess
|
|
def test_stale_vgmstream_cli_env_falls_through_to_path_binary(tmp_path, monkeypatch):
|
|
"""When VGMSTREAM_CLI points at a stale/non-executable path, the
|
|
resolver must log a warning but still find a working binary on PATH.
|
|
This locks in the "log + fall through" contract — a future change
|
|
that turned the env var into a hard failure would silently break
|
|
desktop users who have a working bundled or PATH binary even
|
|
though their env var got stale."""
|
|
stale = tmp_path / "definitely-not-there" # never created
|
|
|
|
path_dir = tmp_path / "path-bin"
|
|
path_dir.mkdir()
|
|
on_path = path_dir / "vgmstream-cli"
|
|
_make_executable(on_path, "#!/bin/sh\nexit 0\n")
|
|
|
|
monkeypatch.setenv("VGMSTREAM_CLI", str(stale))
|
|
monkeypatch.setenv("PATH", str(path_dir))
|
|
monkeypatch.setattr("audio._bundled_bin_dir", lambda: None)
|
|
|
|
assert _vgmstream_cmd() == str(on_path.resolve())
|
|
|
|
|
|
@requires_posix_subprocess
|
|
def test_vgmstream_cli_env_overrides_path_binary(tmp_path, monkeypatch):
|
|
"""VGMSTREAM_CLI must beat a binary on PATH so a user can force a
|
|
known-good build when the system one is broken."""
|
|
override = tmp_path / "override-vgmstream"
|
|
_make_executable(override, "#!/bin/sh\nexit 0\n")
|
|
|
|
path_dir = tmp_path / "path-bin"
|
|
path_dir.mkdir()
|
|
on_path = path_dir / "vgmstream-cli"
|
|
_make_executable(on_path, "#!/bin/sh\nexit 0\n")
|
|
|
|
monkeypatch.setenv("VGMSTREAM_CLI", str(override))
|
|
monkeypatch.setenv("PATH", str(path_dir))
|
|
|
|
assert _vgmstream_cmd() == str(override.resolve())
|
|
|
|
|
|
@requires_posix_subprocess
|
|
def test_vgmstream_cmd_bundled_beats_path_when_env_unset(tmp_path, monkeypatch):
|
|
"""With no VGMSTREAM_CLI env, the desktop-bundled binary must still beat
|
|
a `vgmstream-cli` that happens to be on PATH — that's what protects
|
|
desktop users from a system Homebrew/distro binary built without the
|
|
codecs we rely on. Pinned here directly because `_vgmstream_cmd` now
|
|
reimplements the bundled-vs-PATH ordering rather than delegating to
|
|
`_bundled_or_path`, so the generic `_bundled_or_path` tests wouldn't
|
|
catch a regression in this specific resolver."""
|
|
bundle_bin = tmp_path / "resources" / "bin"
|
|
bundle_bin.mkdir(parents=True)
|
|
bundled_exe = bundle_bin / "vgmstream-cli"
|
|
_make_executable(bundled_exe, "#!/bin/sh\nexit 0\n")
|
|
|
|
path_dir = tmp_path / "path-bin"
|
|
path_dir.mkdir()
|
|
on_path = path_dir / "vgmstream-cli"
|
|
_make_executable(on_path, "#!/bin/sh\nexit 0\n")
|
|
|
|
monkeypatch.delenv("VGMSTREAM_CLI", raising=False)
|
|
monkeypatch.setattr("audio._bundled_bin_dir", lambda: bundle_bin)
|
|
monkeypatch.setenv("PATH", str(path_dir))
|
|
|
|
assert _vgmstream_cmd() == str(bundled_exe)
|
|
|
|
|
|
@requires_posix_subprocess
|
|
def test_bundled_binary_skipped_when_not_executable(tmp_path, monkeypatch):
|
|
"""A bundled binary file that has lost its +x bit (an unpacked tar
|
|
that didn't preserve perms, an over-restrictive umask, etc.) must
|
|
not be returned as the resolved decoder — `_vgmstream_cmd` should
|
|
fall through to PATH and pick the working binary there. Otherwise a
|
|
bad bundle marker silently blocks an otherwise-fine installation."""
|
|
bundle_bin = tmp_path / "resources" / "bin"
|
|
bundle_bin.mkdir(parents=True)
|
|
bundled_exe = bundle_bin / "vgmstream-cli"
|
|
bundled_exe.write_text("#!/bin/sh\nexit 0\n", encoding="utf-8")
|
|
# Deliberately do NOT chmod +x. Bundled file exists but isn't runnable.
|
|
|
|
path_dir = tmp_path / "path-bin"
|
|
path_dir.mkdir()
|
|
on_path = path_dir / "vgmstream-cli"
|
|
_make_executable(on_path, "#!/bin/sh\nexit 0\n")
|
|
|
|
monkeypatch.delenv("VGMSTREAM_CLI", raising=False)
|
|
monkeypatch.setattr("audio._bundled_bin_dir", lambda: bundle_bin)
|
|
monkeypatch.setenv("PATH", str(path_dir))
|
|
|
|
# Should resolve the PATH binary, not the unexecutable bundled one.
|
|
assert _vgmstream_cmd() == str(on_path.resolve())
|
|
|
|
|
|
@requires_posix_subprocess
|
|
def test_vgmstream_cli_env_overrides_bundled_binary(tmp_path, monkeypatch):
|
|
"""VGMSTREAM_CLI must beat the desktop-bundled binary too, so a user
|
|
with a broken bundled release can still force a known-good build."""
|
|
bundle_bin = tmp_path / "resources" / "bin"
|
|
bundle_bin.mkdir(parents=True)
|
|
bundled_exe = bundle_bin / "vgmstream-cli"
|
|
_make_executable(bundled_exe, "#!/bin/sh\nexit 0\n")
|
|
|
|
override = tmp_path / "override-vgmstream"
|
|
_make_executable(override, "#!/bin/sh\nexit 0\n")
|
|
|
|
# Fake the bundle-detection so it points at our temp bundle layout.
|
|
monkeypatch.setattr("audio._bundled_bin_dir", lambda: bundle_bin)
|
|
monkeypatch.setenv("VGMSTREAM_CLI", str(override))
|
|
monkeypatch.setenv("PATH", "")
|
|
|
|
assert _vgmstream_cmd() == str(override.resolve())
|
|
|
|
|
|
@requires_posix_subprocess
|
|
def test_vgmstream_cmd_falls_back_to_repo_local_binary(tmp_path, monkeypatch):
|
|
exe = tmp_path / "vgmstream" / "cli" / "vgmstream-cli"
|
|
exe.parent.mkdir(parents=True)
|
|
_make_executable(exe, "#!/bin/sh\nexit 0\n")
|
|
|
|
monkeypatch.delenv("VGMSTREAM_CLI", raising=False)
|
|
monkeypatch.setenv("PATH", "")
|
|
monkeypatch.setattr("audio._repo_root", lambda: tmp_path)
|
|
# Disable the desktop-bundle detection — on a developer machine where
|
|
# `<parents[2]>/bin/vgmstream-cli` happens to exist (the layout
|
|
# _bundled_bin_dir scans for) this assertion would resolve that binary
|
|
# instead of the temp repo-local one and fail for env-dependent reasons.
|
|
monkeypatch.setattr("audio._bundled_bin_dir", lambda: None)
|
|
|
|
assert _vgmstream_cmd() == str(exe.resolve())
|
|
|
|
|
|
@requires_posix_subprocess
|
|
def test_convert_wem_error_scrubs_unrelated_absolute_paths(tmp_path, monkeypatch):
|
|
"""If a decoder happens to emit an absolute path the caller never
|
|
passed in (a plugin search path, a system file the decoder couldn't
|
|
open, etc.), the generic-path regex in `_scrub_paths` should redact
|
|
that too. This pins the behavior the explicit-paths pass alone
|
|
couldn't deliver."""
|
|
decoder_dir = tmp_path / "tooldir"
|
|
decoder_dir.mkdir()
|
|
exe = decoder_dir / "vgmstream-cli"
|
|
# Emit an unrelated absolute path the caller doesn't know about.
|
|
_make_executable(
|
|
exe,
|
|
'#!/bin/sh\necho "could not open /etc/unrelated-secret/file.cfg" 1>&2\nexit 2\n',
|
|
)
|
|
|
|
monkeypatch.delenv("VGMSTREAM_CLI", raising=False)
|
|
monkeypatch.setenv("PATH", str(decoder_dir))
|
|
monkeypatch.setattr("audio._bundled_bin_dir", lambda: None)
|
|
|
|
wem_path = tmp_path / "song.wem"
|
|
wem_path.write_bytes(b"")
|
|
|
|
with pytest.raises(RuntimeError) as ei:
|
|
convert_wem(str(wem_path), str(tmp_path / "out"))
|
|
|
|
msg = str(ei.value)
|
|
assert "/etc/unrelated-secret/file.cfg" not in msg
|
|
# Basename of the unrelated path is fine and informative.
|
|
assert "file.cfg" in msg
|
|
|
|
|
|
@requires_posix_subprocess
|
|
def test_convert_wem_truncates_huge_decoder_stderr(tmp_path, monkeypatch):
|
|
"""A decoder that dumps an enormous multi-line banner on failure
|
|
must not let that whole banner ride along into the `audio_error`
|
|
payload — clients receive these over WebSocket. Each fragment is
|
|
truncated to one line, capped under the per-fragment char limit.
|
|
|
|
Uses a pure-shell loop (not `yes | head`) because PATH is narrowed
|
|
to the decoder dir for this test, so external commands wouldn't be
|
|
on the search path."""
|
|
decoder_dir = tmp_path / "tooldir"
|
|
decoder_dir.mkdir()
|
|
exe = decoder_dir / "vgmstream-cli"
|
|
# First line is the actionable error; many subsequent lines are noise.
|
|
body = (
|
|
'#!/bin/sh\n'
|
|
'echo "boom: real reason for failure" 1>&2\n'
|
|
'i=0\n'
|
|
'while [ $i -lt 50 ]; do\n'
|
|
' echo "noise line $i with filler content to expand the payload" 1>&2\n'
|
|
' i=$((i + 1))\n'
|
|
'done\n'
|
|
'exit 2\n'
|
|
)
|
|
_make_executable(exe, body)
|
|
|
|
monkeypatch.delenv("VGMSTREAM_CLI", raising=False)
|
|
monkeypatch.setenv("PATH", str(decoder_dir))
|
|
monkeypatch.setattr("audio._bundled_bin_dir", lambda: None)
|
|
|
|
wem_path = tmp_path / "song.wem"
|
|
wem_path.write_bytes(b"")
|
|
|
|
with pytest.raises(RuntimeError) as ei:
|
|
convert_wem(str(wem_path), str(tmp_path / "out"))
|
|
|
|
msg = str(ei.value)
|
|
# The actionable first line survives.
|
|
assert "real reason for failure" in msg
|
|
# The 50 noise lines do NOT — payload is bounded.
|
|
assert "noise line" not in msg
|
|
# Sanity cap on absolute size of the message.
|
|
assert len(msg) < 2000
|
|
|
|
|
|
@requires_posix_subprocess
|
|
def test_convert_wem_no_decoder_keeps_install_guidance(tmp_path, monkeypatch):
|
|
"""If every decoder is missing entirely (including via a stale
|
|
VGMSTREAM_CLI override), the user-facing error must still be the
|
|
actionable "install vgmstream-cli" guidance — not "Failed to
|
|
decode WEM" with only a resolution note. The stale-override note
|
|
should ride along as a prefix so the misconfigured user understands
|
|
why their override was ignored, but the install guidance is the
|
|
payload."""
|
|
stale = tmp_path / "no-such-binary"
|
|
|
|
monkeypatch.setenv("VGMSTREAM_CLI", str(stale))
|
|
monkeypatch.setenv("PATH", "")
|
|
monkeypatch.setattr("audio._bundled_bin_dir", lambda: None)
|
|
monkeypatch.setattr("audio._repo_root", lambda: tmp_path / "no-repo-binaries-here")
|
|
# Force ffmpeg/ww2ogg out of the picture too so we hit the no-decoder branch.
|
|
monkeypatch.setattr("audio._ffmpeg_cmd", lambda: None)
|
|
monkeypatch.setattr("audio.shutil.which", lambda name: None)
|
|
|
|
wem_path = tmp_path / "song.wem"
|
|
wem_path.write_bytes(b"")
|
|
|
|
with pytest.raises(RuntimeError) as ei:
|
|
convert_wem(str(wem_path), str(tmp_path / "out"))
|
|
|
|
msg = str(ei.value)
|
|
# Install guidance is present.
|
|
assert "Install vgmstream-cli" in msg
|
|
# The stale-override note rides along.
|
|
assert "VGMSTREAM_CLI" in msg
|
|
assert "ignored" in msg
|
|
# We did NOT mislead the user into thinking a decoder ran and failed.
|
|
assert "Failed to decode WEM" not in msg
|
|
|
|
|
|
@requires_posix_subprocess
|
|
def test_convert_wem_appends_install_hint_when_only_ffmpeg_tried(tmp_path, monkeypatch):
|
|
"""If vgmstream-cli isn't found but ffmpeg is and runs+fails, the
|
|
user must still see the install hint — ffmpeg is commonly present
|
|
on user systems and usually can't decode Wwise WEMs, so without
|
|
this hint a user who actually needs vgmstream-cli sees only
|
|
'unsupported input' and never learns what to install."""
|
|
# No decoder dir, no env, no bundled.
|
|
monkeypatch.delenv("VGMSTREAM_CLI", raising=False)
|
|
monkeypatch.setattr("audio._bundled_bin_dir", lambda: None)
|
|
monkeypatch.setattr("audio._repo_root", lambda: tmp_path / "no-repo-binaries")
|
|
|
|
# Provide a fake ffmpeg that always fails.
|
|
ffmpeg_dir = tmp_path / "ffmpeg-dir"
|
|
ffmpeg_dir.mkdir()
|
|
fake_ffmpeg = ffmpeg_dir / "ffmpeg"
|
|
_make_executable(fake_ffmpeg, '#!/bin/sh\necho "unsupported format" 1>&2\nexit 1\n')
|
|
monkeypatch.setattr("audio._ffmpeg_cmd", lambda: str(fake_ffmpeg))
|
|
# No vgmstream-cli, no ww2ogg on PATH.
|
|
monkeypatch.setenv("PATH", "")
|
|
|
|
wem_path = tmp_path / "song.wem"
|
|
wem_path.write_bytes(b"")
|
|
|
|
with pytest.raises(RuntimeError) as ei:
|
|
convert_wem(str(wem_path), str(tmp_path / "out"))
|
|
|
|
msg = str(ei.value)
|
|
# ffmpeg ran and was reported.
|
|
assert "ffmpeg" in msg
|
|
# And the install hint rides along even though a decoder did run.
|
|
# Assert the install-hint *intent* directly — a previous version
|
|
# of this check accepted any mention of "vgmstream-cli", which
|
|
# would still pass if the binary name appeared only in unrelated
|
|
# resolver-note text (e.g. "VGMSTREAM_CLI=foo ignored") and the
|
|
# actionable install line regressed away.
|
|
assert "install vgmstream-cli" in msg.lower()
|
|
|
|
|
|
@requires_posix_subprocess
|
|
def test_convert_wem_handles_ffmpeg_oserror(tmp_path, monkeypatch):
|
|
"""If ffmpeg launches with OSError (wrong architecture, missing
|
|
loader), convert_wem must catch it like the vgmstream branch does
|
|
so the browser receives the aggregated scrubbed error rather than
|
|
the raw exception, and the ww2ogg fallback below still gets a turn."""
|
|
# Skip vgmstream entirely.
|
|
monkeypatch.delenv("VGMSTREAM_CLI", raising=False)
|
|
monkeypatch.setattr("audio._bundled_bin_dir", lambda: None)
|
|
monkeypatch.setattr("audio._repo_root", lambda: tmp_path / "no-repo-binaries")
|
|
monkeypatch.setenv("PATH", "")
|
|
|
|
# Fake ffmpeg path that subprocess.run will raise OSError on.
|
|
bogus_ffmpeg = "/nonexistent/path/to/ffmpeg-binary"
|
|
monkeypatch.setattr("audio._ffmpeg_cmd", lambda: bogus_ffmpeg)
|
|
|
|
# ww2ogg is not installed; we only want to *prove* that the lookup
|
|
# ran, which is the observable evidence that the ffmpeg OSError
|
|
# didn't escape convert_wem and short-circuit the fallback chain.
|
|
ww2ogg_called = []
|
|
|
|
def fake_which(name):
|
|
if name == "ww2ogg":
|
|
ww2ogg_called.append(name)
|
|
return None # not installed
|
|
return None
|
|
monkeypatch.setattr("audio.shutil.which", fake_which)
|
|
|
|
wem_path = tmp_path / "song.wem"
|
|
wem_path.write_bytes(b"")
|
|
|
|
with pytest.raises(RuntimeError) as ei:
|
|
convert_wem(str(wem_path), str(tmp_path / "out"))
|
|
|
|
msg = str(ei.value)
|
|
# ffmpeg OSError was caught and reported, not raised raw.
|
|
assert "ffmpeg mp3" in msg or "ffmpeg wav" in msg
|
|
assert "failed to invoke" in msg
|
|
# The absolute bogus_ffmpeg path was scrubbed to its basename.
|
|
assert "/nonexistent/path/to" not in msg
|
|
# ww2ogg lookup ran (proves ffmpeg OSError didn't escape convert_wem).
|
|
assert ww2ogg_called == ["ww2ogg"]
|
|
|
|
|
|
@requires_posix_subprocess
|
|
def test_convert_wem_falls_back_to_wav_when_post_vgmstream_ffmpeg_fails(tmp_path, monkeypatch):
|
|
"""vgmstream decodes WEM → WAV successfully, then we try to
|
|
transcode WAV → MP3 via ffmpeg. If that ffmpeg invocation raises
|
|
OSError (wrong arch, missing loader), convert_wem must catch it
|
|
and return the WAV — not let the raw exception escape."""
|
|
decoder_dir = tmp_path / "tooldir"
|
|
decoder_dir.mkdir()
|
|
vgmstream_exe = decoder_dir / "vgmstream-cli"
|
|
# Fake vgmstream that "successfully decodes" by creating a non-empty
|
|
# output WAV at the path supplied via -o.
|
|
_make_executable(
|
|
vgmstream_exe,
|
|
'#!/bin/sh\nshift; out=$1; printf "RIFFfakewavdata" > "$out"\nexit 0\n',
|
|
)
|
|
|
|
monkeypatch.delenv("VGMSTREAM_CLI", raising=False)
|
|
monkeypatch.setenv("PATH", str(decoder_dir))
|
|
monkeypatch.setattr("audio._bundled_bin_dir", lambda: None)
|
|
|
|
# Bogus ffmpeg that subprocess.run will raise OSError on.
|
|
monkeypatch.setattr("audio._ffmpeg_cmd", lambda: "/nonexistent/path/to/ffmpeg-binary")
|
|
|
|
wem_path = tmp_path / "song.wem"
|
|
wem_path.write_bytes(b"")
|
|
out_base = tmp_path / "out"
|
|
|
|
# Should NOT raise — should fall back to returning the WAV.
|
|
result = convert_wem(str(wem_path), str(out_base))
|
|
assert result.endswith(".wav")
|
|
assert Path(result).exists()
|
|
|
|
|
|
@requires_posix_subprocess
|
|
def test_convert_wem_handles_vgmstream_oserror(tmp_path, monkeypatch):
|
|
"""If the resolved vgmstream-cli launches with OSError (wrong
|
|
architecture, missing loader, broken bundle), `_decode_wem_to_wav`
|
|
must catch it so the browser receives the scrubbed aggregated
|
|
error and the ffmpeg / ww2ogg fallbacks still get a turn. This is
|
|
one of the production failure modes this PR is meant to improve
|
|
(a desktop bundle from the wrong arch landing on a user's box)."""
|
|
# Point VGMSTREAM_CLI at a bogus path that resolves *as if* it
|
|
# existed — we don't want to fall through to "stale env" treatment,
|
|
# we want subprocess.run to actually be called with this path and
|
|
# raise OSError. Easiest way is to make the resolver return a path
|
|
# that doesn't exist via a direct monkeypatch of _vgmstream_cmd
|
|
# (bypassing the resolver's exec check).
|
|
bogus_vgmstream = "/nonexistent/path/to/vgmstream-binary"
|
|
|
|
def fake_vgmstream_cmd(resolution_notes=None):
|
|
return bogus_vgmstream
|
|
monkeypatch.setattr("audio._vgmstream_cmd", fake_vgmstream_cmd)
|
|
monkeypatch.setattr("audio._ffmpeg_cmd", lambda: None)
|
|
|
|
ww2ogg_called = []
|
|
|
|
def fake_which(name):
|
|
if name == "ww2ogg":
|
|
ww2ogg_called.append(name)
|
|
return None
|
|
return None
|
|
monkeypatch.setattr("audio.shutil.which", fake_which)
|
|
|
|
wem_path = tmp_path / "song.wem"
|
|
wem_path.write_bytes(b"")
|
|
|
|
with pytest.raises(RuntimeError) as ei:
|
|
convert_wem(str(wem_path), str(tmp_path / "out"))
|
|
|
|
msg = str(ei.value)
|
|
# vgmstream OSError was caught, scrubbed, and reported.
|
|
assert "vgmstream" in msg
|
|
assert "failed to invoke" in msg
|
|
# The absolute bogus path got scrubbed.
|
|
assert "/nonexistent/path/to" not in msg
|
|
# The ww2ogg fallback also ran.
|
|
assert ww2ogg_called == ["ww2ogg"]
|
|
|
|
|
|
@requires_posix_subprocess
|
|
def test_convert_wem_surfaces_stale_vgmstream_cli_to_user(tmp_path, monkeypatch):
|
|
"""When VGMSTREAM_CLI is set but invalid AND no other decoder works,
|
|
the user-facing error must mention that the override was ignored —
|
|
otherwise users see the generic "no decoder found" guidance even
|
|
though they did set the env var the guidance tells them to set."""
|
|
stale = tmp_path / "definitely-not-there" # never created
|
|
|
|
monkeypatch.setenv("VGMSTREAM_CLI", str(stale))
|
|
monkeypatch.setenv("PATH", "")
|
|
monkeypatch.setattr("audio._bundled_bin_dir", lambda: None)
|
|
monkeypatch.setattr("audio._repo_root", lambda: tmp_path / "no-repo-binaries-here")
|
|
|
|
wem_path = tmp_path / "song.wem"
|
|
wem_path.write_bytes(b"")
|
|
|
|
with pytest.raises(RuntimeError) as ei:
|
|
convert_wem(str(wem_path), str(tmp_path / "out"))
|
|
|
|
msg = str(ei.value)
|
|
assert "VGMSTREAM_CLI" in msg
|
|
assert "ignored" in msg
|
|
# But the absolute env value itself must not leak.
|
|
assert str(stale) not in msg
|
|
|
|
|
|
@requires_posix_subprocess
|
|
def test_convert_wem_error_does_not_leak_absolute_paths(tmp_path, monkeypatch):
|
|
"""convert_wem's RuntimeError is surfaced to the browser as
|
|
`audio_error`. Make sure neither the wem input path nor the decoder
|
|
binary path appear verbatim in that error — leaking install/user
|
|
paths to the client is needless info disclosure."""
|
|
decoder_dir = tmp_path / "private" / "secret-install"
|
|
decoder_dir.mkdir(parents=True)
|
|
exe = decoder_dir / "vgmstream-cli"
|
|
_make_executable(exe, '#!/bin/sh\necho "failed to read /unrelated/private/file"\nexit 2\n')
|
|
|
|
monkeypatch.delenv("VGMSTREAM_CLI", raising=False)
|
|
monkeypatch.setenv("PATH", str(decoder_dir))
|
|
monkeypatch.setattr("audio._bundled_bin_dir", lambda: None)
|
|
|
|
wem_path = tmp_path / "user-dir" / "deep" / "song_p.wem"
|
|
wem_path.parent.mkdir(parents=True)
|
|
wem_path.write_bytes(b"")
|
|
|
|
with pytest.raises(RuntimeError) as ei:
|
|
convert_wem(str(wem_path), str(tmp_path / "out"))
|
|
|
|
msg = str(ei.value)
|
|
# The basename of the wem is informative and OK to keep.
|
|
assert "song_p.wem" in msg
|
|
# The full input path must not appear.
|
|
assert str(wem_path) not in msg
|
|
# The decoder install path must not appear either.
|
|
assert str(decoder_dir) not in msg
|
|
assert str(exe) not in msg
|
|
|
|
|
|
@requires_posix_subprocess
|
|
def test_convert_wem_reports_decode_failure_not_missing_decoder(tmp_path, monkeypatch):
|
|
exe = tmp_path / "vgmstream-cli"
|
|
_make_executable(exe, "#!/bin/sh\necho boom 1>&2\nexit 1\n")
|
|
|
|
monkeypatch.delenv("VGMSTREAM_CLI", raising=False)
|
|
monkeypatch.setenv("PATH", str(tmp_path))
|
|
# Same isolation concern as above — _vgmstream_cmd checks the bundled
|
|
# location first, so a developer-machine `<parents[2]>/bin/vgmstream-cli`
|
|
# would beat our PATH-resident fake and make convert_wem use the real
|
|
# binary, which won't produce the "boom" error we're asserting on.
|
|
monkeypatch.setattr("audio._bundled_bin_dir", lambda: None)
|
|
|
|
with pytest.raises(RuntimeError, match=r"Failed to decode WEM .*vgmstream: boom"):
|
|
convert_wem(str(tmp_path / "missing.wem"), str(tmp_path / "out"))
|