예제 #1
0
def change_type(context: MutationContext, draw: Draw,
                schema: Schema) -> MutationResult:
    """Change type of values accepted by a schema."""
    if "type" not in schema:
        # The absence of this keyword means that the schema values can be of any type;
        # Therefore, we can't choose a different type
        return MutationResult.FAILURE
    if context.media_type == "application/x-www-form-urlencoded":
        # Form data should be an object, do not change it
        return MutationResult.FAILURE
    if context.is_header_location:
        return MutationResult.FAILURE
    candidates = _get_type_candidates(context, schema)
    if not candidates:
        # Schema covers all possible types, not possible to choose something else
        return MutationResult.FAILURE
    if len(candidates) == 1:
        new_type = candidates.pop()
        schema["type"] = new_type
        prevent_unsatisfiable_schema(schema, new_type)
        return MutationResult.SUCCESS
    # Choose one type that will be present in the final candidates list
    candidate = draw(st.sampled_from(sorted(candidates)))
    candidates.remove(candidate)
    enabled_types = draw(st.shared(FeatureStrategy(),
                                   key="types"))  # type: ignore
    remaining_candidates = [candidate] + sorted([
        candidate
        for candidate in candidates if enabled_types.is_enabled(candidate)
    ])
    new_type = draw(st.sampled_from(remaining_candidates))
    schema["type"] = new_type
    prevent_unsatisfiable_schema(schema, new_type)
    return MutationResult.SUCCESS
예제 #2
0
def remove_required_property(context: MutationContext, draw: Draw,
                             schema: Schema) -> MutationResult:
    """Remove a required property.

    Effect: Some property won't be generated.
    """
    required = schema.get("required")
    if not required:
        # No required properties - can't mutate
        return MutationResult.FAILURE
    if len(required) == 1:
        property_name = draw(st.sampled_from(sorted(required)))
    else:
        candidate = draw(st.sampled_from(sorted(required)))
        enabled_properties = draw(
            st.shared(FeatureStrategy(), key="properties"))  # type: ignore
        candidates = [candidate] + sorted(
            [prop for prop in required if enabled_properties.is_enabled(prop)])
        property_name = draw(st.sampled_from(candidates))
    required.remove(property_name)
    if not required:
        # In JSON Schema Draft 4, `required` must contain at least one string
        # To keep the schema conformant, remove the `required` key completely
        del schema["required"]
    # An optional property still can be generated, and to avoid it, we need to remove it from other keywords.
    properties = schema.get("properties", {})
    properties.pop(property_name, None)
    if properties == {}:
        schema.pop("properties", None)
    schema["type"] = "object"
    # This property still can be generated via `patternProperties`, but this implementation doesn't cover this case
    # Its probability is relatively low, and the complete solution compatible with Draft 4 will require extra complexity
    # The output filter covers cases like this
    return MutationResult.SUCCESS
예제 #3
0
def change_properties(context: MutationContext, draw: Draw,
                      schema: Schema) -> MutationResult:
    """Mutate individual object schema properties.

    Effect: Some properties will not validate the original schema
    """
    properties = sorted(schema.get("properties", {}).items())
    if not properties:
        # No properties to mutate
        return MutationResult.FAILURE
    # Order properties randomly and iterate over them until at least one mutation is successfully applied to at least
    # one property
    ordered_properties = draw(ordered(properties, unique_by=lambda x: x[0]))
    for property_name, property_schema in ordered_properties:
        if apply_until_success(context, draw,
                               property_schema) == MutationResult.SUCCESS:
            # It is still possible to generate "positive" cases, for example, when this property is optional.
            # They are filtered out on the upper level anyway, but to avoid performance penalty we adjust the schema
            # so the generated samples are less likely to be "positive"
            required = schema.setdefault("required", [])
            if property_name not in required:
                required.append(property_name)
            # If `type` is already there, then it should contain "object" as we check it upfront
            # Otherwise restrict `type` to "object"
            if "type" not in schema:
                schema["type"] = "object"
            break
    else:
        # No successful mutations
        return MutationResult.FAILURE
    enabled_properties = draw(st.shared(FeatureStrategy(),
                                        key="properties"))  # type: ignore
    enabled_mutations = draw(st.shared(FeatureStrategy(),
                                       key="mutations"))  # type: ignore
    for name, property_schema in properties:
        # Skip already mutated property
        if name == property_name:  # pylint: disable=undefined-loop-variable
            # Pylint: `properties` variable has at least one element as it is checked at the beginning of the function
            # Then those properties are ordered and iterated over, therefore `property_name` is always defined
            continue
        if enabled_properties.is_enabled(name):
            for mutation in get_mutations(draw, property_schema):
                if enabled_mutations.is_enabled(mutation.__name__):
                    mutation(context, draw, property_schema)
    return MutationResult.SUCCESS
예제 #4
0
def negate_constraints(context: MutationContext, draw: Draw,
                       schema: Schema) -> MutationResult:
    """Negate schema constrains while keeping the original type."""
    if canonicalish(schema) == {}:
        return MutationResult.FAILURE
    copied = schema.copy()
    schema.clear()
    is_negated = False

    def is_mutation_candidate(k: str) -> bool:
        # Should we negate this key?
        return not (k in ("type", "properties", "items", "minItems") or
                    (k == "additionalProperties"
                     and context.is_header_location))

    enabled_keywords = draw(st.shared(FeatureStrategy(),
                                      key="keywords"))  # type: ignore
    candidates = []
    mutation_candidates = [key for key in copied if is_mutation_candidate(key)]
    if mutation_candidates:
        # There should be at least one mutated keyword
        candidate = draw(
            st.sampled_from(
                [key for key in copied if is_mutation_candidate(key)]))
        candidates.append(candidate)
        # If the chosen candidate has dependency, then the dependency should also be present in the final schema
        if candidate in DEPENDENCIES:
            candidates.append(DEPENDENCIES[candidate])
    for key, value in copied.items():
        if is_mutation_candidate(key):
            if key in candidates or enabled_keywords.is_enabled(key):
                is_negated = True
                negated = schema.setdefault("not", {})
                negated[key] = value
                if key in DEPENDENCIES:
                    # If this keyword has a dependency, then it should be also negated
                    dependency = DEPENDENCIES[key]
                    if dependency not in negated:
                        negated[dependency] = copied[
                            dependency]  # Assuming the schema is valid
        else:
            schema[key] = value
    if is_negated:
        return MutationResult.SUCCESS
    return MutationResult.FAILURE
예제 #5
0
    def __init__(self, machine):
        SearchStrategy.__init__(self)
        self.machine = machine
        self.rules = list(machine.rules())

        self.enabled_rules_strategy = st.shared(FeatureStrategy(),
                                                key=("enabled rules", machine))

        # The order is a bit arbitrary. Primarily we're trying to group rules
        # that write to the same location together, and to put rules with no
        # target first as they have less effect on the structure. We order from
        # fewer to more arguments on grounds that it will plausibly need less
        # data. This probably won't work especially well and we could be
        # smarter about it, but it's better than just doing it in definition
        # order.
        self.rules.sort(key=lambda rule: (
            sorted(rule.targets),
            len(rule.arguments),
            rule.function.__name__,
        ))
예제 #6
0
 def mutate(self, draw: Draw) -> Schema:
     # On the top level, Schemathesis creates "object" schemas for all parameter "in" values except "body", which is
     # taken as-is. Therefore we can only apply mutations that won't change the Open API semantics of the schema.
     mutations: List[Mutation]
     if self.location in ("header", "cookie", "query"):
         # These objects follow this pattern:
         # {
         #     "properties": properties,
         #     "additionalProperties": False,
         #     "type": "object",
         #     "required": required
         # }
         # Open API semantics expect mapping; therefore, they should have the "object" type.
         # We can:
         #   - remove required parameters
         #   - negate constraints (only `additionalProperties` in this case)
         #   - mutate individual properties
         mutations = draw(
             ordered((remove_required_property, negate_constraints,
                      change_properties)))
     elif self.is_path_location:
         # The same as above, but we can only mutate individual properties as their names are predefined in the
         # path template, and all of them are required.
         mutations = [change_properties]
     else:
         # Body can be of any type and does not have any specific type semantic.
         mutations = draw(ordered(get_mutations(draw, self.schema)))
     keywords, non_keywords = split_schema(self.schema)
     # Deep copy all keywords to avoid modifying the original schema
     new_schema = deepcopy(keywords)
     enabled_mutations = draw(st.shared(FeatureStrategy(),
                                        key="mutations"))  # type: ignore
     result = MutationResult.FAILURE
     for mutation in mutations:
         if enabled_mutations.is_enabled(mutation.__name__):
             result |= mutation(self, draw, new_schema)
     if result.is_failure:
         # If we failed to apply anything, then reject the whole case
         reject()  # type: ignore
     new_schema.update(non_keywords)
     if self.is_header_location:
         # All headers should have names that can be sent over network
         new_schema["propertyNames"] = {
             "type": "string",
             "format": "_header_name"
         }
         for sub_schema in new_schema.get("properties", {}).values():
             sub_schema["type"] = "string"
             if len(sub_schema) == 1:
                 sub_schema["format"] = "_header_value"
         if draw(st.booleans()):
             # In headers, `additionalProperties` are False by default, which means that Schemathesis won't generate
             # any headers that are not defined. This change adds the possibility of generating valid extra headers
             new_schema["additionalProperties"] = {
                 "type": "string",
                 "format": "_header_value"
             }
     # Empty array or objects may match the original schema
     if "array" in get_type(new_schema) and new_schema.get(
             "items") and "minItems" not in new_schema.get("not", {}):
         new_schema.setdefault("minItems", 1)
     if ("object" in get_type(new_schema) and new_schema.get("properties")
             and "minProperties" not in new_schema.get("not", {})):
         new_schema.setdefault("minProperties", 1)
     return new_schema
예제 #7
0
# This file is part of Hypothesis, which may be found at
# https://github.com/HypothesisWorks/hypothesis/
#
# Copyright the Hypothesis Authors.
# Individual contributors are listed in AUTHORS.rst and the git log.
#
# This Source Code Form is subject to the terms of the Mozilla Public License,
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
# obtain one at https://mozilla.org/MPL/2.0/.

from hypothesis import given, strategies as st
from hypothesis.strategies._internal.featureflags import FeatureFlags, FeatureStrategy

from tests.common.debug import find_any, minimal

STRAT = FeatureStrategy()


def test_can_all_be_enabled():
    find_any(STRAT, lambda x: all(x.is_enabled(i) for i in range(100)))


def test_minimizes_open():
    features = range(10)

    flags = minimal(STRAT, lambda x: [x.is_enabled(i) for i in features])

    assert all(flags.is_enabled(i) for i in features)


def test_minimizes_individual_features_to_open():