mirror of
https://github.com/kuhyx/testsAndMisc-archive.git
synced 2026-07-04 15:03:04 +02:00
Fix ruff violations in ~15 source files and ~60+ test files to minimize per-file-ignores in pyproject.toml. Remaining ignores are justified with comments explaining why each suppression is necessary. Source fixes: FBT003 (keyword args), S310 (URL validation), SLF001 (private access), T201 (print→logging), C901 (complexity), E501 (line length), E402 (import order). Test fixes: SIM117 (combined with), FBT (boolean args), PERF203 (try in loop), S310/S607 (URLs/executables), E402/E501 (imports/lines), S108 (tmp paths), PLR0913 (too many args), ARG (unused args), ANN (type annotations), RUF059 (unused unpacked vars), PT019 (fixture naming). Remaining per-file-ignores (with justifications): - Tests: ARG, D, PLC0415, PLR2004, S101, SLF001 - music_gen sources: PLC0415 (heavy ML lazy imports) - moviepy_showcase: PLC0415 (circular dependency) - generate_images: PLR0913 (matplotlib helpers need many params) - praca_magisterska_video: E501, E402 (long paths, mpl.use)
484 lines
16 KiB
Python
484 lines
16 KiB
Python
"""Tests for _cinema_parsing module."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import subprocess
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import MagicMock, mock_open, patch
|
|
|
|
import pytest
|
|
|
|
from python_pkg.cinema_planner._cinema_parsing import (
|
|
_exit_no_pdf_support,
|
|
_parse_cinema_city_pdf_basic,
|
|
_try_parse_interactive_line,
|
|
_try_parse_manual_line,
|
|
_try_parse_time,
|
|
extract_date_from_html,
|
|
parse_cinema_city_html,
|
|
parse_cinema_city_pdf,
|
|
parse_cinema_city_text,
|
|
parse_duration,
|
|
parse_manual_line,
|
|
parse_time,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
import contextlib
|
|
|
|
|
|
class TestParseTime:
|
|
"""Tests for parse_time."""
|
|
|
|
def test_standard_time(self) -> None:
|
|
assert parse_time("18:20") == 18 * 60 + 20
|
|
|
|
def test_time_with_spaces(self) -> None:
|
|
assert parse_time(" 09:05 ") == 9 * 60 + 5
|
|
|
|
def test_time_with_dot(self) -> None:
|
|
assert parse_time("14.30") == 14 * 60 + 30
|
|
|
|
def test_single_digit_hour(self) -> None:
|
|
assert parse_time("9:05") == 9 * 60 + 5
|
|
|
|
def test_midnight(self) -> None:
|
|
assert parse_time("0:00") == 0
|
|
|
|
def test_invalid_format(self) -> None:
|
|
with pytest.raises(ValueError, match="Invalid time format"):
|
|
parse_time("abc")
|
|
|
|
def test_invalid_no_colon(self) -> None:
|
|
with pytest.raises(ValueError, match="Invalid time format"):
|
|
parse_time("1820")
|
|
|
|
|
|
class TestParseDuration:
|
|
"""Tests for parse_duration."""
|
|
|
|
def test_minutes_with_min(self) -> None:
|
|
assert parse_duration("110 min") == 110
|
|
|
|
def test_minutes_with_min_no_space(self) -> None:
|
|
assert parse_duration("90min") == 90
|
|
|
|
def test_hours_and_minutes(self) -> None:
|
|
assert parse_duration("1h 46m") == 106
|
|
|
|
def test_hours_only(self) -> None:
|
|
assert parse_duration("2h") == 120
|
|
|
|
def test_minutes_only_m(self) -> None:
|
|
assert parse_duration("46m") == 46
|
|
|
|
def test_colon_format(self) -> None:
|
|
assert parse_duration("1:46") == 106
|
|
|
|
def test_pure_number(self) -> None:
|
|
assert parse_duration("110") == 110
|
|
|
|
def test_invalid_format(self) -> None:
|
|
with pytest.raises(ValueError, match="Invalid duration format"):
|
|
parse_duration("abc")
|
|
|
|
|
|
class TestParseManualLine:
|
|
"""Tests for parse_manual_line."""
|
|
|
|
def test_basic_line(self) -> None:
|
|
result = parse_manual_line("Inception, 10:30 or 14:00, 2h 28m")
|
|
assert result is not None
|
|
assert result.name == "Inception"
|
|
assert result.start_times == [10 * 60 + 30, 14 * 60]
|
|
assert result.duration == 148
|
|
|
|
def test_empty_line(self) -> None:
|
|
assert parse_manual_line("") is None
|
|
|
|
def test_comment_line(self) -> None:
|
|
assert parse_manual_line("# comment") is None
|
|
|
|
def test_whitespace_line(self) -> None:
|
|
assert parse_manual_line(" ") is None
|
|
|
|
def test_too_few_parts(self) -> None:
|
|
with pytest.raises(ValueError, match="Invalid line format"):
|
|
parse_manual_line("Movie, 10:30")
|
|
|
|
def test_single_time(self) -> None:
|
|
result = parse_manual_line("Movie A, 18:20, 1h 46m")
|
|
assert result is not None
|
|
assert result.start_times == [18 * 60 + 20]
|
|
|
|
def test_multiple_times(self) -> None:
|
|
result = parse_manual_line("Movie B, 10:00 or 14:00 or 18:00, 120")
|
|
assert result is not None
|
|
assert len(result.start_times) == 3
|
|
|
|
def test_duration_with_comma(self) -> None:
|
|
# If duration part contains comma, the rest after parts[1] is duration
|
|
result = parse_manual_line("Movie C, 10:00, 1h, 30m")
|
|
assert result is not None
|
|
|
|
|
|
class TestTryParseTime:
|
|
"""Tests for _try_parse_time."""
|
|
|
|
def test_valid(self) -> None:
|
|
assert _try_parse_time("10:30") == 10 * 60 + 30
|
|
|
|
def test_invalid(self) -> None:
|
|
assert _try_parse_time("abc") is None
|
|
|
|
|
|
class TestTryParseManualLine:
|
|
"""Tests for _try_parse_manual_line."""
|
|
|
|
def test_valid_line(self) -> None:
|
|
result = _try_parse_manual_line("Movie, 10:00, 90min")
|
|
assert result is not None
|
|
assert result.name == "Movie"
|
|
|
|
def test_invalid_line_with_error_stream(self) -> None:
|
|
stream = MagicMock()
|
|
result = _try_parse_manual_line("bad line", stream)
|
|
assert result is None
|
|
stream.write.assert_called_once()
|
|
|
|
def test_invalid_line_no_error_stream(self) -> None:
|
|
result = _try_parse_manual_line("bad line")
|
|
assert result is None
|
|
|
|
def test_empty_line(self) -> None:
|
|
result = _try_parse_manual_line("")
|
|
assert result is None
|
|
|
|
|
|
class TestTryParseInteractiveLine:
|
|
"""Tests for _try_parse_interactive_line."""
|
|
|
|
def test_valid_line(self) -> None:
|
|
result = _try_parse_interactive_line("Movie, 10:00, 90min")
|
|
assert result is not None
|
|
assert result.name == "Movie"
|
|
|
|
def test_invalid_line(self) -> None:
|
|
result = _try_parse_interactive_line("bad line")
|
|
assert result is None
|
|
|
|
def test_empty_line(self) -> None:
|
|
result = _try_parse_interactive_line("")
|
|
assert result is None
|
|
|
|
|
|
class TestExtractDateFromHtml:
|
|
"""Tests for extract_date_from_html."""
|
|
|
|
def test_found_date(self) -> None:
|
|
assert extract_date_from_html("schedule 2025-01-25 data") == "2025-01-25"
|
|
|
|
def test_no_date(self) -> None:
|
|
assert extract_date_from_html("no date here") is None
|
|
|
|
def test_non_202x_date(self) -> None:
|
|
assert extract_date_from_html("1999-01-01") is None
|
|
|
|
|
|
class TestParseCinemaCityHtml:
|
|
"""Tests for parse_cinema_city_html."""
|
|
|
|
def _make_html_section(
|
|
self,
|
|
name: str,
|
|
duration: int,
|
|
times: list[str],
|
|
*,
|
|
genre: str = "",
|
|
) -> str:
|
|
genre_html = ""
|
|
if genre:
|
|
genre_html = f'<span class="mr-sm">{genre}<span>x</span></span>'
|
|
times_html = "".join(
|
|
f'<button class="btn btn-primary btn-lg">{t}</button>' for t in times
|
|
)
|
|
return (
|
|
f'class="row movie-row">'
|
|
f'<span class="qb-movie-name">{name}</span>'
|
|
f"{genre_html}"
|
|
f"<span>{duration} min</span>"
|
|
f"{times_html}"
|
|
)
|
|
|
|
def _patch_open(self, html: str) -> contextlib.AbstractContextManager[MagicMock]:
|
|
return patch.object(Path, "open", mock_open(read_data=html))
|
|
|
|
def test_parse_single_movie(self) -> None:
|
|
html = "header" + self._make_html_section("Movie A", 120, ["10:00", "14:00"])
|
|
with self._patch_open(html):
|
|
movies, _ = parse_cinema_city_html("test.html")
|
|
assert len(movies) == 1
|
|
assert movies[0].name == "Movie A"
|
|
assert movies[0].duration == 120
|
|
assert len(movies[0].start_times) == 2
|
|
|
|
def test_parse_with_date(self) -> None:
|
|
html = "2025-01-25 stuff" + self._make_html_section("Movie A", 90, ["18:00"])
|
|
with self._patch_open(html):
|
|
_, date = parse_cinema_city_html("test.html")
|
|
assert date == "2025-01-25"
|
|
|
|
def test_parse_with_genres(self) -> None:
|
|
html = "header" + self._make_html_section(
|
|
"Horror Film", 100, ["20:00"], genre="Horror, Thriller"
|
|
)
|
|
with self._patch_open(html):
|
|
movies, _ = parse_cinema_city_html("test.html")
|
|
assert len(movies) == 1
|
|
assert "Horror" in movies[0].genres
|
|
assert "Thriller" in movies[0].genres
|
|
|
|
def test_no_name_match(self) -> None:
|
|
html = 'header class="row movie-row"> no name here'
|
|
with self._patch_open(html):
|
|
movies, _ = parse_cinema_city_html("test.html")
|
|
assert len(movies) == 0
|
|
|
|
def test_no_duration_match(self) -> None:
|
|
html = (
|
|
'header class="row movie-row">'
|
|
'<span class="qb-movie-name">Movie</span>'
|
|
"no duration here"
|
|
'<button class="btn btn-primary btn-lg">10:00</button>'
|
|
)
|
|
with self._patch_open(html):
|
|
movies, _ = parse_cinema_city_html("test.html")
|
|
assert len(movies) == 0
|
|
|
|
def test_no_times_match(self) -> None:
|
|
html = (
|
|
'header class="row movie-row">'
|
|
'<span class="qb-movie-name">Movie</span>'
|
|
"<span>100 min</span>"
|
|
)
|
|
with self._patch_open(html):
|
|
movies, _ = parse_cinema_city_html("test.html")
|
|
assert len(movies) == 0
|
|
|
|
def test_alternate_time_pattern(self) -> None:
|
|
html = (
|
|
'header class="row movie-row">'
|
|
'<span class="qb-movie-name">Movie</span>'
|
|
"<span>100 min</span>"
|
|
"> 10:00 (HTTPS://something"
|
|
)
|
|
with self._patch_open(html):
|
|
movies, _ = parse_cinema_city_html("test.html")
|
|
assert len(movies) == 1
|
|
|
|
def test_deduplicate_movies(self) -> None:
|
|
section = self._make_html_section("Movie A", 120, ["10:00"])
|
|
html = "header" + section + section
|
|
with self._patch_open(html):
|
|
movies, _ = parse_cinema_city_html("test.html")
|
|
assert len(movies) == 1
|
|
|
|
def test_no_genre_match(self) -> None:
|
|
html = (
|
|
'header class="row movie-row">'
|
|
'<span class="qb-movie-name">Movie</span>'
|
|
"<span>100 min</span>"
|
|
'<button class="btn btn-primary btn-lg">10:00</button>'
|
|
)
|
|
with self._patch_open(html):
|
|
movies, _ = parse_cinema_city_html("test.html")
|
|
assert len(movies) == 1
|
|
assert movies[0].genres == []
|
|
|
|
|
|
class TestParseCinemaCityPdf:
|
|
"""Tests for parse_cinema_city_pdf."""
|
|
|
|
@patch("python_pkg.cinema_planner._cinema_parsing._pdfplumber")
|
|
def test_with_pdfplumber(self, mock_pdfplumber: MagicMock) -> None:
|
|
mock_page = MagicMock()
|
|
mock_page.extract_text.return_value = "MOVIE TITLE\n110 min\n10:00\n"
|
|
mock_pdf = MagicMock()
|
|
mock_pdf.pages = [mock_page]
|
|
mock_pdfplumber.open.return_value.__enter__ = MagicMock(
|
|
return_value=mock_pdf,
|
|
)
|
|
mock_pdfplumber.open.return_value.__exit__ = MagicMock(return_value=False)
|
|
result = parse_cinema_city_pdf("test.pdf")
|
|
assert isinstance(result, list)
|
|
|
|
@patch(
|
|
"python_pkg.cinema_planner._cinema_parsing._pdfplumber",
|
|
None,
|
|
)
|
|
@patch(
|
|
"python_pkg.cinema_planner._cinema_parsing._parse_cinema_city_pdf_basic",
|
|
)
|
|
def test_fallback_to_basic(self, mock_basic: MagicMock) -> None:
|
|
mock_basic.return_value = []
|
|
result = parse_cinema_city_pdf("test.pdf")
|
|
mock_basic.assert_called_once_with("test.pdf")
|
|
assert result == []
|
|
|
|
@patch("python_pkg.cinema_planner._cinema_parsing._pdfplumber")
|
|
def test_pdfplumber_page_no_text(
|
|
self,
|
|
mock_pdfplumber: MagicMock,
|
|
) -> None:
|
|
mock_page = MagicMock()
|
|
mock_page.extract_text.return_value = None
|
|
mock_pdf = MagicMock()
|
|
mock_pdf.pages = [mock_page]
|
|
mock_pdfplumber.open.return_value.__enter__ = MagicMock(
|
|
return_value=mock_pdf,
|
|
)
|
|
mock_pdfplumber.open.return_value.__exit__ = MagicMock(return_value=False)
|
|
result = parse_cinema_city_pdf("test.pdf")
|
|
assert result == []
|
|
|
|
|
|
class TestParseCinemaCityPdfBasic:
|
|
"""Tests for _parse_cinema_city_pdf_basic."""
|
|
|
|
@patch("python_pkg.cinema_planner._cinema_parsing._fitz")
|
|
def test_with_fitz(self, mock_fitz: MagicMock) -> None:
|
|
mock_page = MagicMock()
|
|
mock_page.get_text.return_value = "MOVIE TITLE\n110 min\n10:00\n"
|
|
mock_doc = MagicMock()
|
|
mock_doc.__iter__ = MagicMock(return_value=iter([mock_page]))
|
|
mock_fitz.open.return_value = mock_doc
|
|
result = _parse_cinema_city_pdf_basic("test.pdf")
|
|
mock_doc.close.assert_called_once()
|
|
assert isinstance(result, list)
|
|
|
|
@patch("python_pkg.cinema_planner._cinema_parsing._fitz", None)
|
|
@patch("python_pkg.cinema_planner._cinema_parsing.shutil")
|
|
def test_pdftotext_success(self, mock_shutil: MagicMock) -> None:
|
|
mock_shutil.which.return_value = "/usr/bin/pdftotext"
|
|
mock_result = MagicMock()
|
|
mock_result.stdout = "MOVIE TITLE\n110 min\n10:00\n"
|
|
with patch(
|
|
"python_pkg.cinema_planner._cinema_parsing.subprocess.run",
|
|
return_value=mock_result,
|
|
):
|
|
result = _parse_cinema_city_pdf_basic("test.pdf")
|
|
assert isinstance(result, list)
|
|
|
|
@patch("python_pkg.cinema_planner._cinema_parsing._fitz", None)
|
|
@patch("python_pkg.cinema_planner._cinema_parsing.shutil")
|
|
def test_no_pdftotext(self, mock_shutil: MagicMock) -> None:
|
|
mock_shutil.which.return_value = None
|
|
with pytest.raises(SystemExit):
|
|
_parse_cinema_city_pdf_basic("test.pdf")
|
|
|
|
@patch("python_pkg.cinema_planner._cinema_parsing._fitz", None)
|
|
@patch("python_pkg.cinema_planner._cinema_parsing.shutil")
|
|
def test_pdftotext_process_error(self, mock_shutil: MagicMock) -> None:
|
|
mock_shutil.which.return_value = "/usr/bin/pdftotext"
|
|
with (
|
|
patch(
|
|
"python_pkg.cinema_planner._cinema_parsing.subprocess.run",
|
|
side_effect=subprocess.CalledProcessError(1, "pdftotext"),
|
|
),
|
|
pytest.raises(SystemExit),
|
|
):
|
|
_parse_cinema_city_pdf_basic("test.pdf")
|
|
|
|
|
|
class TestExitNoPdfSupport:
|
|
"""Tests for _exit_no_pdf_support."""
|
|
|
|
def test_exits(self) -> None:
|
|
with pytest.raises(SystemExit):
|
|
_exit_no_pdf_support()
|
|
|
|
|
|
class TestParseCinemaCityText:
|
|
"""Tests for parse_cinema_city_text."""
|
|
|
|
def test_single_movie(self) -> None:
|
|
text = "MOVIE TITLE\n110 min\n10:00\n14:00\n"
|
|
result = parse_cinema_city_text(text)
|
|
assert len(result) == 1
|
|
assert result[0].name == "Movie Title"
|
|
assert result[0].duration == 110
|
|
assert len(result[0].start_times) == 2
|
|
|
|
def test_multiple_movies(self) -> None:
|
|
text = "FIRST MOVIE\n90 min\n10:00\nSECOND MOVIE\n120 min\n14:00\n18:00\n"
|
|
result = parse_cinema_city_text(text)
|
|
assert len(result) == 2
|
|
|
|
def test_movie_without_duration(self) -> None:
|
|
text = "MOVIE TITLE\n10:00\n14:00\n"
|
|
result = parse_cinema_city_text(text)
|
|
assert len(result) == 1
|
|
assert result[0].duration == 120 # default
|
|
|
|
def test_no_times(self) -> None:
|
|
text = "MOVIE TITLE\n110 min\nno times here\n"
|
|
result = parse_cinema_city_text(text)
|
|
assert len(result) == 0
|
|
|
|
def test_empty_text(self) -> None:
|
|
result = parse_cinema_city_text("")
|
|
assert result == []
|
|
|
|
def test_title_too_short(self) -> None:
|
|
text = "AB\n110 min\n10:00\n"
|
|
result = parse_cinema_city_text(text)
|
|
assert len(result) == 0
|
|
|
|
def test_lowercase_line_ignored_as_title(self) -> None:
|
|
text = "some lowercase text\n110 min\n10:00\n"
|
|
result = parse_cinema_city_text(text)
|
|
assert len(result) == 0
|
|
|
|
def test_duration_in_lookahead(self) -> None:
|
|
text = "MOVIE TITLE\nsome other line\n95 min\n10:00\n"
|
|
result = parse_cinema_city_text(text)
|
|
assert len(result) == 1
|
|
assert result[0].duration == 95
|
|
|
|
def test_deduplicates_times(self) -> None:
|
|
text = "MOVIE TITLE\n110 min\n10:00\n10:00\n"
|
|
result = parse_cinema_city_text(text)
|
|
assert len(result) == 1
|
|
assert len(result[0].start_times) == 1
|
|
|
|
def test_movie_saved_when_new_title_found(self) -> None:
|
|
text = "FIRST MOVIE\n90 min\n10:00\nSECOND MOVIE\n120 min\n14:00\n"
|
|
result = parse_cinema_city_text(text)
|
|
assert len(result) == 2
|
|
assert result[0].name == "First Movie"
|
|
assert result[1].name == "Second Movie"
|
|
|
|
def test_time_on_same_line_as_other_text(self) -> None:
|
|
text = "MOVIE TITLE\n110 min\nSome text 10:00 more text\n"
|
|
result = parse_cinema_city_text(text)
|
|
assert len(result) == 1
|
|
|
|
def test_try_parse_time_returns_none(self) -> None:
|
|
# Time pattern \b(\d{1,2}:\d{2})\b matches but parse_time fails
|
|
# This can happen when parse_time validates more strictly
|
|
text = "MOVIE TITLE\n110 min\n10:00\n"
|
|
with patch(
|
|
"python_pkg.cinema_planner._cinema_parsing._try_parse_time",
|
|
side_effect=lambda _t: None,
|
|
):
|
|
result = parse_cinema_city_text(text)
|
|
assert len(result) == 0
|
|
|
|
def test_movie_no_times_not_saved(self) -> None:
|
|
# Movie with title but no valid times on subsequent lines
|
|
text = "MOVIE ONE\n110 min\nno times\nMOVIE TWO\n90 min\n10:00\n"
|
|
result = parse_cinema_city_text(text)
|
|
assert len(result) == 1
|
|
assert result[0].name == "Movie Two"
|