diff --git a/tests/test_parser.py b/tests/test_parser.py deleted file mode 100644 index 714c0fc..0000000 --- a/tests/test_parser.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Tests for episode range parser.""" - -import pytest - -from fastanime.cli.utils.parser import parse_episode_range - - -class TestParseEpisodeRange: - """Test cases for the parse_episode_range function.""" - - @pytest.fixture - def episodes(self): - """Sample episode list for testing.""" - return ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"] - - def test_no_range_returns_all_episodes(self, episodes): - """Test that None or empty range returns all episodes.""" - result = list(parse_episode_range(None, episodes)) - assert result == episodes - - def test_colon_only_returns_all_episodes(self, episodes): - """Test that ':' returns all episodes.""" - result = list(parse_episode_range(":", episodes)) - assert result == episodes - - def test_start_end_range(self, episodes): - """Test start:end range format.""" - result = list(parse_episode_range("2:5", episodes)) - assert result == ["3", "4", "5"] - - def test_start_only_range(self, episodes): - """Test start: range format.""" - result = list(parse_episode_range("5:", episodes)) - assert result == ["6", "7", "8", "9", "10"] - - def test_end_only_range(self, episodes): - """Test :end range format.""" - result = list(parse_episode_range(":3", episodes)) - assert result == ["1", "2", "3"] - - def test_start_end_step_range(self, episodes): - """Test start:end:step range format.""" - result = list(parse_episode_range("2:8:2", episodes)) - assert result == ["3", "5", "7"] - - def test_single_number_range(self, episodes): - """Test single number format (start from index).""" - result = list(parse_episode_range("5", episodes)) - assert result == ["6", "7", "8", "9", "10"] - - def test_empty_start_end_in_three_part_range_raises_error(self, episodes): - """Test that empty parts in start:end:step format raise error.""" - with pytest.raises(ValueError, match="When using 3 parts"): - list(parse_episode_range(":5:2", episodes)) - - with pytest.raises(ValueError, match="When using 3 parts"): - list(parse_episode_range("2::2", episodes)) - - with pytest.raises(ValueError, match="When using 3 parts"): - list(parse_episode_range("2:5:", episodes)) - - def test_invalid_integer_raises_error(self, episodes): - """Test that invalid integers raise ValueError.""" - with pytest.raises(ValueError, match="Must be a valid integer"): - list(parse_episode_range("abc", episodes)) - - with pytest.raises(ValueError, match="Start and end must be valid integers"): - list(parse_episode_range("2:abc", episodes)) - - with pytest.raises(ValueError, match="All parts must be valid integers"): - list(parse_episode_range("2:5:abc", episodes)) - - def test_zero_step_raises_error(self, episodes): - """Test that zero step raises ValueError.""" - with pytest.raises(ValueError, match="Step value must be positive"): - list(parse_episode_range("2:5:0", episodes)) - - def test_negative_step_raises_error(self, episodes): - """Test that negative step raises ValueError.""" - with pytest.raises(ValueError, match="Step value must be positive"): - list(parse_episode_range("2:5:-1", episodes)) - - def test_too_many_colons_raises_error(self, episodes): - """Test that too many colons raise ValueError.""" - with pytest.raises(ValueError, match="Too many colon separators"): - list(parse_episode_range("2:5:7:9", episodes)) - - def test_edge_case_empty_list(self): - """Test behavior with empty episode list.""" - result = list(parse_episode_range(":", [])) - assert result == [] - - def test_edge_case_single_episode(self): - """Test behavior with single episode.""" - episodes = ["1"] - result = list(parse_episode_range(":", episodes)) - assert result == ["1"] - - result = list(parse_episode_range("0:1", episodes)) - assert result == ["1"] - - def test_numerical_sorting(self): - """Test that episodes are sorted numerically, not lexicographically.""" - episodes = ["10", "2", "1", "11", "3"] - result = list(parse_episode_range(":", episodes)) - assert result == ["1", "2", "3", "10", "11"] - - def test_index_out_of_bounds_behavior(self, episodes): - """Test behavior when indices exceed available episodes.""" - # Python slicing handles out-of-bounds gracefully - result = list(parse_episode_range("15:", episodes)) - assert result == [] # No episodes beyond index 15 - - result = list(parse_episode_range(":20", episodes)) - assert result == episodes # All episodes (slice stops at end) diff --git a/tests/test_torrent_downloader.py b/tests/test_torrent_downloader.py deleted file mode 100644 index 87c5793..0000000 --- a/tests/test_torrent_downloader.py +++ /dev/null @@ -1,210 +0,0 @@ -""" -Tests for the TorrentDownloader class. -""" - -import tempfile -import unittest -from pathlib import Path -from unittest.mock import Mock, patch - - -from fastanime.core.downloader.torrents import ( - TorrentDownloader, - TorrentDownloadError, - LIBTORRENT_AVAILABLE, -) -from fastanime.core.exceptions import DependencyNotFoundError - - -class TestTorrentDownloader(unittest.TestCase): - """Test cases for TorrentDownloader class.""" - - def setUp(self): - """Set up test fixtures.""" - self.temp_dir = Path(tempfile.mkdtemp()) - self.downloader = TorrentDownloader( - download_path=self.temp_dir, - max_upload_rate=100, - max_download_rate=200, - max_connections=50, - ) - - def tearDown(self): - """Clean up test fixtures.""" - import shutil - - shutil.rmtree(self.temp_dir, ignore_errors=True) - - def test_init(self): - """Test TorrentDownloader initialization.""" - self.assertEqual(self.downloader.download_path, self.temp_dir) - self.assertEqual(self.downloader.max_upload_rate, 100) - self.assertEqual(self.downloader.max_download_rate, 200) - self.assertEqual(self.downloader.max_connections, 50) - self.assertTrue(self.temp_dir.exists()) - - def test_init_creates_download_directory(self): - """Test that download directory is created if it doesn't exist.""" - non_existent_dir = self.temp_dir / "new_dir" - self.assertFalse(non_existent_dir.exists()) - - downloader = TorrentDownloader(download_path=non_existent_dir) - self.assertTrue(non_existent_dir.exists()) - - @patch("fastanime.core.downloader.torrents.shutil.which") - def test_download_with_webtorrent_cli_not_available(self, mock_which): - """Test webtorrent CLI fallback when not available.""" - mock_which.return_value = None - - with self.assertRaises(DependencyNotFoundError) as context: - self.downloader.download_with_webtorrent_cli("magnet:test") - - self.assertIn("webtorrent CLI is not available", str(context.exception)) - - @patch("fastanime.core.downloader.torrents.subprocess.run") - @patch("fastanime.core.downloader.torrents.shutil.which") - def test_download_with_webtorrent_cli_success(self, mock_which, mock_run): - """Test successful webtorrent CLI download.""" - mock_which.return_value = "/usr/bin/webtorrent" - mock_result = Mock() - mock_result.stdout = f"Downloaded test-file to {self.temp_dir}/test-file" - mock_run.return_value = mock_result - - # Create a dummy file to simulate download - test_file = self.temp_dir / "test-file" - test_file.touch() - - result = self.downloader.download_with_webtorrent_cli("magnet:test") - - mock_run.assert_called_once() - self.assertEqual(result, test_file) - - @patch("fastanime.core.downloader.torrents.subprocess.run") - @patch("fastanime.core.downloader.torrents.shutil.which") - def test_download_with_webtorrent_cli_failure(self, mock_which, mock_run): - """Test webtorrent CLI download failure.""" - mock_which.return_value = "/usr/bin/webtorrent" - mock_run.side_effect = subprocess.CalledProcessError( - 1, "webtorrent", stderr="Error" - ) - - with self.assertRaises(TorrentDownloadError) as context: - self.downloader.download_with_webtorrent_cli("magnet:test") - - self.assertIn("webtorrent CLI failed", str(context.exception)) - - @unittest.skipUnless(LIBTORRENT_AVAILABLE, "libtorrent not available") - def test_setup_libtorrent_session(self): - """Test libtorrent session setup when available.""" - session = self.downloader._setup_libtorrent_session() - self.assertIsNotNone(session) - - @unittest.skipIf(LIBTORRENT_AVAILABLE, "libtorrent is available") - def test_setup_libtorrent_session_not_available(self): - """Test libtorrent session setup when not available.""" - with self.assertRaises(DependencyNotFoundError): - self.downloader._setup_libtorrent_session() - - @patch("fastanime.core.downloader.torrents.LIBTORRENT_AVAILABLE", False) - def test_download_with_libtorrent_not_available(self): - """Test libtorrent download when not available.""" - with self.assertRaises(DependencyNotFoundError) as context: - self.downloader.download_with_libtorrent("magnet:test") - - self.assertIn("libtorrent is not available", str(context.exception)) - - def test_progress_callback(self): - """Test progress callback functionality.""" - callback_mock = Mock() - downloader = TorrentDownloader( - download_path=self.temp_dir, progress_callback=callback_mock - ) - - # The callback should be stored - self.assertEqual(downloader.progress_callback, callback_mock) - - @patch.object(TorrentDownloader, "download_with_webtorrent_cli") - @patch.object(TorrentDownloader, "download_with_libtorrent") - def test_download_prefers_libtorrent(self, mock_libtorrent, mock_webtorrent): - """Test that download method prefers libtorrent by default.""" - mock_libtorrent.return_value = self.temp_dir / "test" - - with patch("fastanime.core.downloader.torrents.LIBTORRENT_AVAILABLE", True): - result = self.downloader.download("magnet:test", prefer_libtorrent=True) - - mock_libtorrent.assert_called_once() - mock_webtorrent.assert_not_called() - - @patch.object(TorrentDownloader, "download_with_webtorrent_cli") - @patch.object(TorrentDownloader, "download_with_libtorrent") - def test_download_fallback_to_webtorrent(self, mock_libtorrent, mock_webtorrent): - """Test fallback to webtorrent when libtorrent fails.""" - mock_libtorrent.side_effect = DependencyNotFoundError("libtorrent not found") - mock_webtorrent.return_value = self.temp_dir / "test" - - with patch("fastanime.core.downloader.torrents.LIBTORRENT_AVAILABLE", True): - result = self.downloader.download("magnet:test") - - mock_libtorrent.assert_called_once() - mock_webtorrent.assert_called_once() - self.assertEqual(result, self.temp_dir / "test") - - @patch.object(TorrentDownloader, "download_with_webtorrent_cli") - @patch.object(TorrentDownloader, "download_with_libtorrent") - def test_download_all_methods_fail(self, mock_libtorrent, mock_webtorrent): - """Test when all download methods fail.""" - mock_libtorrent.side_effect = DependencyNotFoundError("libtorrent not found") - mock_webtorrent.side_effect = DependencyNotFoundError("webtorrent not found") - - with self.assertRaises(TorrentDownloadError) as context: - self.downloader.download("magnet:test") - - self.assertIn("All torrent download methods failed", str(context.exception)) - - def test_magnet_link_detection(self): - """Test detection of magnet links.""" - magnet_link = "magnet:?xt=urn:btih:test" - http_link = "http://example.com/test.torrent" - file_path = "/path/to/test.torrent" - - # These would be tested in integration tests with actual libtorrent - # Here we just verify the method exists and handles different input types - self.assertTrue(magnet_link.startswith("magnet:")) - self.assertTrue(http_link.startswith(("http://", "https://"))) - self.assertFalse(file_path.startswith(("magnet:", "http://", "https://"))) - - -class TestLegacyFunction(unittest.TestCase): - """Test the legacy function for backward compatibility.""" - - def setUp(self): - """Set up test fixtures.""" - self.temp_dir = Path(tempfile.mkdtemp()) - - def tearDown(self): - """Clean up test fixtures.""" - import shutil - - shutil.rmtree(self.temp_dir, ignore_errors=True) - - @patch.object(TorrentDownloader, "download_with_webtorrent_cli") - def test_legacy_function(self, mock_download): - """Test the legacy download_torrent_with_webtorrent_cli function.""" - from fastanime.core.downloader.torrents import ( - download_torrent_with_webtorrent_cli, - ) - - test_path = self.temp_dir / "test.mkv" - mock_download.return_value = test_path - - result = download_torrent_with_webtorrent_cli(test_path, "magnet:test") - - mock_download.assert_called_once_with("magnet:test") - self.assertEqual(result, test_path) - - -if __name__ == "__main__": - # Add subprocess import for the test - import subprocess - - unittest.main()