mirror of https://github.com/scrapy/scrapy.git
156 lines
5.0 KiB
Python
156 lines
5.0 KiB
Python
import time
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from scrapy import Request
|
|
from scrapy.core.downloader import Downloader, Slot
|
|
from scrapy.exceptions import ScrapyDeprecationWarning
|
|
from scrapy.utils.spider import DefaultSpider
|
|
from scrapy.utils.test import get_crawler
|
|
from tests.mockserver.http import MockServer
|
|
from tests.spiders import MetaSpider
|
|
from tests.utils.decorators import coroutine_test, inline_callbacks_test
|
|
|
|
|
|
class DownloaderSlotsSettingsTestSpider(MetaSpider):
|
|
name = "downloader_slots"
|
|
|
|
custom_settings = {
|
|
"DOWNLOAD_DELAY": 1,
|
|
"RANDOMIZE_DOWNLOAD_DELAY": False,
|
|
"DOWNLOAD_SLOTS": {
|
|
"quotes.toscrape.com": {
|
|
"concurrency": 1,
|
|
"delay": 2,
|
|
"randomize_delay": False,
|
|
"throttle": False,
|
|
},
|
|
"books.toscrape.com": {"delay": 3, "randomize_delay": False},
|
|
},
|
|
}
|
|
|
|
def __init__(self, *args: Any, **kwargs: Any):
|
|
super().__init__(*args, **kwargs)
|
|
assert self.mockserver
|
|
self.default_slot = self.mockserver.host
|
|
self.times: dict[str, list[float]] = {}
|
|
|
|
async def start(self):
|
|
slots = [*self.custom_settings.get("DOWNLOAD_SLOTS", {}), None]
|
|
for slot in slots:
|
|
url = self.mockserver.url(f"/?downloader_slot={slot}")
|
|
self.times[slot or self.default_slot] = []
|
|
yield Request(url, callback=self.parse, meta={"download_slot": slot})
|
|
|
|
def parse(self, response):
|
|
slot = response.meta.get("download_slot", self.default_slot)
|
|
self.times[slot].append(time.time())
|
|
url = self.mockserver.url(f"/?downloader_slot={slot}&req=2")
|
|
yield Request(url, callback=self.not_parse, meta={"download_slot": slot})
|
|
|
|
def not_parse(self, response):
|
|
slot = response.meta.get("download_slot", self.default_slot)
|
|
self.times[slot].append(time.time())
|
|
|
|
|
|
class TestCrawl:
|
|
@classmethod
|
|
def setup_class(cls):
|
|
cls.mockserver = MockServer()
|
|
cls.mockserver.__enter__()
|
|
|
|
@classmethod
|
|
def teardown_class(cls):
|
|
cls.mockserver.__exit__(None, None, None)
|
|
|
|
@inline_callbacks_test
|
|
def test_delay(self):
|
|
crawler = get_crawler(DownloaderSlotsSettingsTestSpider)
|
|
yield crawler.crawl(mockserver=self.mockserver)
|
|
slots = crawler.engine.downloader.slots
|
|
times = crawler.spider.times
|
|
tolerance = 0.3
|
|
|
|
delays_real = {k: v[1] - v[0] for k, v in times.items()}
|
|
error_delta = {
|
|
k: 1 - min(delays_real[k], v.delay) / max(delays_real[k], v.delay)
|
|
for k, v in slots.items()
|
|
}
|
|
|
|
assert max(list(error_delta.values())) < tolerance
|
|
|
|
|
|
@coroutine_test
|
|
async def test_params():
|
|
params = {
|
|
"concurrency": 1,
|
|
"delay": 2,
|
|
"randomize_delay": False,
|
|
}
|
|
settings = {
|
|
"DOWNLOAD_SLOTS": {
|
|
"example.com": params,
|
|
},
|
|
}
|
|
crawler = get_crawler(DefaultSpider, settings_dict=settings)
|
|
crawler.spider = crawler._create_spider()
|
|
downloader = Downloader(crawler)
|
|
request = Request("https://example.com")
|
|
_, actual = downloader._get_slot(request)
|
|
downloader.close()
|
|
expected = Slot(**params)
|
|
for param in params:
|
|
assert getattr(expected, param) == getattr(actual, param), (
|
|
f"Slot.{param}: {getattr(expected, param)!r} != {getattr(actual, param)!r}"
|
|
)
|
|
|
|
|
|
@coroutine_test
|
|
async def test_get_slot_deprecated_spider_arg():
|
|
crawler = get_crawler(DefaultSpider)
|
|
crawler.spider = crawler._create_spider()
|
|
downloader = Downloader(crawler)
|
|
request = Request("https://example.com")
|
|
|
|
with pytest.warns(
|
|
ScrapyDeprecationWarning,
|
|
match=r"Passing a 'spider' argument to Downloader\._get_slot\(\) is deprecated",
|
|
):
|
|
key1, slot1 = downloader._get_slot(request, spider=crawler.spider)
|
|
key2, slot2 = downloader._get_slot(request)
|
|
downloader.close()
|
|
|
|
assert key1 == key2
|
|
assert slot1 == slot2
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"priority_queue_class",
|
|
[
|
|
"scrapy.pqueues.ScrapyPriorityQueue",
|
|
"scrapy.pqueues.DownloaderAwarePriorityQueue",
|
|
],
|
|
)
|
|
@coroutine_test
|
|
async def test_none_slot_with_priority_queue(
|
|
mockserver: MockServer, priority_queue_class: str
|
|
) -> None:
|
|
"""Test specific cases for None slot handling with different priority queues."""
|
|
crawler = get_crawler(
|
|
DownloaderSlotsSettingsTestSpider,
|
|
settings_dict={"SCHEDULER_PRIORITY_QUEUE": priority_queue_class},
|
|
)
|
|
await crawler.crawl_async(mockserver=mockserver)
|
|
assert isinstance(crawler.spider, DownloaderSlotsSettingsTestSpider)
|
|
|
|
assert hasattr(crawler.spider, "times")
|
|
assert None not in crawler.spider.times
|
|
assert crawler.spider.default_slot in crawler.spider.times
|
|
assert len(crawler.spider.times[crawler.spider.default_slot]) == 2
|
|
|
|
assert crawler.stats
|
|
stats = crawler.stats
|
|
assert stats.get_value("spider_exceptions", 0) == 0
|
|
assert stats.get_value("downloader/exception_count", 0) == 0
|