Skip to content

Commit

Permalink
✨ add shrink reproduce phase
Browse files Browse the repository at this point in the history
  • Loading branch information
MeditationDuck committed Oct 1, 2024
1 parent e80447d commit 7744226
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 39 deletions.
30 changes: 19 additions & 11 deletions wake/cli/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,16 @@ def shell_complete(
default=None,
)

@click.option(
"--shrinked",
type=click.Path(exists=True, dir_okay=False, readable=True), # Ensure it's an existing file
help="Path of shrinked file.",
is_flag=False,
flag_value=-1,
default=None,
)


@click.argument("paths_or_pytest_args", nargs=-1, type=FileAndPassParamType())
@click.pass_context
def run_test(
Expand All @@ -264,6 +274,7 @@ def run_test(
dist: str,
verbosity: int,
shrink: Optional[str],
shrinked: Optional[str],
paths_or_pytest_args: Tuple[str, ...],
) -> None:
"""Execute Wake tests using pytest."""
Expand Down Expand Up @@ -314,16 +325,7 @@ def run_test(
random_seeds.append(os.urandom(8))

if no_pytest:
run_no_pytest(
config,
debug,
proc_count,
coverage,
random_seeds,
random_states_byte,
attach_first,
paths_or_pytest_args,
)
pass
else:
pytest_args = list(paths_or_pytest_args)

Expand Down Expand Up @@ -370,7 +372,7 @@ def run_test(
)
else:
from wake.testing.pytest_plugin_single import PytestWakePluginSingle
from wake.development.globals import set_fuzz_mode,set_sequence_initial_internal_state, set_error_flow_num
from wake.development.globals import set_fuzz_mode,set_sequence_initial_internal_state, set_error_flow_num, set_shrinked_path

def extract_executed_flow_number(crash_log_file_path):
if crash_log_file_path is not None:
Expand Down Expand Up @@ -404,6 +406,9 @@ def extract_internal_state(crash_log_file_path):
pass # Handle the case where the value after ":" is not a valid hex string
return None

if shrinked is not None and shrink is not None:
raise click.BadParameter("Both shrink and shrieked cannot be provided at the same time.")

if shrink is not None:
number = extract_executed_flow_number(shrink)
assert number is not None, "Unexpected file format"
Expand All @@ -413,6 +418,9 @@ def extract_internal_state(crash_log_file_path):
assert beginning_random_state_bytes is not None, "Unexpected file format"
set_sequence_initial_internal_state(beginning_random_state_bytes)

if shrinked:
set_fuzz_mode(2)
set_shrinked_path(Path(shrinked))
sys.exit(
pytest.main(
pytest_args,
Expand Down
9 changes: 9 additions & 0 deletions wake/development/globals.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@
_error_flow_num: int = 0


_shrinked_path: Optional[Path] = None

def set_shrinked_path(path: Path):
global _shrinked_path
_shrinked_path = path

def get_shrinked_path() -> Optional[Path]:
return _shrinked_path


def attach_debugger(
e_type: Optional[Type[BaseException]],
Expand Down
177 changes: 151 additions & 26 deletions wake/testing/fuzzing/fuzz_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@

from typing_extensions import get_type_hints

from wake.development.globals import random, set_sequence_initial_internal_state, get_fuzz_mode, get_sequence_initial_internal_state, set_error_flow_num, get_error_flow_num
from wake.development.globals import random, set_sequence_initial_internal_state, get_fuzz_mode, get_sequence_initial_internal_state, set_error_flow_num, get_error_flow_num, get_config, get_shrinked_path

from ..core import get_connected_chains
from .generators import generate

import pickle
from dataclasses import dataclass

from wake.utils.file_utils import is_relative_to
from pathlib import Path
from datetime import datetime

import traceback

from wake.development.transactions import Error

def flow(
*,
weight: int = 100,
Expand All @@ -39,6 +47,23 @@ def decorator(fn):

return decorator

@dataclass
class FlowState:
random_state: bytes
flow_num: int
flow_name: str
flow: Callable # Store the function itself
flow_params: List[Any] # Store the list of arguments
required: bool = True
before_inv_random_state: bytes = b""



@dataclass
class ShrinkedInfoFile:
initial_state: bytes
required_flows: List[FlowState]


class FuzzTest:
_sequence_num: int
Expand Down Expand Up @@ -157,15 +182,7 @@ def run(
elif(fuzz_mode == 1):
print("fuzz test shrink start! First of all correct random and flow information!!! >_<")

@dataclass
class FlowState:
random_state: bytes
flow_num: int
flow_name: str
flow: Callable # Store the function itself
flow_params: List[Any] # Store the list of arguments
required: bool = True
before_inv_random_state: bytes = b""


error_flow_num = get_error_flow_num()
flow_state: List[FlowState] = []
Expand All @@ -185,6 +202,7 @@ class FlowState:
self.pre_sequence()

exception = False
exception_content = None
try:
for j in range(flows_count):
valid_flows = [
Expand Down Expand Up @@ -257,7 +275,13 @@ class FlowState:

for snapshot, chain in zip(snapshots, chains):
chain.revert(snapshot)
except Exception:
except Exception as e:
exception_content = e

## LOGGING EXCEPTION RESULT
# It could log only flow number,
# but ideally, it should log the lines of code in the test.

exception = True

for snapshot, chain in zip(snapshots, chains):
Expand Down Expand Up @@ -285,7 +309,9 @@ def __init__(self):
)
snapshots = [chain.snapshot() for chain in chains]

print("Shrinking flow: ", curr)

print("progress: ", (curr* 100) / (error_flow_num+1), "%")
random.setstate(pickle.loads(initial_state))

self._flow_num = 0
Expand All @@ -295,11 +321,9 @@ def __init__(self):
try:
for j in range(flows_count):

print(j, " ", error_flow_num)
if j > error_flow_num:
raise OverRunException()

print(j, "th flow")
curr_flow_state = flow_state[j]
random.setstate(pickle.loads(curr_flow_state.random_state))
flow = curr_flow_state.flow
Expand All @@ -312,13 +336,10 @@ def __init__(self):
flow(self, *flow_params)
flows_counter[flow] += 1
self.post_flow(flow)
print(flow.__name__, ": is executed")
else:
print("skip flow")


assert flow_state[j].before_inv_random_state is not None
random.setstate(pickle.loads(curr_flow_state.before_inv_random_state))
print("flow executed")
if not dry_run:
self.pre_invariants()
for inv in invariants:
Expand All @@ -331,18 +352,62 @@ def __init__(self):
if invariant_periods[inv] == getattr(inv, "period"):
invariant_periods[inv] = 0
self.post_invariants()
print(f"success {j} th")
self.post_sequence()
except OverRunException:
print("overrun")
exception = False # since it is not test exception
except Exception as e:
exception = True
print("exception in ", j)
# print("exception in ", j)
for snapshot, chain in zip(snapshots, chains):
chain.revert(snapshot)

if self._flow_num == error_flow_num:
def compare_exceptions(e1, e2):
print(type(e1))
print(type(e2))
if type(e1) != type(e2):
print("type not equal")
return False

if type(e1) == Error and type(e2) == Error:
# if error was transaction message error the compare message content as well
if e1.message != e2.message:
return False

tb1 = traceback.extract_tb(e1.__traceback__)
tb2 = traceback.extract_tb(e2.__traceback__)
frames_up = 0
frame1 = None
for frame1 in tb1:
if is_relative_to(
Path(frame1.filename), Path.cwd()
) and not is_relative_to(
Path(frame1.filename), Path().cwd() / "pytypes"
):
break
frame2 = None
for frame2 in tb1:
if is_relative_to(
Path(frame2.filename), Path.cwd()
) and not is_relative_to(
Path(frame2.filename), Path().cwd() / "pytypes"
):
break

print(frame1)
print(frame2)
if frame1 is None or frame2 is None:
print("frame is none!!!!!!!!!!!!!!")
if frame1 is not None and frame2 is not None and (frame1.filename != frame2.filename
or frame1.lineno != frame2.lineno
or frame1.name != frame2.name
):
return False
return True

# Check exception type and exception lines in the testing file.

if self._flow_num == error_flow_num and compare_exceptions(e, exception_content):

# the removed flow is not required to reproduce same error. @ try remove next flow
print("remove worked!!, ", curr)
assert flow_state[curr].required == False
Expand All @@ -360,13 +425,8 @@ def __init__(self):
print("probably overrun!")
flow_state[curr].required = True
# the removed flow is required to reproduce same error. @ this flow should not removed # restore current flow and remove next flow

print("True!!", flow_state[curr].required, curr)
curr += 1




print("Shrinking completed")
print("Error flow number: ", error_flow_num)
print("Shrinked flow count:", sum([1 for i in range(len(flow_state)) if flow_state[i].required == True]))
Expand All @@ -376,6 +436,71 @@ def __init__(self):
print(flow_state[i].flow_name, " : ", flow_state[i].flow_params)


crash_logs_dir = get_config().project_root_path / ".wake" / "logs" / "shrinked"
# shutil.rmtree(crash_logs_dir, ignore_errors=True)
crash_logs_dir.mkdir(parents=True, exist_ok=True)
# write crash log file.
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
# Assuming `call.execinfo` contains the crash information
crash_log_file = crash_logs_dir / F"{timestamp}.bin"


#initial_state

required_flows: List[FlowState] = []
for i in range(len(flow_state)):
if flow_state[i].required:
required_flows.append(flow_state[i])

store_data: ShrinkedInfoFile = ShrinkedInfoFile(
initial_state=initial_state,
required_flows=required_flows
)
# Write to a JSON file
with open(crash_log_file, 'wb') as f:
pickle.dump(store_data, f)

print(f"shrinked file written to {crash_log_file}")

elif fuzz_mode == 2:

shrinked_path = get_shrinked_path()
if shrinked_path is None:
raise Exception("Shrinked path not found")
with open(shrinked_path, 'rb') as f:
store_data: ShrinkedInfoFile = pickle.load(f)

self._flow_num = 0
self._sequence_num = 0
self.pre_sequence()
flows: List[Callable] = self.__get_methods("flow")

invariant_periods: DefaultDict[Callable[[None], None], int] = defaultdict(
int
)
for j in range(len(store_data.required_flows)):
flow = next((flow for flow in flows if store_data.required_flows[j].flow_name == flow.__name__), None)
if flow is None:
raise Exception("Flow not found")
flow_params = store_data.required_flows[j].flow_params

random.setstate(pickle.loads(store_data.required_flows[j].random_state))
self.pre_flow(flow)
flow(self, *flow_params)
self.post_flow(flow)
random.setstate(pickle.loads(store_data.required_flows[j].before_inv_random_state))
if not dry_run:
self.pre_invariants()
for inv in invariants:
if invariant_periods[inv] == 0:
self.pre_invariant(inv)
inv(self)
self.post_invariant(inv)

invariant_periods[inv] += 1
if invariant_periods[inv] == getattr(inv, "period"):
invariant_periods[inv] = 0
self.post_invariants()

else:
raise Exception("Invalid fuzz mode")
Expand Down
4 changes: 2 additions & 2 deletions wake/testing/pytest_plugin_multiprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ def pytest_exception_interact(self, node, call, report):

state = get_sequence_initial_internal_state()
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')

crash_log_file = self._crash_log_dir / F"crash_log_{timestamp}.txt"

with crash_log_file.open('w') as f:
Expand Down Expand Up @@ -229,7 +229,7 @@ def sigint_handler(signum, frame):
try:
indexes = self._conn.recv()
for i in range(len(indexes)):
# set random seed before each test item
# set random seed before each test item
if self._random_state is not None:
random.setstate(pickle.loads(self._random_state))
console.print(f"Using random state '{random.getstate()[1]}'")
Expand Down
2 changes: 2 additions & 0 deletions wake/testing/pytest_plugin_single.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ def pytest_exception_interact(self, node, call, report):
f.write(f"\nInternal state of beginning of sequence : {state.hex()}\n")
f.write(f"executed flow number : {get_error_flow_num()}\n")

console.print(f"Crash log written to {crash_log_file}")

def pytest_runtestloop(self, session: Session):
if (
session.testsfailed
Expand Down

0 comments on commit 7744226

Please sign in to comment.