Some checks failed
SonarQube Code Quality Scan / SonarQube Scan (push) Failing after 1m20s
- Fix dead exception handler in discord_rpc.py (unreachable DiscordNotFound) - Replace QMediaPlayer=None stub with proper _StubQMediaPlayer in player.py - Make AudioVisualizer color stops Qt-free (raw tuples instead of QColor) - Add conftest QApplication creation check for headless CI detection - Add test_gui_widgets.py _qt_available() guard for headless environments - Add test_main.py (30 tests) for main.py entry point and env vars - Add test_import_fallbacks.py (14 tests) for Qt import fallback paths - Extend test_gui_helpers.py with _css, _get_thumbnail_nam, _cached_thumb_path tests - Extend test_discord_rpc.py with direct _connect() test for DiscordNotFound - Extend test_player_helpers.py with SearchWorker test-mode tests - Ensure import fallback tests clean up sys.modules to avoid cross-test pollution 100% coverage achieved on: config.py, discord_rpc.py, main.py, music_db.py
226 lines
6.9 KiB
Python
226 lines
6.9 KiB
Python
"""Tests for helper functions in the GUI module (gui.py)."""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
|
|
# ── _fmt_ms ───────────────────────────────────────────────────────────────────
|
|
|
|
class TestFmtMs:
|
|
"""Tests for the _fmt_ms helper."""
|
|
|
|
def _import(self):
|
|
from gui import _fmt_ms
|
|
return _fmt_ms
|
|
|
|
def test_zero(self):
|
|
fn = self._import()
|
|
assert fn(0) == "0:00"
|
|
|
|
def test_seconds_only(self):
|
|
fn = self._import()
|
|
assert fn(5_000) == "0:05"
|
|
assert fn(59_000) == "0:59"
|
|
|
|
def test_minutes(self):
|
|
fn = self._import()
|
|
assert fn(60_000) == "1:00"
|
|
assert fn(90_000) == "1:30"
|
|
assert fn(3599_000) == "59:59"
|
|
|
|
def test_hours_display(self):
|
|
fn = self._import()
|
|
assert fn(3600_000) == "60:00"
|
|
assert fn(3661_000) == "61:01"
|
|
|
|
def test_large_duration(self):
|
|
fn = self._import()
|
|
assert fn(5999_000) == "99:59"
|
|
|
|
|
|
# ── _artists_str ─────────────────────────────────────────────────────────────
|
|
|
|
class TestArtistsStr:
|
|
"""Tests for the _artists_str helper."""
|
|
|
|
def _import(self):
|
|
from gui import _artists_str
|
|
return _artists_str
|
|
|
|
def test_none(self):
|
|
fn = self._import()
|
|
assert fn(None) == ""
|
|
|
|
def test_empty_list(self):
|
|
fn = self._import()
|
|
assert fn([]) == ""
|
|
|
|
def test_not_a_list(self):
|
|
fn = self._import()
|
|
assert fn("str") == ""
|
|
assert fn(42) == ""
|
|
|
|
def test_single_artist(self):
|
|
fn = self._import()
|
|
assert fn([{"name": "Artist One", "id": "a1"}]) == "Artist One"
|
|
|
|
def test_multiple_artists(self):
|
|
fn = self._import()
|
|
artists = [
|
|
{"name": "Artist One", "id": "a1"},
|
|
{"name": "Artist Two", "id": "a2"},
|
|
]
|
|
assert fn(artists) == "Artist One, Artist Two"
|
|
|
|
def test_artist_without_name_key(self):
|
|
fn = self._import()
|
|
assert fn([{"id": "a1"}]) == ""
|
|
|
|
def test_mixed_valid_and_invalid(self):
|
|
fn = self._import()
|
|
artists = [
|
|
{"name": "Valid", "id": "v1"},
|
|
{"id": "no_name"},
|
|
{"name": "Also Valid", "id": "v2"},
|
|
]
|
|
assert fn(artists) == "Valid, Also Valid"
|
|
|
|
|
|
# ── _norm_artists ────────────────────────────────────────────────────────────
|
|
|
|
class TestNormArtists:
|
|
"""Tests for the _norm_artists helper."""
|
|
|
|
def _import(self):
|
|
from gui import _norm_artists
|
|
return _norm_artists
|
|
|
|
def test_none(self):
|
|
fn = self._import()
|
|
assert fn(None) == []
|
|
|
|
def test_not_a_list(self):
|
|
fn = self._import()
|
|
assert fn("not a list") == []
|
|
assert fn(42) == []
|
|
|
|
def test_empty_list(self):
|
|
fn = self._import()
|
|
assert fn([]) == []
|
|
|
|
def test_filters_non_dicts(self):
|
|
fn = self._import()
|
|
artists = [{"name": "Real"}, "just a string", 123, {"name": "Another"}]
|
|
result = fn(artists)
|
|
assert len(result) == 2
|
|
assert result[0]["name"] == "Real"
|
|
assert result[1]["name"] == "Another"
|
|
|
|
def test_all_valid(self):
|
|
fn = self._import()
|
|
artists = [{"name": "A"}, {"name": "B"}]
|
|
assert fn(artists) == artists
|
|
|
|
|
|
# ── _artists_from_json ───────────────────────────────────────────────────────
|
|
|
|
class TestArtistsFromJson:
|
|
"""Tests for the _artists_from_json helper."""
|
|
|
|
def _import(self):
|
|
from gui import _artists_from_json
|
|
return _artists_from_json
|
|
|
|
def test_valid_json_array(self):
|
|
fn = self._import()
|
|
j = json.dumps([{"name": "Artist A", "id": "a1"}, {"name": "Artist B", "id": "a2"}])
|
|
assert fn(j) == "Artist A, Artist B"
|
|
|
|
def test_single_artist_json(self):
|
|
fn = self._import()
|
|
j = json.dumps([{"name": "Solo Artist", "id": "s1"}])
|
|
assert fn(j) == "Solo Artist"
|
|
|
|
def test_invalid_json(self):
|
|
fn = self._import()
|
|
assert fn("not json") == "not json"
|
|
|
|
def test_empty_array(self):
|
|
fn = self._import()
|
|
assert fn("[]") == ""
|
|
|
|
def test_plain_string_input(self):
|
|
fn = self._import()
|
|
assert fn("Just a name") == "Just a name"
|
|
|
|
def test_jagged_array_skips_non_dicts(self):
|
|
fn = self._import()
|
|
j = json.dumps([{"name": "Real"}, 42, "str", {"name": "Also Real"}])
|
|
assert fn(j) == "Real, Also Real"
|
|
|
|
|
|
# ── _css ───────────────────────────────────────────────────────────────────
|
|
|
|
class TestCss:
|
|
"""Tests for the _css helper."""
|
|
|
|
def _import(self):
|
|
from gui import _css
|
|
return _css
|
|
|
|
def test_joins_multiple_parts(self):
|
|
fn = self._import()
|
|
assert fn("color: red;", "background: blue;") == "color: red;background: blue;"
|
|
|
|
def test_single_part(self):
|
|
fn = self._import()
|
|
assert fn("color: red;") == "color: red;"
|
|
|
|
def test_empty_parts(self):
|
|
fn = self._import()
|
|
assert fn() == ""
|
|
|
|
|
|
# ── _get_thumbnail_nam ────────────────────────────────────────────────────
|
|
|
|
class TestGetThumbnailNam:
|
|
"""Tests for the _get_thumbnail_nam helper."""
|
|
|
|
def _import(self):
|
|
from gui import _get_thumbnail_nam
|
|
return _get_thumbnail_nam
|
|
|
|
def test_returns_network_access_manager(self):
|
|
fn = self._import()
|
|
nam = fn()
|
|
# Should return some kind of object (QNetworkAccessManager or MagicMock)
|
|
assert nam is not None
|
|
|
|
def test_is_singleton(self):
|
|
fn = self._import()
|
|
nam1 = fn()
|
|
nam2 = fn()
|
|
assert nam1 is nam2
|
|
|
|
|
|
# ── _cached_thumb_path ────────────────────────────────────────────────────
|
|
|
|
class TestCachedThumbPath:
|
|
"""Tests for the _cached_thumb_path helper."""
|
|
|
|
def _import(self):
|
|
from gui import _cached_thumb_path, THUMB_CACHE_DIR
|
|
return _cached_thumb_path, THUMB_CACHE_DIR
|
|
|
|
def test_uses_cache_dir(self):
|
|
fn, cache_dir = self._import()
|
|
path = fn("video123")
|
|
assert str(path) == str(cache_dir / "video123.jpg")
|
|
|
|
def test_ends_with_jpg(self):
|
|
fn, _ = self._import()
|
|
path = fn("abc_def")
|
|
assert path.suffix == ".jpg"
|
|
assert "abc_def" in path.name
|