mirror of https://github.com/scrapy/scrapy.git
Report crawl errors in scrapy check
This commit is contained in:
parent
65b37286cc
commit
2bfcff5638
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue