fix tester wrt. idempotency

This commit is contained in:
Joeri Exelmans 2024-12-02 13:38:43 +01:00
parent d92587ac42
commit f6af9d01ae
2 changed files with 118 additions and 62 deletions

View file

@ -1,42 +1,54 @@
import abc
from difflib import ndiff from difflib import ndiff
from lib.controller import Controller, pretty_time from lib.controller import Controller, pretty_time
from lib.tracer import Tracer from lib.tracer import Tracer
from lib.yakindu_helpers import YakinduTimerServiceAdapter, trace_output_events from lib.yakindu_helpers import YakinduTimerServiceAdapter, trace_output_events
# Can we ignore event in 'trace' at position 'idx' with respect to idempotency? class AbstractEnvironmentState:
def can_ignore(trace, idx, IDEMPOTENT): # should return the new state after handling the event
(timestamp, event_name, value) = trace[idx] @abc.abstractmethod
if event_name in IDEMPOTENT: def handle_event(self, event_name, param):
# If the same event occurred earlier, with the same parameter value, then this event can be ignored: pass
for (earlier_timestamp, earlier_event_name, earlier_value) in reversed(trace[0:idx]): # should compare states *by value*
if (earlier_event_name, earlier_value) == (event_name, value): @abc.abstractmethod
# same event name and same parameter value (timestamps allowed to differ) def __eq__(self, other):
return True pass
elif event_name == earlier_event_name:
# same event name, but different parameter value:
# stop looking into the past:
break
# If the same event occurs later event, but with the same timestamp, this event is overwritten and can be ignored:
for (later_timestamp, later_event_name, later_value) in trace[idx+1:]:
if (later_timestamp, later_event_name) == (timestamp, event_name):
# if a later event with same name and timestamp occurs, ours will be overwritten:
return True
if later_timestamp != timestamp:
# no need to look further into the future:
break
return False
def postprocess_trace(trace, INITIAL, IDEMPOTENT): # # Can we ignore event in 'trace' at position 'idx' with respect to idempotency?
# Prepend trace with events that set assumed initial state: # def can_ignore(trace, idx):
result = [(0, event_name, value) for (event_name, value) in INITIAL] + trace # (timestamp, event_name, value) = trace[idx]
# if event_name in IDEMPOTENT:
# # If the same event occurred earlier, with the same parameter value, then this event can be ignored:
# for (earlier_timestamp, earlier_event_name, earlier_value) in reversed(trace[0:idx]):
# if (earlier_event_name, earlier_value) == (event_name, value):
# # same event name and same parameter value (timestamps allowed to differ)
# return True
# elif event_name == earlier_event_name:
# # same event name, but different parameter value:
# # stop looking into the past:
# break
# # If the same event occurs later event, but with the same timestamp, this event is overwritten and can be ignored:
# for (later_timestamp, later_event_name, later_value) in trace[idx+1:]:
# if (later_timestamp, later_event_name) == (timestamp, event_name):
# # if a later event with same name and timestamp occurs, ours will be overwritten:
# return True
# if later_timestamp != timestamp:
# # no need to look further into the future:
# break
# return False
def postprocess_trace(trace, environment_class):
env_state = environment_class()
filtered_trace = []
# Remove events that have no effect: # Remove events that have no effect:
while True: for timestamp, event_name, param in trace:
filtered = [tup for (idx, tup) in enumerate(result) if not can_ignore(result, idx, IDEMPOTENT)] new_env_state = env_state.handle_event(event_name, param)
# Keep on filtering until no more events could be removed: if new_env_state != env_state:
if len(filtered) == len(result): # event had an effect
return filtered filtered_trace.append((timestamp, event_name, param))
result = filtered env_state = new_env_state
return filtered_trace
def compare_traces(expected, actual): def compare_traces(expected, actual):
i = 0 i = 0
@ -56,7 +68,7 @@ def compare_traces(expected, actual):
print("Traces match.") print("Traces match.")
return True return True
def run_scenario(input_trace, expected_output_trace, statechart_class, INITIAL, IDEMPOTENT, verbose=False): def run_scenario(input_trace, expected_output_trace, statechart_class, environment_class, verbose=False):
controller = Controller() controller = Controller()
sc = statechart_class() sc = statechart_class()
tracer = Tracer(verbose=False) tracer = Tracer(verbose=False)
@ -81,11 +93,8 @@ def run_scenario(input_trace, expected_output_trace, statechart_class, INITIAL,
actual_output_trace = tracer.output_events actual_output_trace = tracer.output_events
clean_expected = postprocess_trace(expected_output_trace, INITIAL, IDEMPOTENT) clean_expected = postprocess_trace(expected_output_trace, environment_class)
clean_actual = postprocess_trace(actual_output_trace, INITIAL, IDEMPOTENT) clean_actual = postprocess_trace(actual_output_trace, environment_class)
# clean_expected = expected_output_trace
# clean_actual = actual_output_trace
def print_diff(): def print_diff():
# The diff printed will be a diff of the 'raw' traces, not of the cleaned up traces # The diff printed will be a diff of the 'raw' traces, not of the cleaned up traces
@ -128,6 +137,7 @@ def run_scenario(input_trace, expected_output_trace, statechart_class, INITIAL,
print("\n\"Useless events\" are ignored by the comparison algorithm, and will never cause your test to fail. In this assignment, your solution is allowed to contain useless events.") print("\n\"Useless events\" are ignored by the comparison algorithm, and will never cause your test to fail. In this assignment, your solution is allowed to contain useless events.")
if not compare_traces(clean_expected, clean_actual): if not compare_traces(clean_expected, clean_actual):
# even though we compared the 'normalized' traces, we print the *raw* traces, not to confuse the user!
print("Raw diff between expected and actual output event trace:") print("Raw diff between expected and actual output event trace:")
print_diff() print_diff()
return False return False
@ -135,11 +145,11 @@ def run_scenario(input_trace, expected_output_trace, statechart_class, INITIAL,
print_diff() print_diff()
return True return True
def run_scenarios(scenarios, statechart_class, initial, idempotent, verbose=True): def run_scenarios(scenarios, statechart_class, environment_class, verbose=True):
ok = True ok = True
for scenario in scenarios: for scenario in scenarios:
print(f"Running scenario: {scenario["name"]}") print(f"Running scenario: {scenario["name"]}")
ok = run_scenario(scenario["input_events"], scenario["output_events"], statechart_class, initial, idempotent, verbose=verbose) and ok ok = run_scenario(scenario["input_events"], scenario["output_events"], statechart_class, environment_class, verbose=verbose) and ok
print("--------") print("--------")
if ok: if ok:
print("All scenarios passed.") print("All scenarios passed.")

View file

@ -1,5 +1,6 @@
import functools import functools
from lib.test import run_scenarios import dataclasses
from lib.test import run_scenarios, AbstractEnvironmentState
from srcgen.lock_controller import LockController from srcgen.lock_controller import LockController
# from srcgen.solution import Solution as LockController # Teacher's solution # from srcgen.solution import Solution as LockController # Teacher's solution
@ -206,27 +207,72 @@ SCENARIOS = [
} }
] ]
# The following output events are safe to repeat: (with same value) LOW = 0
# This will be taken into account while comparing traces. HIGH = 1
# Do not change this:
IDEMPOTENT = [ # Simulated state of the 'plant'.
"open_doors", # This is used for checking whether an event has any effect wrt. idempotency
"close_doors", @dataclasses.dataclass
"red_light", class PlantState(AbstractEnvironmentState):
"green_light", # initial state of the plant
"set_request_pending", door_low_open: bool = False
"open_flow", door_high_open: bool = False
"close_flow", flow_low_open: bool = False
] flow_high_open: bool = False
# We pretend that initially, these events occur: light_low: str = "RED"
# Do not change this: light_high: str = "RED"
INITIAL = [ request_is_pending: bool = False
("open_doors", 0), sensor_is_broken: bool = False
("close_doors", 1),
("green_light", 0), def handle_event(self, event_name, param):
("red_light", 1), if event_name == "open_doors":
("set_request_pending", False) if param == LOW:
] return dataclasses.replace(self, door_low_open=True)
elif param == HIGH:
return dataclasses.replace(self, door_high_open=True)
else:
raise Exception(f"invalid param for event '{event_name}': {param}")
elif event_name == "close_doors":
if param == LOW:
return dataclasses.replace(self, door_low_open=False)
elif param == HIGH:
return dataclasses.replace(self, door_high_open=False)
else:
raise Exception(f"invalid param for event '{event_name}': {param}")
elif event_name == "open_flow":
if param == LOW:
return dataclasses.replace(self, flow_low_open=True)
elif param == HIGH:
return dataclasses.replace(self, flow_high_open=True)
else:
raise Exception(f"invalid param for event '{event_name}': {param}")
elif event_name == "close_flow":
if param == LOW:
return dataclasses.replace(self, flow_low_open=False)
elif param == HIGH:
return dataclasses.replace(self, flow_high_open=False)
else:
raise Exception(f"invalid param for event '{event_name}': {param}")
elif event_name == "green_light":
if param == LOW:
return dataclasses.replace(self, light_low="GREEN")
elif param == HIGH:
return dataclasses.replace(self, light_high="GREEN")
else:
raise Exception(f"invalid param for event '{event_name}': {param}")
elif event_name == "red_light":
if param == LOW:
return dataclasses.replace(self, light_low="RED")
elif param == HIGH:
return dataclasses.replace(self, light_high="RED")
else:
raise Exception(f"invalid param for event '{event_name}': {param}")
elif event_name == "set_request_pending":
return dataclasses.replace(self, request_is_pending=param)
elif event_name == "set_sensor_broken":
return dataclasses.replace(self, sensor_is_broken=param)
else:
raise Exception("don't know how to handle event:", event_name)
if __name__ == "__main__": if __name__ == "__main__":
run_scenarios(SCENARIOS, LockController, INITIAL, IDEMPOTENT, verbose=False) run_scenarios(SCENARIOS, LockController, PlantState, verbose=False)