scrapy/tests/test_crawl.py

1048 lines
40 KiB
Python

from __future__ import annotations
import json
import logging
from ipaddress import IPv4Address
from socket import gethostbyname
from typing import TYPE_CHECKING, Any
from urllib.parse import urlencode, urlparse
import pytest
from cryptography.x509 import load_der_x509_certificate
from twisted.internet.defer import succeed
from twisted.internet.ssl import Certificate
from twisted.python.failure import Failure
from scrapy import Spider, signals
from scrapy.crawler import AsyncCrawlerRunner, Crawler, CrawlerRunner
from scrapy.exceptions import CloseSpider, ScrapyDeprecationWarning, StopDownload
from scrapy.http import Request
from scrapy.http.response import Response
from scrapy.utils.defer import ensure_awaitable, maybe_deferred_to_future
from scrapy.utils.engine import format_engine_status, get_engine_status
from scrapy.utils.python import to_unicode
from scrapy.utils.test import get_crawler, get_reactor_settings
from tests import NON_EXISTING_RESOLVABLE
from tests.spiders import (
AsyncDefAsyncioGenComplexSpider,
AsyncDefAsyncioGenExcSpider,
AsyncDefAsyncioGenLoopSpider,
AsyncDefAsyncioGenSpider,
AsyncDefAsyncioReqsReturnSpider,
AsyncDefAsyncioReturnSingleElementSpider,
AsyncDefAsyncioReturnSpider,
AsyncDefAsyncioSpider,
AsyncDefDeferredDirectSpider,
AsyncDefDeferredMaybeWrappedSpider,
AsyncDefDeferredWrappedSpider,
AsyncDefSpider,
BrokenStartSpider,
BytesReceivedCallbackSpider,
BytesReceivedErrbackSpider,
CrawlSpiderWithAsyncCallback,
CrawlSpiderWithAsyncGeneratorCallback,
CrawlSpiderWithErrback,
CrawlSpiderWithParseMethod,
CrawlSpiderWithProcessRequestCallbackKeywordArguments,
DelaySpider,
DuplicateStartSpider,
FollowAllSpider,
HeadersReceivedCallbackSpider,
HeadersReceivedErrbackSpider,
SimpleSpider,
SingleRequestSpider,
StartGoodAndBadOutput,
StartItemSpider,
)
from tests.utils.decorators import coroutine_test
if TYPE_CHECKING:
from scrapy.statscollectors import StatsCollector
from tests.mockserver.http import MockServer
class TestCrawl:
@coroutine_test
async def test_follow_all(self, mockserver: MockServer) -> None:
crawler = get_crawler(FollowAllSpider)
await crawler.crawl_async(mockserver=mockserver)
assert isinstance(crawler.spider, FollowAllSpider)
assert len(crawler.spider.urls_visited) == 11 # 10 + start_url
@coroutine_test
async def test_fixed_delay(self, mockserver: MockServer) -> None:
await self._test_delay(mockserver, total=3, delay=0.2)
@coroutine_test
async def test_randomized_delay(self, mockserver: MockServer) -> None:
await self._test_delay(mockserver, total=3, delay=0.1, randomize=True)
@staticmethod
async def _test_delay(
mockserver: MockServer, total: int, delay: float, randomize: bool = False
) -> None:
crawl_kwargs = {
"maxlatency": delay * 2,
"mockserver": mockserver,
"total": total,
}
tolerance = 1 - (0.6 if randomize else 0.2)
settings = {"DOWNLOAD_DELAY": delay, "RANDOMIZE_DOWNLOAD_DELAY": randomize}
crawler = get_crawler(FollowAllSpider, settings)
await crawler.crawl_async(**crawl_kwargs)
assert crawler.spider
assert isinstance(crawler.spider, FollowAllSpider)
times = crawler.spider.times
total_time = times[-1] - times[0]
average = total_time / (len(times) - 1)
assert average > delay * tolerance, f"download delay too small: {average}"
# Ensure that the same test parameters would cause a failure if no
# download delay is set. Otherwise, it means we are using a combination
# of ``total`` and ``delay`` values that are too small for the test
# code above to have any meaning.
settings["DOWNLOAD_DELAY"] = 0
crawler = get_crawler(FollowAllSpider, settings)
await crawler.crawl_async(**crawl_kwargs)
assert crawler.spider
assert isinstance(crawler.spider, FollowAllSpider)
times = crawler.spider.times
total_time = times[-1] - times[0]
average = total_time / (len(times) - 1)
assert average <= delay / tolerance, "test total or delay values are too small"
@coroutine_test
async def test_timeout_success(self, mockserver: MockServer) -> None:
crawler = get_crawler(DelaySpider)
await crawler.crawl_async(n=0.5, mockserver=mockserver)
assert isinstance(crawler.spider, DelaySpider)
assert crawler.spider.t1 > 0
assert crawler.spider.t2 > 0
assert crawler.spider.t2 > crawler.spider.t1
@coroutine_test
async def test_timeout_failure(self, mockserver: MockServer) -> None:
crawler = get_crawler(DelaySpider, {"DOWNLOAD_TIMEOUT": 0.35})
await crawler.crawl_async(n=0.5, mockserver=mockserver)
assert isinstance(crawler.spider, DelaySpider)
assert crawler.spider.t1 > 0
assert crawler.spider.t2 == 0
assert crawler.spider.t2_err > 0
assert crawler.spider.t2_err > crawler.spider.t1
# server hangs after receiving response headers
crawler = get_crawler(DelaySpider, {"DOWNLOAD_TIMEOUT": 0.35})
await crawler.crawl_async(n=0.5, b=1, mockserver=mockserver)
assert isinstance(crawler.spider, DelaySpider)
assert crawler.spider.t1 > 0
assert crawler.spider.t2 == 0
assert crawler.spider.t2_err > 0
assert crawler.spider.t2_err > crawler.spider.t1
@coroutine_test
async def test_retry_503(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
crawler = get_crawler(SimpleSpider)
with caplog.at_level(logging.DEBUG):
await crawler.crawl_async(
mockserver.url("/status?n=503"), mockserver=mockserver
)
self._assert_retried(caplog.text)
@coroutine_test
async def test_retry_conn_failed(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
crawler = get_crawler(SimpleSpider)
with caplog.at_level(logging.DEBUG):
await crawler.crawl_async(
"http://localhost:65432/status?n=503", mockserver=mockserver
)
self._assert_retried(caplog.text)
@coroutine_test
async def test_retry_dns_error(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
if NON_EXISTING_RESOLVABLE:
pytest.skip("Non-existing hosts are resolvable")
crawler = get_crawler(SimpleSpider)
with caplog.at_level(logging.DEBUG):
# try to fetch the homepage of a nonexistent domain
await crawler.crawl_async(
"http://dns.resolution.invalid./", mockserver=mockserver
)
self._assert_retried(caplog.text)
@coroutine_test
async def test_start_bug_before_yield(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
with caplog.at_level(logging.ERROR):
crawler = get_crawler(BrokenStartSpider)
await crawler.crawl_async(fail_before_yield=1, mockserver=mockserver)
assert len(caplog.records) == 1
record = caplog.records[0]
assert record.exc_info is not None
assert record.exc_info[0] is ZeroDivisionError
@coroutine_test
async def test_start_bug_yielding(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
with caplog.at_level(logging.ERROR):
crawler = get_crawler(BrokenStartSpider)
await crawler.crawl_async(fail_yielding=1, mockserver=mockserver)
assert len(caplog.records) == 1
record = caplog.records[0]
assert record.exc_info is not None
assert record.exc_info[0] is ZeroDivisionError
@coroutine_test
async def test_start_items(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
items = []
def _on_item_scraped(item):
items.append(item)
with caplog.at_level(logging.ERROR):
crawler = get_crawler(StartItemSpider)
crawler.signals.connect(_on_item_scraped, signals.item_scraped)
await crawler.crawl_async(mockserver=mockserver)
assert len(caplog.records) == 0
assert items == [{"name": "test item"}]
@coroutine_test
async def test_start_unsupported_output(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
"""Anything that is not a request is assumed to be an item, avoiding a
potentially expensive call to itemadapter.is_item(), and letting
instead things fail when ItemAdapter is actually used on the
corresponding non-item object."""
items = []
def _on_item_scraped(item):
items.append(item)
with caplog.at_level(logging.ERROR):
crawler = get_crawler(StartGoodAndBadOutput)
crawler.signals.connect(_on_item_scraped, signals.item_scraped)
await crawler.crawl_async(mockserver=mockserver)
assert len(caplog.records) == 0
assert len(items) == 3
assert not any(isinstance(item, Request) for item in items)
@coroutine_test
async def test_start_dupes(self, mockserver: MockServer) -> None:
settings = {"CONCURRENT_REQUESTS": 1}
crawler = get_crawler(DuplicateStartSpider, settings)
await crawler.crawl_async(
dont_filter=True, distinct_urls=2, dupe_factor=3, mockserver=mockserver
)
assert isinstance(crawler.spider, DuplicateStartSpider)
assert crawler.spider.visited == 6
crawler = get_crawler(DuplicateStartSpider, settings)
await crawler.crawl_async(
dont_filter=False,
distinct_urls=3,
dupe_factor=4,
mockserver=mockserver,
)
assert isinstance(crawler.spider, DuplicateStartSpider)
assert crawler.spider.visited == 3
@coroutine_test
async def test_unbounded_response(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
# Completeness of responses without Content-Length or Transfer-Encoding
# can not be determined, we treat them as valid but flagged as "partial"
query = urlencode(
{
"raw": """\
HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
X-Powered-By: Servlet 2.4; JBoss-4.2.3.GA (build: SVNTag=JBoss_4_2_3_GA date=200807181417)/JBossWeb-2.0
Set-Cookie: JSESSIONID=08515F572832D0E659FD2B0D8031D75F; Path=/
Pragma: no-cache
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Cache-Control: no-cache
Cache-Control: no-store
Content-Type: text/html;charset=UTF-8
Content-Language: en
Date: Tue, 27 Aug 2013 13:05:05 GMT
Connection: close
foo body
with multiples lines
"""
}
)
crawler = get_crawler(SimpleSpider)
with caplog.at_level(logging.INFO):
await crawler.crawl_async(
mockserver.url(f"/raw?{query}"), mockserver=mockserver
)
assert caplog.text.count("Got response 200") == 1
@coroutine_test
async def test_retry_conn_lost(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
# connection lost after receiving data
crawler = get_crawler(SimpleSpider)
with caplog.at_level(logging.DEBUG):
await crawler.crawl_async(
mockserver.url("/drop?abort=0"), mockserver=mockserver
)
self._assert_retried(caplog.text)
@coroutine_test
async def test_retry_conn_aborted(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
# connection lost before receiving data
crawler = get_crawler(SimpleSpider)
with caplog.at_level(logging.DEBUG):
await crawler.crawl_async(
mockserver.url("/drop?abort=1"), mockserver=mockserver
)
self._assert_retried(caplog.text)
@staticmethod
def _assert_retried(log: str) -> None:
assert str(log).count("Retrying") == 2
assert str(log).count("Gave up retrying") == 1
@coroutine_test
async def test_referer_header(self, mockserver: MockServer) -> None:
"""Referer header is set by RefererMiddleware unless it is already set"""
req0 = Request(mockserver.url("/echo?headers=1&body=0"), dont_filter=True)
req1 = req0.replace()
req2 = req0.replace(headers={"Referer": None})
req3 = req0.replace(headers={"Referer": "http://example.com"})
req0.meta["next"] = req1
req1.meta["next"] = req2
req2.meta["next"] = req3
crawler = get_crawler(SingleRequestSpider)
await crawler.crawl_async(seed=req0, mockserver=mockserver)
assert isinstance(crawler.spider, SingleRequestSpider)
# basic asserts in case of weird communication errors
assert "responses" in crawler.spider.meta
assert "failures" not in crawler.spider.meta
# start() doesn't set Referer header
echo0 = json.loads(to_unicode(crawler.spider.meta["responses"][0].body))
assert "Referer" not in echo0["headers"]
# following request sets Referer to the source request url
echo1 = json.loads(to_unicode(crawler.spider.meta["responses"][1].body))
assert echo1["headers"].get("Referer") == [req0.url]
# next request avoids Referer header
echo2 = json.loads(to_unicode(crawler.spider.meta["responses"][2].body))
assert "Referer" not in echo2["headers"]
# last request explicitly sets a Referer header
echo3 = json.loads(to_unicode(crawler.spider.meta["responses"][3].body))
assert echo3["headers"].get("Referer") == ["http://example.com"]
@coroutine_test
async def test_engine_status(self, mockserver: MockServer) -> None:
est: list[list[tuple[str, Any]]] = []
def cb(response):
assert crawler.engine
est.append(get_engine_status(crawler.engine))
crawler = get_crawler(SingleRequestSpider)
await crawler.crawl_async(
seed=mockserver.url("/"), callback_func=cb, mockserver=mockserver
)
assert isinstance(crawler.spider, SingleRequestSpider)
assert len(est) == 1, est
s = dict(est[0])
assert s["engine.spider.name"] == crawler.spider.name
assert s["len(engine.scraper.slot.active)"] == 1
@coroutine_test
async def test_format_engine_status(self, mockserver: MockServer) -> None:
est: list[str] = []
def cb(response):
assert crawler.engine
est.append(format_engine_status(crawler.engine))
crawler = get_crawler(SingleRequestSpider)
await crawler.crawl_async(
seed=mockserver.url("/"), callback_func=cb, mockserver=mockserver
)
assert isinstance(crawler.spider, SingleRequestSpider)
assert len(est) == 1, est
est = est[0].split("\n")[2:-2] # remove header & footer
# convert to dict
est_split = [x.split(":") for x in est]
est = [x for sublist in est_split for x in sublist] # flatten
est = [x.lstrip().rstrip() for x in est]
it = iter(est)
s = dict(zip(it, it, strict=True))
assert s["engine.spider.name"] == crawler.spider.name
assert s["len(engine.scraper.slot.active)"] == "1"
@coroutine_test
async def test_open_spider_error_on_faulty_pipeline(
self, mockserver: MockServer
) -> None:
settings = {
"ITEM_PIPELINES": {
"tests.pipelines.ZeroDivisionErrorPipeline": 300,
}
}
crawler = get_crawler(SimpleSpider, settings)
with pytest.raises(ZeroDivisionError):
await crawler.crawl_async(
mockserver.url("/status?n=200"), mockserver=mockserver
)
assert not crawler.crawling
@coroutine_test
async def test_open_spider_error_on_faulty_pipeline_crawl(
self, mockserver: MockServer
) -> None:
# cover the except block in Crawler.crawl()
settings = {
"ITEM_PIPELINES": {
"tests.pipelines.ZeroDivisionErrorPipeline": 300,
}
}
crawler = get_crawler(SimpleSpider, settings)
with pytest.raises(ZeroDivisionError):
await maybe_deferred_to_future(
crawler.crawl(mockserver.url("/status?n=200"), mockserver=mockserver)
)
assert not crawler.crawling
@coroutine_test
async def test_crawlerrunner_accepts_crawler(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
crawler = Crawler(SimpleSpider, get_reactor_settings())
runner = CrawlerRunner()
with caplog.at_level(logging.DEBUG):
await maybe_deferred_to_future(
runner.crawl(
crawler,
mockserver.url("/status?n=200"),
mockserver=mockserver,
)
)
assert "Got response 200" in caplog.text
@coroutine_test
async def test_crawl_multiple(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
settings_dict = get_reactor_settings()
runner_cls = (
CrawlerRunner
if settings_dict.get("TWISTED_REACTOR_ENABLED", True)
else AsyncCrawlerRunner
)
runner = runner_cls(settings_dict)
runner.crawl(
SimpleSpider,
mockserver.url("/status?n=200"),
mockserver=mockserver,
)
runner.crawl(
SimpleSpider,
mockserver.url("/status?n=503"),
mockserver=mockserver,
)
with caplog.at_level(logging.DEBUG):
await ensure_awaitable(runner.join())
self._assert_retried(caplog.text)
assert "Got response 200" in caplog.text
@coroutine_test
async def test_unknown_url_scheme(self, caplog: pytest.LogCaptureFixture) -> None:
crawler = get_crawler(SimpleSpider)
await crawler.crawl_async("foo://bar")
assert "NotSupported: Unsupported URL scheme 'foo'" in caplog.text
class TestCrawlSpider:
@staticmethod
async def _run_spider(
spider_cls: type[Spider], mockserver: MockServer
) -> tuple[list[Any], StatsCollector]:
items = []
def _on_item_scraped(item):
items.append(item)
crawler = get_crawler(spider_cls)
crawler.signals.connect(_on_item_scraped, signals.item_scraped)
await crawler.crawl_async(
mockserver.url("/status?n=200"), mockserver=mockserver
)
assert crawler.stats
return items, crawler.stats
@coroutine_test
async def test_crawlspider_with_parse(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
crawler = get_crawler(CrawlSpiderWithParseMethod)
with caplog.at_level(logging.INFO):
await crawler.crawl_async(mockserver=mockserver)
assert "[parse] status 200 (foo: None)" in caplog.text
assert "[parse] status 201 (foo: None)" in caplog.text
assert "[parse] status 202 (foo: bar)" in caplog.text
@coroutine_test
async def test_crawlspider_with_async_callback(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
crawler = get_crawler(CrawlSpiderWithAsyncCallback)
with caplog.at_level(logging.INFO):
await crawler.crawl_async(mockserver=mockserver)
assert "[parse_async] status 200 (foo: None)" in caplog.text
assert "[parse_async] status 201 (foo: None)" in caplog.text
assert "[parse_async] status 202 (foo: bar)" in caplog.text
@coroutine_test
async def test_crawlspider_with_async_generator_callback(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
crawler = get_crawler(CrawlSpiderWithAsyncGeneratorCallback)
with caplog.at_level(logging.INFO):
await crawler.crawl_async(mockserver=mockserver)
assert "[parse_async_gen] status 200 (foo: None)" in caplog.text
assert "[parse_async_gen] status 201 (foo: None)" in caplog.text
assert "[parse_async_gen] status 202 (foo: bar)" in caplog.text
@coroutine_test
async def test_crawlspider_with_errback(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
crawler = get_crawler(CrawlSpiderWithErrback)
with caplog.at_level(logging.INFO):
await crawler.crawl_async(mockserver=mockserver)
assert "[parse] status 200 (foo: None)" in caplog.text
assert "[parse] status 201 (foo: None)" in caplog.text
assert "[parse] status 202 (foo: bar)" in caplog.text
assert "[errback] status 404" in caplog.text
assert "[errback] status 500" in caplog.text
assert "[errback] status 501" in caplog.text
@coroutine_test
async def test_crawlspider_process_request_cb_kwargs(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
crawler = get_crawler(CrawlSpiderWithProcessRequestCallbackKeywordArguments)
with caplog.at_level(logging.INFO):
await crawler.crawl_async(mockserver=mockserver)
assert "[parse] status 200 (foo: process_request)" in caplog.text
assert "[parse] status 201 (foo: process_request)" in caplog.text
assert "[parse] status 202 (foo: bar)" in caplog.text
@coroutine_test
async def test_async_def_parse(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
crawler = get_crawler(AsyncDefSpider)
with caplog.at_level(logging.INFO):
await crawler.crawl_async(
mockserver.url("/status?n=200"), mockserver=mockserver
)
assert "Got response 200" in caplog.text
@pytest.mark.only_asyncio
@coroutine_test
async def test_async_def_asyncio_parse(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
crawler = get_crawler(
AsyncDefAsyncioSpider,
{
"TWISTED_REACTOR": "twisted.internet.asyncioreactor.AsyncioSelectorReactor"
},
)
with caplog.at_level(logging.INFO):
await crawler.crawl_async(
mockserver.url("/status?n=200"), mockserver=mockserver
)
assert "Got response 200" in caplog.text
@pytest.mark.only_asyncio
@coroutine_test
async def test_async_def_asyncio_parse_items_list(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
with caplog.at_level(logging.INFO):
items, _ = await self._run_spider(AsyncDefAsyncioReturnSpider, mockserver)
assert "Got response 200" in caplog.text
assert {"id": 1} in items
assert {"id": 2} in items
@pytest.mark.only_asyncio
@coroutine_test
async def test_async_def_asyncio_parse_items_single_element(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
items = []
def _on_item_scraped(item):
items.append(item)
crawler = get_crawler(AsyncDefAsyncioReturnSingleElementSpider)
crawler.signals.connect(_on_item_scraped, signals.item_scraped)
with caplog.at_level(logging.INFO):
await crawler.crawl_async(
mockserver.url("/status?n=200"), mockserver=mockserver
)
assert "Got response 200" in caplog.text
assert {"foo": 42} in items
@pytest.mark.only_asyncio
@coroutine_test
async def test_async_def_asyncgen_parse(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
with caplog.at_level(logging.INFO):
_, stats = await self._run_spider(AsyncDefAsyncioGenSpider, mockserver)
assert "Got response 200" in caplog.text
itemcount = stats.get_value("item_scraped_count")
assert itemcount == 1
@pytest.mark.only_asyncio
@coroutine_test
async def test_async_def_asyncgen_parse_loop(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
with caplog.at_level(logging.INFO):
items, stats = await self._run_spider(
AsyncDefAsyncioGenLoopSpider, mockserver
)
assert "Got response 200" in caplog.text
itemcount = stats.get_value("item_scraped_count")
assert itemcount == 10
for i in range(10):
assert {"foo": i} in items
@pytest.mark.only_asyncio
@coroutine_test
async def test_async_def_asyncgen_parse_exc(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
with caplog.at_level(logging.INFO):
items, stats = await self._run_spider(
AsyncDefAsyncioGenExcSpider, mockserver
)
assert "Spider error processing" in caplog.text
assert "ValueError" in caplog.text
itemcount = stats.get_value("item_scraped_count")
assert itemcount == 7
for i in range(7):
assert {"foo": i} in items
@pytest.mark.only_asyncio
@coroutine_test
async def test_async_def_asyncgen_parse_complex(
self, mockserver: MockServer
) -> None:
items, stats = await self._run_spider(
AsyncDefAsyncioGenComplexSpider, mockserver
)
itemcount = stats.get_value("item_scraped_count")
assert itemcount == 156
# some random items
for i in [1, 4, 21, 22, 207, 311]:
assert {"index": i} in items
for i in [10, 30, 122]:
assert {"index2": i} in items
@pytest.mark.only_asyncio
@coroutine_test
async def test_async_def_asyncio_parse_reqs_list(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
with caplog.at_level(logging.INFO):
await self._run_spider(AsyncDefAsyncioReqsReturnSpider, mockserver)
for req_id in range(3):
assert f"Got response 200, req_id {req_id}" in caplog.text
@pytest.mark.only_not_asyncio
@coroutine_test
async def test_async_def_deferred_direct(self, mockserver: MockServer) -> None:
items, _ = await self._run_spider(AsyncDefDeferredDirectSpider, mockserver)
assert items == [{"code": 200}]
@pytest.mark.only_asyncio
@coroutine_test
async def test_async_def_deferred_wrapped(self, mockserver: MockServer) -> None:
items, _ = await self._run_spider(AsyncDefDeferredWrappedSpider, mockserver)
assert items == [{"code": 200}]
@coroutine_test
async def test_async_def_deferred_maybe_wrapped(
self, mockserver: MockServer
) -> None:
items, _ = await self._run_spider(
AsyncDefDeferredMaybeWrappedSpider, mockserver
)
assert items == [{"code": 200}]
@coroutine_test
async def test_response_ssl_certificate_none(self, mockserver: MockServer) -> None:
crawler = get_crawler(SingleRequestSpider)
url = mockserver.url("/echo?body=test", is_secure=False)
await crawler.crawl_async(seed=url, mockserver=mockserver)
assert isinstance(crawler.spider, SingleRequestSpider)
assert crawler.spider.meta["responses"][0].certificate is None
@pytest.mark.filterwarnings(
r"ignore:.*You should use cryptography's X\.509 APIs:DeprecationWarning"
)
@pytest.mark.parametrize(
"url",
[
"/echo?body=test",
pytest.param(
"/status?n=200",
marks=pytest.mark.xfail(
'config.getoption("--reactor") != "none"',
reason="With HTTP11DownloadHandler, responses with no body are returned early and contain no certificate",
strict=True,
),
),
],
)
@coroutine_test
async def test_response_ssl_certificate(
self, mockserver: MockServer, url: str
) -> None:
crawler = get_crawler(SingleRequestSpider)
url = mockserver.url(url, is_secure=True)
await crawler.crawl_async(seed=url, mockserver=mockserver)
assert isinstance(crawler.spider, SingleRequestSpider)
cert = crawler.spider.meta["responses"][0].certificate
assert cert is not None
if isinstance(cert, Certificate): # Twisted
assert cert.getSubject().commonName == b"localhost"
assert cert.getIssuer().commonName == b"localhost"
elif isinstance(cert, bytes): # DER bytes
cert_x509 = load_der_x509_certificate(cert)
assert cert_x509.subject.rfc4514_string() == "CN=localhost,O=Scrapy,C=IE"
assert cert_x509.issuer.rfc4514_string() == "CN=localhost,O=Scrapy,C=IE"
@pytest.mark.parametrize(
"url",
[
"/echo?body=test",
pytest.param(
"/status?n=200",
marks=pytest.mark.xfail(
'config.getoption("--reactor") != "none"',
reason="With HTTP11DownloadHandler, responses with no body are returned early and contain no ip_address",
strict=True,
),
),
],
)
@coroutine_test
async def test_response_ip_address(self, mockserver: MockServer, url: str) -> None:
crawler = get_crawler(SingleRequestSpider)
url = mockserver.url(url)
expected_netloc, _ = urlparse(url).netloc.split(":")
await crawler.crawl_async(seed=url, mockserver=mockserver)
assert isinstance(crawler.spider, SingleRequestSpider)
ip_address = crawler.spider.meta["responses"][0].ip_address
assert isinstance(ip_address, IPv4Address)
assert str(ip_address) == gethostbyname(expected_netloc)
@coroutine_test
async def test_bytes_received_stop_download_callback(
self, mockserver: MockServer
) -> None:
crawler = get_crawler(BytesReceivedCallbackSpider)
await crawler.crawl_async(mockserver=mockserver)
assert isinstance(crawler.spider, BytesReceivedCallbackSpider)
assert crawler.spider.meta.get("failure") is None
assert isinstance(crawler.spider.meta["response"], Response)
assert crawler.spider.meta["response"].body == crawler.spider.meta.get(
"bytes_received"
)
assert (
len(crawler.spider.meta["response"].body)
< crawler.spider.full_response_length
)
@coroutine_test
async def test_bytes_received_stop_download_errback(
self, mockserver: MockServer
) -> None:
crawler = get_crawler(BytesReceivedErrbackSpider)
await crawler.crawl_async(mockserver=mockserver)
assert isinstance(crawler.spider, BytesReceivedErrbackSpider)
assert crawler.spider.meta.get("response") is None
assert isinstance(crawler.spider.meta["failure"], Failure)
assert isinstance(crawler.spider.meta["failure"].value, StopDownload)
assert isinstance(crawler.spider.meta["failure"].value.response, Response)
assert crawler.spider.meta[
"failure"
].value.response.body == crawler.spider.meta.get("bytes_received")
assert (
len(crawler.spider.meta["failure"].value.response.body)
< crawler.spider.full_response_length
)
@coroutine_test
async def test_headers_received_stop_download_callback(
self, mockserver: MockServer
) -> None:
crawler = get_crawler(HeadersReceivedCallbackSpider)
await crawler.crawl_async(mockserver=mockserver)
assert isinstance(crawler.spider, HeadersReceivedCallbackSpider)
assert crawler.spider.meta.get("failure") is None
assert isinstance(crawler.spider.meta["response"], Response)
assert crawler.spider.meta["response"].headers == crawler.spider.meta.get(
"headers_received"
)
@coroutine_test
async def test_headers_received_stop_download_errback(
self, mockserver: MockServer
) -> None:
crawler = get_crawler(HeadersReceivedErrbackSpider)
await crawler.crawl_async(mockserver=mockserver)
assert isinstance(crawler.spider, HeadersReceivedErrbackSpider)
assert crawler.spider.meta.get("response") is None
assert isinstance(crawler.spider.meta["failure"], Failure)
assert isinstance(crawler.spider.meta["failure"].value, StopDownload)
assert isinstance(crawler.spider.meta["failure"].value.response, Response)
assert crawler.spider.meta[
"failure"
].value.response.headers == crawler.spider.meta.get("headers_received")
@coroutine_test
async def test_spider_callback_deferred_deprecated(
self, mockserver: MockServer
) -> None:
def cb(response: Response) -> Any:
return succeed(None)
crawler = get_crawler(SingleRequestSpider)
with pytest.warns(
ScrapyDeprecationWarning,
match="Returning Deferreds from spider callbacks is deprecated",
):
await crawler.crawl_async(seed=mockserver.url("/"), callback_func=cb)
@coroutine_test
async def test_spider_errback(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
failures = []
def eb(failure: Failure) -> Failure:
failures.append(failure)
return failure
crawler = get_crawler(SingleRequestSpider)
with caplog.at_level(logging.INFO):
await crawler.crawl_async(
seed=mockserver.url("/status?n=400"), errback_func=eb
)
assert len(failures) == 1
assert "HTTP status code is not handled or not allowed" in caplog.text
assert "Spider error processing" not in caplog.text
@coroutine_test
async def test_spider_errback_silence(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
failures = []
def eb(failure: Failure) -> None:
failures.append(failure)
crawler = get_crawler(SingleRequestSpider)
with caplog.at_level(logging.INFO):
await crawler.crawl_async(
seed=mockserver.url("/status?n=400"), errback_func=eb
)
assert len(failures) == 1
assert "HTTP status code is not handled or not allowed" not in caplog.text
assert "Spider error processing" not in caplog.text
@coroutine_test
async def test_spider_errback_exception(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
def eb(failure: Failure) -> None:
raise ValueError("foo")
crawler = get_crawler(SingleRequestSpider)
with caplog.at_level(logging.INFO):
await crawler.crawl_async(
seed=mockserver.url("/status?n=400"), errback_func=eb
)
assert "Spider error processing" in caplog.text
@coroutine_test
async def test_spider_errback_item(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
def eb(failure: Failure) -> Any:
return {"foo": "bar"}
crawler = get_crawler(SingleRequestSpider)
with caplog.at_level(logging.INFO):
await crawler.crawl_async(
seed=mockserver.url("/status?n=400"), errback_func=eb
)
assert "HTTP status code is not handled or not allowed" not in caplog.text
assert "Spider error processing" not in caplog.text
assert "'item_scraped_count': 1" in caplog.text
@coroutine_test
async def test_spider_errback_request(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
def eb(failure: Failure) -> Request:
return Request(mockserver.url("/"))
crawler = get_crawler(SingleRequestSpider)
with caplog.at_level(logging.DEBUG):
await crawler.crawl_async(
seed=mockserver.url("/status?n=400"), errback_func=eb
)
assert "HTTP status code is not handled or not allowed" not in caplog.text
assert "Spider error processing" not in caplog.text
assert "Crawled (200)" in caplog.text
@coroutine_test
async def test_spider_errback_downloader_error(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
failures = []
def eb(failure: Failure) -> Failure:
failures.append(failure)
return failure
crawler = get_crawler(SingleRequestSpider)
with caplog.at_level(logging.INFO):
await crawler.crawl_async(
seed=mockserver.url("/drop?abort=1"), errback_func=eb
)
assert len(failures) == 1
assert "Error downloading" in caplog.text
assert "Spider error processing" not in caplog.text
@coroutine_test
async def test_spider_errback_downloader_error_exception(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
def eb(failure: Failure) -> None:
raise ValueError("foo")
crawler = get_crawler(SingleRequestSpider)
with caplog.at_level(logging.INFO):
await crawler.crawl_async(
seed=mockserver.url("/drop?abort=1"), errback_func=eb
)
assert "Error downloading" in caplog.text
assert "Spider error processing" in caplog.text
@coroutine_test
async def test_spider_errback_downloader_error_item(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
def eb(failure: Failure) -> Any:
return {"foo": "bar"}
crawler = get_crawler(SingleRequestSpider)
with caplog.at_level(logging.INFO):
await crawler.crawl_async(
seed=mockserver.url("/drop?abort=1"), errback_func=eb
)
assert "HTTP status code is not handled or not allowed" not in caplog.text
assert "Spider error processing" not in caplog.text
assert "'item_scraped_count': 1" in caplog.text
@coroutine_test
async def test_spider_errback_downloader_error_request(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
def eb(failure: Failure) -> Request:
return Request(mockserver.url("/"))
crawler = get_crawler(SingleRequestSpider)
with caplog.at_level(logging.DEBUG):
await crawler.crawl_async(
seed=mockserver.url("/drop?abort=1"), errback_func=eb
)
assert "HTTP status code is not handled or not allowed" not in caplog.text
assert "Spider error processing" not in caplog.text
assert "Crawled (200)" in caplog.text
@coroutine_test
async def test_spider_errback_deferred_deprecated(
self, mockserver: MockServer
) -> None:
def eb(failure: Failure) -> Any:
return succeed(None)
crawler = get_crawler(SingleRequestSpider)
with pytest.warns(
ScrapyDeprecationWarning,
match="Returning Deferreds from spider errbacks is deprecated",
):
await crawler.crawl_async(
seed=mockserver.url("/status?n=400"), errback_func=eb
)
@coroutine_test
async def test_raise_closespider(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
def cb(response):
raise CloseSpider
crawler = get_crawler(SingleRequestSpider)
with caplog.at_level(logging.INFO):
await crawler.crawl_async(seed=mockserver.url("/"), callback_func=cb)
assert "Closing spider (cancelled)" in caplog.text
assert "Spider error processing" not in caplog.text
@coroutine_test
async def test_raise_closespider_reason(
self, caplog: pytest.LogCaptureFixture, mockserver: MockServer
) -> None:
def cb(response):
raise CloseSpider("my_reason")
crawler = get_crawler(SingleRequestSpider)
with caplog.at_level(logging.INFO):
await crawler.crawl_async(seed=mockserver.url("/"), callback_func=cb)
assert "Closing spider (my_reason)" in caplog.text
assert "Spider error processing" not in caplog.text