diff --git a/fuzzer.py b/fuzzer.py index 0acbf57..489a491 100644 --- a/fuzzer.py +++ b/fuzzer.py @@ -1,4 +1,4 @@ -from random import randrange, choice +from random import randrange, choice, random import os from frozendict import frozendict import tqdm @@ -9,7 +9,7 @@ Range = tuple[int, int] INT_RANGE: Range = (-1000, 1000) STRING_LEN_RANGE: Range = (0, 10) -STRING_CHAR_RANGE: Range = (ord('a'), ord('z') + 1) +STRING_CHAR_RANGE: Range = (32, 127) POOL_SIZE: int = 1000 OUT_DIR = os.path.join(os.path.dirname(__file__), "tests") @@ -19,11 +19,14 @@ def random_int() -> int: return randrange(INT_RANGE[0], INT_RANGE[1]) +def random_chr() -> str: + chr_from, chr_to = STRING_CHAR_RANGE + return chr(randrange(chr_from, chr_to)) + + def random_str() -> str: length = randrange(STRING_LEN_RANGE[0], STRING_LEN_RANGE[1]) - chr_from, chr_to = STRING_CHAR_RANGE - chars = [chr(randrange(chr_from, chr_to)) for _ in range(length)] - return "".join(chars) + return "".join([random_chr() for _ in range(length)]) def max_cases(args: list[Arg]) -> int: @@ -49,6 +52,23 @@ def random_arg(arg_type: str) -> any: raise ValueError(f"Arg type '{arg_type}' not supported") +def random_mutate(arg_type: str, arg_value: any) -> any: + if arg_type == 'str': + if len(arg_value) == 0: + return arg_value + + prob = 1.0 / len(arg_value) + for pos in range(len(arg_value)): + if random() < prob: + arg_value[pos] = random_chr() + + return arg_value + elif arg_type == 'int': + return random_int() # TODO: ask what to do here + else: + raise ValueError(f"Arg type '{arg_type}' not supported") + + def random_params(arguments: list[Arg]) -> Params: test_input: dict[str, any] = {} @@ -93,7 +113,7 @@ def get_test_cases(f_name: str, arguments: list[Arg], n: int) -> set[Params]: n = min(n, max_cases(arguments) // 3) # bound n by 1/3rd of the max possible number of tests - with tqdm.tqdm(total=n, desc=f"Tests for {BranchTransformer.to_original_name(f_name)}") as pbar: + with ((tqdm.tqdm(total=n, desc=f"Tests for {BranchTransformer.to_original_name(f_name)}") as pbar)): def consider_test_case(params: dict[str, any]): t = frozendict(params) @@ -116,18 +136,26 @@ def get_test_cases(f_name: str, arguments: list[Arg], n: int) -> set[Params]: if kind == 'mutation': arg_name = choice(list(chosen_test.keys())) # choose name to mutate - chosen_test[arg_name] = random_arg(types[arg_name]) # choose new value for this name + chosen_test[arg_name] = random_mutate(types[arg_name], chosen_test[arg_name]) consider_test_case(chosen_test) elif kind == 'crossover': + # Select a property at random and swap properties + arg_name = choice(list(chosen_test.keys())) + # pick other distinct sample other_chosen_test: dict[str, any] = chosen_test while frozendict(chosen_test) == frozendict(other_chosen_test): other_chosen_test = dict(choice(pool_list)) - # Select a property at random and swap properties - arg_name = choice(list(chosen_test.keys())) - chosen_test[arg_name], other_chosen_test[arg_name] = other_chosen_test[arg_name], chosen_test[arg_name] + if types[arg_name] == 'str': + # Crossover for strings intermingles the strings of the two chosen tests + chosen_test[arg_name], other_chosen_test[arg_name] = \ + str_crossover(chosen_test[arg_name], other_chosen_test[arg_name]) + else: # types[arg_name] == 'int' + # Crossover for integers swaps the values from the two tests + chosen_test[arg_name], other_chosen_test[arg_name] = \ + other_chosen_test[arg_name], chosen_test[arg_name] consider_test_case(chosen_test) consider_test_case(other_chosen_test) @@ -137,6 +165,16 @@ def get_test_cases(f_name: str, arguments: list[Arg], n: int) -> set[Params]: return tests +def str_crossover(parent1: str, parent2: str): + if len(parent1) > 1 and len(parent2) > 1: + pos = randrange(1, len(parent1) - 1) + offspring1 = parent1[:pos] + parent2[pos:] + offspring2 = parent2[:pos] + parent1[pos:] + return offspring1, offspring2 + + return parent1, parent2 + + def get_test_case_source(f_name: str, test_case: Params, i: int, indent: int): f_name_orig = BranchTransformer.to_original_name(f_name) space = " " * (4 * indent) diff --git a/instrument.py b/instrument.py index fb7d652..e4823dc 100644 --- a/instrument.py +++ b/instrument.py @@ -38,10 +38,14 @@ archive_false_branches: dict[int, str] = {} class BranchTransformer(ast.NodeTransformer): branch_num: int instrumented_name: Optional[str] + in_assert: bool + in_return: bool def __init__(self): self.branch_num = 0 self.instrumented_name = None + self.in_assert = False + self.in_return = False @staticmethod def to_instrumented_name(name: str): @@ -53,13 +57,15 @@ class BranchTransformer(ast.NodeTransformer): return name[:len(name) - len(SUFFIX)] def visit_Assert(self, ast_node): - # Disable recursion in asserts, i.e. do not instrument assert conditions - # TODO: may fail if assertion calls method (which must be renamed) + self.in_assert = True + self.generic_visit(ast_node) + self.in_assert = False return ast_node def visit_Return(self, ast_node): - # Same thing for return statements - # TODO: may fail if return statement calls method (which must be renamed) + self.in_return = True + self.generic_visit(ast_node) + self.in_return = False return ast_node def visit_FunctionDef(self, ast_node): @@ -75,8 +81,8 @@ class BranchTransformer(ast.NodeTransformer): return ast_node def visit_Compare(self, ast_node): - if ast_node.ops[0] in [ast.Is, ast.IsNot, ast.In, ast.NotIn]: - return ast_node + if ast_node.ops[0] in [ast.Is, ast.IsNot, ast.In, ast.NotIn] or self.in_assert or self.in_return: + return self.generic_visit(ast_node) self.branch_num += 1 return ast.Call(func=ast.Name("evaluate_condition", ast.Load()), @@ -162,9 +168,9 @@ def get_fitness_cgi(individual): def random_string(): - l = random.randint(0, MAX_STRING_LENGTH) + length = random.randint(0, MAX_STRING_LENGTH) s = "" - for i in range(l): + for i in range(length): random_character = chr(random.randrange(32, 127)) s = s + random_character return s