178 lines
6.5 KiB
Python
178 lines
6.5 KiB
Python
import asyncio
|
|
import unittest
|
|
from unittest.mock import MagicMock, AsyncMock, patch
|
|
|
|
from crawler.config import CrawlerConfig
|
|
from crawler.engine import Crawler
|
|
|
|
|
|
class TestTorTimeoutFallback(unittest.IsolatedAsyncioTestCase):
|
|
|
|
def test_config_defaults_and_env(self):
|
|
config = CrawlerConfig(start_url="https://example.com")
|
|
self.assertEqual(config.tor_max_consecutive_timeouts, 5)
|
|
|
|
dict_data = config.to_dict()
|
|
self.assertEqual(dict_data["tor_max_consecutive_timeouts"], 5)
|
|
|
|
dict_data["tor_max_consecutive_timeouts"] = 3
|
|
restored = CrawlerConfig.from_dict(dict_data)
|
|
self.assertEqual(restored.tor_max_consecutive_timeouts, 3)
|
|
|
|
async def test_consecutive_timeout_fallback_trigger(self):
|
|
config = CrawlerConfig(
|
|
start_url="https://example.com",
|
|
use_tor=True,
|
|
tor_max_consecutive_timeouts=5,
|
|
)
|
|
events = []
|
|
async def on_event(ev):
|
|
events.append(ev)
|
|
|
|
crawler = Crawler(config=config, event_callback=on_event)
|
|
# Mock tor_manager so we don't need real Tor daemon for unit testing
|
|
crawler.tor_manager = MagicMock()
|
|
crawler.tor_manager.renew_circuit = MagicMock(return_value=True)
|
|
|
|
mock_context = MagicMock()
|
|
|
|
# 1. First 4 consecutive ERR_TIMED_OUT errors
|
|
for i in range(1, 5):
|
|
await crawler._on_page_result(
|
|
context=mock_context,
|
|
url=f"https://example.com/page{i}",
|
|
source_url="https://example.com",
|
|
success=False,
|
|
error_msg="page.goto: net::ERR_TIMED_OUT at https://example.com",
|
|
)
|
|
self.assertEqual(crawler._consecutive_timeouts, i)
|
|
self.assertEqual(crawler.tor_manager.renew_circuit.call_count, 0)
|
|
|
|
# 2. 5th ERR_TIMED_OUT error triggers fallback circuit rotation!
|
|
await crawler._on_page_result(
|
|
context=mock_context,
|
|
url="https://example.com/page5",
|
|
source_url="https://example.com",
|
|
success=False,
|
|
error_msg="page.goto: net::ERR_TIMED_OUT at https://example.com",
|
|
)
|
|
# Counter should be reset to 0 after rotation
|
|
self.assertEqual(crawler._consecutive_timeouts, 0)
|
|
self.assertEqual(crawler.tor_manager.renew_circuit.call_count, 1)
|
|
|
|
async def test_success_resets_counter(self):
|
|
config = CrawlerConfig(
|
|
start_url="https://example.com",
|
|
use_tor=True,
|
|
tor_max_consecutive_timeouts=5,
|
|
)
|
|
crawler = Crawler(config=config)
|
|
crawler.tor_manager = MagicMock()
|
|
crawler.tor_manager.renew_circuit = MagicMock(return_value=True)
|
|
|
|
mock_context = MagicMock()
|
|
|
|
# 3 timeouts
|
|
for i in range(1, 4):
|
|
await crawler._on_page_result(
|
|
context=mock_context,
|
|
url=f"https://example.com/page{i}",
|
|
source_url=None,
|
|
success=False,
|
|
error_msg="net::ERR_TIMED_OUT",
|
|
)
|
|
self.assertEqual(crawler._consecutive_timeouts, 3)
|
|
|
|
# 1 success -> should reset counter to 0
|
|
await crawler._on_page_result(
|
|
context=mock_context,
|
|
url="https://example.com/success_page",
|
|
source_url=None,
|
|
success=True,
|
|
error_msg=None,
|
|
)
|
|
self.assertEqual(crawler._consecutive_timeouts, 0)
|
|
self.assertEqual(crawler.tor_manager.renew_circuit.call_count, 0)
|
|
|
|
async def test_non_timeout_error_resets_counter(self):
|
|
config = CrawlerConfig(
|
|
start_url="https://example.com",
|
|
use_tor=True,
|
|
tor_max_consecutive_timeouts=5,
|
|
)
|
|
crawler = Crawler(config=config)
|
|
crawler.tor_manager = MagicMock()
|
|
|
|
mock_context = MagicMock()
|
|
|
|
# 2 timeouts
|
|
for i in range(2):
|
|
await crawler._on_page_result(
|
|
context=mock_context,
|
|
url=f"https://example.com/page{i}",
|
|
source_url=None,
|
|
success=False,
|
|
error_msg="net::ERR_TIMED_OUT",
|
|
)
|
|
self.assertEqual(crawler._consecutive_timeouts, 2)
|
|
|
|
# HTTP 404 error -> non-timeout/non-rate-limit resets counter
|
|
await crawler._on_page_result(
|
|
context=mock_context,
|
|
url="https://example.com/missing",
|
|
source_url=None,
|
|
success=False,
|
|
error_msg="HTTP 404",
|
|
)
|
|
self.assertEqual(crawler._consecutive_timeouts, 0)
|
|
|
|
def test_user_agent_config(self):
|
|
config_random = CrawlerConfig(start_url="https://example.com", user_agent="random")
|
|
ua1 = config_random.get_user_agent()
|
|
self.assertIsNotNone(ua1)
|
|
self.assertIn("Mozilla/5.0", ua1)
|
|
|
|
config_custom = CrawlerConfig(start_url="https://example.com", user_agent="MyCustomBot/1.0")
|
|
self.assertEqual(config_custom.get_user_agent(), "MyCustomBot/1.0")
|
|
|
|
config_default = CrawlerConfig(start_url="https://example.com", user_agent="default")
|
|
self.assertIsNone(config_default.get_user_agent())
|
|
|
|
async def test_consecutive_rate_limit_fallback_trigger(self):
|
|
config = CrawlerConfig(
|
|
start_url="https://example.com",
|
|
use_tor=True,
|
|
tor_max_consecutive_timeouts=3,
|
|
)
|
|
crawler = Crawler(config=config)
|
|
crawler.tor_manager = MagicMock()
|
|
crawler.tor_manager.renew_circuit = MagicMock(return_value=True)
|
|
|
|
mock_context = MagicMock()
|
|
|
|
# 2 HTTP 503 errors
|
|
for i in range(2):
|
|
await crawler._on_page_result(
|
|
context=mock_context,
|
|
url=f"https://example.com/page{i}",
|
|
source_url="https://example.com",
|
|
success=False,
|
|
error_msg="HTTP 503 (Service Temporarily Unavailable / Rate Limited) [Retry-After: 154s]",
|
|
)
|
|
self.assertEqual(crawler._consecutive_timeouts, 2)
|
|
self.assertEqual(crawler.tor_manager.renew_circuit.call_count, 0)
|
|
|
|
# 3rd HTTP 503 error triggers circuit renewal
|
|
await crawler._on_page_result(
|
|
context=mock_context,
|
|
url="https://example.com/page2",
|
|
source_url="https://example.com",
|
|
success=False,
|
|
error_msg="HTTP 503 (Service Temporarily Unavailable / Rate Limited) [Retry-After: 154s]",
|
|
)
|
|
self.assertEqual(crawler._consecutive_timeouts, 0)
|
|
self.assertEqual(crawler.tor_manager.renew_circuit.call_count, 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|