Files
Tunetti/tests/test_player_helpers.py
NikkeDoy 93f5239ef7
Some checks failed
SonarQube Code Quality Scan / SonarQube Scan (push) Failing after 2m37s
| Add tests
2026-06-03 18:53:24 +03:00

278 lines
9.9 KiB
Python

"""Tests for helper functions in the player module (player.py)."""
import os
import struct
import math
import tempfile
import pytest
# ── _best_thumbnail ───────────────────────────────────────────────────────────
class TestBestThumbnail:
"""Tests for the _best_thumbnail helper."""
def _import(self):
from player import _best_thumbnail
return _best_thumbnail
def test_none_or_empty(self):
fn = self._import()
assert fn(None) == ""
assert fn([]) == ""
def test_string_returned_as_is(self):
fn = self._import()
assert fn("https://example.com/thumb.jpg") == "https://example.com/thumb.jpg"
def test_list_returns_last_url(self):
fn = self._import()
thumbs = [
{"url": "https://example.com/small.jpg"},
{"url": "https://example.com/medium.jpg"},
{"url": "https://example.com/large.jpg"},
]
assert fn(thumbs) == "https://example.com/large.jpg"
def test_list_without_url_keys(self):
fn = self._import()
thumbs = [{"width": 100}, {"width": 200}]
assert fn(thumbs) == ""
def test_list_with_non_dict_items(self):
fn = self._import()
assert fn(["string_thumb"]) == "string_thumb"
def test_non_list_non_string(self):
fn = self._import()
assert fn(123) == "123"
# ── _build_song_dict ─────────────────────────────────────────────────────────
class TestBuildSongDict:
"""Tests for the _build_song_dict helper."""
def _import(self):
from player import _build_song_dict
return _build_song_dict
def test_minimal_result(self):
fn = self._import()
result = {"videoId": "abc123", "title": "Test Song"}
song = fn(result)
assert song["videoId"] == "abc123"
assert song["title"] == "Test Song"
assert song["artists"] == []
assert song["album"] is None
assert song["duration"] == 0
assert song["duration_label"] == "?"
assert song["thumbnail"] == ""
assert song["videoType"] == ""
assert song["resultType"] == "song"
def test_full_result(self):
fn = self._import()
result = {
"videoId": "xyz789",
"title": "Full Song",
"artists": [{"name": "Artist", "id": "a1"}],
"album": {"name": "Album", "id": "al1"},
"duration_seconds": 300,
"duration": "5:00",
"thumbnails": [{"url": "https://ex.com/t.jpg"}],
"videoType": "MUSIC_VIDEO_TYPE_ATV",
"resultType": "video",
}
song = fn(result)
assert song["videoId"] == "xyz789"
assert song["title"] == "Full Song"
assert song["artists"] == [{"name": "Artist", "id": "a1"}]
assert song["album"] == {"name": "Album", "id": "al1"}
assert song["duration"] == 300
assert song["duration_label"] == "5:00"
assert song["thumbnail"] == "https://ex.com/t.jpg"
assert song["videoType"] == "MUSIC_VIDEO_TYPE_ATV"
assert song["resultType"] == "video"
def test_missing_video_id_defaults_to_empty(self):
fn = self._import()
song = fn({})
assert song["videoId"] == ""
assert song["title"] == "Unknown"
assert song["duration"] == 0
def test_thumbnail_from_thumbnails_list(self):
fn = self._import()
result = {
"thumbnails": [
{"url": "https://ex.com/small.jpg"},
{"url": "https://ex.com/large.jpg"},
]
}
song = fn(result)
assert song["thumbnail"] == "https://ex.com/large.jpg"
# ── _generate_test_wav ───────────────────────────────────────────────────────
class TestGenerateTestWav:
"""Tests for the _generate_test_wav helper."""
def _import(self):
from player import _generate_test_wav
return _generate_test_wav
def test_generates_valid_wav_file(self, tmp_path):
"""Generated WAV has valid RIFF header and correct size."""
fn = self._import()
wav_path = str(tmp_path / "test.wav")
fn(wav_path, duration_s=1)
with open(wav_path, "rb") as f:
data = f.read()
# RIFF header
assert data[:4] == b"RIFF"
assert data[8:12] == b"WAVE"
assert data[12:16] == b"fmt "
# PCM format
(audio_format, num_channels, sample_rate, byte_rate,
block_align, bits_per_sample) = struct.unpack_from("<HHIIHH", data, 20)
assert audio_format == 1 # PCM
assert num_channels == 1 # mono
assert sample_rate == 44100
assert bits_per_sample == 16
# data chunk: "data" marker at offset 36, size at offset 40
data_size = struct.unpack_from("<I", data, 40)[0]
expected_size = 44100 * 2 # 1 sec * 2 bytes per sample
assert data_size == expected_size
def test_generates_sine_wave_content(self, tmp_path):
"""Generated WAV contains a recognizable 440 Hz sine wave."""
fn = self._import()
wav_path = str(tmp_path / "test.wav")
fn(wav_path, duration_s=1)
with open(wav_path, "rb") as f:
f.seek(44)
samples = f.read()
# Decode first 100 samples
first_samples = []
for i in range(100):
sample = struct.unpack_from("<h", samples, i * 2)[0]
first_samples.append(sample)
# The samples should alternate (sine wave), not be all zero
assert any(s != 0 for s in first_samples), "Sine wave should produce non-zero samples"
# First sample should be 0 (sine starts at 0)
assert abs(first_samples[0]) < 3276, "Sine wave should start near 0"
# ── _stale_files ─────────────────────────────────────────────────────────────
class TestStaleFiles:
"""Tests for the _stale_files helper."""
def _import_fixtures(self):
from player import _tmp_dir, _stale_files
return _tmp_dir, _stale_files
def test_returns_empty_when_no_files(self, monkeypatch):
"""No files in tmp dir → empty list."""
_tmp_dir, _stale_files = self._import_fixtures()
monkeypatch.setattr("player._TUNETTI_TMP", tempfile.mkdtemp(prefix="tunetti_test_"))
result = _stale_files("nonexistent_video")
assert result == []
def test_returns_matching_files_ordered_by_mtime(self, monkeypatch):
"""Matching files are returned newest first."""
_tmp_dir, _stale_files = self._import_fixtures()
import time
td = tempfile.mkdtemp(prefix="tunetti_test_")
monkeypatch.setattr("player._TUNETTI_TMP", td)
# Create two files with different mtimes
old_file = os.path.join(td, "vid123.mp3")
new_file = os.path.join(td, "vid123.m4a")
with open(old_file, "w") as f:
f.write("data")
old_mtime = time.time() - 10
os.utime(old_file, (old_mtime, old_mtime))
with open(new_file, "w") as f:
f.write("data")
result = _stale_files("vid123")
assert len(result) == 2
# Newest first
assert os.path.basename(result[0]) == "vid123.m4a"
assert os.path.basename(result[1]) == "vid123.mp3"
def test_ignores_temp_files(self, monkeypatch):
"""Files with '.temp.' in name are excluded."""
_tmp_dir, _stale_files = self._import_fixtures()
td = tempfile.mkdtemp(prefix="tunetti_test_")
monkeypatch.setattr("player._TUNETTI_TMP", td)
# A "temp" download in progress and a completed file
with open(os.path.join(td, "vid123.temp.xyz.part"), "w") as f:
f.write("incomplete")
with open(os.path.join(td, "vid123.mp3"), "w") as f:
f.write("complete")
result = _stale_files("vid123")
assert len(result) == 1
assert "vid123.mp3" in result[0]
def test_ignores_empty_files(self, monkeypatch):
"""Zero-byte files are excluded."""
_tmp_dir, _stale_files = self._import_fixtures()
td = tempfile.mkdtemp(prefix="tunetti_test_")
monkeypatch.setattr("player._TUNETTI_TMP", td)
with open(os.path.join(td, "vid123.mp3"), "w") as f:
pass # empty
result = _stale_files("vid123")
assert result == []
# ── _build_resolved_dict ──────────────────────────────────────────────────────
class TestBuildResolvedDict:
"""Tests for DownloadWorker._build_resolved_dict."""
def _import(self):
from player import DownloadWorker
return DownloadWorker._build_resolved_dict
def test_builds_resolved_dict(self):
fn = self._import()
song = {"videoId": "abc", "title": "Original"}
info = {"duration": 120, "thumbnail": "https://ex.com/t.jpg", "title": "Actual Title"}
temp_path = "/tmp/tunetti_abc.mp3"
resolved = fn(song, info, temp_path)
assert resolved["_local_path"] == temp_path
assert resolved["duration"] == 120
assert resolved["thumbnail"] == "https://ex.com/t.jpg"
assert resolved["title"] == "Actual Title"
assert resolved["videoId"] == "abc"
def test_falls_back_to_song_metadata(self):
fn = self._import()
song = {"videoId": "abc", "duration": 99, "title": "Fallback"}
info = {} # empty info
resolved = fn(song, info, "/tmp/t.mp3")
assert resolved["duration"] == 99
assert resolved["title"] == "Fallback"
assert resolved["thumbnail"] == ""