def _random_insertion(self) -> bool: changed = False pos = 0 if len(self._elements) > 0: pos = randomness.next_int(0, len(self._elements) + 1) # This is so ugly... key_type = (get_args(self.ret_val.variable_type)[0] if get_args(self.ret_val.variable_type) else None) val_type = (get_args(self.ret_val.variable_type)[1] if get_args(self.ret_val.variable_type) else None) possibles_keys = self.test_case.get_objects(key_type, self.get_position()) possibles_values = self.test_case.get_objects(val_type, self.get_position()) alpha = 0.5 exponent = 1 while randomness.next_float() <= pow(alpha, exponent): exponent += 1 if len(possibles_keys) > 0 and len(possibles_values) > 0: self._elements.insert( pos, ( randomness.choice(possibles_keys), randomness.choice(possibles_values), ), ) changed = True return changed
def select_concrete_type(self, select_from: Optional[Type]) -> Optional[Type]: """Select a concrete type from the given type. This is required e.g. when handling union types. Currently only unary types, Any and Union are handled.""" if select_from == Any: return randomness.choice(self.get_all_generatable_types()) if is_union_type(select_from): possible_types = get_args(select_from) if possible_types is not None and len(possible_types) > 0: return randomness.choice(possible_types) return None return select_from
def _create_tuple( self, test_case: tc.TestCase, parameter_type: Type, position: int, recursion_depth: int, ) -> vr.VariableReference: args = get_args(parameter_type) if len(args) == 0: # Untyped tuple, time to guess... size = randomness.next_int(0, config.configuration.collection_size) args = [ randomness.choice( self._test_cluster.get_all_generatable_types()) for _ in range(size) ] elements = [] for arg_type in args: previous_length = test_case.size() var = self._create_or_reuse_variable(test_case, arg_type, position, recursion_depth + 1, True) if var is not None: elements.append(var) position += test_case.size() - previous_length ret = test_case.add_statement( coll_stmt.TupleStatement(test_case, parameter_type, elements), position) ret.distance = recursion_depth return ret
def _select_random_variable_for_call( test_case: tc.TestCase, position: int) -> Optional[vr.VariableReference]: """Randomly select one of the variables in the test defined up to position to insert a call for. Args: test_case: The test case position: The last position Returns: A candidate, if found """ candidates: List[vr.VariableReference] = [ var for var in test_case.get_all_objects(position) if not var.is_primitive() and not var.is_type_unknown() and not isinstance( test_case.get_statement(var.get_statement_position()), prim.NoneStatement, ) ] if len(candidates) == 0: return None # TODO(fk) sort based on distance and use rank selection. return randomness.choice(candidates)
def _get_variable_fallback( self, test_case: tc.TestCase, parameter_type: Optional[Type], position: int, recursion_depth: int, allow_none: bool, ) -> Optional[vr.VariableReference]: """Best effort approach to return some kind of matching variable.""" objects = test_case.get_objects(parameter_type, position) # No objects to choose from, so either create random type variable or use None. if not objects: if config.INSTANCE.guess_unknown_types and randomness.next_float( ) <= 0.85: return self._create_random_type_variable( test_case, position, recursion_depth, allow_none) if allow_none: return self._create_none(test_case, parameter_type, position, recursion_depth) raise ConstructionFailedException( f"No objects for type {parameter_type}") # Could not create, so re-use an existing variable. self._logger.debug("Choosing from %d existing objects: %s", len(objects), objects) reference = randomness.choice(objects) self._logger.debug("Use existing object of type %s: %s", parameter_type, reference) return reference
def delete_statement_gracefully(test_case: tc.TestCase, position: int) -> bool: """Try to delete the statement that is defined at the given index. We try to find replacements for the variable that is provided by this statement Args: test_case: The test case position: The position Returns: Whether or not the deletion was successful """ variable = test_case.get_statement(position).return_value changed = False for i in range(position + 1, test_case.size()): alternatives = test_case.get_objects(variable.variable_type, i) try: alternatives.remove(variable) except ValueError: pass if len(alternatives) > 0: statement = test_case.get_statement(i) if statement.references(variable): statement.replace(variable, randomness.choice(alternatives)) changed = True deleted = TestFactory.delete_statement(test_case, position) return deleted or changed
def change_random_call(self, test_case: tc.TestCase, statement: stmt.Statement) -> bool: """Change the call represented by this statement to another one. Args: test_case: The test case statement: The new statement Returns: Whether or not the operation was successful """ if statement.return_value.is_type_unknown(): return False objects = test_case.get_all_objects(statement.get_position()) type_ = statement.return_value.variable_type assert type_, "Cannot change change call, when type is unknown" calls = self._get_possible_calls(type_, objects) acc_object = statement.accessible_object() if acc_object in calls: calls.remove(acc_object) if len(calls) == 0: return False call = randomness.choice(calls) try: self.change_call(test_case, statement, call) return True except ConstructionFailedException: self._logger.info("Failed to change call for statement.") return False
def get_random_accessible(self) -> Optional[GenericAccessibleObject]: """Provide a random accessible of the unit under test. Returns: A random accessible """ if self.num_accessible_objects_under_test() == 0: return None return randomness.choice(list(self._accessible_objects_under_test))
def get_random_object(self, parameter_type: Type, position: int) -> vr.VariableReference: """Get a random object of the given type up to the given position (exclusive).""" variables = self.get_objects(parameter_type, position) if len(variables) == 0: raise ConstructionFailedException( f"Found no variables of type {parameter_type} at position {position}" ) return randomness.choice(variables)
def _random_replacement(self) -> bool: p_per_element = 1.0 / len(self._elements) changed = False for i, elem in enumerate(self._elements): if randomness.next_float() < p_per_element: # TODO(fk) what if the current type is not correct? replace = randomness.choice( self.test_case.get_objects(elem.variable_type, self.get_position()) + [elem]) self._elements[i] = replace changed |= replace != elem return changed
def mutate(self) -> bool: if randomness.next_float( ) >= config.INSTANCE.change_parameter_probability: return False objects = self.test_case.get_objects(self.source.variable_type, self.get_position()) objects.remove(self.source) if len(objects) > 0: self.source = randomness.choice(objects) return True return False
def _random_replacement(self) -> bool: p_per_element = 1.0 / len(self._elements) changed = False for i, elem in enumerate(self._elements): if randomness.next_float() < p_per_element: if randomness.next_bool(): # TODO(fk) what if the current type is not correct? new_key = randomness.choice( self.test_case.get_objects(elem[0].variable_type, self.get_position()) + [elem[0]]) replace = (new_key, elem[1]) else: new_value = randomness.choice( self.test_case.get_objects(elem[1].variable_type, self.get_position()) + [elem[1]]) replace = (elem[0], new_value) self._elements[i] = replace changed |= replace != elem return changed
def _mutate_special_parameters(self, p_per_param: float) -> bool: # We mutate the callee here, as the special parameter. if randomness.next_float() < p_per_param: callee = self.callee objects = self.test_case.get_objects(callee.variable_type, self.get_position()) objects.remove(callee) if len(objects) > 0: self.callee = randomness.choice(objects) return True return False
def _get_random_non_none_object(test_case: tc.TestCase, type_: Type, position: int) -> vr.VariableReference: variables = test_case.get_objects(type_, position) variables = [ var for var in variables if not isinstance( test_case.get_statement(var.get_statement_position()), prim.NoneStatement, ) ] if len(variables) == 0: raise ConstructionFailedException( f"Found no variables of type {type_} at position {position}") return randomness.choice(variables)
def evolve(self) -> None: """Evolve the current population and replace it with a new one.""" new_generation = [] new_generation.extend(self.elitism()) while not self.is_next_population_full(new_generation): parent1 = self._selection_function.select(self._population, 1)[0] parent2 = self._selection_function.select(self._population, 1)[0] offspring1 = parent1.clone() offspring2 = parent2.clone() try: if randomness.next_float() <= config.INSTANCE.crossover_rate: self._crossover_function.cross_over(offspring1, offspring2) offspring1.mutate() offspring2.mutate() except ConstructionFailedException as ex: self._logger.info("Crossover/Mutation failed: %s", ex) continue fitness_parents = min(parent1.get_fitness(), parent2.get_fitness()) fitness_offspring = min(offspring1.get_fitness(), offspring2.get_fitness()) length_parents = ( parent1.total_length_of_test_cases + parent2.total_length_of_test_cases ) length_offspring = ( offspring1.total_length_of_test_cases + offspring2.total_length_of_test_cases ) best_individual = self._get_best_individual() if (fitness_offspring < fitness_parents) or ( fitness_offspring == fitness_parents and length_offspring <= length_parents ): for offspring in [offspring1, offspring2]: if ( offspring.total_length_of_test_cases <= 2 * best_individual.total_length_of_test_cases ): new_generation.append(offspring) else: new_generation.append(randomness.choice([parent1, parent2])) else: new_generation.append(parent1) new_generation.append(parent2) self._population = new_generation self._sort_population() StatisticsTracker().current_individual(self._get_best_individual())
def _reuse_variable(self, test_case: tc.TestCase, parameter_type: Optional[Type], position: int) -> Optional[vr.VariableReference]: """Reuse an existing variable, if possible.""" objects = test_case.get_objects(parameter_type, position) probability = (config.INSTANCE.primitive_reuse_probability if is_primitive_type(parameter_type) else config.INSTANCE.object_reuse_probability) if objects and randomness.next_float() <= probability: var = randomness.choice(objects) self._logger.debug("Reusing variable %s for type %s", var, parameter_type) return var return None
def _create_random_type_variable( self, test_case: tc.TestCase, position: int, recursion_depth: int, allow_none: bool, ) -> Optional[vr.VariableReference]: return self._create_or_reuse_variable( test_case=test_case, parameter_type=randomness.choice( self._test_cluster.get_all_generatable_types()), position=position, recursion_depth=recursion_depth + 1, allow_none=allow_none, )
def _mutate_parameter(self, arg: Union[int, str]) -> bool: """Replace the given parameter with another one that also fits the parameter type. Args: arg: the parameter Returns: True, if the parameter was mutated. """ to_mutate = self._get_argument(arg) param_type = self._get_parameter_type(arg) possible_replacements = self.test_case.get_objects( param_type, self.get_position()) if to_mutate in possible_replacements: possible_replacements.remove(to_mutate) # Consider duplicating an existing statement/variable. copy: Optional[stmt.Statement] = None if self._param_count_of_type( param_type) > len(possible_replacements) + 1: original_param_source = self.test_case.get_statement( to_mutate.get_statement_position()) copy = original_param_source.clone(self.test_case) copy.mutate() possible_replacements.append(copy.ret_val) # TODO(fk) Use param_type instead of to_mutate.variable_type, # to make the selection broader, but this requires access to # the test cluster, to select a concrete type. # Using None as parameter value is also a possibility. none_statement = prim.NoneStatement(self.test_case, to_mutate.variable_type) possible_replacements.append(none_statement.ret_val) replacement = randomness.choice(possible_replacements) if copy and replacement is copy.ret_val: # The chosen replacement is a copy, so we have to add it to the test case. self.test_case.add_statement(copy, self.get_position()) elif replacement is none_statement.ret_val: # The chosen replacement is a none statement, so we have to add it to the # test case. self.test_case.add_statement(none_statement, self.get_position()) self._replace_argument(arg, replacement) return True
def get_random_call_for(self, type_: Type) -> GenericAccessibleObject: """Get a random modifier for the given type. Args: type_: The type Returns: A random modifier for that type Raises: ConstructionFailedException: if no modifiers for the type exist """ accessible_objects = self.get_modifiers_for(type_) if len(accessible_objects) == 0: raise ConstructionFailedException("No modifiers for " + str(type_)) return randomness.choice(list(accessible_objects))
def _attempt_generation_for_type( self, test_case: tc.TestCase, position: int, recursion_depth: int, allow_none: bool, type_generators: Set[GenericAccessibleObject], ) -> Optional[vr.VariableReference]: type_generator = randomness.choice(list(type_generators)) return self.append_generic_statement( test_case, type_generator, position=position, recursion_depth=recursion_depth + 1, allow_none=allow_none, )
def _breed_next_generation(self) -> List[tcc.TestCaseChromosome]: offspring_population: List[tcc.TestCaseChromosome] = [] for _ in range(int(config.configuration.population / 2)): parent_1 = self._selection_function.select(self._population)[0] parent_2 = self._selection_function.select(self._population)[0] offspring_1 = cast(tcc.TestCaseChromosome, parent_1.clone()) offspring_2 = cast(tcc.TestCaseChromosome, parent_2.clone()) # Apply crossover if randomness.next_float() <= config.configuration.crossover_rate: try: self._crossover_function.cross_over(offspring_1, offspring_2) except ConstructionFailedException: self._logger.debug("CrossOver failed.") continue # Apply mutation on offspring_1 self._mutate(offspring_1) if offspring_1.has_changed() and offspring_1.size() > 0: offspring_population.append(offspring_1) # Apply mutation on offspring_2 self._mutate(offspring_2) if offspring_2.has_changed() and offspring_2.size() > 0: offspring_population.append(offspring_2) # Add new randomly generated tests for _ in range( int( config.configuration.population * config.configuration.test_insertion_probability ) ): if len(self._archive.covered_goals) == 0 or randomness.next_bool(): tch: tcc.TestCaseChromosome = self._chromosome_factory.get_chromosome() for fitness_function in self._fitness_functions: tch.add_fitness_function(fitness_function) else: tch = randomness.choice(list(self._archive.solutions)).clone() tch.mutate() if tch.has_changed() and tch.size() > 0: offspring_population.append(tch) self._logger.debug("Number of offsprings = %d", len(offspring_population)) return offspring_population
def _get_variable_fallback( self, test_case: tc.TestCase, parameter_type: Optional[Type], position: int, recursion_depth: int, allow_none: bool, ) -> Optional[vr.VariableReference]: """Best effort approach to return some kind of matching variable. Args: test_case: The test case to take the variable from parameter_type: the type of the variable that is needed position: the position to limit the search recursion_depth: the current recursion level allow_none: whether or not a None value is allowed Returns: A variable if found Raises: ConstructionFailedException: if construction of an object failed """ objects = test_case.get_objects(parameter_type, position) # No objects to choose from, so either create random type variable or use None. if not objects: if config.INSTANCE.guess_unknown_types and randomness.next_float( ) <= 0.85: return self._create_random_type_variable( test_case, position, recursion_depth, allow_none) if allow_none: return self._create_none(test_case, parameter_type, position, recursion_depth) raise ConstructionFailedException( f"No objects for type {parameter_type}") # Could not create, so re-use an existing variable. self._logger.debug("Choosing from %d existing objects: %s", len(objects), objects) reference = randomness.choice(objects) self._logger.debug("Use existing object of type %s: %s", parameter_type, reference) return reference
def _create_or_reuse_variable( self, test_case: tc.TestCase, parameter_type: Optional[Type], position: int, recursion_depth: int, allow_none: bool, exclude: Optional[vr.VariableReference] = None, ) -> Optional[vr.VariableReference]: if is_type_unknown(parameter_type): if config.INSTANCE.guess_unknown_types: parameter_type = randomness.choice( self._test_cluster.get_all_generatable_types()) else: return None if (reused_variable := self._reuse_variable(test_case, parameter_type, position)) is not None: return reused_variable
def get_random_object(self, parameter_type: Optional[Type], position: int) -> vr.VariableReference: """Get a random object of the given type up to the given position (exclusive). Args: parameter_type: the parameter type position: the position Returns: A random object of given type up to the given position Raises: ConstructionFailedException: if no object could be found """ variables = self.get_objects(parameter_type, position) if len(variables) == 0: raise ConstructionFailedException( f"Found no variables of type {parameter_type} at position {position}" ) return randomness.choice(variables)
def provide_random_type(self, respect_confidence: bool = True) -> SignatureType: """Provides a random type from the possible types. If the `respect_confidence` parameter is set, it will sample based on the confidence level, otherwise it will randomly choose from all types. Args: respect_confidence: Whether or not the confidence level shall be respected Returns: A random signature type """ assert len(self._elements) > 0 if not respect_confidence: return randomness.choice(tuple(self._elements)).signature_type # use the fact that zip behaves almost like its own inverse to unzip the set # of pairs to two sequences. Seems like magic but is actually a nice thing. signatures, confidences = tuple( zip(*[(element.signature_type, element.confidence) for element in self._elements])) return randomness.choices(signatures, weights=confidences)[0]
def _reuse_variable(self, test_case: tc.TestCase, parameter_type: Optional[Type], position: int) -> Optional[vr.VariableReference]: """Reuse an existing variable, if possible. Args: test_case: the test case to take the variable from parameter_type: the type of the variable that is needed position: the position to limit the search Returns: A matching existing variable, if existing """ objects = test_case.get_objects(parameter_type, position) probability = (config.INSTANCE.primitive_reuse_probability if is_primitive_type(parameter_type) else config.INSTANCE.object_reuse_probability) if objects and randomness.next_float() <= probability: var = randomness.choice(objects) self._logger.debug("Reusing variable %s for type %s", var, parameter_type) return var return None
def random_element(self, type_: Type[Types]) -> Types: return randomness.choice(tuple(self._dynamic_pool[type_]))
def _random_element(self, type_: str) -> Types: assert self._constants is not None return randomness.choice(tuple(self._constants[type_]))
def test_choice(): sequence = ["a", "b", "c"] result = randomness.choice(sequence) assert result in ("a", "b", "c")
def _mutate_parameter(self, param_name: str, inf_sig: InferredSignature) -> bool: """Replace the given parameter with another one that also fits the parameter type. Args: param_name: the name of the parameter that should be mutated. Returns: True, if the parameter was mutated. """ current = self._args.get(param_name, None) param_type = inf_sig.parameters[param_name] possible_replacements = self.test_case.get_objects( param_type, self.get_position()) # Param has to be optional, otherwise it would be set. if current is None: # Create value for currently unset parameter. if (randomness.next_float() > config.configuration.skip_optional_parameter_probability): if len(possible_replacements) > 0: self._args[param_name] = randomness.choice( possible_replacements) return True return False if (is_optional_parameter(inf_sig, param_name) and randomness.next_float() < config.configuration.skip_optional_parameter_probability): # unset parameters that are not necessary with a certain probability, # e.g., if they have default value or are *args, **kwargs. self._args.pop(param_name) if current in possible_replacements: possible_replacements.remove(current) # Consider duplicating an existing statement/variable. copy: Optional[stmt.Statement] = None if self._param_count_of_type( param_type) > len(possible_replacements) + 1: original_param_source = self.test_case.get_statement( current.get_statement_position()) copy = original_param_source.clone(self.test_case) copy.mutate() possible_replacements.append(copy.ret_val) # TODO(fk) Use param_type instead of to_mutate.variable_type, # to make the selection broader, but this requires access to # the test cluster, to select a concrete type. # Using None as parameter value is also a possibility. none_statement = prim.NoneStatement(self.test_case, current.variable_type) possible_replacements.append(none_statement.ret_val) replacement = randomness.choice(possible_replacements) if copy and replacement is copy.ret_val: # The chosen replacement is a copy, so we have to add it to the test case. self.test_case.add_statement(copy, self.get_position()) elif replacement is none_statement.ret_val: # The chosen replacement is a none statement, so we have to add it to the # test case. self.test_case.add_statement(none_statement, self.get_position()) self._args[param_name] = replacement return True