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