Report crawl errors in scrapy check

This commit is contained in:
Adrian Chaves 2026-08-12 20:29:34 +02:00
parent 65b37286cc
commit 2bfcff5638
2 changed files with 61 additions and 3 deletions

View File

@ -1,10 +1,14 @@
import argparse
import asyncio
import time
from collections import defaultdict
from collections.abc import AsyncIterator
from collections.abc import AsyncIterator, Awaitable
from typing import Any, ClassVar
from unittest import TestCase, TextTestRunner
from unittest import TextTestResult as _TextTestResult
from unittest import TextTestRunner
from twisted.internet.defer import Deferred
from twisted.python.failure import Failure
from scrapy import Spider
from scrapy.commands import ScrapyCommand
@ -42,6 +46,41 @@ class TextTestResult(_TextTestResult):
write("\n")
def _report_crawl_errors(
crawl: Awaitable[None], spidername: str, result: TextTestResult
) -> None:
"""Make an exception that stops *crawl* before its contracts can run show
up as an error in *result*, instead of being silently discarded."""
class CrawlTestCase(TestCase):
def runTest(self) -> None:
pass
def __str__(self) -> str:
return f"[{spidername}] crawl"
def report(exception: BaseException) -> None:
result.addError(
CrawlTestCase(),
(type(exception), exception, exception.__traceback__), # type: ignore[arg-type]
)
if isinstance(crawl, Deferred):
def on_failure(failure: Failure) -> None:
assert failure.value is not None
report(failure.value)
crawl.addErrback(on_failure)
elif isinstance(crawl, asyncio.Task):
def on_done(task: asyncio.Task[None]) -> None:
if not task.cancelled() and (exception := task.exception()) is not None:
report(exception)
crawl.add_done_callback(on_done)
class Command(ScrapyCommand):
requires_project = True
default_settings: ClassVar[dict[str, Any]] = {"LOG_ENABLED": False}
@ -100,7 +139,8 @@ class Command(ScrapyCommand):
for method in tested_methods:
contract_reqs[spidercls.name].append(method)
elif tested_methods:
self.crawler_process.crawl(spidercls)
crawl = self.crawler_process.crawl(spidercls)
_report_crawl_errors(crawl, spidercls.name, result)
# start checks
if opts.list:

View File

@ -6,6 +6,8 @@ from typing import TYPE_CHECKING
from unittest import TestCase
from unittest.mock import MagicMock, Mock, PropertyMock, call, patch
import pytest
from scrapy.commands.check import Command, TextTestResult
from tests.utils.bases.commands import TestProjectBase
from tests.utils.cmdline import proc
@ -79,6 +81,22 @@ class CheckSpider(scrapy.Spider):
"""
self._test_contract(proj_path, contracts, parse_def, use_reactor=False)
@pytest.mark.parametrize("use_reactor", [True, False])
def test_check_crawl_error(self, proj_path: Path, use_reactor: bool) -> None:
self._write_contract(proj_path, "@returns requests 0", "pass")
self._append_settings(
proj_path / self.project_name,
"\nITEM_PIPELINES = {'nonexistent.module.Pipeline': 300}\n",
)
args = ["check"]
if not use_reactor:
args += ["-s", "TWISTED_REACTOR_ENABLED=False"]
ret, _, err = proc(*args, cwd=proj_path)
assert f"[{self.spider_name}] crawl" in err
assert "ModuleNotFoundError" in err
assert "FAILED (errors=1)" in err
assert ret == 1
def test_check_returns_items_contract(self, proj_path: Path) -> None:
contracts = """
@returns items 1