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
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
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
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
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__, ))
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
# 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():