diff --git a/src/api/app.py b/src/api/app.py index 9b76608..25b35fe 100644 --- a/src/api/app.py +++ b/src/api/app.py @@ -13,6 +13,7 @@ MCDC_MODES, excel_export_rows, generate_mcdc_report, + parse_support_template, safe_excel_filename, testcase_table_rows_from_dict, testcase_table_rows_to_csv, @@ -40,14 +41,15 @@ def index() -> str: async def generate_cases( source: UploadFile = File(...), headers: list[UploadFile] | None = File(default=None), + support_template: UploadFile | None = File(default=None), target_function: str = Form(default=""), input_variables: str = Form(default=""), output_variables: str = Form(default=""), compile_flags: str = Form(default=""), - excel_format_version: str = Form(default="1.3"), + excel_format_version: str = Form(default=""), excel_architecture: str = Form(default=""), excel_scope: str = Form(default=""), - excel_name: str = Form(default="mcdc_testcases"), + excel_name: str = Form(default=""), max_conditions: int = Form(default=12), mcdc_mode: str = Form(default="unique-cause"), ) -> dict[str, object]: @@ -73,6 +75,18 @@ async def generate_cases( header_path.write_bytes(await header.read()) header_paths.append(header_path) + interface_override = None + template_metadata: ExcelExportMetadata | None = None + if support_template is not None and support_template.filename: + if not support_template.filename.lower().endswith(".xlsx"): + raise HTTPException(status_code=400, detail="Support template must be a .xlsx file.") + template_path = workspace / safe_name(support_template.filename) + template_path.write_bytes(await support_template.read()) + try: + interface_override, template_metadata = parse_support_template(template_path) + except (ValueError, KeyError) as error: + raise HTTPException(status_code=400, detail=f"Could not parse support template: {error}") + output_dir = workspace / "out" parsed_input_variables, manual_inputs = parse_variable_setup(input_variables) parsed_output_variables, manual_outputs = parse_variable_setup(output_variables) @@ -88,12 +102,16 @@ async def generate_cases( output_variables=parsed_output_variables, manual_outputs=manual_outputs, mcdc_mode=mcdc_mode, + interface_override=interface_override, ) + # When a support template carries metadata, use it as the default for any blank + # Excel export field so the generated sheet preserves the template's identity. + template_metadata = template_metadata or ExcelExportMetadata() excel_metadata = ExcelExportMetadata( - format_version=excel_format_version.strip() or "1.3", - architecture=excel_architecture.strip(), - scope=excel_scope.strip(), - name=excel_name.strip() or "mcdc_testcases", + format_version=excel_format_version.strip() or template_metadata.format_version or "1.3", + architecture=excel_architecture.strip() or template_metadata.architecture, + scope=excel_scope.strip() or template_metadata.scope, + name=excel_name.strip() or template_metadata.name or "mcdc_testcases", ) json_path, harness_path, gap_report_path, excel_path = write_report_artifacts( report, @@ -580,6 +598,8 @@ def render_index_html() -> str: + + diff --git a/src/cli.py b/src/cli.py index a929326..5545d99 100644 --- a/src/cli.py +++ b/src/cli.py @@ -3,7 +3,13 @@ import argparse from pathlib import Path -from src.services.mcdc_generator import MCDC_MODES, generate_mcdc_report, write_report_artifacts +from src.services.mcdc_generator import ( + MCDC_MODES, + ExcelExportMetadata, + generate_mcdc_report, + parse_support_template, + write_report_artifacts, +) ManualValue = int | float | bool | str @@ -28,6 +34,11 @@ def build_parser() -> argparse.ArgumentParser: default=[], help="Optional .h file recorded for future harness/build integration.", ) + parser.add_argument( + "--support-template", + type=Path, + help="Optional support Excel (.xlsx) template defining the Input/Parameter/Output columns to generate.", + ) parser.add_argument( "--target-function", help="Optional function name to record in generated reports.", @@ -88,6 +99,18 @@ def main(argv: list[str] | None = None) -> int: if not include_dir.exists() or not include_dir.is_dir(): raise SystemExit(f"Include directory not found: {include_dir}") + interface_override = None + excel_metadata: ExcelExportMetadata | None = None + if args.support_template is not None: + if not args.support_template.exists(): + raise SystemExit(f"Support template not found: {args.support_template}") + if args.support_template.suffix.lower() != ".xlsx": + raise SystemExit("Support template must be a .xlsx file.") + try: + interface_override, excel_metadata = parse_support_template(args.support_template) + except (ValueError, KeyError) as error: + raise SystemExit(f"Could not parse support template: {error}") + input_variables, manual_inputs = parse_variable_setup(args.input_variable) output_variables, manual_outputs = parse_variable_setup(args.output_variable) report = generate_mcdc_report( @@ -102,8 +125,11 @@ def main(argv: list[str] | None = None) -> int: output_variables=output_variables, manual_outputs=manual_outputs, mcdc_mode=args.mcdc_mode, + interface_override=interface_override, + ) + json_path, harness_path, gap_report_path, excel_path = write_report_artifacts( + report, args.output_dir, excel_metadata=excel_metadata ) - json_path, harness_path, gap_report_path, excel_path = write_report_artifacts(report, args.output_dir) print(f"Generated MC/DC target score ({report.mcdc_mode}): {report.score:.1%}") print(f"Cases: {json_path}") diff --git a/src/services/mcdc_generator.py b/src/services/mcdc_generator.py index ceacb38..8c2ff58 100644 --- a/src/services/mcdc_generator.py +++ b/src/services/mcdc_generator.py @@ -11,10 +11,11 @@ import shutil import subprocess from tempfile import TemporaryDirectory +from xml.etree import ElementTree from xml.sax.saxutils import escape, quoteattr from zipfile import ZIP_DEFLATED, ZipFile from pathlib import Path -from typing import Any +from typing import Any, Callable BOOLEAN_OPERATORS = {"&&", "||"} @@ -95,6 +96,7 @@ class InterfaceAnalysis: array_sizes: dict[str, int] initial_values: dict[str, Any] assignment_sources: dict[str, tuple[str, ...]] + assignment_expressions: dict[str, str] assignment_targets: tuple[str, ...] condition_input_roots: tuple[str, ...] condition_output_roots: tuple[str, ...] @@ -286,6 +288,7 @@ class MCDCReport: coverage_status: str = "not checked" warnings: tuple[str, ...] = field(default_factory=tuple) interface_graph: dict[str, Any] = field(default_factory=dict) + interface_override: "InterfaceOverride | None" = None @property def score(self) -> float: @@ -358,6 +361,184 @@ class ExcelExportMetadata: name: str = "mcdc_testcases" +@dataclass(frozen=True) +class InterfaceOverride: + """A test interface (column layout) supplied by a support Excel template. + + The names define exactly which Input / Parameter / Output columns the generated + sheet contains and in what order; ``array_sizes`` records how many element columns a + repeated header expanded to (defaults to 1 for scalars).""" + + input_names: tuple[str, ...] = field(default_factory=tuple) + parameter_names: tuple[str, ...] = field(default_factory=tuple) + output_names: tuple[str, ...] = field(default_factory=tuple) + array_sizes: dict[str, int] = field(default_factory=dict) + + +_XLSX_NS = "{http://schemas.openxmlformats.org/spreadsheetml/2006/main}" + + +def column_index_from_reference(reference: str) -> int: + """Convert an A1-style cell reference to a 0-based column index (``A`` -> 0).""" + letters = "".join(character for character in reference if character.isalpha()) + index = 0 + for character in letters: + index = index * 26 + (ord(character.upper()) - ord("A") + 1) + return index - 1 + + +def read_xlsx_grid(path: Path) -> list[list[Any]]: + """Read the first worksheet of an .xlsx file into a dense 2-D grid of cell values. + + Mirrors the project's hand-rolled xlsx writer (zip of XML, no openpyxl dependency). + Resolves shared strings, inline strings, booleans, and numbers; missing cells become + ``None``.""" + + with ZipFile(path) as archive: + shared_strings = _read_shared_strings(archive) + worksheets = sorted( + name for name in archive.namelist() if re.fullmatch(r"xl/worksheets/sheet\d+\.xml", name) + ) + if not worksheets: + raise ValueError(f"No worksheet found in {path}.") + sheet_xml = archive.read(worksheets[0]) + + root = ElementTree.fromstring(sheet_xml) + rows_by_number: dict[int, list[Any]] = {} + for row_element in root.iter(f"{_XLSX_NS}row"): + row_number = int(row_element.get("r", "0") or 0) + cells: dict[int, Any] = {} + for cell in row_element.iter(f"{_XLSX_NS}c"): + reference = cell.get("r") + if not reference: + continue + cells[column_index_from_reference(reference)] = _read_cell_value(cell, shared_strings) + width = max(cells) + 1 if cells else 0 + rows_by_number[row_number] = [cells.get(index) for index in range(width)] + + if not rows_by_number: + return [] + max_row = max(rows_by_number) + return [rows_by_number.get(number, []) for number in range(1, max_row + 1)] + + +def _read_shared_strings(archive: ZipFile) -> list[str]: + try: + data = archive.read("xl/sharedStrings.xml") + except KeyError: + return [] + root = ElementTree.fromstring(data) + return [ + "".join(node.text or "" for node in si.iter(f"{_XLSX_NS}t")) + for si in root.iter(f"{_XLSX_NS}si") + ] + + +def _read_cell_value(cell: ElementTree.Element, shared_strings: list[str]) -> Any: + cell_type = cell.get("t") + if cell_type == "inlineStr": + inline = cell.find(f"{_XLSX_NS}is") + if inline is None: + return None + return "".join(node.text or "" for node in inline.iter(f"{_XLSX_NS}t")) + value_node = cell.find(f"{_XLSX_NS}v") + if value_node is None or value_node.text is None: + return None + text = value_node.text + if cell_type == "s": + index = int(text) + return shared_strings[index] if 0 <= index < len(shared_strings) else None + if cell_type == "b": + return text.strip() not in ("0", "") + if cell_type in ("str", "e"): + return text + return _coerce_numeric(text) + + +def _coerce_numeric(text: str) -> Any: + cleaned = text.strip() + try: + if re.fullmatch(r"[+-]?\d+", cleaned): + return int(cleaned) + return float(cleaned) + except ValueError: + return text + + +def parse_support_template(path: Path) -> tuple[InterfaceOverride, ExcelExportMetadata]: + """Parse a support Excel template into an interface override and export metadata. + + The template uses the ATG "Format Version 1.3" layout: rows 1-4 carry metadata + (label in column A, value in column B); a ``Mode`` row carries the group label at the + start of each group; the following ``Step`` row carries the column variable names. + Data rows below are ignored. Consecutive repeated headers collapse to an array size.""" + + grid = read_xlsx_grid(Path(path)) + + def label_of(row: list[Any]) -> Any: + value = row[0] if row else None + return value.strip() if isinstance(value, str) else value + + metadata_values: dict[str, Any] = {} + mode_row_index: int | None = None + header_row_index: int | None = None + for index, row in enumerate(grid): + label = label_of(row) + if label in ("Format Version", "Architecture", "Scope", "Name"): + metadata_values[label] = row[1] if len(row) > 1 else None + elif label == "Mode": + mode_row_index = index + elif label == "Step": + header_row_index = index + + if mode_row_index is None or header_row_index is None: + raise ValueError("Support template is missing the 'Mode' and 'Step' header rows.") + + mode_row = grid[mode_row_index] + header_row = grid[header_row_index] + groups: dict[str, list[str]] = {"Inputs": [], "Parameters": [], "Outputs": []} + array_sizes: dict[str, int] = {} + current_group: str | None = None + width = max(len(mode_row), len(header_row)) + for column in range(1, width): # column 0 holds "Mode"/"Step" + mode_value = mode_row[column] if column < len(mode_row) else None + if isinstance(mode_value, str) and mode_value.strip(): + current_group = mode_value.strip() + header_value = header_row[column] if column < len(header_row) else None + if not isinstance(header_value, str) or not header_value.strip(): + continue + name = header_value.strip() + bucket = groups.get(current_group or "") + if bucket is None: # Comment or any other non-interface group + continue + if bucket and bucket[-1] == name: # consecutive repeat => array element + array_sizes[name] = array_sizes.get(name, 1) + 1 + else: + bucket.append(name) + + override = InterfaceOverride( + input_names=tuple(groups["Inputs"]), + parameter_names=tuple(groups["Parameters"]), + output_names=tuple(groups["Outputs"]), + array_sizes=array_sizes, + ) + metadata = ExcelExportMetadata( + format_version=_metadata_text(metadata_values.get("Format Version"), "1.3"), + architecture=_metadata_text(metadata_values.get("Architecture"), ""), + scope=_metadata_text(metadata_values.get("Scope"), ""), + name=_metadata_text(metadata_values.get("Name"), "mcdc_testcases"), + ) + return override, metadata + + +def _metadata_text(value: Any, default: str) -> str: + if value is None or value == "": + return default + if isinstance(value, float) and value.is_integer(): + return str(int(value)) + return str(value) + + def generate_mcdc_report( source_path: Path, max_conditions: int = MAX_ENUMERATED_CONDITIONS, @@ -370,11 +551,15 @@ def generate_mcdc_report( output_variables: tuple[str, ...] = (), manual_outputs: dict[str, TableValue] | None = None, mcdc_mode: str = "unique-cause", + interface_override: InterfaceOverride | None = None, ) -> MCDCReport: if mcdc_mode not in MCDC_MODES: raise ValueError(f"Unsupported MC/DC mode: {mcdc_mode}") - source = source_path.read_text(encoding="utf-8") + try: + source = source_path.read_text(encoding="utf-8") + except UnicodeDecodeError: + source = source_path.read_text(encoding="latin-1") clean_source = strip_comments_and_strings(source) decisions = extract_decisions(clean_source) detected_input_variables = extract_function_parameters(clean_source, target_function) @@ -386,11 +571,12 @@ def generate_mcdc_report( toolchain_details = detect_toolchain_details() coverage_ready, coverage_status = summarize_coverage_readiness(toolchain_details) + solver_ctx = build_solver_context(interface_analysis) return MCDCReport( source=str(source_path), source_text=source, - decisions=tuple(generate_decision_result(decision, max_conditions, mcdc_mode) for decision in decisions), + decisions=tuple(generate_decision_result(decision, max_conditions, mcdc_mode, solver_ctx) for decision in decisions), input_variables=tuple(dict.fromkeys((*detected_input_variables, *input_variables))), manual_inputs=manual_inputs or {}, output_variables=tuple(dict.fromkeys(output_variables)), @@ -406,6 +592,7 @@ def generate_mcdc_report( coverage_status=coverage_status, warnings=tuple(warnings), interface_graph=interface_analysis.variable_graph.to_dict(), + interface_override=interface_override, ) @@ -760,12 +947,21 @@ def testcase_table(report: MCDCReport) -> dict[str, Any]: def targetlink_logged_interface_table(report: MCDCReport) -> dict[str, Any] | None: - interface = extract_log_var_interface(report.source_text) - if interface is None: - return None - - input_names, parameter_names, output_names, array_sizes, initial_values = interface - output_names = targetlink_output_order(output_names) + if report.interface_override is not None: + override = report.interface_override + # A user-supplied support template defines the exact column layout and order; + # parameter/array defaults still come from the C source's initial values. + initial_values = dict(analyze_c_interface(report.source_text).initial_values) + input_names = list(override.input_names) + parameter_names = list(override.parameter_names) + output_names = list(override.output_names) + array_sizes = dict(override.array_sizes) + else: + interface = extract_log_var_interface(report.source_text) + if interface is None: + return None + input_names, parameter_names, output_names, array_sizes, initial_values = interface + output_names = targetlink_output_order(output_names) input_columns, input_keys = expand_interface_columns(input_names, array_sizes) parameter_columns, parameter_keys = expand_interface_columns(parameter_names, array_sizes) output_columns, output_keys = expand_interface_columns(output_names, array_sizes) @@ -979,7 +1175,7 @@ def analyze_c_interface(source: str) -> InterfaceAnalysis: initial_values = extract_top_level_initial_values(source, global_order) global_names = set(global_order) local_names = frozenset(extract_local_variables(function_source, global_names)) - assignment_sources, assignment_targets = extract_assignment_dependencies(function_source, global_names, set(function_parameters)) + assignment_sources, assignment_targets, assignment_expressions = extract_assignment_dependencies(function_source, global_names, set(function_parameters)) variable_graph = build_variable_graph( clean_source, function_source, @@ -1035,6 +1231,7 @@ def analyze_c_interface(source: str) -> InterfaceAnalysis: array_sizes=dict(array_sizes), initial_values=initial_values, assignment_sources=assignment_sources, + assignment_expressions=assignment_expressions, assignment_targets=tuple(ordered_assignment_targets), condition_input_roots=tuple(order_known_roots(condition_input_roots, global_order, function_parameters)), condition_output_roots=tuple(order_known_roots(condition_output_roots, global_order, function_parameters)), @@ -1132,9 +1329,11 @@ def extract_assignment_dependencies( source: str, global_names: set[str], parameter_names: set[str], -) -> tuple[dict[str, tuple[str, ...]], set[str]]: +) -> tuple[dict[str, tuple[str, ...]], set[str], dict[str, str]]: assignment_sources: dict[str, tuple[str, ...]] = {} assignment_targets: set[str] = set() + assignment_expressions: dict[str, str] = {} + multiply_assigned: set[str] = set() assignment_pattern = re.compile( r"(?])(?P[A-Za-z_]\w*(?:\s*(?:->|\.)\s*[A-Za-z_]\w*)?(?:\s*\[[^\]]+\])?|\*\s*[A-Za-z_]\w*|\(\s*\*\s*[A-Za-z_]\w*\s*\))\s*=(?!=)\s*(?P[^;]+);" ) @@ -1146,7 +1345,16 @@ def extract_assignment_dependencies( roots = expression_root_variables(rhs, global_names, parameter_names, assignment_sources) if roots: assignment_sources[lhs] = tuple(dict.fromkeys(roots)) - return assignment_sources, assignment_targets + # Record the RHS expression so the input solver can expand intermediates. + # Only single-assignment names are expandable; a name written more than once + # is ambiguous (depends on control flow), so it is dropped and treated as a + # directly settable variable instead. + if lhs in assignment_expressions or lhs in multiply_assigned: + multiply_assigned.add(lhs) + assignment_expressions.pop(lhs, None) + else: + assignment_expressions[lhs] = rhs.strip() + return assignment_sources, assignment_targets, assignment_expressions def build_variable_graph( @@ -1512,7 +1720,9 @@ def targetlink_generic_interface_inputs(report: MCDCReport, input_names: list[st if not input_names: return None analysis = analyze_c_interface(report.source_text) - root_names = set(analysis.global_order) | set(analysis.function_parameters) + settable, expr_map, assignment_sources = build_solver_context(analysis, set(input_names)) + graph_root_names = set(analysis.global_order) | set(analysis.function_parameters) + trace_chain = lambda name: analysis.variable_graph.trace_chain(name, graph_root_names) scenarios: list[dict[str, Any]] = [] seen: set[tuple[str, tuple[tuple[str, TableValue], ...], bool]] = set() @@ -1520,34 +1730,25 @@ def targetlink_generic_interface_inputs(report: MCDCReport, input_names: list[st for row in decision.cases: inputs = {name: report.manual_inputs.get(name, "MANUAL") for name in input_names} notes = list(row.notes) - for condition_index, (condition, desired) in enumerate(zip(decision.decision.conditions, row.values)): - candidate = value_for_condition(condition, desired) - if candidate is None: - continue - candidate_name, value = candidate - trace_key = f"{decision.decision.id}:{condition_index}" - trace = analysis.variable_graph.condition_traces.get(trace_key) - roots = tuple(trace.get("roots", ())) if trace else () - if not roots: - roots = resolve_root_variables( - candidate_name, - set(analysis.global_order), - set(analysis.function_parameters), - analysis.assignment_sources, - ) - assignable_roots = [root for root in roots if root in inputs] - if len(assignable_roots) != 1: - if len(assignable_roots) > 1: - notes.append( - f"MANUAL: ambiguous roots [{', '.join(assignable_roots)}] for `{candidate_name}`." - ) - continue - root = assignable_roots[0] - if root in inputs: - inputs[root] = value - if root != candidate_name: - chain = trace.get("chain", []) if trace else analysis.variable_graph.trace_chain(candidate_name, root_names) - notes.append(f"`{condition}` traced {' -> '.join(chain)}.") + for condition, desired in zip(decision.decision.conditions, row.values): + # Expand the condition (which usually tests an intermediate variable) + # back to the external inputs that drive it, assigning the concrete + # literal values needed to satisfy the desired truth value. + solved, solve_notes = solve_condition_inputs( + condition, + desired, + settable=settable, + expr_map=expr_map, + assignment_sources=assignment_sources, + trace_chain=trace_chain, + prior=inputs, + ) + for name, value in solved.items(): + if name in inputs: + inputs[name] = value + for note in solve_notes: + if note not in notes: + notes.append(note) key = ( decision.decision.id, tuple((name, inputs[name]) for name in input_names), @@ -2168,7 +2369,12 @@ def split_parameter_list(raw_parameters: str) -> list[str]: return parameters -def generate_decision_result(decision: Decision, max_conditions: int, mcdc_mode: str = "unique-cause") -> DecisionResult: +def generate_decision_result( + decision: Decision, + max_conditions: int, + mcdc_mode: str = "unique-cause", + solver_ctx: SolverContext | None = None, +) -> DecisionResult: warnings: list[str] = [] condition_count = len(decision.conditions) @@ -2177,10 +2383,10 @@ def generate_decision_result(decision: Decision, max_conditions: int, mcdc_mode: if condition_count > max_conditions: if mcdc_mode in {"unique-cause", "masking"}: - direct_result = generate_direct_chain_result(decision) + direct_result = generate_direct_chain_result(decision, solver_ctx) if direct_result is not None: return direct_result - grouped_result = generate_direct_grouped_and_result(decision) + grouped_result = generate_direct_grouped_and_result(decision, solver_ctx) if grouped_result is not None: return grouped_result message = f"Decision has {condition_count} conditions; capped at {max_conditions} to avoid path explosion." @@ -2204,7 +2410,7 @@ def generate_decision_result(decision: Decision, max_conditions: int, mcdc_mode: for values in truth_rows } if mcdc_mode == "multicondition": - return generate_multicondition_result(decision, truth_rows, outcomes) + return generate_multicondition_result(decision, truth_rows, outcomes, solver_ctx) independence_pairs = find_independence_pairs(decision, truth_rows, outcomes, mcdc_mode) @@ -2221,7 +2427,7 @@ def generate_decision_result(decision: Decision, max_conditions: int, mcdc_mode: environment_gaps: list[Gap] = [] seen_note_gaps: set[tuple[str, str]] = set() for values, covers in sorted(selected_rows.items()): - assignments, notes = concretize_conditions(decision.conditions, values) + assignments, notes = concretize_conditions(decision.conditions, values, solver_ctx) rows.append( MCDCRow( values=values, @@ -2259,7 +2465,7 @@ def generate_decision_result(decision: Decision, max_conditions: int, mcdc_mode: ) -def generate_direct_chain_result(decision: Decision) -> DecisionResult | None: +def generate_direct_chain_result(decision: Decision, solver_ctx: SolverContext | None = None) -> DecisionResult | None: operator = detect_uniform_chain_operator(decision.expression, decision.conditions) if operator is None or has_duplicate_conditions(decision.conditions): return None @@ -2287,7 +2493,7 @@ def generate_direct_chain_result(decision: Decision) -> DecisionResult | None: environment_gaps: list[Gap] = [] seen_note_gaps: set[tuple[str, str]] = set() for values, covers in sorted(selected_rows.items()): - assignments, notes = concretize_conditions(decision.conditions, values) + assignments, notes = concretize_conditions(decision.conditions, values, solver_ctx) rows.append( MCDCRow( values=values, @@ -2317,7 +2523,7 @@ def generate_direct_chain_result(decision: Decision) -> DecisionResult | None: ) -def generate_direct_grouped_and_result(decision: Decision) -> DecisionResult | None: +def generate_direct_grouped_and_result(decision: Decision, solver_ctx: SolverContext | None = None) -> DecisionResult | None: groups = parse_top_level_and_groups(decision.expression, decision.conditions) if groups is None or has_duplicate_conditions(decision.conditions): return None @@ -2350,7 +2556,7 @@ def generate_direct_grouped_and_result(decision: Decision) -> DecisionResult | N outcome = evaluate_decision(decision.expression, decision.conditions, values) if outcome is None: return None - assignments, notes = concretize_conditions(decision.conditions, values) + assignments, notes = concretize_conditions(decision.conditions, values, solver_ctx) rows.append( MCDCRow( values=values, @@ -2458,11 +2664,12 @@ def generate_multicondition_result( decision: Decision, truth_rows: list[tuple[bool, ...]], outcomes: dict[tuple[bool, ...], bool | None], + solver_ctx: SolverContext | None = None, ) -> DecisionResult: rows: list[MCDCRow] = [] covered_conditions = set() for values in truth_rows: - assignments, notes = concretize_conditions(decision.conditions, values) + assignments, notes = concretize_conditions(decision.conditions, values, solver_ctx) rows.append( MCDCRow( values=values, @@ -2654,11 +2861,305 @@ def replace_condition_occurrences( return replaced -def concretize_conditions(conditions: tuple[str, ...], values: tuple[bool, ...]) -> tuple[dict[str, TableValue], tuple[str, ...]]: +SolverContext = tuple[set[str], dict[str, str], dict[str, tuple[str, ...]]] + + +def build_solver_context(analysis: "InterfaceAnalysis", settable: set[str] | None = None) -> SolverContext: + """Build the (settable, expr_map, assignment_sources) triple the input solver needs. + + ``settable`` defaults to every global and function parameter (the variables that + can be driven directly). ``expr_map`` is the set of single-assignment intermediates + (locals such as ``Sa9_LogicalOperator1``) that must be expanded to reach the real + inputs; names that are themselves settable are excluded so feedback state variables + are set directly rather than expanded. ``assignment_sources`` lets the solver fall + back to single-root resolution for arithmetic intermediates compared to a literal. + """ + + if settable is None: + settable = set(analysis.global_order) | set(analysis.function_parameters) + expr_map = {name: rhs for name, rhs in analysis.assignment_expressions.items() if name not in settable} + return settable, expr_map, analysis.assignment_sources + + +def clean_boolean_expression(expression: str) -> str: + value = expression.strip() + while True: + candidate = strip_enclosing_parentheses(value) + candidate = strip_simple_casts(candidate).strip() + candidate = strip_enclosing_parentheses(candidate) + if candidate == value: + return candidate + value = candidate + + +def split_top_level(expression: str, operator: str) -> list[str]: + parts: list[str] = [] + depth = 0 + start = 0 + index = 0 + while index < len(expression): + char = expression[index] + if char == "(": + depth += 1 + elif char == ")": + depth -= 1 + elif depth == 0 and expression[index : index + 2] == operator: + parts.append(expression[start:index].strip()) + start = index + 2 + index += 2 + continue + index += 1 + parts.append(expression[start:].strip()) + return [part for part in parts if part] + + +def parse_condition_atom(expression: str) -> tuple[str | None, str | None, int | float | None]: + """Parse a leaf condition into (name, operator, literal). + + A bare identifier (or pointer dereference) returns operator/literal as ``None`` and + is interpreted by the caller as a truthiness test (``name != 0``).""" + + cleaned = clean_boolean_expression(expression) + numeric_literal = r"([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?)(?:[fFlLuU]*)" + match = re.fullmatch(rf"(.+?)\s*(==|!=|>=|<=|>|<)\s*{numeric_literal}", cleaned) + if match: + name, operator, literal_text = match.groups() + return canonical_lvalue(name), operator, parse_c_numeric_literal(literal_text) + if re.fullmatch(r"[A-Za-z_]\w*(?:\s*(?:->|\.)\s*[A-Za-z_]\w*)?(?:\s*\[[^\]]+\])?", cleaned): + return canonical_lvalue(cleaned), None, None + pointer_match = re.fullmatch(r"\(?\s*\*\s*([A-Za-z_]\w*)\s*\)?", cleaned) + if pointer_match: + return "*" + pointer_match.group(1), None, None + return None, None, None + + +def value_satisfies_atom( + value: TableValue, operator: str | None, literal: int | float | None, want: bool +) -> bool: + """Return True if ``value`` makes the atom (``name operator literal``) evaluate to ``want``. + + A ``None`` operator means a bare truthiness test (``name != 0``). Used so the solver can + keep an already-assigned input when it still satisfies a later condition instead of + overwriting it with a conflicting value.""" + + if isinstance(value, bool): + numeric: float = 1.0 if value else 0.0 + else: + try: + numeric = float(value) + except (TypeError, ValueError): + return False + if operator is None: + holds = numeric != 0 + else: + comparisons = { + "==": numeric == literal, + "!=": numeric != literal, + ">": numeric > literal, + ">=": numeric >= literal, + "<": numeric < literal, + "<=": numeric <= literal, + } + if operator not in comparisons: + return False + holds = comparisons[operator] + return holds == want + + +def solve_condition_inputs( + condition: str, + desired: bool, + *, + settable: set[str], + expr_map: dict[str, str], + assignment_sources: dict[str, tuple[str, ...]] | None = None, + trace_chain: Callable[[str], list[str]] | None = None, + prior: dict[str, TableValue] | None = None, +) -> tuple[dict[str, TableValue], list[str]]: + """Expand a decision condition down to concrete external-input values. + + ``settable`` is the set of variable names backed by input/parameter columns that can + be assigned directly. ``expr_map`` maps an intermediate variable to the right-hand + side of its single defining assignment so the solver can recurse into the logic that + computes it (e.g. ``Sa4_LogicalOperator2`` -> ``(x == 10) || (y == 10)``). + ``assignment_sources`` lets the solver fall back to single-root resolution for + arithmetic intermediates compared with a literal (e.g. an aliased input). ``prior`` + holds the values already chosen by earlier conditions in the same decision row; the + solver keeps a prior value when it still satisfies the current condition and prefers + operands that don't conflict with it, so two conditions sharing one input don't fight + (e.g. one needing ``hydact == 30`` and another needing ``hydact != 20``). Returns the + inferred input assignments plus notes for anything that could not be resolved. + """ + + assignment_sources = assignment_sources or {} + prior = prior or {} + notes: list[str] = [] + + def note_once(message: str) -> None: + if message not in notes: + notes.append(message) + + def conflicts_with_prior(result: dict[str, TableValue]) -> bool: + for name, value in result.items(): + existing = prior.get(name) + if existing is not None and existing != "MANUAL" and existing != value: + return True + return False + + def merge_all(parts: list[str], want: bool, seen: frozenset[str]) -> dict[str, TableValue] | None: + combined: dict[str, TableValue] = {} + solved_any = False + for part in parts: + result = solve(part, want, seen) + if result is None: + continue + solved_any = True + for name, value in result.items(): + if name in combined and combined[name] != value: + note_once(f"Conflicting inferred values for `{name}`; kept first value {combined[name]!r}.") + continue + combined[name] = value + return combined if solved_any else None + + def solve_any(parts: list[str], want: bool, seen: frozenset[str]) -> dict[str, TableValue] | None: + # Only one operand needs to hold (OR true / AND false). Prefer one that does not + # conflict with inputs already chosen by earlier conditions; fall back to the first + # solvable operand otherwise. Notes from operands we do not adopt are rolled back so + # they don't pollute the row's setup notes. + fallback: dict[str, TableValue] | None = None + fallback_notes: list[str] = [] + leftover_notes: list[str] = [] + for part in parts: + mark = len(notes) + result = solve(part, want, seen) + added = notes[mark:] + del notes[mark:] + if result: + if not conflicts_with_prior(result): + notes.extend(added) + return result + if fallback is None: + fallback, fallback_notes = result, added + else: + leftover_notes.extend(added) + if fallback is not None: + notes.extend(fallback_notes) + return fallback + notes.extend(leftover_notes) + return None + + def resolve_single_root(name: str) -> str | None: + """Return the sole settable input driving ``name``. + + Returns the root variable name, ``""`` when several inputs drive the value + (ambiguous), or ``None`` when no settable root exists. + """ + + roots = resolve_root_variables(name, settable, set(), assignment_sources) + assignable = [root for root in roots if root in settable] + if len(assignable) == 1: + return assignable[0] + if len(assignable) > 1: + note_once(f"MANUAL: ambiguous roots [{', '.join(assignable)}] for `{name}`.") + return "" + return None + + def solve_atom(expression: str, want: bool, seen: frozenset[str]) -> dict[str, TableValue] | None: + name, operator, literal = parse_condition_atom(expression) + if name is None: + note_once(f"MANUAL: could not parse condition `{expression.strip()}`.") + return None + if name in settable: + existing = prior.get(name) + if existing is not None and existing != "MANUAL" and value_satisfies_atom(existing, operator, literal, want): + return {name: existing} + if operator is None: + return {name: 1 if want else 0} + return {name: candidate_number(operator, literal, want)} + + is_truthiness = operator is None or (literal == 0 and operator in ("!=", "==")) + # Boolean intermediates tested for truthiness are expanded through their defining + # expression so every driving input is constrained. + if is_truthiness and name in expr_map and name not in seen: + inner_want = want if operator != "==" else not want + return solve(expr_map[name], inner_want, seen | {name}) + + # Otherwise (a comparison against a literal, or a truthiness test of a variable we + # cannot expand) fall back to single-root resolution: an arithmetic/aliased + # intermediate driven by exactly one input maps the comparison onto that input. + root = resolve_single_root(name) + if root == "": # ambiguous (multiple driving inputs) + return None + if root is None: + note_once(f"MANUAL: no settable input found for `{name}`.") + return None + if root != name and trace_chain is not None: + chain = trace_chain(name) + if len(chain) > 1: + note_once(f"`{name}` traced {' -> '.join(chain)}.") + resolve_op = operator or "!=" + resolve_literal = literal if literal is not None else 0 + return {root: candidate_number(resolve_op, resolve_literal, want)} + + def solve(expression: str, want: bool, seen: frozenset[str]) -> dict[str, TableValue] | None: + expr = clean_boolean_expression(expression) + while expr.startswith("!") and not expr.startswith("!="): + want = not want + expr = clean_boolean_expression(expr[1:]) + if not expr: + return None + + or_parts = split_top_level(expr, "||") + if len(or_parts) > 1: + # OR is true when any operand is true; false only when all are false. + return solve_any(or_parts, True, seen) if want else merge_all(or_parts, False, seen) + + and_parts = split_top_level(expr, "&&") + if len(and_parts) > 1: + # AND is true when all operands are true; false when any one is false. + return merge_all(and_parts, True, seen) if want else solve_any(and_parts, False, seen) + + return solve_atom(expr, want, seen) + + result = solve(condition, desired, frozenset()) + return (result or {}), notes + + +def concretize_conditions( + conditions: tuple[str, ...], + values: tuple[bool, ...], + solver_ctx: SolverContext | None = None, +) -> tuple[dict[str, TableValue], tuple[str, ...]]: assignments: dict[str, TableValue] = {} notes: list[str] = [] + def record(name: str, value: TableValue) -> None: + if name in assignments and assignments[name] != value: + notes.append(f"Conflicting inferred values for `{name}`; kept first value {assignments[name]!r}.") + return + assignments[name] = value + for condition, desired in zip(conditions, values): + if solver_ctx is not None: + settable, expr_map, assignment_sources = solver_ctx + solved, solve_notes = solve_condition_inputs( + condition, + desired, + settable=settable, + expr_map=expr_map, + assignment_sources=assignment_sources, + prior=assignments, + ) + if solved: + for name, value in solved.items(): + record(name, value) + continue + # Fall through to the simple per-condition inference below, preserving the + # previous behaviour for conditions the solver could not expand. + for note in solve_notes: + if note not in notes: + notes.append(note) + candidate = value_for_condition(condition, desired) if candidate is None: notes.append(f"No simple concrete value inferred for `{condition}` = {desired}.") diff --git a/tests/evaluation/SIL_SV_ATG_1 (1).xlsx b/tests/evaluation/SIL_SV_ATG_1 (1).xlsx new file mode 100644 index 0000000..587a4b4 Binary files /dev/null and b/tests/evaluation/SIL_SV_ATG_1 (1).xlsx differ diff --git a/tests/evaluation/SIL_SV_ATG_1_sample.xlsx b/tests/evaluation/SIL_SV_ATG_1_sample.xlsx new file mode 100644 index 0000000..7eba6cf Binary files /dev/null and b/tests/evaluation/SIL_SV_ATG_1_sample.xlsx differ diff --git a/tests/evaluation/ft-hfs_failinform-fh4-4.c b/tests/evaluation/ft-hfs_failinform-fh4-4.c new file mode 100644 index 0000000..d94d908 --- /dev/null +++ b/tests/evaluation/ft-hfs_failinform-fh4-4.c @@ -0,0 +1,809 @@ +/* CONFIDENTIAL */ +/*_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_*/ +/*_/_/_/ $DWG-No.:HFS_FAILINFORM-FH4 _/_/_/_*/ +/*_/_/_/ $Content:油圧回路デバイス故障・正常通知処理(2段LU油圧回路用) _/_/_/_*/ +/*_/_/_/ $Category:FT _/_/_/_*/ +/*_/_/_/ $Date:2024/07/18 _/_/_/_*/ +/*_/_/_/ $Design:高松 SCSK _/_/_/_*/ +/*_/_/_/ $Check: _/_/_/_*/ +/*_/_/_/ $Header: _/_/_/_*/ +/*_/_/_/ $Copyright(C) 2024 HONDA MOTOR CO., LTD. _/_/_/_*/ +/*_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_*/ + +/*###############################################################################################*/ +/*### $INCLUDE FILES$ ####*/ +/*###############################################################################################*/ + +#include "hos.h" +#include "def.h" +#include "hgsub.h" +#include "tl_basetypes.h" + +/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ +/*+++ $RAM_EXTERN$ +++*/ +/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ +extern VS15 VS15tmpatfact_hf; +extern VFLG VFLGf_hydonfaildetinh_hf; +extern VFLG VFLGf_phydoffsta_tm; +extern VFLG VFLGf_psenofffaildetinh_hf; +extern VFLG VFLGf_psenonfaildetinh_hf; +extern VFLG VFLGf_psenonluhioff_hf; +extern VFLG VFLGf_psenonluoff_hf; +extern VFLG VFLGf_psenonluonrdy_hf; +extern VFLG VFLGf_shaofffaildetinh_hf; +extern VFLG VFLGf_shaonfaildetinh_hf; +extern VFLG VFLGf_shbofffaildetinh_hf; +extern VFLG VFLGf_shbonfaildetinh_hf; +extern VFLG VFLGf_shcofffaildetinh_hf; +extern VFLG VFLGf_shconfaildetinh_hf; +extern VU08 VU08xhydact_hf; +extern VU08 VU08xpsenelefailsta_hf; +extern VU08 VU08xpsenfailsta_hf; +extern VU08 VU08xshaelefailsta_hf; +extern VU08 VU08xshafailsta_hf; +extern VU08 VU08xshahissta_hf; +extern VU08 VU08xshbelefailsta_hf; +extern VU08 VU08xshbfailsta_hf; +extern VU08 VU08xshbhissta_hf; +extern VU08 VU08xshcelefailsta_hf; +extern VU08 VU08xshcfailsta_hf; +extern VU08 VU08xshchissta_hf; + +/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ +/*+++ $RAM_PUBLIC$ +++*/ +/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ +#pragma section TM10M +VTIM VTIMthydoffok_hf; +#pragma section +#pragma section TM10M +VTIM VTIMtpsenoffok_hf; +#pragma section +#pragma section TM10M +VTIM VTIMtpsenoffokhydoff_hf; +#pragma section +#pragma section TM10M +VTIM VTIMtpsenoffokhydon_hf; +#pragma section +#pragma section TM10M +VTIM VTIMtpsenonluhi_hf; +#pragma section +#pragma section TM10M +VTIM VTIMtpsenonlulo_hf; +#pragma section +#pragma section TM10M +VTIM VTIMtshaoffok_hf; +#pragma section +#pragma section TM10M +VTIM VTIMtshaonok_hf; +#pragma section +#pragma section TM10M +VTIM VTIMtshboffok_hf; +#pragma section +#pragma section TM10M +VTIM VTIMtshbonok_hf; +#pragma section +#pragma section TM10M +VTIM VTIMtshcoffok_hf; +#pragma section +#pragma section TM10M +VTIM VTIMtshconok_hf; +#pragma section +VFLG VFLGf_hydfailfsapmt_tm; +VFLG VFLGf_hydoffok_hf; +VFLG VFLGf_hydoffok_tm; +VFLG VFLGf_hydonfail_tm; +VFLG VFLGf_phydcironok_tm; +VFLG VFLGf_psenofffail_tm; +VFLG VFLGf_psenoffok_tm; +VFLG VFLGf_psenonfail_tm; +VFLG VFLGf_shaofffail_tm; +VFLG VFLGf_shaoffok_tm; +VFLG VFLGf_shaonfail_tm; +VFLG VFLGf_shaonok_tm; +VFLG VFLGf_shboffchkok_hf; +VFLG VFLGf_shbofffail_tm; +VFLG VFLGf_shboffok_tm; +VFLG VFLGf_shbonfail_tm; +VFLG VFLGf_shbonok_tm; +VFLG VFLGf_shcoffchkok_hf; +VFLG VFLGf_shcofffail_tm; +VFLG VFLGf_shcoffok_tm; +VFLG VFLGf_shconfail_tm; +VFLG VFLGf_shconok_tm; + + +/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ +/*+++ $RAM_STATIC$ +++*/ +/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ +static VFLG X_Sa50_UnitDelay; +static VFLG X_Sa57_UnitDelay; +static VFLG X_Sa64_UnitDelay; +static VFLG X_Sa65_UnitDelay; +static VFLG X_Sa73_UnitDelay; +static VFLG X_Sa78_UnitDelay; +static VFLG X_Sa83_UnitDelay; +static VFLG X_Sa88_UnitDelay; +static VFLG X_Sa93_UnitDelay; +static VFLG X_Sa98_UnitDelay; + + +/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ +/*+++ $DATA_EXTERN$ +++*/ +/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ + +/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ +/*+++ $DATA_PUBLIC$ +++*/ +/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ +CS15 XS15tmp_thydoffok[10] = +{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + +}; +CS15 XS15tmp_tplostbl[10] = +{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + +}; +CS15 XS15tmp_tpsenonok[10] = +{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + +}; +CS15 XS15tmp_tshaoffok[10] = +{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + +}; +CS15 XS15tmp_tshaonok[10] = +{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + +}; +CS15 XS15tmp_tshboffok[10] = +{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + +}; +CS15 XS15tmp_tshbonok[10] = +{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + +}; +CS15 XS15tmp_tshcoffok[10] = +{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + +}; +CS15 XS15tmp_tshconok[10] = +{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + +}; +CU15 XU15thydoffok[10] = +{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + +}; +CU15 XU15tplostbl[10] = +{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + +}; +CU15 XU15tpsenonok[10] = +{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + +}; +CU15 XU15tshaoffok[10] = +{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + +}; +CU15 XU15tshaonok[10] = +{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + +}; +CU15 XU15tshboffok[10] = +{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + +}; +CU15 XU15tshbonok[10] = +{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + +}; +CU15 XU15tshcoffok[10] = +{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + +}; +CU15 XU15tshconok[10] = +{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 + +}; +CU15 CU15tpsenoffokhydoff = 1; +CU15 CU15tpsenoffokhydon = 1; +TBL S_TBLthydoffok = +{ + 10, + SIZE_S15, + SIZE_U15, +(void *) XS15tmp_thydoffok, +(void *) XU15thydoffok +}; +TBL S_TBLtplostbl = +{ + 10, + SIZE_S15, + SIZE_U15, +(void *) XS15tmp_tplostbl, +(void *) XU15tplostbl +}; +TBL S_TBLtpsenonok = +{ + 10, + SIZE_S15, + SIZE_U15, +(void *) XS15tmp_tpsenonok, +(void *) XU15tpsenonok +}; +TBL S_TBLtshaoffok = +{ + 10, + SIZE_S15, + SIZE_U15, +(void *) XS15tmp_tshaoffok, +(void *) XU15tshaoffok +}; +TBL S_TBLtshaonok = +{ + 10, + SIZE_S15, + SIZE_U15, +(void *) XS15tmp_tshaonok, +(void *) XU15tshaonok +}; +TBL S_TBLtshboffok = +{ + 10, + SIZE_S15, + SIZE_U15, +(void *) XS15tmp_tshboffok, +(void *) XU15tshboffok +}; +TBL S_TBLtshbonok = +{ + 10, + SIZE_S15, + SIZE_U15, +(void *) XS15tmp_tshbonok, +(void *) XU15tshbonok +}; +TBL S_TBLtshcoffok = +{ + 10, + SIZE_S15, + SIZE_U15, +(void *) XS15tmp_tshcoffok, +(void *) XU15tshcoffok +}; +TBL S_TBLtshconok = +{ + 10, + SIZE_S15, + SIZE_U15, +(void *) XS15tmp_tshconok, +(void *) XU15tshconok +}; + +/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ +/*+++ $DATA_STATIC$ +++*/ +/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ + +/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ +/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ +/*+++ $FUNCTION PROTOTYPE$ +++*/ +/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ +/*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ +void J_hfs_failinform(void); +void JINIT_hfs_failinform(void); +void JDTCCLR_hfs_failinform(void); + +/*************************************************************************************************/ +/*************************************************************************************************/ +/**** $Function:hfs_failinform ****/ +/**** $Content:油圧回路デバイス故障・正常を通知する ****/ +/**** ****/ +/**** $argument: なし ****/ +/**** $return value: なし ****/ +/*************************************************************************************************/ +/*************************************************************************************************/ +void J_hfs_failinform(void) +{ + + Int16 Sa11_TSHAOFFOK; + Int16 Sa13_TSHAONOK; + Int16 Sa15_TSHBOFFOK; + Int16 Sa17_TSHBONOK; + Int16 Sa19_TSHCOFFOK; + Int16 Sa21_TSHCONOK; + Int16 Sa4_THYDOFFOK; + Int16 Sa61_out___Non__old___immediate; + Int16 Sa62_out___Non__old___immediate; + Int16 Sa7_TPLOSTBL; + Int16 Sa9_TPSENONOK; + + FLG Sa11_LogicalOperator2; + FLG Sa13_LogicalOperator; + FLG Sa15_LogicalOperator2; + FLG Sa15_RelationalOperator2; + FLG Sa17_LogicalOperator; + FLG Sa19_LogicalOperator; + FLG Sa21_LogicalOperator; + FLG Sa4_LogicalOperator2; + FLG Sa60_LogicalOperator; + FLG Sa7_LogicalOperator2; + FLG Sa7_LogicalOperator3; + FLG Sa7_LogicalOperator5; + FLG Sa86_LogicalOperator; + FLG Sa9_LogicalOperator1; + FLG Sa9_LogicalOperator3; + + VFLGf_hydonfail_tm = (Int8) (((!(VFLGf_hydonfaildetinh_hf != 0)) && ((VFLGf_psenonluhioff_hf != 0) || (VFLGf_psenonluonrdy_hf != 0) || (VFLGf_psenonluoff_hf != 0))) || (VFLGf_hydonfail_tm != 0)); + + Sa4_THYDOFFOK = (Int16) GE_Ipl_tbl(&S_TBLthydoffok, (Int32) VS15tmpatfact_hf); + + Sa4_LogicalOperator2 = (Int8) (((VU08xshbhissta_hf == 10) || (VU08xshchissta_hf == 10)) && (!(VFLGf_hydonfaildetinh_hf != 0))); + + if (Sa4_LogicalOperator2 != 0) { + + if (!(X_Sa50_UnitDelay != 0)) { + + VTIMthydoffok_hf = Sa4_THYDOFFOK; + } + } + else { + + VTIMthydoffok_hf = Sa4_THYDOFFOK; + } + + X_Sa50_UnitDelay = 1; + + if (VFLGf_hydonfail_tm != 0) { + + VFLGf_hydoffok_tm = 0; + } + else { + + if ((Sa4_LogicalOperator2 != 0) && (VTIMthydoffok_hf <= 0)) { + + VFLGf_hydoffok_tm = 1; + } + else { + + VFLGf_hydoffok_tm = VFLGf_hydoffok_hf; + } + } + + VFLGf_hydoffok_hf = VFLGf_hydoffok_tm; + + Sa9_TPSENONOK = (Int16) GE_Ipl_tbl(&S_TBLtpsenonok, (Int32) VS15tmpatfact_hf); + + Sa9_LogicalOperator1 = (Int8) ((VU08xhydact_hf == 20) && (!(VFLGf_psenofffaildetinh_hf != 0))); + + if (Sa9_LogicalOperator1 != 0) { + + if (!(X_Sa65_UnitDelay != 0)) { + + VTIMtpsenonlulo_hf = Sa9_TPSENONOK; + } + } + else { + + VTIMtpsenonlulo_hf = Sa9_TPSENONOK; + } + + X_Sa65_UnitDelay = 1; + + Sa7_TPLOSTBL = (Int16) GE_Ipl_tbl(&S_TBLtplostbl, (Int32) VS15tmpatfact_hf); + + Sa7_LogicalOperator5 = (Int8) ((VU08xhydact_hf == 0) && (!(VFLGf_psenonfaildetinh_hf != 0))); + + if (Sa7_LogicalOperator5 != 0) { + + if (!(X_Sa57_UnitDelay != 0)) { + + VTIMtpsenoffok_hf = Sa7_TPLOSTBL; + } + } + else { + + VTIMtpsenoffok_hf = Sa7_TPLOSTBL; + } + + X_Sa57_UnitDelay = 1; + + Sa60_LogicalOperator = (Int8) ((Sa7_LogicalOperator5 != 0) && (VTIMtpsenoffok_hf <= 0)); + + Sa7_LogicalOperator2 = (Int8) ((Sa60_LogicalOperator != 0) && (VFLGf_phydoffsta_tm != 0)); + + if (Sa7_LogicalOperator2 != 0) { + + Sa61_out___Non__old___immediate = VTIMtpsenoffokhydoff_hf; + } + else { + + Sa61_out___Non__old___immediate = CU15tpsenoffokhydoff; + } + + VTIMtpsenoffokhydoff_hf = Sa61_out___Non__old___immediate; + + Sa7_LogicalOperator3 = (Int8) ((Sa60_LogicalOperator != 0) && (!(VFLGf_phydoffsta_tm != 0))); + + if (Sa7_LogicalOperator3 != 0) { + + Sa62_out___Non__old___immediate = VTIMtpsenoffokhydon_hf; + } + else { + + Sa62_out___Non__old___immediate = CU15tpsenoffokhydon; + } + + VTIMtpsenoffokhydon_hf = Sa62_out___Non__old___immediate; + + if (!(VFLGf_shcofffaildetinh_hf != 0)) { + + VFLGf_shcofffail_tm = (Int8) (VU08xshcfailsta_hf == 240); + } + + if (!(VFLGf_shaofffaildetinh_hf != 0)) { + + VFLGf_shaofffail_tm = (Int8) (VU08xshafailsta_hf == 240); + } + + Sa9_LogicalOperator3 = (Int8) ((VU08xhydact_hf == 30) && (!(VFLGf_psenofffaildetinh_hf != 0))); + + if (Sa9_LogicalOperator3 != 0) { + + if (!(X_Sa64_UnitDelay != 0)) { + + VTIMtpsenonluhi_hf = Sa9_TPSENONOK; + } + } + else { + + VTIMtpsenonluhi_hf = Sa9_TPSENONOK; + } + + X_Sa64_UnitDelay = 1; + + if (!(VFLGf_psenofffaildetinh_hf != 0)) { + + VFLGf_psenofffail_tm = (Int8) (VU08xpsenfailsta_hf == 240); + } + + if (VFLGf_psenofffail_tm != 0) { + + VFLGf_phydcironok_tm = 0; + } + else { + + if (((Sa9_LogicalOperator3 != 0) && (VTIMtpsenonluhi_hf <= 0)) || ((Sa9_LogicalOperator1 != 0) && (VTIMtpsenonlulo_hf <= 0))) { + + VFLGf_phydcironok_tm = 1; + } + } + + if (!(VFLGf_psenonfaildetinh_hf != 0)) { + + VFLGf_psenonfail_tm = (Int8) (VU08xpsenfailsta_hf == 230); + } + + if (VFLGf_psenonfail_tm != 0) { + + VFLGf_psenoffok_tm = 0; + } + else { + + if (((Sa7_LogicalOperator2 != 0) && (Sa61_out___Non__old___immediate <= 0)) || ((Sa7_LogicalOperator3 != 0) && (Sa62_out___Non__old___immediate <= 0))) { + + VFLGf_psenoffok_tm = 1; + } + } + + Sa21_TSHCONOK = (Int16) GE_Ipl_tbl(&S_TBLtshconok, (Int32) VS15tmpatfact_hf); + + Sa21_LogicalOperator = (Int8) ((VU08xshchissta_hf == 20) && (!(VFLGf_shcofffaildetinh_hf != 0))); + + if (Sa21_LogicalOperator != 0) { + + if (!(X_Sa98_UnitDelay != 0)) { + + VTIMtshconok_hf = Sa21_TSHCONOK; + } + } + else { + + VTIMtshconok_hf = Sa21_TSHCONOK; + } + + X_Sa98_UnitDelay = 1; + + if (VFLGf_shcofffail_tm != 0) { + + VFLGf_shconok_tm = 0; + } + else { + + if ((Sa21_LogicalOperator != 0) && (VTIMtshconok_hf <= 0)) { + + VFLGf_shconok_tm = 1; + } + } + + Sa19_TSHCOFFOK = (Int16) GE_Ipl_tbl(&S_TBLtshcoffok, (Int32) VS15tmpatfact_hf); + + Sa19_LogicalOperator = (Int8) ((VU08xshchissta_hf == 10) && (!(VFLGf_shconfaildetinh_hf != 0))); + + if (Sa19_LogicalOperator != 0) { + + if (!(X_Sa93_UnitDelay != 0)) { + + VTIMtshcoffok_hf = Sa19_TSHCOFFOK; + } + } + else { + + VTIMtshcoffok_hf = Sa19_TSHCOFFOK; + } + + X_Sa93_UnitDelay = 1; + + if (!(VFLGf_shconfaildetinh_hf != 0)) { + + VFLGf_shconfail_tm = (Int8) (VU08xshcfailsta_hf == 230); + } + + VFLGf_shcoffchkok_hf = (Int8) ((Sa19_LogicalOperator != 0) && (VTIMtshcoffok_hf <= 0)); + + if (VFLGf_shconfail_tm != 0) { + + VFLGf_shcoffok_tm = 0; + } + else { + + if (VFLGf_shcoffchkok_hf != 0) { + + VFLGf_shcoffok_tm = 1; + } + } + + Sa17_TSHBONOK = (Int16) GE_Ipl_tbl(&S_TBLtshbonok, (Int32) VS15tmpatfact_hf); + + Sa17_LogicalOperator = (Int8) ((VU08xshbhissta_hf == 20) && (!(VFLGf_shbofffaildetinh_hf != 0))); + + if (Sa17_LogicalOperator != 0) { + + if (!(X_Sa88_UnitDelay != 0)) { + + VTIMtshbonok_hf = Sa17_TSHBONOK; + } + } + else { + + VTIMtshbonok_hf = Sa17_TSHBONOK; + } + + X_Sa88_UnitDelay = 1; + + if (!(VFLGf_shbofffaildetinh_hf != 0)) { + + VFLGf_shbofffail_tm = (Int8) (VU08xshbfailsta_hf == 240); + } + + if (VFLGf_shbofffail_tm != 0) { + + VFLGf_shbonok_tm = 0; + } + else { + + if ((Sa17_LogicalOperator != 0) && (VTIMtshbonok_hf <= 0)) { + + VFLGf_shbonok_tm = 1; + } + } + + Sa15_TSHBOFFOK = (Int16) GE_Ipl_tbl(&S_TBLtshboffok, (Int32) VS15tmpatfact_hf); + + Sa15_RelationalOperator2 = (Int8) (VU08xshbhissta_hf == 10); + + Sa15_LogicalOperator2 = (Int8) (((Sa15_RelationalOperator2 != 0) || (VU08xshbhissta_hf == 20)) && (!(VFLGf_shbonfaildetinh_hf != 0))); + + if (Sa15_LogicalOperator2 != 0) { + + if (!(X_Sa83_UnitDelay != 0)) { + + VTIMtshboffok_hf = Sa15_TSHBOFFOK; + } + } + else { + + VTIMtshboffok_hf = Sa15_TSHBOFFOK; + } + + X_Sa83_UnitDelay = 1; + + if (!(VFLGf_shbonfaildetinh_hf != 0)) { + + VFLGf_shbonfail_tm = (Int8) (VU08xshbfailsta_hf == 230); + } + + Sa86_LogicalOperator = (Int8) ((Sa15_LogicalOperator2 != 0) && (VTIMtshboffok_hf <= 0)); + + if (VFLGf_shbonfail_tm != 0) { + + VFLGf_shboffok_tm = 0; + } + else { + + if (Sa86_LogicalOperator != 0) { + + VFLGf_shboffok_tm = 1; + } + } + + Sa13_TSHAONOK = (Int16) GE_Ipl_tbl(&S_TBLtshaonok, (Int32) VS15tmpatfact_hf); + + Sa13_LogicalOperator = (Int8) ((VU08xshahissta_hf == 20) && (!(VFLGf_shaofffaildetinh_hf != 0))); + + if (Sa13_LogicalOperator != 0) { + + if (!(X_Sa78_UnitDelay != 0)) { + + VTIMtshaonok_hf = Sa13_TSHAONOK; + } + } + else { + + VTIMtshaonok_hf = Sa13_TSHAONOK; + } + + X_Sa78_UnitDelay = 1; + + if (VFLGf_shaofffail_tm != 0) { + + VFLGf_shaonok_tm = 0; + } + else { + + if ((Sa13_LogicalOperator != 0) && (VTIMtshaonok_hf <= 0)) { + + VFLGf_shaonok_tm = 1; + } + } + + Sa11_TSHAOFFOK = (Int16) GE_Ipl_tbl(&S_TBLtshaoffok, (Int32) VS15tmpatfact_hf); + + Sa11_LogicalOperator2 = (Int8) (((VU08xshahissta_hf == 10) || (VU08xshahissta_hf == 20)) && (!(VFLGf_shaonfaildetinh_hf != 0))); + + if (Sa11_LogicalOperator2 != 0) { + + if (!(X_Sa73_UnitDelay != 0)) { + + VTIMtshaoffok_hf = Sa11_TSHAOFFOK; + } + } + else { + + VTIMtshaoffok_hf = Sa11_TSHAOFFOK; + } + + X_Sa73_UnitDelay = 1; + + if (!(VFLGf_shaonfaildetinh_hf != 0)) { + + VFLGf_shaonfail_tm = (Int8) (VU08xshafailsta_hf == 230); + } + + if (VFLGf_shaonfail_tm != 0) { + + VFLGf_shaoffok_tm = 0; + } + else { + + if ((Sa11_LogicalOperator2 != 0) && (VTIMtshaoffok_hf <= 0)) { + + VFLGf_shaoffok_tm = 1; + } + } + + VFLGf_hydfailfsapmt_tm = (Int8) ((VFLGf_shaonfail_tm != 0) || (VFLGf_shaofffail_tm != 0) || (VFLGf_shbonfail_tm != 0) || (VFLGf_shbofffail_tm != 0) || (VFLGf_shconfail_tm != 0) || + (VFLGf_shcofffail_tm != 0) || (VFLGf_psenonfail_tm != 0) || (VFLGf_psenofffail_tm != 0) || (VFLGf_hydonfail_tm != 0) || ((VU08xshaelefailsta_hf == 240) || (VU08xshaelefailsta_hf == 230) || + (VU08xshbelefailsta_hf == 240) || (VU08xshbelefailsta_hf == 230) || (VU08xshcelefailsta_hf == 240) || (VU08xshcelefailsta_hf == 230) || (VU08xpsenelefailsta_hf == 240) || (VU08xpsenelefailsta_hf + == 230))); + + VFLGf_shboffchkok_hf = (Int8) ((Sa15_RelationalOperator2 != 0) && (Sa86_LogicalOperator != 0)); +} + +/*************************************************************************************************/ +/**** $Function: hfs_failinform(イニシャル処理) ****/ +/**** $Content: イニシャル処理 ****/ +/*************************************************************************************************/ +void JINIT_hfs_failinform(void) +{ + VTIMtshconok_hf = 0x0064; + VTIMthydoffok_hf = 0x0064; + VTIMtpsenoffok_hf = 0x0064; + VTIMtshcoffok_hf = 0x0064; + VTIMtpsenonluhi_hf = 0x0064; + VTIMtshaonok_hf = 0x0064; + VTIMtshboffok_hf = 0x0064; + VTIMtshaoffok_hf = 0x0064; + VTIMtshbonok_hf = 0x0064; + VTIMtpsenonlulo_hf = 0x0064; + VTIMtpsenoffokhydon_hf = CU15tpsenoffokhydon; + VTIMtpsenoffokhydoff_hf = CU15tpsenoffokhydoff; +} +/*************************************************************************************************/ +/**** $Function: hfs_failinform(テスタA処理) ****/ +/**** $Content: DTC クリア処理 ****/ +/*************************************************************************************************/ +void JDTCCLR_hfs_failinform(void) +{ + VTIMtshconok_hf = 0x0064; + VTIMthydoffok_hf = 0x0064; + VFLGf_psenonfail_tm = 0x0; + VTIMtpsenoffok_hf = 0x0064; + VFLGf_shbofffail_tm = 0x0; + VFLGf_shcoffok_tm = 0x0; + VTIMtshcoffok_hf = 0x0064; + VTIMtpsenonluhi_hf = 0x0064; + VFLGf_shbonfail_tm = 0x0; + VTIMtshaonok_hf = 0x0064; + VTIMtshboffok_hf = 0x0064; + VFLGf_phydcironok_tm = 0x0; + VFLGf_shbonok_tm = 0x0; + VTIMtshbonok_hf = 0x0064; + VFLGf_shboffok_tm = 0x0; + VFLGf_shaofffail_tm = 0x0; + VFLGf_psenoffok_tm = 0x0; + VFLGf_shconok_tm = 0x0; + VFLGf_hydonfail_tm = 0x0; + VFLGf_shaoffok_tm = 0x0; + VFLGf_shcofffail_tm = 0x0; + VFLGf_psenofffail_tm = 0x0; + VFLGf_shaonfail_tm = 0x0; + VFLGf_hydoffok_hf = 0x0; + VFLGf_shconfail_tm = 0x0; + VFLGf_shaonok_tm = 0x0; + VTIMtshaoffok_hf = 0x0064; + VTIMtpsenonlulo_hf = 0x0064; + VTIMtpsenoffokhydon_hf = CU15tpsenoffokhydon; + VTIMtpsenoffokhydoff_hf = CU15tpsenoffokhydoff; +} + + + + + +/*-----------------------------------------------------------------------------------------------*/ +/*-----------------------------------------------------------------------------------------------*/ +/*--- REVISION MANAGEMENT ---*/ +/*-----------------------------------------------------------------------------------------------*/ +/*-----------------------------------------------------------------------------------------------*/ + +/*-----------------------------------------------------------------------------------------------*/ +/*--- ---*/ +/*--- $Rev: date: name: company: minute: content: ---*/ +/*--- $Rev:40 2026/03/06 ManhVo FS EAGLE4.1p3 TL4.3p5 ---*/ +/*--- ---*/ +/*-----------------------------------------------------------------------------------------------*/ +/*--- + $Log$ + ---*/ +/*-----------------------------------------------------------------------------------------------*/ + + diff --git a/tests/integration/test_fastapi_app.py b/tests/integration/test_fastapi_app.py index bb573dd..185df7f 100644 --- a/tests/integration/test_fastapi_app.py +++ b/tests/integration/test_fastapi_app.py @@ -1,5 +1,6 @@ import base64 from io import BytesIO +from pathlib import Path from zipfile import ZipFile from fastapi.testclient import TestClient @@ -23,6 +24,8 @@ def test_web_app_exposes_table_and_excel_controls() -> None: assert response.status_code == 200 assert "Testcase_table" in response.text + assert 'name="support_template"' in response.text + assert "Support template (Excel)" in response.text assert "Export Excel" in response.text assert "Export CSV" in response.text assert "Ccode_interface" in response.text @@ -119,6 +122,57 @@ def test_web_app_generates_mcdc_artifacts() -> None: assert "mcdc_testcases.xlsx" in payload["downloads"] +def test_web_app_generate_follows_support_template() -> None: + client = TestClient(app) + + source_bytes = Path("tests/evaluation/ft-hfs_failinform-fh4-4.c").read_bytes() + template_bytes = Path("tests/evaluation/SIL_SV_ATG_1_sample.xlsx").read_bytes() + + response = client.post( + "/api/generate", + data={"target_function": "J_hfs_failinform", "mcdc_mode": "masking"}, + files={ + "source": ("ft.c", source_bytes, "text/x-c"), + "support_template": ( + "template.xlsx", + template_bytes, + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ), + }, + ) + + payload = response.json() + assert response.status_code == 200 + table = payload["report"]["testcase_table"] + # Columns follow the template exactly. + assert table["input_columns"][0] == "VS15tmpatfact_hf" + assert len(table["input_columns"]) == 26 + assert table["parameter_columns"] == ["CU15tpsenoffokhydoff", "CU15tpsenoffokhydon"] + assert len(table["output_columns"]) == 34 + assert not any( + name.startswith(("XS15", "XU15", "S_TBL")) for name in table["parameter_columns"] + ) + # Template metadata supplies the default Excel name. + assert payload["excel_filename"] == "SIL_SV_ATG_1.xlsx" + assert "SIL_SV_ATG_1.xlsx" in payload["downloads"] + + +def test_web_app_rejects_non_xlsx_support_template() -> None: + client = TestClient(app) + + response = client.post( + "/api/generate", + data={"target_function": "logic"}, + files={ + "source": ("logic.c", b"int logic(int a){ if (a > 0) return 1; return 0; }", "text/x-c"), + "support_template": ("template.txt", b"not an excel file", "text/plain"), + }, + ) + + assert response.status_code == 400 + assert "xlsx" in response.json()["detail"].lower() + + def test_web_app_exports_excel_with_user_metadata() -> None: client = TestClient(app) diff --git a/tests/unit/test_mcdc_generator.py b/tests/unit/test_mcdc_generator.py index f585066..7c69845 100644 --- a/tests/unit/test_mcdc_generator.py +++ b/tests/unit/test_mcdc_generator.py @@ -11,7 +11,10 @@ extract_function_parameters, find_tool_path, generate_mcdc_report, + parse_support_template, + read_xlsx_grid, summarize_coverage_readiness, + testcase_table as build_testcase_table, testcase_table_rows as build_testcase_table_rows, testcase_table_rows_from_dict as build_testcase_table_rows_from_dict, testcase_table_rows_to_csv as build_testcase_table_rows_csv, @@ -733,3 +736,243 @@ def test_finds_llvm_tool_from_configured_bin_directory(tmp_path: Path, monkeypat monkeypatch.setenv("PATH", "") assert find_tool_path("clang") == str(tool) + + +def _rows_for_decision(table: dict, decision_id: str) -> dict[bool, dict]: + """Return {decision_result: row} for the rows of a single decision.""" + return { + bool(row["decision_result"]): row + for row in table["rows"] + if row["decision_id"] == decision_id + } + + +def test_solver_expands_intermediate_and_to_root_inputs(tmp_path: Path) -> None: + # A decision that tests an intermediate variable must be traced back to the real + # external inputs that drive it, with the literal value needed to satisfy it. + source = tmp_path / "intermediate_and.c" + source.write_text( + """ +/*+++ $RAM_EXTERN$ +++*/ +extern UInt8 mode; +extern UInt8 enable; + +/*+++ $RAM_PUBLIC$ +++*/ +UInt8 output_flag; + +void f(void) +{ + UInt8 tmp; + tmp = (mode == 20) && (enable != 0); + if (tmp != 0) { + output_flag = 1; + } +} +""", + encoding="utf-8", + ) + + report = generate_mcdc_report(source, target_function="f", mcdc_mode="masking") + table = report.to_dict()["testcase_table"] + + assert "mode" in table["input_columns"] + assert "enable" in table["input_columns"] + assert "tmp" not in table["input_columns"] + + rows = _rows_for_decision(table, "D1") + # True case satisfies the AND: mode must equal 20 and enable must be truthy. + assert rows[True]["inputs"]["mode"] == 20 + assert rows[True]["inputs"]["enable"] == 1 + # False case breaks the AND via the first operand (mode != 20). + assert rows[False]["inputs"]["mode"] == 21 + + +def test_solver_expands_intermediate_or_of_equalities(tmp_path: Path) -> None: + source = tmp_path / "intermediate_or.c" + source.write_text( + """ +/*+++ $RAM_EXTERN$ +++*/ +extern UInt8 a; +extern UInt8 b; + +/*+++ $RAM_PUBLIC$ +++*/ +UInt8 output_flag; + +void f(void) +{ + UInt8 tmp; + tmp = (a == 10) || (b == 10); + if (tmp != 0) { + output_flag = 1; + } +} +""", + encoding="utf-8", + ) + + report = generate_mcdc_report(source, target_function="f", mcdc_mode="masking") + table = report.to_dict()["testcase_table"] + + rows = _rows_for_decision(table, "D1") + # True case satisfies the OR by setting one operand to its matching value. + assert rows[True]["inputs"]["a"] == 10 + # False case requires every operand to miss its target value. + assert rows[False]["inputs"]["a"] == 11 + assert rows[False]["inputs"]["b"] == 11 + + +def test_solver_sets_feedback_state_variable_directly(tmp_path: Path) -> None: + # A state/feedback variable that is read in a decision and written later is set + # directly rather than expanded (its value depends on the previous cycle). + source = tmp_path / "feedback.c" + source.write_text( + """ +/*+++ $RAM_EXTERN$ +++*/ +extern UInt8 trigger; + +/*+++ $RAM_PUBLIC$ +++*/ +UInt8 state_flag; +UInt8 output_flag; + +void f(void) +{ + if (state_flag != 0) { + output_flag = 1; + } + state_flag = trigger; +} +""", + encoding="utf-8", + ) + + report = generate_mcdc_report(source, target_function="f", mcdc_mode="masking") + table = report.to_dict()["testcase_table"] + + assert "state_flag" in table["input_columns"] + rows = _rows_for_decision(table, "D1") + assert rows[True]["inputs"]["state_flag"] == 1 + assert rows[False]["inputs"]["state_flag"] == 0 + + +def test_solver_keeps_shared_input_consistent_across_conditions(tmp_path: Path) -> None: + # Two derived conditions in one decision share VU08xmode; a single value (30) makes + # the first true and the second false. The solver must not clobber that value with the + # second condition's off-value (21), which would break the first. + source = tmp_path / "shared_input.c" + source.write_text( + """ +/*+++ $RAM_EXTERN$ +++*/ +extern UInt8 VU08xmode; +extern UInt8 inhibit; + +/*+++ $RAM_PUBLIC$ +++*/ +UInt8 output_flag; + +void f(void) +{ + UInt8 hi; + UInt8 lo; + hi = (VU08xmode == 30) && (!(inhibit != 0)); + lo = (VU08xmode == 20) && (!(inhibit != 0)); + if ((hi != 0) || (lo != 0)) { + output_flag = 1; + } +} +""", + encoding="utf-8", + ) + + report = generate_mcdc_report(source, target_function="f", mcdc_mode="masking") + table = report.to_dict()["testcase_table"] + decision = next(d for d in report.to_dict()["decisions"] if d["id"] == "D1") + hi_index = decision["conditions"].index("hi != 0") + lo_index = decision["conditions"].index("lo != 0") + + rows = [row for row in table["rows"] if row["decision_id"] == "D1"] + assert rows + for row in rows: + wants = row["mcdc_condition_values"] + mode = row["inputs"]["VU08xmode"] + # Whenever a condition is intended true, the shared input must carry the value + # that actually makes it true (and not be overwritten by the other condition). + if wants[hi_index]: + assert mode == 30 + if wants[lo_index]: + assert mode == 20 + + +SAMPLE_TEMPLATE = Path("tests/evaluation/SIL_SV_ATG_1_sample.xlsx") +SAMPLE_SOURCE = Path("tests/evaluation/ft-hfs_failinform-fh4-4.c") + + +def test_parse_support_template_reads_columns_and_metadata() -> None: + override, metadata = parse_support_template(SAMPLE_TEMPLATE) + + assert override.input_names[0] == "VS15tmpatfact_hf" + assert len(override.input_names) == 26 + assert "VU08xshbhissta_hf" in override.input_names + assert override.parameter_names == ("CU15tpsenoffokhydoff", "CU15tpsenoffokhydon") + assert override.output_names[0] == "VTIMthydoffok_hf" + assert len(override.output_names) == 34 + assert override.array_sizes == {} + assert metadata.name == "SIL_SV_ATG_1" + assert metadata.scope == "HFS_FAILINFORM_FH4.c:1:HFS_FAILINFORM_FH4" + assert metadata.format_version == "1.3" + + +def test_parse_support_template_collapses_repeated_array_columns(tmp_path: Path) -> None: + template = tmp_path / "template.xlsx" + group_row = ["Mode", "Inputs", "Inputs", "Inputs", "Parameters", "Outputs"] + name_row = ["Step", "scalar_in", "arr", "arr", "gain", "result"] + write_testcase_workbook_rows( + [group_row, name_row], + template, + ExcelExportMetadata(name="CraftedTemplate", architecture="ARCH", scope="SCOPE"), + ) + + override, metadata = parse_support_template(template) + + assert override.input_names == ("scalar_in", "arr") + assert override.array_sizes == {"arr": 2} + assert override.parameter_names == ("gain",) + assert override.output_names == ("result",) + assert metadata.name == "CraftedTemplate" + assert metadata.architecture == "ARCH" + + +def test_read_xlsx_grid_round_trips_written_workbook(tmp_path: Path) -> None: + workbook = tmp_path / "grid.xlsx" + write_testcase_workbook_rows( + [["Mode", "Inputs"], ["Step", "x"], [0, 7]], + workbook, + ExcelExportMetadata(name="Grid"), + ) + + grid = read_xlsx_grid(workbook) + + assert grid[0][0] == "Format Version" + assert any(row and row[0] == "Step" for row in grid) + + +def test_interface_override_drives_generated_columns_and_values() -> None: + override, _ = parse_support_template(SAMPLE_TEMPLATE) + + report = generate_mcdc_report( + SAMPLE_SOURCE, + target_function="J_hfs_failinform", + interface_override=override, + ) + table = build_testcase_table(report) + + # The template is authoritative: columns match its layout exactly. + assert list(table["input_columns"]) == list(override.input_names) + assert list(table["parameter_columns"]) == list(override.parameter_names) + assert list(table["output_columns"]) == list(override.output_names) + # No auto-detected array parameters leak in. + assert not any( + name.startswith(("XS15", "XU15", "S_TBL")) for name in table["parameter_columns"] + ) + # The solver still drives real inputs, and parameters carry their C initial value. + d1 = _rows_for_decision(table, "D1") + assert d1[True]["inputs"]["VU08xshbhissta_hf"] == 10 + assert d1[True]["parameters"]["CU15tpsenoffokhydoff"] == 1