fixed fuzzer
This commit is contained in:
parent
25409235cc
commit
4272d6c3d4
2 changed files with 62 additions and 18 deletions
58
fuzzer.py
58
fuzzer.py
|
@ -1,4 +1,4 @@
|
||||||
from random import randrange, choice
|
from random import randrange, choice, random
|
||||||
import os
|
import os
|
||||||
from frozendict import frozendict
|
from frozendict import frozendict
|
||||||
import tqdm
|
import tqdm
|
||||||
|
@ -9,7 +9,7 @@ Range = tuple[int, int]
|
||||||
|
|
||||||
INT_RANGE: Range = (-1000, 1000)
|
INT_RANGE: Range = (-1000, 1000)
|
||||||
STRING_LEN_RANGE: Range = (0, 10)
|
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
|
POOL_SIZE: int = 1000
|
||||||
|
|
||||||
OUT_DIR = os.path.join(os.path.dirname(__file__), "tests")
|
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])
|
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:
|
def random_str() -> str:
|
||||||
length = randrange(STRING_LEN_RANGE[0], STRING_LEN_RANGE[1])
|
length = randrange(STRING_LEN_RANGE[0], STRING_LEN_RANGE[1])
|
||||||
chr_from, chr_to = STRING_CHAR_RANGE
|
return "".join([random_chr() for _ in range(length)])
|
||||||
chars = [chr(randrange(chr_from, chr_to)) for _ in range(length)]
|
|
||||||
return "".join(chars)
|
|
||||||
|
|
||||||
|
|
||||||
def max_cases(args: list[Arg]) -> int:
|
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")
|
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:
|
def random_params(arguments: list[Arg]) -> Params:
|
||||||
test_input: dict[str, any] = {}
|
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
|
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]):
|
def consider_test_case(params: dict[str, any]):
|
||||||
t = frozendict(params)
|
t = frozendict(params)
|
||||||
|
|
||||||
|
@ -116,18 +136,26 @@ def get_test_cases(f_name: str, arguments: list[Arg], n: int) -> set[Params]:
|
||||||
|
|
||||||
if kind == 'mutation':
|
if kind == 'mutation':
|
||||||
arg_name = choice(list(chosen_test.keys())) # choose name to mutate
|
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)
|
consider_test_case(chosen_test)
|
||||||
elif kind == 'crossover':
|
elif kind == 'crossover':
|
||||||
|
# Select a property at random and swap properties
|
||||||
|
arg_name = choice(list(chosen_test.keys()))
|
||||||
|
|
||||||
# pick other distinct sample
|
# pick other distinct sample
|
||||||
other_chosen_test: dict[str, any] = chosen_test
|
other_chosen_test: dict[str, any] = chosen_test
|
||||||
while frozendict(chosen_test) == frozendict(other_chosen_test):
|
while frozendict(chosen_test) == frozendict(other_chosen_test):
|
||||||
other_chosen_test = dict(choice(pool_list))
|
other_chosen_test = dict(choice(pool_list))
|
||||||
|
|
||||||
# Select a property at random and swap properties
|
if types[arg_name] == 'str':
|
||||||
arg_name = choice(list(chosen_test.keys()))
|
# Crossover for strings intermingles the strings of the two chosen tests
|
||||||
chosen_test[arg_name], other_chosen_test[arg_name] = other_chosen_test[arg_name], chosen_test[arg_name]
|
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(chosen_test)
|
||||||
consider_test_case(other_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
|
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):
|
def get_test_case_source(f_name: str, test_case: Params, i: int, indent: int):
|
||||||
f_name_orig = BranchTransformer.to_original_name(f_name)
|
f_name_orig = BranchTransformer.to_original_name(f_name)
|
||||||
space = " " * (4 * indent)
|
space = " " * (4 * indent)
|
||||||
|
|
|
@ -38,10 +38,14 @@ archive_false_branches: dict[int, str] = {}
|
||||||
class BranchTransformer(ast.NodeTransformer):
|
class BranchTransformer(ast.NodeTransformer):
|
||||||
branch_num: int
|
branch_num: int
|
||||||
instrumented_name: Optional[str]
|
instrumented_name: Optional[str]
|
||||||
|
in_assert: bool
|
||||||
|
in_return: bool
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.branch_num = 0
|
self.branch_num = 0
|
||||||
self.instrumented_name = None
|
self.instrumented_name = None
|
||||||
|
self.in_assert = False
|
||||||
|
self.in_return = False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def to_instrumented_name(name: str):
|
def to_instrumented_name(name: str):
|
||||||
|
@ -53,13 +57,15 @@ class BranchTransformer(ast.NodeTransformer):
|
||||||
return name[:len(name) - len(SUFFIX)]
|
return name[:len(name) - len(SUFFIX)]
|
||||||
|
|
||||||
def visit_Assert(self, ast_node):
|
def visit_Assert(self, ast_node):
|
||||||
# Disable recursion in asserts, i.e. do not instrument assert conditions
|
self.in_assert = True
|
||||||
# TODO: may fail if assertion calls method (which must be renamed)
|
self.generic_visit(ast_node)
|
||||||
|
self.in_assert = False
|
||||||
return ast_node
|
return ast_node
|
||||||
|
|
||||||
def visit_Return(self, ast_node):
|
def visit_Return(self, ast_node):
|
||||||
# Same thing for return statements
|
self.in_return = True
|
||||||
# TODO: may fail if return statement calls method (which must be renamed)
|
self.generic_visit(ast_node)
|
||||||
|
self.in_return = False
|
||||||
return ast_node
|
return ast_node
|
||||||
|
|
||||||
def visit_FunctionDef(self, ast_node):
|
def visit_FunctionDef(self, ast_node):
|
||||||
|
@ -75,8 +81,8 @@ class BranchTransformer(ast.NodeTransformer):
|
||||||
return ast_node
|
return ast_node
|
||||||
|
|
||||||
def visit_Compare(self, ast_node):
|
def visit_Compare(self, ast_node):
|
||||||
if ast_node.ops[0] in [ast.Is, ast.IsNot, ast.In, ast.NotIn]:
|
if ast_node.ops[0] in [ast.Is, ast.IsNot, ast.In, ast.NotIn] or self.in_assert or self.in_return:
|
||||||
return ast_node
|
return self.generic_visit(ast_node)
|
||||||
|
|
||||||
self.branch_num += 1
|
self.branch_num += 1
|
||||||
return ast.Call(func=ast.Name("evaluate_condition", ast.Load()),
|
return ast.Call(func=ast.Name("evaluate_condition", ast.Load()),
|
||||||
|
@ -162,9 +168,9 @@ def get_fitness_cgi(individual):
|
||||||
|
|
||||||
|
|
||||||
def random_string():
|
def random_string():
|
||||||
l = random.randint(0, MAX_STRING_LENGTH)
|
length = random.randint(0, MAX_STRING_LENGTH)
|
||||||
s = ""
|
s = ""
|
||||||
for i in range(l):
|
for i in range(length):
|
||||||
random_character = chr(random.randrange(32, 127))
|
random_character = chr(random.randrange(32, 127))
|
||||||
s = s + random_character
|
s = s + random_character
|
||||||
return s
|
return s
|
||||||
|
|
Reference in a new issue