Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions pylib/tests/test_sound.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
from anki.sound import AV_REF_RE, SoundOrVideoTag, strip_av_refs


def test_sound_tag_path_relative_joins_media_folder():
def test_sound_tag_path_relative_joins_media_folder(tmp_path):
tag = SoundOrVideoTag(filename="audio.mp3")
assert tag.path("/media") == "/media/audio.mp3"
assert tag.path(str(tmp_path)) == os.path.join(str(tmp_path), "audio.mp3")


def test_sound_tag_path_absolute_ignores_media_folder():
Expand All @@ -17,6 +17,27 @@ def test_sound_tag_path_absolute_ignores_media_folder():
assert tag.path("/media") == abs_path


def test_sound_tag_path_with_directory_separator_uses_abspath():
# filename with a separator but not absolute - goes through os.path.abspath,
# so the media folder is ignored and the file's directory is used instead.
tag = SoundOrVideoTag(filename="subdir/audio.mp3")
result = tag.path("/media")
expected = os.path.join(os.path.abspath("subdir"), "audio.mp3")
assert result == expected


def test_sound_tag_path_applies_media_file_filter(monkeypatch, tmp_path):
# hooks.media_file_filter is applied to the tail component of the path.
import anki.sound as sound_module

def fake_filter(fname: str) -> str:
return fname.replace("audio", "renamed")

monkeypatch.setattr(sound_module.hooks, "media_file_filter", fake_filter)
tag = SoundOrVideoTag(filename="audio.mp3")
assert tag.path(str(tmp_path)) == os.path.join(str(tmp_path), "renamed.mp3")


def test_strip_av_refs_removes_play_tag():
assert strip_av_refs("Hello [anki:play:q:0] world") == "Hello world"

Expand Down
100 changes: 96 additions & 4 deletions qt/tests/test_sound.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,122 @@

import shutil
import subprocess
import wave
from pathlib import Path
from unittest.mock import MagicMock

import pytest

from aqt.sound import _packagedCmd, is_audio_file
import aqt
from anki.sound import SoundOrVideoTag
from anki.utils import is_mac, is_win
from aqt.sound import MpvManager, _packagedCmd, is_audio_file


def test_is_audio_file_recognizes_common_formats():
for ext in ("mp3", "wav", "ogg", "flac", "m4a", "opus", "spx", "oga"):
assert is_audio_file(f"test.{ext}")


def test_is_audio_file_is_case_insensitive():
for ext in ("MP3", "WAV", "OGG", "FLAC"):
assert is_audio_file(f"test.{ext}")


def test_is_audio_file_rejects_non_audio():
# mp4/avi are video-only; jpg/png/pdf are not media Anki plays via mpv.
for ext in ("mp4", "avi", "jpg", "png", "pdf"):
assert not is_audio_file(f"test.{ext}")


def test_mpv_binary_runs():
cmd, env = _packagedCmd(["mpv"])
def test_is_audio_file_rejects_no_extension():
assert not is_audio_file("audiofile")


def test_packagedcmd_returns_absolute_path_when_anki_audio_available():
# _packagedCmd should prefer the binary bundled in anki_audio over a
# system-wide one on macOS and Windows. This is the regression caught by
# issue #5015: an updated anki_audio build was not being picked up.
if not (is_mac or is_win):
pytest.skip("anki_audio binary preference is only used on macOS/Windows")

try:
from pathlib import Path as _Path

import anki_audio

audio_pkg_path = _Path(anki_audio.__file__).parent
except ImportError:
pytest.skip("anki_audio package not installed")

cmd, _env = _packagedCmd(["mpv"])
mpv_path = Path(cmd[0])

assert mpv_path.is_absolute(), "expected an absolute path to the packaged binary"
assert str(audio_pkg_path) in str(mpv_path), (
f"expected binary inside anki_audio package at {audio_pkg_path}, got {mpv_path}"
)


def _resolved_mpv_command(args: list[str]) -> tuple[list[str], dict[str, str]]:
cmd, env = _packagedCmd(args)
mpv = cmd[0]
if not Path(mpv).is_absolute():
mpv = shutil.which(mpv)
if mpv is None:
pytest.skip("mpv not found")
cmd[0] = mpv

return cmd, env


def test_mpv_binary_runs():
cmd, env = _resolved_mpv_command(["mpv"])
result = subprocess.run(cmd + ["--version"], env=env, capture_output=True)
assert result.returncode == 0, result.stderr.decode()

result = subprocess.run([mpv, "--version"], env=env, capture_output=True)

@pytest.fixture
def generated_wav(tmp_path: Path) -> Path:
wav_path = tmp_path / "silence.wav"
with wave.open(str(wav_path), "wb") as wav:
wav.setnchannels(1)
wav.setsampwidth(2)
wav.setframerate(44_100)
wav.writeframes(b"\0\0" * 4_410)
return wav_path


def test_mpv_can_play_generated_wav(generated_wav: Path):
cmd, env = _resolved_mpv_command(
[
"mpv",
"--no-terminal",
"--force-window=no",
"--audio-display=no",
"--keep-open=no",
"--autoload-files=no",
"--ao=null",
"--vo=null",
"--",
str(generated_wav),
]
)

result = subprocess.run(cmd, env=env, capture_output=True, timeout=10)
assert result.returncode == 0, result.stderr.decode()


def test_mpvmanager_can_play_generated_wav(
monkeypatch, tmp_path: Path, generated_wav: Path
):
monkeypatch.setattr(
MpvManager, "default_argv", MpvManager.default_argv + ["--ao=null", "--vo=null"]
)
mock_mw = MagicMock()
mock_mw.taskman.run_in_background.side_effect = (
lambda task, on_done=None, **kwargs: task()
)
monkeypatch.setattr(aqt, "mw", mock_mw)
manager = MpvManager(tmp_path, tmp_path)
manager.play(SoundOrVideoTag(filename=str(generated_wav.name)), lambda _: None)
Loading