Skip to content

Commit

Permalink
feat: GraphQL support in CLI
Browse files Browse the repository at this point in the history
Ref: #746
  • Loading branch information
Stranger6667 committed Mar 27, 2021
1 parent d689d77 commit ba42356
Show file tree
Hide file tree
Showing 10 changed files with 47 additions and 12 deletions.
2 changes: 2 additions & 0 deletions docs/changelog.rst
Expand Up @@ -7,6 +7,7 @@ Changelog
**Added**

- Custom keyword arguments to ``schemathesis.graphql.from_url`` that are proxied to ``requests.post``.
- GraphQL support in CLI. `#746`_

**Changed**

Expand Down Expand Up @@ -1837,6 +1838,7 @@ Deprecated
.. _#768: https://github.com/schemathesis/schemathesis/issues/768
.. _#757: https://github.com/schemathesis/schemathesis/issues/757
.. _#748: https://github.com/schemathesis/schemathesis/issues/748
.. _#746: https://github.com/schemathesis/schemathesis/issues/746
.. _#742: https://github.com/schemathesis/schemathesis/issues/742
.. _#738: https://github.com/schemathesis/schemathesis/issues/738
.. _#737: https://github.com/schemathesis/schemathesis/issues/737
Expand Down
4 changes: 2 additions & 2 deletions src/schemathesis/cli/output/default.py
Expand Up @@ -36,7 +36,7 @@ def display_section_name(title: str, separator: str = "=", extra: str = "", **kw


def display_subsection(result: SerializedTestResult, color: Optional[str] = "red") -> None:
section_name = f"{result.method} {result.path}"
section_name = f"{result.method} {result.path}" if result.verbose_name is None else result.verbose_name
display_section_name(section_name, "_", result.data_generation_method, fg=color)


Expand Down Expand Up @@ -328,7 +328,7 @@ def handle_before_execution(context: ExecutionContext, event: events.BeforeExecu
"""Display what method / path will be tested next."""
# We should display execution result + percentage in the end. For example:
max_length = get_terminal_width() - len(" . [XXX%]") - len(TRUNCATION_PLACEHOLDER)
message = f"{event.method} {event.path}"
message = event.verbose_name
if event.recursion_level > 0:
message = f"{' ' * event.recursion_level}-> {message}"
# This value is not `None` - the value is set in runtime before this line
Expand Down
4 changes: 4 additions & 0 deletions src/schemathesis/exceptions.py
Expand Up @@ -106,6 +106,10 @@ def actual_test(*args: Any, **kwargs: Any) -> NoReturn:
return actual_test


class UnsupportedSpecification(ValueError):
"""The API specification is not supported."""


class NonCheckError(Exception):
"""An error happened in side the runner, but is not related to failed checks.
Expand Down
4 changes: 2 additions & 2 deletions src/schemathesis/loaders.py
Expand Up @@ -12,7 +12,7 @@
from yarl import URL

from .constants import DEFAULT_DATA_GENERATION_METHODS, CodeSampleStyle, DataGenerationMethod
from .exceptions import HTTPError
from .exceptions import HTTPError, UnsupportedSpecification
from .hooks import HookContext, dispatch
from .lazy import LazySchema
from .specs.openapi import definitions
Expand Down Expand Up @@ -205,7 +205,7 @@ def init_openapi_3() -> OpenApi30:
return init_openapi_2()
if "openapi" in raw_schema:
return init_openapi_3()
raise ValueError("Unsupported schema type")
raise UnsupportedSpecification("Unsupported schema type")


def _maybe_validate_schema(
Expand Down
1 change: 1 addition & 0 deletions src/schemathesis/models.py
Expand Up @@ -802,6 +802,7 @@ class TestResult:

method: str = attr.ib() # pragma: no mutate
path: str = attr.ib() # pragma: no mutate
verbose_name: str = attr.ib() # pragma: no mutate
data_generation_method: DataGenerationMethod = attr.ib() # pragma: no mutate
checks: List[Check] = attr.ib(factory=list) # pragma: no mutate
errors: List[Tuple[Exception, Optional[Case]]] = attr.ib(factory=list) # pragma: no mutate
Expand Down
27 changes: 20 additions & 7 deletions src/schemathesis/runner/__init__.py
Expand Up @@ -8,6 +8,7 @@
from .. import loaders
from ..checks import DEFAULT_CHECKS
from ..constants import DEFAULT_DATA_GENERATION_METHODS, DEFAULT_STATEFUL_RECURSION_LIMIT, DataGenerationMethod
from ..exceptions import HTTPError, UnsupportedSpecification
from ..models import CheckFunction
from ..schemas import BaseSchema
from ..stateful import Stateful
Expand Down Expand Up @@ -374,13 +375,25 @@ def load_schema(
if loader in (loaders.from_uri, loaders.from_aiohttp):
loader_options["verify"] = request_tls_verify

return loader(
schema_uri,
validate_schema=validate_schema,
skip_deprecated_operations=skip_deprecated_operations,
force_schema_version=force_schema_version,
**loader_options,
)
try:
return loader(
schema_uri,
validate_schema=validate_schema,
skip_deprecated_operations=skip_deprecated_operations,
force_schema_version=force_schema_version,
**loader_options,
)
except (UnsupportedSpecification, HTTPError) as exc:
from ..specs import graphql # pylint: disable=import-outside-toplevel

if isinstance(schema_uri, dict):
return graphql.from_dict(schema_uri, base_url=base_url)
try:
return graphql.from_url(
schema_uri, base_url=base_url, verify=loader_options["verify"], auth=loader_options.get("auth")
)
except HTTPError:
raise exc from None


def prepare_hypothesis_options(
Expand Down
3 changes: 3 additions & 0 deletions src/schemathesis/runner/events.py
Expand Up @@ -66,6 +66,8 @@ class BeforeExecution(CurrentOperationMixin, ExecutionEvent):
method: str = attr.ib() # pragma: no mutate
# Full path, including the base path
path: str = attr.ib() # pragma: no mutate
# Specification-specific operation name
verbose_name: str = attr.ib() # pragma: no mutate
# Path without the base path
relative_path: str = attr.ib() # pragma: no mutate
# The current level of recursion during stateful testing
Expand All @@ -80,6 +82,7 @@ def from_operation(cls, operation: APIOperation, recursion_level: int, correlati
return cls(
method=operation.method.upper(),
path=operation.full_path,
verbose_name=operation.verbose_name,
relative_path=operation.path,
recursion_level=recursion_level,
correlation_id=correlation_id,
Expand Down
4 changes: 4 additions & 0 deletions src/schemathesis/runner/impl/core.py
Expand Up @@ -131,16 +131,19 @@ def handle_schema_error(
assert error.path is not None
assert error.full_path is not None
method = error.method.upper()
verbose_name = f"{method} {error.path}"
result = TestResult(
method=method,
path=error.full_path,
verbose_name=verbose_name,
data_generation_method=data_generation_method,
)
result.add_error(error)
correlation_id = uuid.uuid4().hex
yield events.BeforeExecution(
method=method,
path=error.full_path,
verbose_name=verbose_name,
relative_path=error.path,
recursion_level=recursion_level,
correlation_id=correlation_id,
Expand Down Expand Up @@ -177,6 +180,7 @@ def run_test( # pylint: disable=too-many-locals
result = TestResult(
method=operation.method.upper(),
path=operation.full_path,
verbose_name=operation.verbose_name,
overridden_headers=headers,
data_generation_method=data_generation_method,
)
Expand Down
2 changes: 2 additions & 0 deletions src/schemathesis/runner/serialization.py
Expand Up @@ -105,6 +105,7 @@ def from_interaction(cls, interaction: Interaction) -> "SerializedInteraction":
class SerializedTestResult:
method: str = attr.ib() # pragma: no mutate
path: str = attr.ib() # pragma: no mutate
verbose_name: str = attr.ib() # pragma: no mutate
has_failures: bool = attr.ib() # pragma: no mutate
has_errors: bool = attr.ib() # pragma: no mutate
has_logs: bool = attr.ib() # pragma: no mutate
Expand All @@ -122,6 +123,7 @@ def from_test_result(cls, result: TestResult) -> "SerializedTestResult":
return SerializedTestResult(
method=result.method,
path=result.path,
verbose_name=result.verbose_name,
has_failures=result.has_failures,
has_errors=result.has_errors,
has_logs=result.has_logs,
Expand Down
8 changes: 7 additions & 1 deletion src/schemathesis/specs/graphql/schemas.py
@@ -1,5 +1,5 @@
from functools import partial
from typing import Any, Dict, Generator, List, Optional, Tuple, cast
from typing import Any, Dict, Generator, List, Optional, Sequence, Tuple, cast
from urllib.parse import urlsplit

import attr
Expand All @@ -14,6 +14,7 @@
from ...hooks import HookDispatcher
from ...models import APIOperation, Case, CheckFunction
from ...schemas import BaseSchema
from ...stateful import Stateful, StatefulTest
from ...utils import GenericResponse, Ok, Result


Expand Down Expand Up @@ -92,3 +93,8 @@ def get_case_strategy(

def get_strategies_from_examples(self, operation: APIOperation) -> List[SearchStrategy[Case]]:
return []

def get_stateful_tests(
self, response: GenericResponse, operation: APIOperation, stateful: Optional[Stateful]
) -> Sequence[StatefulTest]:
return []

0 comments on commit ba42356

Please sign in to comment.