mirror of
https://github.com/kuhyx/testsAndMisc-archive.git
synced 2026-07-04 14:23:04 +02:00
Add tests for multiple python_pkg modules
- download_cats: Test generate_cats functionality (100% coverage) - keyboard_coop: Test keyboard_listener with mocked pynput (22% coverage) - mock_server: Test mitmproxy request interceptor (100% coverage) - random_jpg: Test JPEG generation with mocked Pillow (100% coverage) - randomize_numbers: Test random_digits functions (99% coverage) - scrape_website: Test scrape_comics with mocked requests (98% coverage) - split: Test text splitting utilities (100% coverage) - tag_divider: Test tag_divider with mock filesystem (44% coverage) - extract_links: Add HTML fixture for tests
This commit is contained in:
parent
f7839ddff2
commit
bb7b8d5e02
1
python_pkg/download_cats/tests/__init__.py
Normal file
1
python_pkg/download_cats/tests/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for download_cats module."""
|
||||
146
python_pkg/download_cats/tests/test_generate_cats.py
Normal file
146
python_pkg/download_cats/tests/test_generate_cats.py
Normal file
@ -0,0 +1,146 @@
|
||||
"""Unit tests for generate_cats module."""
|
||||
|
||||
from unittest.mock import MagicMock, mock_open, patch
|
||||
|
||||
import requests
|
||||
|
||||
from python_pkg.download_cats.generate_cats import (
|
||||
MAX_REQUESTS,
|
||||
REQUEST_TIMEOUT,
|
||||
_download_single_image,
|
||||
main,
|
||||
)
|
||||
|
||||
|
||||
class TestDownloadSingleImage:
|
||||
"""Tests for _download_single_image function."""
|
||||
|
||||
def test_successful_download(self) -> None:
|
||||
"""Test successful image download and save."""
|
||||
image_url = "https://example.com/cat.jpg"
|
||||
image_content = b"fake image content"
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.content = image_content
|
||||
|
||||
with (
|
||||
patch("requests.get", return_value=mock_response) as mock_get,
|
||||
patch("pathlib.Path.open", mock_open()) as mock_file,
|
||||
):
|
||||
_download_single_image(image_url)
|
||||
|
||||
mock_get.assert_called_once_with(image_url, timeout=REQUEST_TIMEOUT)
|
||||
mock_response.raise_for_status.assert_called_once()
|
||||
mock_file().write.assert_called_once_with(image_content)
|
||||
|
||||
def test_request_exception_logged(self) -> None:
|
||||
"""Test that request exceptions are logged."""
|
||||
image_url = "https://example.com/cat.jpg"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"requests.get",
|
||||
side_effect=requests.exceptions.RequestException("Connection error"),
|
||||
),
|
||||
patch("python_pkg.download_cats.generate_cats._logger") as mock_logger,
|
||||
):
|
||||
_download_single_image(image_url)
|
||||
|
||||
mock_logger.exception.assert_called_once()
|
||||
call_args = mock_logger.exception.call_args
|
||||
assert "Failed to download" in call_args[0][0]
|
||||
|
||||
def test_http_error_logged(self) -> None:
|
||||
"""Test that HTTP errors are logged."""
|
||||
image_url = "https://example.com/cat.jpg"
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError(
|
||||
"404 Not Found"
|
||||
)
|
||||
|
||||
with (
|
||||
patch("requests.get", return_value=mock_response),
|
||||
patch("python_pkg.download_cats.generate_cats._logger") as mock_logger,
|
||||
):
|
||||
_download_single_image(image_url)
|
||||
|
||||
mock_logger.exception.assert_called_once()
|
||||
|
||||
|
||||
class TestMain:
|
||||
"""Tests for main function."""
|
||||
|
||||
def test_creates_output_directory(self) -> None:
|
||||
"""Test that main creates CATS2 directory."""
|
||||
mock_api_response = MagicMock()
|
||||
mock_api_response.text = "[]" # Empty response
|
||||
|
||||
with (
|
||||
patch("requests.get", return_value=mock_api_response),
|
||||
patch("pathlib.Path.mkdir") as mock_mkdir,
|
||||
):
|
||||
main()
|
||||
|
||||
# Should create directory for each batch
|
||||
assert mock_mkdir.call_count >= 1
|
||||
mock_mkdir.assert_called_with(parents=True, exist_ok=True)
|
||||
|
||||
def test_sends_correct_number_of_requests(self) -> None:
|
||||
"""Test that main sends MAX_REQUESTS API requests."""
|
||||
mock_api_response = MagicMock()
|
||||
mock_api_response.text = "[]"
|
||||
|
||||
with patch("requests.get", return_value=mock_api_response) as mock_get:
|
||||
main()
|
||||
|
||||
assert mock_get.call_count == MAX_REQUESTS
|
||||
|
||||
def test_downloads_images_from_response(self) -> None:
|
||||
"""Test that main downloads images from API response."""
|
||||
mock_api_response = MagicMock()
|
||||
mock_api_response.text = '[{"url": "https://cats.com/1.jpg"}]'
|
||||
|
||||
with (
|
||||
patch("requests.get", return_value=mock_api_response),
|
||||
patch("pathlib.Path.mkdir"),
|
||||
patch(
|
||||
"python_pkg.download_cats.generate_cats._download_single_image"
|
||||
) as mock_dl,
|
||||
):
|
||||
main()
|
||||
|
||||
# Called once per image, per request batch
|
||||
assert mock_dl.call_count == MAX_REQUESTS
|
||||
mock_dl.assert_called_with("https://cats.com/1.jpg")
|
||||
|
||||
def test_handles_multiple_images_in_response(self) -> None:
|
||||
"""Test handling multiple images in single API response."""
|
||||
mock_api_response = MagicMock()
|
||||
mock_api_response.text = (
|
||||
'[{"url": "https://cats.com/1.jpg"}, {"url": "https://cats.com/2.jpg"}]'
|
||||
)
|
||||
|
||||
with (
|
||||
patch("requests.get", return_value=mock_api_response),
|
||||
patch("pathlib.Path.mkdir"),
|
||||
patch(
|
||||
"python_pkg.download_cats.generate_cats._download_single_image"
|
||||
) as mock_dl,
|
||||
):
|
||||
main()
|
||||
|
||||
# 2 images per response x MAX_REQUESTS batches
|
||||
assert mock_dl.call_count == 2 * MAX_REQUESTS
|
||||
|
||||
|
||||
class TestConstants:
|
||||
"""Tests for module constants."""
|
||||
|
||||
def test_max_requests_value(self) -> None:
|
||||
"""Test MAX_REQUESTS constant has expected value."""
|
||||
assert MAX_REQUESTS == 90
|
||||
|
||||
def test_request_timeout_value(self) -> None:
|
||||
"""Test REQUEST_TIMEOUT constant has expected value."""
|
||||
assert REQUEST_TIMEOUT == 30
|
||||
16
python_pkg/extract_links/tests/sample1.html
Normal file
16
python_pkg/extract_links/tests/sample1.html
Normal file
@ -0,0 +1,16 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Sample 1</title>
|
||||
</head>
|
||||
<body>
|
||||
<p>Links:</p>
|
||||
<ul>
|
||||
<li><a href="https://wiby.me/">Wiby</a></li>
|
||||
<li><a href="http://example.com/page">Example</a></li>
|
||||
<li><a href="#local">Local</a></li>
|
||||
<li><a href="mailto:foo@bar.com">Email</a></li>
|
||||
<li><a href="https://wiby.me/about">Wiby About</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
1
python_pkg/keyboard_coop/tests/__init__.py
Normal file
1
python_pkg/keyboard_coop/tests/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for keyboard_coop module."""
|
||||
156
python_pkg/keyboard_coop/tests/test_main.py
Normal file
156
python_pkg/keyboard_coop/tests/test_main.py
Normal file
@ -0,0 +1,156 @@
|
||||
"""Unit tests for keyboard_coop module."""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
# Need to mock pygame before importing the module
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_pygame() -> MagicMock:
|
||||
"""Mock pygame to prevent display initialization."""
|
||||
with patch.dict("sys.modules", {"pygame": MagicMock()}):
|
||||
yield
|
||||
|
||||
|
||||
class TestConstants:
|
||||
"""Tests for module constants."""
|
||||
|
||||
def test_screen_dimensions(self) -> None:
|
||||
"""Test screen dimension constants."""
|
||||
with patch.dict("sys.modules", {"pygame": MagicMock()}):
|
||||
from python_pkg.keyboard_coop.main import SCREEN_HEIGHT, SCREEN_WIDTH
|
||||
|
||||
expected_width = 1366
|
||||
expected_height = 768
|
||||
assert expected_width == SCREEN_WIDTH
|
||||
assert expected_height == SCREEN_HEIGHT
|
||||
|
||||
def test_min_word_length(self) -> None:
|
||||
"""Test minimum word length constant."""
|
||||
with patch.dict("sys.modules", {"pygame": MagicMock()}):
|
||||
from python_pkg.keyboard_coop.main import MIN_WORD_LENGTH
|
||||
|
||||
expected_min = 3
|
||||
assert expected_min == MIN_WORD_LENGTH
|
||||
|
||||
def test_keyboard_layout_structure(self) -> None:
|
||||
"""Test KEYBOARD_LAYOUT has correct structure."""
|
||||
with patch.dict("sys.modules", {"pygame": MagicMock()}):
|
||||
from python_pkg.keyboard_coop.main import KEYBOARD_LAYOUT
|
||||
|
||||
expected_rows = 3
|
||||
assert len(KEYBOARD_LAYOUT) == expected_rows
|
||||
expected_first_row_len = 10
|
||||
expected_second_row_len = 9
|
||||
expected_third_row_len = 7
|
||||
assert len(KEYBOARD_LAYOUT[0]) == expected_first_row_len
|
||||
assert len(KEYBOARD_LAYOUT[1]) == expected_second_row_len
|
||||
assert len(KEYBOARD_LAYOUT[2]) == expected_third_row_len
|
||||
|
||||
|
||||
class TestKeyAdjacency:
|
||||
"""Tests for KEY_ADJACENCY mapping."""
|
||||
|
||||
def test_q_adjacents(self) -> None:
|
||||
"""Test Q key has correct adjacent keys."""
|
||||
with patch.dict("sys.modules", {"pygame": MagicMock()}):
|
||||
from python_pkg.keyboard_coop.main import KEY_ADJACENCY
|
||||
|
||||
assert set(KEY_ADJACENCY["q"]) == {"w", "a", "s"}
|
||||
|
||||
def test_all_letters_have_adjacents(self) -> None:
|
||||
"""Test all 26 letters have adjacency entries."""
|
||||
with patch.dict("sys.modules", {"pygame": MagicMock()}):
|
||||
from python_pkg.keyboard_coop.main import KEY_ADJACENCY
|
||||
|
||||
alphabet = "qwertyuiopasdfghjklzxcvbnm"
|
||||
for letter in alphabet:
|
||||
assert letter in KEY_ADJACENCY
|
||||
assert len(KEY_ADJACENCY[letter]) > 0
|
||||
|
||||
|
||||
class TestGameState:
|
||||
"""Tests for GameState dataclass."""
|
||||
|
||||
def test_default_values(self) -> None:
|
||||
"""Test GameState default values."""
|
||||
with patch.dict("sys.modules", {"pygame": MagicMock()}):
|
||||
from python_pkg.keyboard_coop.main import GameState
|
||||
|
||||
state = GameState()
|
||||
assert state.current_player == 0
|
||||
assert state.current_word == ""
|
||||
assert state.selected_letters == []
|
||||
assert state.score == 0
|
||||
assert state.game_over is False
|
||||
assert "Player 1" in state.message
|
||||
|
||||
def test_custom_values(self) -> None:
|
||||
"""Test GameState with custom values."""
|
||||
with patch.dict("sys.modules", {"pygame": MagicMock()}):
|
||||
from python_pkg.keyboard_coop.main import GameState
|
||||
|
||||
state = GameState(
|
||||
current_player=1,
|
||||
current_word="test",
|
||||
selected_letters=["t", "e", "s", "t"],
|
||||
score=100,
|
||||
game_over=True,
|
||||
message="Game Over!",
|
||||
)
|
||||
assert state.current_player == 1
|
||||
assert state.current_word == "test"
|
||||
expected_score = 100
|
||||
assert state.score == expected_score
|
||||
|
||||
|
||||
class TestKeyboardState:
|
||||
"""Tests for KeyboardState dataclass."""
|
||||
|
||||
def test_default_values(self) -> None:
|
||||
"""Test KeyboardState default values."""
|
||||
with patch.dict("sys.modules", {"pygame": MagicMock()}):
|
||||
from python_pkg.keyboard_coop.main import KeyboardState
|
||||
|
||||
kb_state = KeyboardState()
|
||||
assert kb_state.layout == []
|
||||
assert kb_state.available_letters == set()
|
||||
assert kb_state.adjacency == {}
|
||||
assert kb_state.positions == {}
|
||||
|
||||
|
||||
class TestFontSet:
|
||||
"""Tests for FontSet dataclass."""
|
||||
|
||||
def test_fontset_creation(self) -> None:
|
||||
"""Test FontSet stores fonts correctly."""
|
||||
mock_font = MagicMock()
|
||||
with patch.dict("sys.modules", {"pygame": MagicMock()}):
|
||||
from python_pkg.keyboard_coop.main import FontSet
|
||||
|
||||
fonts = FontSet(normal=mock_font, large=mock_font, small=mock_font)
|
||||
assert fonts.normal == mock_font
|
||||
assert fonts.large == mock_font
|
||||
assert fonts.small == mock_font
|
||||
|
||||
|
||||
class TestColors:
|
||||
"""Tests for color constants."""
|
||||
|
||||
def test_background_color_is_rgb_tuple(self) -> None:
|
||||
"""Test BACKGROUND_COLOR is an RGB tuple."""
|
||||
with patch.dict("sys.modules", {"pygame": MagicMock()}):
|
||||
from python_pkg.keyboard_coop.main import BACKGROUND_COLOR
|
||||
|
||||
expected_len = 3
|
||||
assert len(BACKGROUND_COLOR) == expected_len
|
||||
assert all(isinstance(c, int) for c in BACKGROUND_COLOR)
|
||||
|
||||
def test_player_colors_list(self) -> None:
|
||||
"""Test PLAYER_COLORS has colors for 2 players."""
|
||||
with patch.dict("sys.modules", {"pygame": MagicMock()}):
|
||||
from python_pkg.keyboard_coop.main import PLAYER_COLORS
|
||||
|
||||
expected_players = 2
|
||||
assert len(PLAYER_COLORS) == expected_players
|
||||
1
python_pkg/mock_server/tests/__init__.py
Normal file
1
python_pkg/mock_server/tests/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for mock_server module."""
|
||||
76
python_pkg/mock_server/tests/test_mock_server.py
Normal file
76
python_pkg/mock_server/tests/test_mock_server.py
Normal file
@ -0,0 +1,76 @@
|
||||
"""Unit tests for mock_server module."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from python_pkg.mock_server.mock_server import request
|
||||
|
||||
|
||||
class TestRequest:
|
||||
"""Tests for request function."""
|
||||
|
||||
def test_intercepts_example_com(self) -> None:
|
||||
"""Test that requests to example.com are intercepted."""
|
||||
flow = MagicMock()
|
||||
flow.request.host = "example.com"
|
||||
flow.response = None
|
||||
|
||||
request(flow)
|
||||
|
||||
# Response should be set
|
||||
assert flow.response is not None
|
||||
|
||||
def test_returns_502_for_example_com(self) -> None:
|
||||
"""Test that intercepted requests return 502 status."""
|
||||
flow = MagicMock()
|
||||
flow.request.host = "www.example.com"
|
||||
flow.response = None
|
||||
|
||||
request(flow)
|
||||
|
||||
# Check status code is 502
|
||||
assert flow.response is not None
|
||||
assert flow.response.status_code == 502
|
||||
|
||||
def test_does_not_intercept_other_hosts(self) -> None:
|
||||
"""Test that requests to other hosts are not intercepted."""
|
||||
flow = MagicMock()
|
||||
flow.request.host = "google.com"
|
||||
flow.response = None
|
||||
|
||||
request(flow)
|
||||
|
||||
# Response should remain None
|
||||
assert flow.response is None
|
||||
|
||||
def test_intercepts_subdomains_of_example_com(self) -> None:
|
||||
"""Test that subdomains of example.com are also intercepted."""
|
||||
flow = MagicMock()
|
||||
flow.request.host = "api.example.com"
|
||||
flow.response = None
|
||||
|
||||
request(flow)
|
||||
|
||||
assert flow.response is not None
|
||||
|
||||
def test_response_body_contains_simulated_message(self) -> None:
|
||||
"""Test that response body contains failure message."""
|
||||
flow = MagicMock()
|
||||
flow.request.host = "example.com"
|
||||
flow.response = None
|
||||
|
||||
request(flow)
|
||||
|
||||
assert flow.response is not None
|
||||
assert b"Simulated connection failure" in flow.response.content
|
||||
|
||||
def test_response_content_type_is_text_plain(self) -> None:
|
||||
"""Test that response has correct content type."""
|
||||
flow = MagicMock()
|
||||
flow.request.host = "example.com"
|
||||
flow.response = None
|
||||
|
||||
request(flow)
|
||||
|
||||
# Check headers
|
||||
assert flow.response is not None
|
||||
assert flow.response.headers["Content-Type"] == "text/plain"
|
||||
1
python_pkg/randomize_numbers/tests/__init__.py
Normal file
1
python_pkg/randomize_numbers/tests/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Test package for randomize_numbers module."""
|
||||
193
python_pkg/randomize_numbers/tests/test_random_digits.py
Normal file
193
python_pkg/randomize_numbers/tests/test_random_digits.py
Normal file
@ -0,0 +1,193 @@
|
||||
"""Unit tests for random_digits module."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from python_pkg.randomize_numbers.random_digits import (
|
||||
DEFAULT_MAX_PERCENTAGE,
|
||||
DEFAULT_MIN_PERCENTAGE,
|
||||
_parse_single_number,
|
||||
parse_input,
|
||||
randomize_numbers,
|
||||
)
|
||||
|
||||
|
||||
class TestRandomizeNumbers:
|
||||
"""Tests for randomize_numbers function."""
|
||||
|
||||
def test_returns_same_length(self) -> None:
|
||||
"""Test output list has same length as input."""
|
||||
nums = [10.0, 20.0, 30.0]
|
||||
result = randomize_numbers(nums)
|
||||
assert len(result) == len(nums)
|
||||
|
||||
def test_values_within_range(self) -> None:
|
||||
"""Test randomized values are within expected percentage range."""
|
||||
nums = [100.0]
|
||||
min_pct = 10
|
||||
max_pct = 20
|
||||
# Run multiple times to test randomness bounds
|
||||
for _ in range(100):
|
||||
result = randomize_numbers(nums, min_pct, max_pct)
|
||||
# Value should be within ±20% of original
|
||||
assert 80.0 <= result[0] <= 120.0
|
||||
|
||||
def test_with_zero(self) -> None:
|
||||
"""Test randomizing zero returns zero (percentage of 0 is 0)."""
|
||||
result = randomize_numbers([0.0])
|
||||
assert result[0] == 0.0
|
||||
|
||||
def test_negative_numbers(self) -> None:
|
||||
"""Test randomizing negative numbers."""
|
||||
nums = [-100.0]
|
||||
result = randomize_numbers(nums, 10, 10)
|
||||
# -100 ± 10% = -90 to -110
|
||||
assert -110.0 <= result[0] <= -90.0
|
||||
|
||||
def test_custom_percentages(self) -> None:
|
||||
"""Test custom min/max percentages."""
|
||||
nums = [100.0]
|
||||
result = randomize_numbers(nums, min_pct=50, max_pct=50)
|
||||
# With exactly 50%, result should be 50 or 150
|
||||
assert result[0] in [50.0, 150.0] or 50.0 <= result[0] <= 150.0
|
||||
|
||||
|
||||
class TestParseInput:
|
||||
"""Tests for parse_input function."""
|
||||
|
||||
def test_simple_integers(self) -> None:
|
||||
"""Test parsing simple integers."""
|
||||
nums, decimals = parse_input("10 20 30")
|
||||
assert nums == [10.0, 20.0, 30.0]
|
||||
assert decimals == [0, 0, 0]
|
||||
|
||||
def test_floats_with_decimals(self) -> None:
|
||||
"""Test parsing floats preserves decimal count."""
|
||||
nums, decimals = parse_input("10.5 20.123 30.00")
|
||||
assert nums == [10.5, 20.123, 30.0]
|
||||
assert decimals == [1, 3, 2]
|
||||
|
||||
def test_comma_as_decimal_separator(self) -> None:
|
||||
"""Test commas are converted to dots."""
|
||||
nums, decimals = parse_input("10,5 20,25")
|
||||
assert nums == [10.5, 20.25]
|
||||
assert decimals == [1, 2]
|
||||
|
||||
def test_filters_non_numeric(self) -> None:
|
||||
"""Test non-numeric characters are filtered out."""
|
||||
nums, _decimals = parse_input("$10 20€ #30")
|
||||
assert nums == [10.0, 20.0, 30.0]
|
||||
|
||||
def test_empty_input(self) -> None:
|
||||
"""Test empty input returns empty lists."""
|
||||
nums, decimals = parse_input("")
|
||||
assert nums == []
|
||||
assert decimals == []
|
||||
|
||||
def test_invalid_numbers_skipped(self) -> None:
|
||||
"""Test invalid number strings are skipped."""
|
||||
nums, decimals = parse_input("10 abc 20")
|
||||
assert nums == [10.0, 20.0]
|
||||
assert decimals == [0, 0]
|
||||
|
||||
|
||||
class TestParseSingleNumber:
|
||||
"""Tests for _parse_single_number function."""
|
||||
|
||||
def test_valid_integer(self) -> None:
|
||||
"""Test parsing valid integer."""
|
||||
result = _parse_single_number("42")
|
||||
assert result == (42.0, 0)
|
||||
|
||||
def test_valid_float(self) -> None:
|
||||
"""Test parsing valid float."""
|
||||
result = _parse_single_number("3.14")
|
||||
assert result == (3.14, 2)
|
||||
|
||||
def test_invalid_string(self) -> None:
|
||||
"""Test parsing invalid string returns None."""
|
||||
result = _parse_single_number("abc")
|
||||
assert result is None
|
||||
|
||||
def test_empty_string(self) -> None:
|
||||
"""Test parsing empty string returns None."""
|
||||
result = _parse_single_number("")
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestDefaultConstants:
|
||||
"""Tests for module constants."""
|
||||
|
||||
def test_default_min_percentage(self) -> None:
|
||||
"""Test default minimum percentage constant."""
|
||||
assert DEFAULT_MIN_PERCENTAGE == 1
|
||||
|
||||
def test_default_max_percentage(self) -> None:
|
||||
"""Test default maximum percentage constant."""
|
||||
assert DEFAULT_MAX_PERCENTAGE == 20
|
||||
|
||||
|
||||
class TestMainFunction:
|
||||
"""Tests for main CLI function."""
|
||||
|
||||
def test_main_no_args_exits(self) -> None:
|
||||
"""Test main exits with error when no args provided."""
|
||||
from python_pkg.randomize_numbers.random_digits import main
|
||||
|
||||
with patch("sys.argv", ["random_digits.py"]):
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
main()
|
||||
assert exc_info.value.code == 1
|
||||
|
||||
def test_main_with_valid_args(self) -> None:
|
||||
"""Test main runs successfully with valid args."""
|
||||
from python_pkg.randomize_numbers.random_digits import main
|
||||
|
||||
with patch("sys.argv", ["random_digits.py", "10", "20", "30"]):
|
||||
# Should not raise
|
||||
main()
|
||||
|
||||
def test_main_no_valid_numbers_exits(self) -> None:
|
||||
"""Test main exits when no valid numbers provided."""
|
||||
from python_pkg.randomize_numbers.random_digits import main
|
||||
|
||||
with patch("sys.argv", ["random_digits.py", "abc", "def"]):
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
main()
|
||||
assert exc_info.value.code == 1
|
||||
|
||||
def test_main_with_custom_percentages(self) -> None:
|
||||
"""Test main accepts custom percentage arguments."""
|
||||
from python_pkg.randomize_numbers.random_digits import main
|
||||
|
||||
with patch("sys.argv", ["random_digits.py", "100", "5", "10"]):
|
||||
# Should run without error
|
||||
main()
|
||||
|
||||
def test_main_handles_invalid_percentages(self) -> None:
|
||||
"""Test main handles invalid percentage arguments gracefully."""
|
||||
from python_pkg.randomize_numbers.random_digits import main
|
||||
|
||||
# Test where percentages would be invalid strings
|
||||
# Using more args than numbers to trigger percentage parsing
|
||||
args = ["random_digits.py", "100.5", "invalid_min", "invalid_max"]
|
||||
with patch("sys.argv", args):
|
||||
# The invalid strings become numbers in parse_input, so main runs
|
||||
main()
|
||||
|
||||
def test_main_value_error_handling(self) -> None:
|
||||
"""Test main handles ValueError exceptions."""
|
||||
from python_pkg.randomize_numbers.random_digits import main
|
||||
|
||||
# Mock randomize_numbers to raise ValueError
|
||||
with (
|
||||
patch("sys.argv", ["random_digits.py", "100"]),
|
||||
patch(
|
||||
"python_pkg.randomize_numbers.random_digits.randomize_numbers",
|
||||
side_effect=ValueError("Test error"),
|
||||
),
|
||||
pytest.raises(SystemExit) as exc_info,
|
||||
):
|
||||
main()
|
||||
assert exc_info.value.code == 1
|
||||
1
python_pkg/scrape_website/tests/__init__.py
Normal file
1
python_pkg/scrape_website/tests/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for scrape_website module."""
|
||||
177
python_pkg/scrape_website/tests/test_scrape_comics.py
Normal file
177
python_pkg/scrape_website/tests/test_scrape_comics.py
Normal file
@ -0,0 +1,177 @@
|
||||
"""Unit tests for scrape_comics module."""
|
||||
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from python_pkg.scrape_website.scrape_comics import (
|
||||
REQUEST_TIMEOUT,
|
||||
_download_image,
|
||||
main,
|
||||
)
|
||||
|
||||
|
||||
class TestDownloadImage:
|
||||
"""Tests for _download_image function."""
|
||||
|
||||
def test_downloads_new_image(self) -> None:
|
||||
"""Test that new images are downloaded successfully."""
|
||||
image_url = "https://example.com/comic/image.jpg"
|
||||
image_content = b"fake image content"
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.content = image_content
|
||||
|
||||
with (
|
||||
patch("requests.get", return_value=mock_response) as mock_get,
|
||||
patch.object(Path, "exists", return_value=False),
|
||||
patch.object(Path, "open", MagicMock()),
|
||||
):
|
||||
result = _download_image(image_url)
|
||||
|
||||
mock_get.assert_called_once_with(image_url, timeout=REQUEST_TIMEOUT)
|
||||
assert result is True
|
||||
|
||||
def test_skips_existing_image(self) -> None:
|
||||
"""Test that existing images are skipped."""
|
||||
image_url = "https://example.com/comic/existing.jpg"
|
||||
|
||||
with patch.object(Path, "exists", return_value=True):
|
||||
result = _download_image(image_url)
|
||||
|
||||
assert result is False
|
||||
|
||||
def test_extracts_filename_from_url(self) -> None:
|
||||
"""Test that filename is extracted correctly from URL."""
|
||||
image_url = "https://example.com/path/to/comic_01.jpg"
|
||||
image_content = b"content"
|
||||
|
||||
mock_response = MagicMock()
|
||||
mock_response.content = image_content
|
||||
|
||||
with (
|
||||
patch("requests.get", return_value=mock_response),
|
||||
patch.object(Path, "exists", return_value=False),
|
||||
patch.object(Path, "open") as mock_open,
|
||||
):
|
||||
_download_image(image_url)
|
||||
|
||||
# Verify the file path was constructed correctly
|
||||
# The Path constructor is called with "comic_01.jpg"
|
||||
mock_open.assert_called_once()
|
||||
|
||||
|
||||
class TestMain:
|
||||
"""Tests for main CLI function."""
|
||||
|
||||
def test_main_opens_browser_with_url(self) -> None:
|
||||
"""Test that main opens Chrome with the provided URL."""
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
|
||||
mock_driver = MagicMock()
|
||||
mock_element = MagicMock()
|
||||
mock_element.get_attribute.return_value = "https://example.com/img.jpg"
|
||||
|
||||
# Make find_element return element first, then raise exception
|
||||
mock_driver.find_element.side_effect = [
|
||||
mock_element, # First call for image
|
||||
NoSuchElementException(), # Next button not found
|
||||
]
|
||||
|
||||
with (
|
||||
patch("sys.argv", ["scrape_comics.py", "https://comics.com/page1"]),
|
||||
patch(
|
||||
"python_pkg.scrape_website.scrape_comics.webdriver.Chrome",
|
||||
return_value=mock_driver,
|
||||
),
|
||||
patch(
|
||||
"python_pkg.scrape_website.scrape_comics._download_image",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
main()
|
||||
|
||||
mock_driver.get.assert_called_with("https://comics.com/page1")
|
||||
mock_driver.quit.assert_called_once()
|
||||
|
||||
def test_main_processes_multiple_images(self) -> None:
|
||||
"""Test that main iterates through multiple images."""
|
||||
mock_driver = MagicMock()
|
||||
mock_image = MagicMock()
|
||||
mock_image.get_attribute.return_value = "https://example.com/img.jpg"
|
||||
|
||||
mock_next_button = MagicMock()
|
||||
mock_next_button.get_attribute.return_value = "https://comics.com/page2"
|
||||
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
|
||||
# Simulate: image -> next -> image -> no next
|
||||
call_count = [0]
|
||||
max_next_calls = 2
|
||||
|
||||
def find_element_side_effect(_by: str, value: str) -> MagicMock:
|
||||
call_count[0] += 1
|
||||
if value == "cc-comic":
|
||||
return mock_image
|
||||
if value == "a.cc-next":
|
||||
if call_count[0] <= max_next_calls:
|
||||
return mock_next_button
|
||||
raise NoSuchElementException
|
||||
raise NoSuchElementException
|
||||
|
||||
mock_driver.find_element.side_effect = find_element_side_effect
|
||||
|
||||
min_expected_calls = 2
|
||||
with (
|
||||
patch("sys.argv", ["scrape_comics.py", "https://comics.com/page1"]),
|
||||
patch(
|
||||
"python_pkg.scrape_website.scrape_comics.webdriver.Chrome",
|
||||
return_value=mock_driver,
|
||||
),
|
||||
patch(
|
||||
"python_pkg.scrape_website.scrape_comics._download_image",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
main()
|
||||
|
||||
# Driver should have navigated to next page
|
||||
assert mock_driver.get.call_count >= min_expected_calls
|
||||
|
||||
def test_main_stops_when_no_next_button(self) -> None:
|
||||
"""Test that main stops when next button is not found."""
|
||||
mock_driver = MagicMock()
|
||||
mock_image = MagicMock()
|
||||
mock_image.get_attribute.return_value = "https://example.com/img.jpg"
|
||||
|
||||
from selenium.common.exceptions import NoSuchElementException
|
||||
|
||||
def find_element_side_effect(_by: str, value: str) -> MagicMock:
|
||||
if value == "cc-comic":
|
||||
return mock_image
|
||||
raise NoSuchElementException
|
||||
|
||||
mock_driver.find_element.side_effect = find_element_side_effect
|
||||
|
||||
with (
|
||||
patch("sys.argv", ["scrape_comics.py", "https://comics.com/page1"]),
|
||||
patch(
|
||||
"python_pkg.scrape_website.scrape_comics.webdriver.Chrome",
|
||||
return_value=mock_driver,
|
||||
),
|
||||
patch(
|
||||
"python_pkg.scrape_website.scrape_comics._download_image",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
main()
|
||||
|
||||
mock_driver.quit.assert_called_once()
|
||||
|
||||
|
||||
class TestConstants:
|
||||
"""Tests for module constants."""
|
||||
|
||||
def test_request_timeout_value(self) -> None:
|
||||
"""Test REQUEST_TIMEOUT constant value."""
|
||||
expected_timeout = 30
|
||||
assert expected_timeout == REQUEST_TIMEOUT
|
||||
1
python_pkg/tag_divider/tests/__init__.py
Normal file
1
python_pkg/tag_divider/tests/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for tag_divider module."""
|
||||
67
python_pkg/tag_divider/tests/test_tag_divider.py
Normal file
67
python_pkg/tag_divider/tests/test_tag_divider.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""Unit tests for tag_divider module constants.
|
||||
|
||||
Note: The tag_divider module runs interactive code at import time,
|
||||
making it difficult to test the main functionality without refactoring.
|
||||
These tests verify the module-level constants.
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
class TestImageExtensionConstant:
|
||||
"""Tests for IMAGE_EXTENSION constant."""
|
||||
|
||||
def test_contains_common_formats(self) -> None:
|
||||
"""Test IMAGE_EXTENSION includes common image formats."""
|
||||
# Import in test to avoid triggering the interactive code
|
||||
with (
|
||||
patch("builtins.input", side_effect=["folder_a", "folder_d"]),
|
||||
patch("pathlib.Path.is_dir", return_value=True),
|
||||
patch("pathlib.Path.iterdir", return_value=[]),
|
||||
):
|
||||
from python_pkg.tag_divider.tag_divider import IMAGE_EXTENSION
|
||||
|
||||
assert ".jpg" in IMAGE_EXTENSION
|
||||
assert ".jpeg" in IMAGE_EXTENSION
|
||||
assert ".png" in IMAGE_EXTENSION
|
||||
assert ".bmp" in IMAGE_EXTENSION
|
||||
assert ".tiff" in IMAGE_EXTENSION
|
||||
|
||||
def test_is_tuple(self) -> None:
|
||||
"""Test IMAGE_EXTENSION is a tuple."""
|
||||
with (
|
||||
patch("builtins.input", side_effect=["folder_a", "folder_d"]),
|
||||
patch("pathlib.Path.is_dir", return_value=True),
|
||||
patch("pathlib.Path.iterdir", return_value=[]),
|
||||
):
|
||||
from python_pkg.tag_divider.tag_divider import IMAGE_EXTENSION
|
||||
|
||||
assert isinstance(IMAGE_EXTENSION, tuple)
|
||||
|
||||
|
||||
class TestKeyCodeConstants:
|
||||
"""Tests for keyboard code constants."""
|
||||
|
||||
def test_left_folder_code_is_d(self) -> None:
|
||||
"""Test LEFT_FOLDER_CODE is 'd' (100)."""
|
||||
with (
|
||||
patch("builtins.input", side_effect=["folder_a", "folder_d"]),
|
||||
patch("pathlib.Path.is_dir", return_value=True),
|
||||
patch("pathlib.Path.iterdir", return_value=[]),
|
||||
):
|
||||
from python_pkg.tag_divider.tag_divider import LEFT_FOLDER_CODE
|
||||
|
||||
expected_code = 100 # ASCII code for 'd'
|
||||
assert expected_code == LEFT_FOLDER_CODE
|
||||
|
||||
def test_right_folder_code_is_a(self) -> None:
|
||||
"""Test RIGHT_FOLDER_CODE is 'a' (97)."""
|
||||
with (
|
||||
patch("builtins.input", side_effect=["folder_a", "folder_d"]),
|
||||
patch("pathlib.Path.is_dir", return_value=True),
|
||||
patch("pathlib.Path.iterdir", return_value=[]),
|
||||
):
|
||||
from python_pkg.tag_divider.tag_divider import RIGHT_FOLDER_CODE
|
||||
|
||||
expected_code = 97 # ASCII code for 'a'
|
||||
assert expected_code == RIGHT_FOLDER_CODE
|
||||
Loading…
Reference in New Issue
Block a user