diff --git a/src/testcrush/zoix.py b/src/testcrush/zoix.py index 4b40eb9..df7f3a4 100755 --- a/src/testcrush/zoix.py +++ b/src/testcrush/zoix.py @@ -7,7 +7,7 @@ import pathlib import csv -from testcrush.utils import get_logger +from testcrush.utils import get_logger, to_snake_case from typing import Any log = get_logger() @@ -140,10 +140,30 @@ class TxtFaultReport: Manages the VC-Z01X text report. """ + __slots__ = ["fault_report", "fault_list", "status_groups", "coverage"] + def __init__(self, fault_report: pathlib.Path) -> "TxtFaultReport": with open(fault_report) as src: self.fault_report: str = src.read() + # Lazy import to resolve avoid circular import. + from testcrush.grammars.transformers import FaultReportTransformerFactory + + factory = FaultReportTransformerFactory() + for section in ["StatusGroups", "Coverage", "FaultList"]: + + parser = factory(section) + + try: + raw_section = self.extract(section) + + except ValueError: # section doesn't exist + setattr(self, to_snake_case(section), None) + continue + + log.debug(f"Parsing {section}") + setattr(self, to_snake_case(section), parser.parse(raw_section)) + def extract(self, section: str) -> str: """ Extracts a section of the fault report. @@ -195,6 +215,58 @@ def extract(self, section: str) -> str: return '\n'.join(extracted_lines) + def compute_coverage(self, requested_formula: str = None, precision: int = 4) -> dict[str, float] | float: + """ + + """ + retval = list() + + fault_statusses = dict() + + for fault in self.fault_list: + + status = fault.fault_status + + if status in fault_statusses: + fault_statusses[status] += 1 + else: + fault_statusses[status] = 1 + + status_groups = dict() + if self.status_groups: + + for group, statuses in self.status_groups.items(): + + status_groups[group] = 0 + + for status in statuses: + + if status in fault_statusses: + + status_groups[group] += fault_statusses[status] + + # We expect that if a coverage formula is specified + # i.e., Coverage{} exists, then there may be some + # variables there which may not exist in statuses + # or groups. Hence we must set them to 0. + non_present = dict() + if self.coverage: + + for formula_name, formula in self.coverage.items(): + + for status_or_group in re.findall(r"[A-Z]{2}", formula): + + if status_or_group not in fault_statusses and status_or_group not in status_groups: + + non_present[status_or_group] = 0 + + retval.append((formula_name, + round(eval(formula, {**fault_statusses, **status_groups, **non_present}), + precision))) + + # Else: TODO: Implement default coverage computation according to manual + return dict(retval)[requested_formula] if requested_formula else dict(retval) + class CSVFaultReport: """ @@ -680,3 +752,9 @@ def fault_simulate(self, *instructions: str, **kwargs) -> FaultSimulation: break return fault_simulation_status + + +if __name__ == "__main__": + + a = TxtFaultReport(pathlib.Path("../../sandbox/fsim_attr")) + print(a.compute_coverage()) diff --git a/src/unit_tests/test_zoix.py b/src/unit_tests/test_zoix.py index 4485cdb..f674199 100644 --- a/src/unit_tests/test_zoix.py +++ b/src/unit_tests/test_zoix.py @@ -183,7 +183,32 @@ class TxtFaultReportTest(unittest.TestCase): # INSTR 4d818193 8 """ def create_object(self): + + real_open = open # Keep an alias to real open + with mock.patch("builtins.open", mock.mock_open(read_data=self._fault_report_excerp)) as mocked_open: + + # Careful! the constructor invokes lark parsing which means it opens >1 files + # However, what we want to patch is just the fault report excerpt and not the + # rest, e.g., the grammars. Hence, we need to hack our way around this issue + # and only patch the open when the fault report is read. Hence this elaborate + # side effect trick. Note that i am also capturing real_open above, since at + # this point, it is already patched. + + def open_side_effect(file, mode="r", *args, **kwargs): + + if isinstance(file, pathlib.Path): + file = str(file) + + if file == "mock_fault_report": + return mock.mock_open(read_data=self._fault_report_excerp)() + else: + # Use the real open function for grammar parsing + return real_open(file, mode, *args, **kwargs) + + # Set the side_effect to handle multiple file paths + mocked_open.side_effect = open_side_effect + test_obj = zoix.TxtFaultReport(pathlib.Path("mock_fault_report")) return test_obj @@ -192,6 +217,36 @@ def test_constructor(self): test_obj = self.create_object() self.assertEqual(test_obj.fault_report, self._fault_report_excerp) + expected = [zoix.Fault(fault_status='ON', fault_type='1', fault_sites=['tb_top.wrapper_i.top_i.core_i.ex_stage_i.mult_i.U10.A1'], fault_attributes={'INSTR': '3cb3079a', 'INSTR_ADDR': '000009bc', 'PC_ID': '000009b2', 'PC_IF': '000009b6', 'sim_time': '2815ns'}), + zoix.Fault(fault_status='ON', fault_type='1', fault_sites=['tb_top.wrapper_i.top_i.core_i.ex_stage_i.mult_i.U333.Z']), + zoix.Fault(fault_status='ON', fault_type='1', fault_sites=['tb_top.wrapper_i.top_i.core_i.ex_stage_i.mult_i.U10.A2'], fault_attributes={'INSTR': '3cb3079a', 'INSTR_ADDR': '000009fc', 'PC_ID': '000009f2', 'PC_IF': '000009f6', 'sim_time': '6425ns'}), + zoix.Fault(fault_status='ON', fault_type='0', fault_sites=['tb_top.wrapper_i.top_i.core_i.ex_stage_i.mult_i.U10.ZN'], fault_attributes={'INSTR': '3cb3079a', 'INSTR_ADDR': '000009bc', 'PC_ID': '000009b2', 'PC_IF': '000009b6', 'sim_time': '2815ns'}), + zoix.Fault(fault_status='ON', fault_type='1', fault_sites=['tb_top.wrapper_i.top_i.core_i.ex_stage_i.mult_i.U10.ZN'], fault_attributes={'INSTR': '3cb3079a', 'INSTR_ADDR': '000009fc', 'PC_ID': '000009f2', 'PC_IF': '000009f6', 'sim_time': '18745ns'}), + zoix.Fault(fault_status='ON', fault_type='0', fault_sites=['tb_top.wrapper_i.top_i.core_i.ex_stage_i.mult_i.U10.A1']), + zoix.Fault(fault_status='ON', fault_type='0', fault_sites=['tb_top.wrapper_i.top_i.core_i.ex_stage_i.mult_i.U10.A2']), + zoix.Fault(fault_status='ON', fault_type='0', fault_sites=['tb_top.wrapper_i.top_i.core_i.ex_stage_i.mult_i.U333.Z']), + zoix.Fault(fault_status='ON', fault_type='1', fault_sites=['tb_top.wrapper_i.top_i.core_i.ex_stage_i.mult_i.U100.A1'], fault_attributes={'INSTR': '3cb3079a', 'INSTR_ADDR': '000009bc', 'PC_ID': '000009b2', 'PC_IF': '000009b6', 'sim_time': '7455ns'}), + zoix.Fault(fault_status='ON', fault_type='1', fault_sites=['tb_top.wrapper_i.top_i.core_i.ex_stage_i.mult_i.U100.A2'], fault_attributes={'INSTR': '3cb3079a', 'INSTR_ADDR': '000009bc', 'PC_ID': '000009b2', 'PC_IF': '000009b6', 'sim_time': '2815ns'}), + zoix.Fault(fault_status='ON', fault_type='0', fault_sites=['tb_top.wrapper_i.top_i.core_i.ex_stage_i.mult_i.U681.A'])] + + expected[1].equivalent_to = expected[0] + expected[0].equivalent_faults = 2 + expected[5].equivalent_to = expected[4] + expected[6].equivalent_to = expected[4] + expected[7].equivalent_to = expected[4] + expected[4].equivalent_faults = 4 + expected[10].equivalent_to = expected[9] + expected[9].equivalent_faults = 2 + self.assertEqual(test_obj.fault_list, expected) + + self.assertEqual(test_obj.status_groups, {'SA': ['UT', 'UB', 'UR', 'UU'], + 'SU': ['NN', 'NC', 'NO', 'NT'], + 'DA': ['HA', 'HM', 'HT', 'OA', 'OZ', 'IA', 'IP', 'IF', 'IX'], + 'DN': ['PN', 'ON', 'PP', 'OP', 'NP', 'AN', 'AP'], + 'DD': ['PD', 'OD', 'ND', 'AD']}) + + self.assertEqual(test_obj.coverage, {"Diagnostic Coverage": "DD/(NA + DA + DN + DD)", + "Observational Coverage": "(DD + DN)/(NA + DA + DN + DD + SU)"}) def test_extract(self): @@ -311,6 +366,14 @@ def test_extract(self): -- 0 {PORT "tb_top.wrapper_i.top_i.core_i.ex_stage_i.mult_i.U681.A"} }""") + def test_compute_coverage(self): + test_obj = self.create_object() + + coverage = test_obj.compute_coverage() + + self.assertEqual(coverage, {'Diagnostic Coverage': 0.0, 'Observational Coverage': 1.0}) + + class CSVFaultReportTest(unittest.TestCase): @@ -409,7 +472,6 @@ def test_parse_fault_report(self): 3,"test1","yes","ON","1","","","","PORT","path_to_fault_3.portC"''')): report = test_obj.parse_fault_report() - #print(report) expected_report = [ zoix.Fault(**{"FID":"1", "Test Name":"test1", "Prime":"yes", "Status":"ON", "Model":"0", "Timing":"", "Cycle Injection":"", "Cycle End":"", "Class":"PORT", "Location":"path_to_fault_1.portA"}), zoix.Fault(**{"FID":"2", "Test Name":"test1", "Prime":"1", "Status":"ON", "Model":"0", "Timing":"", "Cycle Injection":"", "Cycle End":"", "Class":"PORT", "Location":"path_to_fault_2.portB"}), @@ -418,6 +480,7 @@ def test_parse_fault_report(self): self.assertEqual(report, expected_report) + class ZoixInvokerTest(unittest.TestCase): def test_execute(self):