Files
SLWChipVerify/slwchipverify.py
2026-04-19 16:14:05 +08:00

1941 lines
66 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""SLWChipVerify面向小型 Verilog 工程的轻量级 RTL 验证 CLI。
当前支持两类规格:
1) truth_table组合逻辑真值表验证
2) sequential基于周期采样的时序验证含 clk/rst
"""
from __future__ import annotations
import argparse
import csv
import json
import re
import shutil
import subprocess
import sys
import tempfile
import xml.etree.ElementTree as ET
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any
TOOL_NAME = "SLWChipVerify"
LOG_PREFIX = f"[{TOOL_NAME}]"
TB_TOP = "__slwchipverify_tb"
MAX_EXACT_COVERAGE_BITS = 12
class ChipVerifyError(Exception):
"""SLWChipVerify 的统一异常类型。"""
@dataclass
class Signal:
"""逻辑信号定义:名称与位宽。"""
name: str
width: int = 1
@dataclass
class Case:
"""组合逻辑测试用例:输入向量与期望输出。"""
name: str
input_values: dict[str, int]
output_values: dict[str, int]
@dataclass
class ObserveSignal:
"""时序观测信号:名称、表达式与位宽。"""
name: str
expr: str
width: int = 1
@dataclass
class SeqExpectation:
"""断言中的单个期望条件。"""
signal: str
op: str
value: int
width: int
@dataclass
class SeqAssertion:
"""时序断言:支持单周期与窗口模式。"""
name: str
cycle: int
expectations: list[SeqExpectation]
mode: str = "cycle"
within: int = 0
until_expectations: list[SeqExpectation] | None = None
@dataclass
class SeqStimulusEvent:
"""时序激励事件:在指定周期写入一组信号值。"""
cycle: int
values: dict[str, int]
@dataclass
class ClockConfig:
"""时钟配置。"""
name: str
period_ns: int
initial: int
@dataclass
class ResetConfig:
"""复位配置。"""
name: str
active: int
cycles: int
@dataclass
class AutoStimulusConfig:
"""自动激励配置:是否启用、起始周期、模式与随机种子。"""
enabled: bool
start_cycle: int
mode: str
seed: int
@dataclass
class SequentialSpec:
"""完整的时序验证规格数据结构。"""
clock: ClockConfig
reset: ResetConfig | None
inputs: list[Signal]
outputs: list[Signal]
observes: list[ObserveSignal]
init_values: dict[str, int]
stimulus: list[SeqStimulusEvent]
assertions: list[SeqAssertion]
max_cycles: int
auto_stimulus: AutoStimulusConfig
def signal_widths(self) -> dict[str, int]:
"""返回可采样信号到位宽的映射。"""
mapping: dict[str, int] = {self.clock.name: 1}
if self.reset is not None:
mapping[self.reset.name] = 1
for sig in self.inputs:
mapping[sig.name] = sig.width
for sig in self.outputs:
mapping[sig.name] = sig.width
for obs in self.observes:
mapping[obs.name] = obs.width
return mapping
def coverage_signal_widths(self) -> dict[str, int]:
"""返回用于覆盖率统计的信号位宽映射。"""
return self.signal_widths()
def parse_args() -> argparse.Namespace:
"""解析命令行参数。"""
parser = argparse.ArgumentParser(
prog="slwchipverify",
description=(
"SLWChipVerifyVerilog 组合/时序验证工具。"
),
)
sub = parser.add_subparsers(dest="command", required=True)
run = sub.add_parser("run", help="Run verification from a JSON spec")
run.add_argument(
"-d",
"--design",
action="append",
required=True,
help="Verilog source file (can be provided multiple times)",
)
run.add_argument("--top", required=True, help="DUT top module name")
run.add_argument("--spec", required=True, help="Path to verification spec JSON")
run.add_argument(
"--workdir",
default=".slwchipverify_build",
help="Build directory used when --keep is set",
)
run.add_argument(
"--report",
help="Optional output path for JSON report (default: no JSON file)",
)
run.add_argument(
"--junit",
help="Optional output path for JUnit XML report",
)
run.add_argument(
"--csv",
help="Optional output path for CSV report",
)
run.add_argument("--iverilog", default="iverilog", help="iverilog executable path")
run.add_argument("--vvp", default="vvp", help="vvp executable path")
run.add_argument(
"--timeout",
type=int,
default=30,
help="Simulation timeout in seconds",
)
run.add_argument(
"--dump-vcd",
help="Optional waveform path relative to workdir, e.g. run.vcd",
)
run.add_argument(
"--keep",
action="store_true",
help="Keep intermediate files in --workdir",
)
return parser.parse_args()
def _parse_signal_item(item: Any, field_name: str) -> Signal:
"""解析输入/输出信号定义项。"""
if isinstance(item, str):
return Signal(name=item, width=1)
if isinstance(item, dict):
name = item.get("name")
width = item.get("width", 1)
if not isinstance(name, str) or not name:
raise ChipVerifyError(f"{field_name} signal name must be a non-empty string")
if not isinstance(width, int) or width <= 0:
raise ChipVerifyError(f"{field_name} signal {name!r} width must be positive int")
return Signal(name=name, width=width)
raise ChipVerifyError(
f"Invalid {field_name} signal entry: expected string or object, got {type(item)}"
)
def _parse_observe_item(item: Any) -> ObserveSignal:
"""解析 observes 数组中的单项配置。"""
if not isinstance(item, dict):
raise ChipVerifyError("Observe entry must be an object")
name = item.get("name")
expr = item.get("expr")
width = item.get("width", 1)
if not isinstance(name, str) or not name:
raise ChipVerifyError("Observe signal name must be a non-empty string")
if not isinstance(expr, str) or not expr:
raise ChipVerifyError(f"Observe signal {name!r} must provide non-empty expr")
if not isinstance(width, int) or width <= 0:
raise ChipVerifyError(f"Observe signal {name!r} width must be positive int")
return ObserveSignal(name=name, expr=expr, width=width)
def _parse_verilog_literal(value: str) -> int | None:
"""解析 Verilog 字面量(如 8'h1f失败时返回 None。"""
m = re.fullmatch(r"(\d+)'([bBdDhHoO])([0-9a-fA-F_]+)", value)
if not m:
return None
_width_txt, base_ch, digits = m.groups()
base_map = {"b": 2, "d": 10, "h": 16, "o": 8}
base = base_map[base_ch.lower()]
digits = digits.replace("_", "")
return int(digits, base)
def parse_value(raw_value: Any, width: int, signal_name: str) -> int:
"""把规格中的值解析为整数并校验位宽范围。"""
if isinstance(raw_value, bool):
value = int(raw_value)
elif isinstance(raw_value, int):
value = raw_value
elif isinstance(raw_value, str):
txt = raw_value.strip()
verilog_val = _parse_verilog_literal(txt)
if verilog_val is not None:
value = verilog_val
elif txt.startswith(("0b", "0B")):
value = int(txt[2:].replace("_", ""), 2)
elif txt.startswith(("0x", "0X")):
value = int(txt[2:].replace("_", ""), 16)
elif txt.startswith(("0o", "0O")):
value = int(txt[2:].replace("_", ""), 8)
else:
value = int(txt.replace("_", ""), 10)
else:
raise ChipVerifyError(
f"Signal {signal_name!r} value must be int/str/bool, got {type(raw_value)}"
)
if value < 0:
raise ChipVerifyError(f"Signal {signal_name!r} value cannot be negative")
max_value = (1 << width) - 1
if value > max_value:
raise ChipVerifyError(
f"Signal {signal_name!r} value {value} exceeds width {width} (max {max_value})"
)
return value
def load_raw_spec(spec_path: Path) -> dict[str, Any]:
"""读取并校验规格 JSON 的根结构。"""
if not spec_path.exists():
raise ChipVerifyError(f"Spec file not found: {spec_path}")
try:
spec = json.loads(spec_path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
raise ChipVerifyError(f"Invalid JSON in spec file: {exc}") from exc
if not isinstance(spec, dict):
raise ChipVerifyError("Spec root must be a JSON object")
return spec
def parse_truth_table_spec(spec: dict[str, Any]) -> tuple[list[Signal], list[Signal], list[Case]]:
"""解析 truth_table 规格并转换为内部结构。"""
raw_inputs = spec.get("inputs")
raw_outputs = spec.get("outputs")
raw_cases = spec.get("cases")
if not isinstance(raw_inputs, list) or not raw_inputs:
raise ChipVerifyError("Spec field 'inputs' must be a non-empty array")
if not isinstance(raw_outputs, list) or not raw_outputs:
raise ChipVerifyError("Spec field 'outputs' must be a non-empty array")
if not isinstance(raw_cases, list) or not raw_cases:
raise ChipVerifyError("Spec field 'cases' must be a non-empty array")
inputs = [_parse_signal_item(item, "input") for item in raw_inputs]
outputs = [_parse_signal_item(item, "output") for item in raw_outputs]
input_names = {s.name for s in inputs}
output_names = {s.name for s in outputs}
if len(input_names) != len(inputs):
raise ChipVerifyError("Duplicate names found in input signals")
if len(output_names) != len(outputs):
raise ChipVerifyError("Duplicate names found in output signals")
overlap = input_names & output_names
if overlap:
joined = ", ".join(sorted(overlap))
raise ChipVerifyError(f"Signal names cannot be both input and output: {joined}")
input_map = {s.name: s for s in inputs}
output_map = {s.name: s for s in outputs}
cases: list[Case] = []
for idx, raw_case in enumerate(raw_cases):
if not isinstance(raw_case, dict):
raise ChipVerifyError(f"Case #{idx} must be an object")
in_obj = raw_case.get("in")
out_obj = raw_case.get("out")
if not isinstance(in_obj, dict) or not isinstance(out_obj, dict):
raise ChipVerifyError(f"Case #{idx} must include object fields 'in' and 'out'")
missing_inputs = input_names - set(in_obj.keys())
extra_inputs = set(in_obj.keys()) - input_names
missing_outputs = output_names - set(out_obj.keys())
extra_outputs = set(out_obj.keys()) - output_names
if missing_inputs:
raise ChipVerifyError(f"Case #{idx} missing input fields: {sorted(missing_inputs)}")
if extra_inputs:
raise ChipVerifyError(f"Case #{idx} has unknown input fields: {sorted(extra_inputs)}")
if missing_outputs:
raise ChipVerifyError(f"Case #{idx} missing output fields: {sorted(missing_outputs)}")
if extra_outputs:
raise ChipVerifyError(f"Case #{idx} has unknown output fields: {sorted(extra_outputs)}")
in_vals = {
name: parse_value(raw, input_map[name].width, name) for name, raw in in_obj.items()
}
out_vals = {
name: parse_value(raw, output_map[name].width, name)
for name, raw in out_obj.items()
}
case_name = str(raw_case.get("name", f"case_{idx}"))
cases.append(Case(name=case_name, input_values=in_vals, output_values=out_vals))
return inputs, outputs, cases
def _expectation_operator(raw_op: str) -> str:
"""标准化并校验断言比较运算符。"""
op = raw_op.lower()
allowed = {"eq", "ne", "gt", "ge", "lt", "le"}
if op not in allowed:
raise ChipVerifyError(f"Unsupported expectation op: {raw_op}")
return op
def parse_sequential_spec(spec: dict[str, Any]) -> SequentialSpec:
"""解析 sequential 规格并生成内部时序验证对象。"""
raw_clock = spec.get("clock", {})
if not isinstance(raw_clock, dict):
raise ChipVerifyError("Spec field 'clock' must be an object")
clock_name = str(raw_clock.get("name", "clk"))
clock_period = raw_clock.get("period_ns", 10)
clock_initial = raw_clock.get("initial", 0)
if not isinstance(clock_period, int) or clock_period <= 0:
raise ChipVerifyError("clock.period_ns must be a positive integer")
if clock_initial not in (0, 1):
raise ChipVerifyError("clock.initial must be 0 or 1")
clock = ClockConfig(name=clock_name, period_ns=clock_period, initial=clock_initial)
reset: ResetConfig | None = None
raw_reset = spec.get("reset")
if raw_reset is not None:
if not isinstance(raw_reset, dict):
raise ChipVerifyError("Spec field 'reset' must be an object when provided")
reset_name = str(raw_reset.get("name", "rst"))
reset_active = raw_reset.get("active", 1)
reset_cycles = raw_reset.get("cycles", 2)
if reset_active not in (0, 1):
raise ChipVerifyError("reset.active must be 0 or 1")
if not isinstance(reset_cycles, int) or reset_cycles < 0:
raise ChipVerifyError("reset.cycles must be a non-negative integer")
reset = ResetConfig(name=reset_name, active=reset_active, cycles=reset_cycles)
raw_inputs = spec.get("inputs", [])
raw_outputs = spec.get("outputs", [])
raw_observes = spec.get("observes", [])
if not isinstance(raw_inputs, list):
raise ChipVerifyError("Spec field 'inputs' must be an array")
if not isinstance(raw_outputs, list):
raise ChipVerifyError("Spec field 'outputs' must be an array")
if not isinstance(raw_observes, list):
raise ChipVerifyError("Spec field 'observes' must be an array")
inputs = [_parse_signal_item(item, "input") for item in raw_inputs]
outputs = [_parse_signal_item(item, "output") for item in raw_outputs]
observes = [_parse_observe_item(item) for item in raw_observes]
input_names = {s.name for s in inputs}
output_names = {s.name for s in outputs}
observe_names = {s.name for s in observes}
if len(input_names) != len(inputs):
raise ChipVerifyError("Duplicate names found in input signals")
if len(output_names) != len(outputs):
raise ChipVerifyError("Duplicate names found in output signals")
if len(observe_names) != len(observes):
raise ChipVerifyError("Duplicate names found in observe signals")
overlaps = [
("input/output", input_names & output_names),
("input/observe", input_names & observe_names),
("output/observe", output_names & observe_names),
]
for label, names in overlaps:
if names:
raise ChipVerifyError(f"Signal name collision in {label}: {sorted(names)}")
if clock.name in input_names or clock.name in output_names or clock.name in observe_names:
raise ChipVerifyError("clock.name cannot overlap with input/output/observe signal names")
if reset is not None and (
reset.name in input_names or reset.name in output_names or reset.name in observe_names
):
raise ChipVerifyError("reset.name cannot overlap with input/output/observe signal names")
if not inputs and not outputs and not observes:
raise ChipVerifyError(
"Sequential spec requires at least one signal in inputs/outputs/observes"
)
signal_widths: dict[str, int] = {clock.name: 1}
if reset is not None:
signal_widths[reset.name] = 1
for sig in inputs:
signal_widths[sig.name] = sig.width
for sig in outputs:
signal_widths[sig.name] = sig.width
for sig in observes:
signal_widths[sig.name] = sig.width
raw_init = spec.get("init", {})
if not isinstance(raw_init, dict):
raise ChipVerifyError("Spec field 'init' must be an object")
init_values: dict[str, int] = {}
writable_init_names = set(input_names)
if reset is not None:
writable_init_names.add(reset.name)
for name, raw in raw_init.items():
if name not in writable_init_names:
raise ChipVerifyError(
f"init field {name!r} is not writable (allowed: inputs and reset only)"
)
init_values[name] = parse_value(raw, signal_widths[name], name)
raw_auto = spec.get("auto_stimulus", {"enabled": True})
if isinstance(raw_auto, bool):
auto_stimulus = AutoStimulusConfig(
enabled=raw_auto,
start_cycle=0,
mode="binary_count",
seed=1,
)
elif isinstance(raw_auto, dict):
enabled = bool(raw_auto.get("enabled", True))
start_cycle = raw_auto.get("start_cycle", 0)
mode = str(raw_auto.get("mode", "binary_count"))
seed = raw_auto.get("seed", 1)
if not isinstance(start_cycle, int) or start_cycle < 0:
raise ChipVerifyError("auto_stimulus.start_cycle must be a non-negative integer")
if mode not in {"binary_count", "random"}:
raise ChipVerifyError(
"auto_stimulus.mode currently supports 'binary_count' or 'random'"
)
if not isinstance(seed, int):
raise ChipVerifyError("auto_stimulus.seed must be an integer")
auto_stimulus = AutoStimulusConfig(
enabled=enabled,
start_cycle=start_cycle,
mode=mode,
seed=seed,
)
else:
raise ChipVerifyError("Spec field 'auto_stimulus' must be bool or object")
raw_stimulus = spec.get("stimulus", [])
if not isinstance(raw_stimulus, list):
raise ChipVerifyError("Spec field 'stimulus' must be an array")
stimulus: list[SeqStimulusEvent] = []
writable_stimulus_names = set(input_names)
if reset is not None:
writable_stimulus_names.add(reset.name)
for idx, item in enumerate(raw_stimulus):
if not isinstance(item, dict):
raise ChipVerifyError(f"Stimulus #{idx} must be an object")
cycle = item.get("cycle")
set_obj = item.get("set")
if not isinstance(cycle, int) or cycle < 0:
raise ChipVerifyError(f"Stimulus #{idx} cycle must be a non-negative integer")
if not isinstance(set_obj, dict) or not set_obj:
raise ChipVerifyError(f"Stimulus #{idx} set must be a non-empty object")
values: dict[str, int] = {}
for name, raw in set_obj.items():
if name not in writable_stimulus_names:
raise ChipVerifyError(
f"Stimulus #{idx} writes unknown/non-writable signal {name!r}"
)
values[name] = parse_value(raw, signal_widths[name], name)
stimulus.append(SeqStimulusEvent(cycle=cycle, values=values))
raw_assertions = spec.get("assertions", [])
if not isinstance(raw_assertions, list):
raise ChipVerifyError("Spec field 'assertions' must be an array")
def _parse_assert_expectations(
raw_expect_obj: dict[str, Any],
assertion_idx: int,
field_label: str,
) -> list[SeqExpectation]:
parsed: list[SeqExpectation] = []
for signal_name, raw_exp in raw_expect_obj.items():
if signal_name not in signal_widths:
raise ChipVerifyError(
f"Assertion #{assertion_idx} references unknown signal {signal_name!r}"
)
width = signal_widths[signal_name]
op = "eq"
raw_value: Any
if isinstance(raw_exp, dict):
op = _expectation_operator(str(raw_exp.get("op", "eq")))
if "value" not in raw_exp:
raise ChipVerifyError(
f"Assertion #{assertion_idx} signal {signal_name!r} in {field_label} missing field 'value'"
)
raw_value = raw_exp["value"]
else:
raw_value = raw_exp
value = parse_value(raw_value, width, signal_name)
parsed.append(
SeqExpectation(signal=signal_name, op=op, value=value, width=width)
)
return parsed
assertions: list[SeqAssertion] = []
for idx, item in enumerate(raw_assertions):
if not isinstance(item, dict):
raise ChipVerifyError(f"Assertion #{idx} must be an object")
cycle = item.get("cycle")
raw_expect = item.get("expect")
if not isinstance(cycle, int) or cycle < 0:
raise ChipVerifyError(f"Assertion #{idx} cycle must be a non-negative integer")
if not isinstance(raw_expect, dict) or not raw_expect:
raise ChipVerifyError(f"Assertion #{idx} expect must be a non-empty object")
name = str(item.get("name", f"assert_{idx}"))
mode = str(item.get("mode", "cycle")).lower()
within = 0
raw_window = item.get("window")
if raw_window is not None:
if not isinstance(raw_window, dict):
raise ChipVerifyError(f"Assertion #{idx} window must be an object")
window_type = str(raw_window.get("type", "eventually")).lower()
window_within = raw_window.get("within")
if window_type not in {"eventually", "always", "never", "until"}:
raise ChipVerifyError(
f"Assertion #{idx} window.type currently supports only 'eventually', 'always', 'never', 'until'"
)
if not isinstance(window_within, int) or window_within < 0:
raise ChipVerifyError(
f"Assertion #{idx} window.within must be a non-negative integer"
)
mode = window_type
within = window_within
elif mode in {"eventually", "always", "never", "until"}:
raw_within = item.get("within")
if not isinstance(raw_within, int) or raw_within < 0:
raise ChipVerifyError(
f"Assertion #{idx} with mode={mode} requires non-negative within"
)
within = raw_within
elif mode == "cycle":
within = 0
else:
raise ChipVerifyError(
f"Assertion #{idx} mode must be 'cycle', 'eventually', 'always', 'never', or 'until'"
)
expectations = _parse_assert_expectations(raw_expect, idx, "expect")
until_expectations: list[SeqExpectation] | None = None
if mode == "until":
raw_until_expect = item.get("until_expect")
if not isinstance(raw_until_expect, dict) or not raw_until_expect:
raise ChipVerifyError(
f"Assertion #{idx} with mode=until requires non-empty 'until_expect' object"
)
until_expectations = _parse_assert_expectations(
raw_until_expect,
idx,
"until_expect",
)
assertions.append(
SeqAssertion(
name=name,
cycle=cycle,
expectations=expectations,
mode=mode,
within=within,
until_expectations=until_expectations,
)
)
max_cycles = spec.get("max_cycles", 16)
if not isinstance(max_cycles, int) or max_cycles <= 0:
raise ChipVerifyError("Spec field 'max_cycles' must be a positive integer")
return SequentialSpec(
clock=clock,
reset=reset,
inputs=inputs,
outputs=outputs,
observes=observes,
init_values=init_values,
stimulus=stimulus,
assertions=assertions,
max_cycles=max_cycles,
auto_stimulus=auto_stimulus,
)
def verilog_decl(keyword: str, sig: Signal) -> str:
"""生成 Verilog 信号声明语句。"""
if sig.width == 1:
return f"{keyword} {sig.name};"
return f"{keyword} [{sig.width - 1}:0] {sig.name};"
def verilog_const(width: int, value: int) -> str:
"""把整数转为指定位宽的 Verilog 十进制常量。"""
return f"{width}'d{value}"
def _format_delay_number(value: float) -> str:
"""把延时值格式化为简洁文本,避免多余尾零。"""
txt = f"{value:.6f}".rstrip("0").rstrip(".")
return txt or "0"
def generate_truth_table_testbench(
top_module: str,
inputs: list[Signal],
outputs: list[Signal],
cases: list[Case],
dump_vcd: str | None,
) -> str:
"""为组合逻辑规格生成可直接仿真的 testbench 文本。"""
lines: list[str] = []
lines.append("`timescale 1ns/1ps")
lines.append(f"module {TB_TOP};")
lines.append("")
for sig in inputs:
lines.append(f" {verilog_decl('reg', sig)}")
for sig in outputs:
lines.append(f" {verilog_decl('wire', sig)}")
lines.append("")
lines.append(" integer pass_count;")
lines.append(" integer fail_count;")
lines.append(" integer case_fail;")
lines.append("")
conn = ", ".join([f".{s.name}({s.name})" for s in (inputs + outputs)])
lines.append(f" {top_module} dut ({conn});")
lines.append("")
lines.append(" initial begin")
lines.append(" pass_count = 0;")
lines.append(" fail_count = 0;")
if dump_vcd:
lines.append(f" $dumpfile(\"{dump_vcd}\");")
lines.append(f" $dumpvars(0, {TB_TOP});")
for idx, case in enumerate(cases):
lines.append("")
lines.append(f" // {case.name}")
for sig in inputs:
value = case.input_values[sig.name]
lines.append(f" {sig.name} = {verilog_const(sig.width, value)};")
lines.append(" #1;")
lines.append(" case_fail = 0;")
for sig in outputs:
expected = case.output_values[sig.name]
exp_v = verilog_const(sig.width, expected)
lines.append(f" if ({sig.name} !== {exp_v}) begin")
lines.append(" case_fail = 1;")
lines.append(
f" $display(\"MISMATCH case={idx} name={case.name} signal={sig.name} expected=%0d got=%0d\", {exp_v}, {sig.name});"
)
lines.append(" end")
lines.append(" if (case_fail == 0) begin")
lines.append(" pass_count = pass_count + 1;")
lines.append(
f" $display(\"CASE_PASS case={idx} name={case.name}\");"
)
lines.append(" end else begin")
lines.append(" fail_count = fail_count + 1;")
lines.append(
f" $display(\"CASE_FAIL case={idx} name={case.name}\");"
)
lines.append(" end")
lines.append("")
lines.append(
f" $display(\"SUMMARY total={len(cases)} pass=%0d fail=%0d\", pass_count, fail_count);"
)
lines.append(" $finish;")
lines.append(" end")
lines.append("endmodule")
return "\n".join(lines) + "\n"
def generate_sequential_testbench(
top_module: str,
seq_spec: SequentialSpec,
dump_vcd: str | None,
) -> str:
"""为时序规格生成带周期采样与覆盖率输出的 testbench。"""
lines: list[str] = []
lines.append("`timescale 1ns/1ps")
lines.append(f"module {TB_TOP};")
lines.append("")
lines.append(f" reg {seq_spec.clock.name};")
if seq_spec.reset is not None:
lines.append(f" reg {seq_spec.reset.name};")
for sig in seq_spec.inputs:
lines.append(f" {verilog_decl('reg', sig)}")
for sig in seq_spec.outputs:
lines.append(f" {verilog_decl('wire', sig)}")
if seq_spec.observes:
lines.append("")
for obs in seq_spec.observes:
if obs.width == 1:
lines.append(f" wire {obs.name};")
else:
lines.append(f" wire [{obs.width - 1}:0] {obs.name};")
lines.append(f" assign {obs.name} = {obs.expr};")
lines.append("")
lines.append(" integer cycle;")
if seq_spec.auto_stimulus.enabled and seq_spec.auto_stimulus.mode == "random" and seq_spec.inputs:
lines.append(" integer rand_seed;")
port_signals: list[str] = [seq_spec.clock.name]
if seq_spec.reset is not None:
port_signals.append(seq_spec.reset.name)
port_signals.extend(sig.name for sig in seq_spec.inputs)
port_signals.extend(sig.name for sig in seq_spec.outputs)
conn = ", ".join([f".{name}({name})" for name in port_signals])
lines.append("")
lines.append(f" {top_module} dut ({conn});")
half_period = _format_delay_number(seq_spec.clock.period_ns / 2.0)
lines.append("")
lines.append(" initial begin")
lines.append(f" {seq_spec.clock.name} = 1'd{seq_spec.clock.initial};")
lines.append(f" forever #{half_period} {seq_spec.clock.name} = ~{seq_spec.clock.name};")
lines.append(" end")
lines.append("")
lines.append(" initial begin")
if seq_spec.auto_stimulus.enabled and seq_spec.auto_stimulus.mode == "random" and seq_spec.inputs:
lines.append(f" rand_seed = {seq_spec.auto_stimulus.seed};")
if seq_spec.reset is not None:
if seq_spec.reset.name in seq_spec.init_values:
init_rst = seq_spec.init_values[seq_spec.reset.name]
elif seq_spec.reset.cycles > 0:
init_rst = seq_spec.reset.active
else:
init_rst = 1 - seq_spec.reset.active
lines.append(f" {seq_spec.reset.name} = 1'd{init_rst};")
for sig in seq_spec.inputs:
init_value = seq_spec.init_values.get(sig.name, 0)
lines.append(f" {sig.name} = {verilog_const(sig.width, init_value)};")
if dump_vcd:
lines.append(f" $dumpfile(\"{dump_vcd}\");")
lines.append(f" $dumpvars(0, {TB_TOP});")
lines.append("")
lines.append(f" for (cycle = 0; cycle < {seq_spec.max_cycles}; cycle = cycle + 1) begin")
if seq_spec.reset is not None:
inactive = 1 - seq_spec.reset.active
lines.append(f" if (cycle < {seq_spec.reset.cycles}) begin")
lines.append(f" {seq_spec.reset.name} = 1'd{seq_spec.reset.active};")
lines.append(" end else begin")
lines.append(f" {seq_spec.reset.name} = 1'd{inactive};")
lines.append(" end")
if seq_spec.auto_stimulus.enabled and seq_spec.inputs:
lines.append(f" if (cycle >= {seq_spec.auto_stimulus.start_cycle}) begin")
if seq_spec.auto_stimulus.mode == "binary_count":
offset = 0
for sig in seq_spec.inputs:
mask = (1 << sig.width) - 1
if offset == 0:
expr = f"((cycle - {seq_spec.auto_stimulus.start_cycle}) & {mask})"
else:
expr = (
f"(((cycle - {seq_spec.auto_stimulus.start_cycle}) >> {offset}) & {mask})"
)
lines.append(
f" {sig.name} = {verilog_const(sig.width, 0)} + {expr};"
)
offset += sig.width
else:
for sig in seq_spec.inputs:
mask = (1 << sig.width) - 1
lines.append(
f" {sig.name} = $random(rand_seed) & {mask};"
)
lines.append(" end")
for event in seq_spec.stimulus:
lines.append(f" if (cycle == {event.cycle}) begin")
for name, value in event.values.items():
width = 1
if seq_spec.reset is not None and name == seq_spec.reset.name:
width = 1
else:
for sig in seq_spec.inputs:
if sig.name == name:
width = sig.width
break
lines.append(f" {name} = {verilog_const(width, value)};")
lines.append(" end")
lines.append(f" @(posedge {seq_spec.clock.name});")
lines.append(" #1;")
coverage_signal_names = [seq_spec.clock.name]
if seq_spec.reset is not None:
coverage_signal_names.append(seq_spec.reset.name)
coverage_signal_names.extend(sig.name for sig in seq_spec.inputs)
coverage_signal_names.extend(sig.name for sig in seq_spec.outputs)
coverage_signal_names.extend(obs.name for obs in seq_spec.observes)
for name in coverage_signal_names:
lines.append(
f" $display(\"COV_SAMPLE cycle=%0d signal={name} value=%0d\", cycle, {name});"
)
lines.append(" end")
lines.append("")
lines.append(f" $display(\"SEQ_DONE cycles=%0d\", {seq_spec.max_cycles});")
lines.append(" $finish;")
lines.append(" end")
lines.append("endmodule")
return "\n".join(lines) + "\n"
def run_command(cmd: list[str], timeout: int) -> subprocess.CompletedProcess[str]:
"""执行外部命令并统一超时控制与输出捕获策略。"""
return subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout,
check=False,
)
def parse_truth_table_summary(stdout: str) -> tuple[int, int, int]:
"""从仿真输出中提取组合验证总计信息。"""
m = re.search(r"SUMMARY total=(\d+) pass=(\d+) fail=(\d+)", stdout)
if not m:
raise ChipVerifyError("Simulation finished without SUMMARY line")
total_s, pass_s, fail_s = m.groups()
return int(total_s), int(pass_s), int(fail_s)
def parse_sequential_done(stdout: str) -> int:
"""从仿真输出中提取时序验证实际运行周期数。"""
m = re.search(r"SEQ_DONE cycles=(\d+)", stdout)
if not m:
raise ChipVerifyError("Simulation finished without SEQ_DONE line")
return int(m.group(1))
def parse_cov_samples(stdout: str) -> dict[int, dict[str, int]]:
"""解析覆盖率采样日志,按周期聚合信号值。"""
pattern = re.compile(r"^COV_SAMPLE cycle=(\d+) signal=([A-Za-z_][A-Za-z0-9_]*) value=(\d+)$")
samples: dict[int, dict[str, int]] = {}
for line in stdout.splitlines():
m = pattern.match(line.strip())
if not m:
continue
cycle_s, signal_name, value_s = m.groups()
cycle = int(cycle_s)
value = int(value_s)
if cycle not in samples:
samples[cycle] = {}
samples[cycle][signal_name] = value
return samples
def compute_coverage(
samples: dict[int, dict[str, int]],
signal_widths: dict[str, int],
) -> dict[str, dict[str, Any]]:
"""根据采样数据计算唯一值覆盖与翻转次数等指标。"""
stats: dict[str, dict[str, Any]] = {}
for name, width in signal_widths.items():
stats[name] = {
"width": width,
"samples": 0,
"unique_values": set(),
"toggle_count": 0,
"_last_value": None,
}
for cycle in sorted(samples.keys()):
cycle_values = samples[cycle]
for name, value in cycle_values.items():
if name not in stats:
continue
entry = stats[name]
entry["samples"] += 1
entry["unique_values"].add(value)
last_value = entry["_last_value"]
if last_value is not None and value != last_value:
entry["toggle_count"] += 1
entry["_last_value"] = value
result: dict[str, dict[str, Any]] = {}
for name, entry in stats.items():
width = int(entry["width"])
unique_values = sorted(entry["unique_values"])
signal_result: dict[str, Any] = {
"width": width,
"samples": int(entry["samples"]),
"unique_count": len(unique_values),
"unique_values": unique_values,
"toggle_count": int(entry["toggle_count"]),
}
if width <= MAX_EXACT_COVERAGE_BITS:
total_bins = 1 << width
signal_result["total_bins"] = total_bins
signal_result["value_coverage_pct"] = round(
100.0 * len(unique_values) / total_bins,
2,
)
else:
signal_result["total_bins"] = None
signal_result["value_coverage_pct"] = None
result[name] = signal_result
return result
def _eval_relation(actual: int, op: str, expected: int) -> bool:
"""按比较操作符判断断言是否成立。"""
if op == "eq":
return actual == expected
if op == "ne":
return actual != expected
if op == "gt":
return actual > expected
if op == "ge":
return actual >= expected
if op == "lt":
return actual < expected
if op == "le":
return actual <= expected
raise ChipVerifyError(f"Unsupported assertion operator: {op}")
def _expectation_ok(sample: dict[str, int], exp: SeqExpectation) -> bool:
"""判断单个期望在当前采样点是否满足。"""
actual = sample.get(exp.signal)
if actual is None:
return False
return _eval_relation(actual, exp.op, exp.value)
def _expectations_all_ok(sample: dict[str, int], expectations: list[SeqExpectation]) -> bool:
"""判断一组期望在当前采样点是否全部满足。"""
return all(_expectation_ok(sample, exp) for exp in expectations)
def evaluate_sequential_assertions(
assertions: list[SeqAssertion],
samples: dict[int, dict[str, int]],
cycles: int,
) -> tuple[list[dict[str, Any]], list[str]]:
"""评估时序断言结果,并产出可读失败信息与机器可解析日志。"""
results: list[dict[str, Any]] = []
mismatch_lines: list[str] = []
for assertion in assertions:
if assertion.mode == "cycle":
cycle_values = samples.get(assertion.cycle)
failed_items: list[str] = []
if cycle_values is None:
failed_items.append(
f"cycle {assertion.cycle} not sampled"
)
else:
for exp in assertion.expectations:
if exp.signal not in cycle_values:
failed_items.append(
f"signal {exp.signal} missing at cycle {assertion.cycle}"
)
mismatch_lines.append(
"MISMATCH_ASSERT "
f"mode=cycle cycle={assertion.cycle} name={assertion.name} "
f"signal={exp.signal} op={exp.op} expected={exp.value} got=NA"
)
continue
actual = cycle_values[exp.signal]
if not _eval_relation(actual, exp.op, exp.value):
failed_items.append(
f"{exp.signal} {exp.op} {exp.value} (got {actual})"
)
mismatch_lines.append(
"MISMATCH_ASSERT "
f"mode=cycle cycle={assertion.cycle} name={assertion.name} "
f"signal={exp.signal} op={exp.op} expected={exp.value} got={actual}"
)
if failed_items:
if cycle_values is None:
mismatch_lines.append(
"MISMATCH_ASSERT "
f"mode=cycle cycle={assertion.cycle} name={assertion.name} "
"signal=* op=eq expected=NA got=NA"
)
results.append(
{
"name": assertion.name,
"status": "fail",
"mode": assertion.mode,
"cycle": assertion.cycle,
"within": assertion.within,
"matched_cycle": None,
"message": f"cycle assertion failed at cycle {assertion.cycle}",
"details": "; ".join(failed_items),
}
)
else:
results.append(
{
"name": assertion.name,
"status": "pass",
"mode": assertion.mode,
"cycle": assertion.cycle,
"within": assertion.within,
"matched_cycle": assertion.cycle,
"message": f"cycle assertion passed at cycle {assertion.cycle}",
"details": "",
}
)
continue
if assertion.mode not in {"eventually", "always", "never", "until"}:
raise ChipVerifyError(f"Unsupported assertion mode: {assertion.mode}")
window_start = assertion.cycle
window_end = assertion.cycle + assertion.within
effective_end = min(window_end, cycles - 1)
observed_cycles: list[int] = []
if effective_end >= window_start:
observed_cycles = list(range(window_start, effective_end + 1))
observed_details: list[str] = []
for c in observed_cycles:
cycle_values = samples.get(c, {})
signal_pairs = []
for exp in assertion.expectations:
if exp.signal in cycle_values:
signal_pairs.append(f"{exp.signal}={cycle_values[exp.signal]}")
else:
signal_pairs.append(f"{exp.signal}=NA")
observed_details.append(f"{c}[{', '.join(signal_pairs)}]")
window_partially_outside = window_end >= cycles
observed_detail_text = "observed: " + "; ".join(observed_details)
if assertion.mode == "eventually":
matched_cycle: int | None = None
for c in observed_cycles:
if _expectations_all_ok(samples.get(c, {}), assertion.expectations):
matched_cycle = c
break
if matched_cycle is not None:
results.append(
{
"name": assertion.name,
"status": "pass",
"mode": assertion.mode,
"cycle": assertion.cycle,
"within": assertion.within,
"matched_cycle": matched_cycle,
"message": (
"eventually assertion satisfied "
f"at cycle {matched_cycle} in [{window_start}, {window_end}]"
),
"details": "",
}
)
continue
reason = "not_satisfied"
if window_partially_outside:
reason = "not_satisfied_in_observed_window"
mismatch_lines.append(
"MISMATCH_ASSERT "
f"mode=eventually window={window_start}:{window_end} "
f"name={assertion.name} reason={reason}"
)
results.append(
{
"name": assertion.name,
"status": "fail",
"mode": assertion.mode,
"cycle": assertion.cycle,
"within": assertion.within,
"matched_cycle": None,
"message": (
"eventually assertion not satisfied "
f"in [{window_start}, {window_end}]"
),
"details": observed_detail_text,
}
)
continue
if assertion.mode == "always":
violating_cycle: int | None = None
for c in observed_cycles:
if not _expectations_all_ok(samples.get(c, {}), assertion.expectations):
violating_cycle = c
break
if violating_cycle is not None:
mismatch_lines.append(
"MISMATCH_ASSERT "
f"mode=always window={window_start}:{window_end} "
f"name={assertion.name} reason=violation cycle={violating_cycle}"
)
results.append(
{
"name": assertion.name,
"status": "fail",
"mode": assertion.mode,
"cycle": assertion.cycle,
"within": assertion.within,
"matched_cycle": None,
"message": (
"always assertion violated "
f"at cycle {violating_cycle} in [{window_start}, {window_end}]"
),
"details": observed_detail_text,
}
)
continue
if window_partially_outside:
mismatch_lines.append(
"MISMATCH_ASSERT "
f"mode=always window={window_start}:{window_end} "
f"name={assertion.name} reason=incomplete_window"
)
results.append(
{
"name": assertion.name,
"status": "fail",
"mode": assertion.mode,
"cycle": assertion.cycle,
"within": assertion.within,
"matched_cycle": None,
"message": (
"always assertion window exceeds simulated cycles"
),
"details": observed_detail_text,
}
)
continue
results.append(
{
"name": assertion.name,
"status": "pass",
"mode": assertion.mode,
"cycle": assertion.cycle,
"within": assertion.within,
"matched_cycle": window_end,
"message": (
"always assertion satisfied "
f"for [{window_start}, {window_end}]"
),
"details": "",
}
)
continue
if assertion.mode == "never":
hit_cycle: int | None = None
for c in observed_cycles:
if _expectations_all_ok(samples.get(c, {}), assertion.expectations):
hit_cycle = c
break
if hit_cycle is not None:
mismatch_lines.append(
"MISMATCH_ASSERT "
f"mode=never window={window_start}:{window_end} "
f"name={assertion.name} reason=hit cycle={hit_cycle}"
)
results.append(
{
"name": assertion.name,
"status": "fail",
"mode": assertion.mode,
"cycle": assertion.cycle,
"within": assertion.within,
"matched_cycle": hit_cycle,
"message": (
"never assertion violated "
f"at cycle {hit_cycle} in [{window_start}, {window_end}]"
),
"details": observed_detail_text,
}
)
continue
if window_partially_outside:
mismatch_lines.append(
"MISMATCH_ASSERT "
f"mode=never window={window_start}:{window_end} "
f"name={assertion.name} reason=incomplete_window"
)
results.append(
{
"name": assertion.name,
"status": "fail",
"mode": assertion.mode,
"cycle": assertion.cycle,
"within": assertion.within,
"matched_cycle": None,
"message": "never assertion window exceeds simulated cycles",
"details": observed_detail_text,
}
)
continue
results.append(
{
"name": assertion.name,
"status": "pass",
"mode": assertion.mode,
"cycle": assertion.cycle,
"within": assertion.within,
"matched_cycle": None,
"message": (
"never assertion satisfied "
f"for [{window_start}, {window_end}]"
),
"details": "",
}
)
continue
until_expectations = assertion.until_expectations or []
if not until_expectations:
raise ChipVerifyError(
f"Assertion {assertion.name!r} with mode=until requires until_expectations"
)
trigger_cycle: int | None = None
for c in observed_cycles:
if _expectations_all_ok(samples.get(c, {}), until_expectations):
trigger_cycle = c
break
if trigger_cycle is None:
reason = "no_trigger"
if window_partially_outside:
reason = "no_trigger_in_observed_window"
mismatch_lines.append(
"MISMATCH_ASSERT "
f"mode=until window={window_start}:{window_end} "
f"name={assertion.name} reason={reason}"
)
results.append(
{
"name": assertion.name,
"status": "fail",
"mode": assertion.mode,
"cycle": assertion.cycle,
"within": assertion.within,
"matched_cycle": None,
"message": (
"until assertion failed because trigger condition was not met "
f"in [{window_start}, {window_end}]"
),
"details": observed_detail_text,
}
)
continue
violating_cycle: int | None = None
for c in observed_cycles:
if c >= trigger_cycle:
break
if not _expectations_all_ok(samples.get(c, {}), assertion.expectations):
violating_cycle = c
break
if violating_cycle is not None:
mismatch_lines.append(
"MISMATCH_ASSERT "
f"mode=until window={window_start}:{window_end} "
f"name={assertion.name} reason=precondition_violation cycle={violating_cycle}"
)
results.append(
{
"name": assertion.name,
"status": "fail",
"mode": assertion.mode,
"cycle": assertion.cycle,
"within": assertion.within,
"matched_cycle": trigger_cycle,
"message": (
"until assertion violated before trigger "
f"at cycle {violating_cycle} (trigger at {trigger_cycle})"
),
"details": observed_detail_text,
}
)
continue
results.append(
{
"name": assertion.name,
"status": "pass",
"mode": assertion.mode,
"cycle": assertion.cycle,
"within": assertion.within,
"matched_cycle": trigger_cycle,
"message": (
"until assertion satisfied "
f"with trigger at cycle {trigger_cycle} in [{window_start}, {window_end}]"
),
"details": "",
}
)
return results, mismatch_lines
def parse_truth_table_case_results(stdout: str) -> list[dict[str, Any]]:
"""解析组合逻辑逐用例通过/失败明细。"""
pass_re = re.compile(r"^CASE_PASS case=(\d+) name=(.+)$")
fail_re = re.compile(r"^CASE_FAIL case=(\d+) name=(.+)$")
mismatch_re = re.compile(
r"^MISMATCH case=(\d+) name=(.*?) signal=([A-Za-z_][A-Za-z0-9_]*) expected=(\d+) got=(\d+)$"
)
status_map: dict[int, tuple[str, str]] = {}
mismatch_map: dict[int, list[str]] = {}
for line in stdout.splitlines():
txt = line.strip()
m = pass_re.match(txt)
if m:
idx = int(m.group(1))
name = m.group(2)
status_map[idx] = (name, "pass")
continue
m = fail_re.match(txt)
if m:
idx = int(m.group(1))
name = m.group(2)
status_map[idx] = (name, "fail")
continue
m = mismatch_re.match(txt)
if m:
idx = int(m.group(1))
signal_name = m.group(3)
expected = m.group(4)
got = m.group(5)
mismatch_map.setdefault(idx, []).append(
f"{signal_name}: expected {expected}, got {got}"
)
continue
case_indices = sorted(set(status_map.keys()) | set(mismatch_map.keys()))
results: list[dict[str, Any]] = []
for idx in case_indices:
name, status = status_map.get(idx, (f"case_{idx}", "fail"))
detail_lines = mismatch_map.get(idx, [])
if status == "pass":
message = "case passed"
details = ""
else:
message = "case failed"
details = "; ".join(detail_lines)
results.append(
{
"name": name,
"status": status,
"case": idx,
"message": message,
"details": details,
"mode": "case",
"cycle": None,
"within": None,
"matched_cycle": None,
}
)
return results
def ensure_executable(name: str, path: str) -> None:
"""校验外部可执行程序是否可用。"""
if shutil.which(path) is None:
raise ChipVerifyError(f"Executable not found for {name}: {path}")
def _load_spec_kind(spec: dict[str, Any]) -> str:
"""读取并校验规格类型字段。"""
kind = str(spec.get("kind", "truth_table"))
if kind not in {"truth_table", "sequential"}:
raise ChipVerifyError("Spec field 'kind' must be 'truth_table' or 'sequential'")
return kind
def _emit_reports(
args: argparse.Namespace,
report: dict[str, Any],
testcases: list[dict[str, Any]],
) -> None:
"""按用户参数输出 JSON/JUnit/CSV 三类报告。"""
write_report(args.report, report)
write_junit_report(args.junit, report, testcases)
write_csv_report(args.csv, report, testcases)
def verify_run(args: argparse.Namespace) -> int:
"""执行一次完整验证流程(编译、仿真、判定、落盘报告)。"""
ensure_executable("iverilog", args.iverilog)
ensure_executable("vvp", args.vvp)
design_files = [Path(p).resolve() for p in args.design]
for vf in design_files:
if not vf.exists():
raise ChipVerifyError(f"Design file not found: {vf}")
spec_path = Path(args.spec).resolve()
raw_spec = load_raw_spec(spec_path)
spec_kind = _load_spec_kind(raw_spec)
report: dict[str, Any] = {
"timestamp": datetime.now().isoformat(timespec="seconds"),
"tool": "SLWChipVerify",
"top": args.top,
"spec": str(spec_path),
"kind": spec_kind,
"design_files": [str(p) for p in design_files],
"result": "error",
}
coverage_widths: dict[str, int] | None = None
seq_spec: SequentialSpec | None = None
if spec_kind == "truth_table":
inputs, outputs, cases = parse_truth_table_spec(raw_spec)
tb_text = generate_truth_table_testbench(
args.top,
inputs,
outputs,
cases,
args.dump_vcd,
)
else:
seq_spec = parse_sequential_spec(raw_spec)
tb_text = generate_sequential_testbench(
args.top,
seq_spec,
args.dump_vcd,
)
coverage_widths = seq_spec.coverage_signal_widths()
temp_dir_ctx: tempfile.TemporaryDirectory[str] | None = None
if args.keep:
workdir = Path(args.workdir).resolve()
workdir.mkdir(parents=True, exist_ok=True)
else:
temp_dir_ctx = tempfile.TemporaryDirectory(prefix="slwchipverify_")
workdir = Path(temp_dir_ctx.name).resolve()
try:
tb_path = workdir / f"{TB_TOP}.v"
sim_bin = workdir / "slwchipverify_sim.out"
tb_path.write_text(tb_text, encoding="utf-8")
compile_cmd = [
args.iverilog,
"-g2012",
"-s",
TB_TOP,
"-o",
str(sim_bin),
*[str(p) for p in design_files],
str(tb_path),
]
compile_result = run_command(compile_cmd, args.timeout)
report["compile"] = {
"command": compile_cmd,
"returncode": compile_result.returncode,
"stdout": compile_result.stdout,
"stderr": compile_result.stderr,
}
if compile_result.returncode != 0:
report["result"] = "compile_error"
error_text = compile_result.stderr.strip() or compile_result.stdout.strip()
print(f"{LOG_PREFIX} Compile failed")
print(error_text)
_emit_reports(
args,
report,
[
{
"name": "compile",
"status": "error",
"message": "compile_error",
"details": error_text,
"mode": "meta",
"cycle": None,
"within": None,
"matched_cycle": None,
}
],
)
return 2
sim_cmd = [args.vvp, str(sim_bin)]
sim_result = run_command(sim_cmd, args.timeout)
report["simulate"] = {
"command": sim_cmd,
"returncode": sim_result.returncode,
"stdout": sim_result.stdout,
"stderr": sim_result.stderr,
}
if sim_result.returncode != 0:
report["result"] = "runtime_error"
error_text = sim_result.stderr.strip() or sim_result.stdout.strip()
print(f"{LOG_PREFIX} Simulation failed")
print(error_text)
_emit_reports(
args,
report,
[
{
"name": "simulation",
"status": "error",
"message": "runtime_error",
"details": error_text,
"mode": "meta",
"cycle": None,
"within": None,
"matched_cycle": None,
}
],
)
return 2
if spec_kind == "truth_table":
total, passed, failed = parse_truth_table_summary(sim_result.stdout)
mismatch_lines = [
line for line in sim_result.stdout.splitlines() if line.startswith("MISMATCH ")
]
case_results = parse_truth_table_case_results(sim_result.stdout)
report["summary"] = {"total": total, "passed": passed, "failed": failed}
report["mismatches"] = mismatch_lines
report["case_results"] = case_results
if failed == 0:
report["result"] = "pass"
print(f"{LOG_PREFIX} PASS: {passed}/{total} cases")
_emit_reports(args, report, case_results)
return 0
report["result"] = "fail"
print(f"{LOG_PREFIX} FAIL: passed={passed}, failed={failed}, total={total}")
for line in mismatch_lines:
print(line)
_emit_reports(args, report, case_results)
return 1
if seq_spec is None:
raise ChipVerifyError("Internal error: sequential spec is missing")
cycles = parse_sequential_done(sim_result.stdout)
samples = parse_cov_samples(sim_result.stdout)
coverage = compute_coverage(samples, coverage_widths or {})
assertion_results, mismatch_lines = evaluate_sequential_assertions(
seq_spec.assertions,
samples,
cycles,
)
passed = sum(1 for item in assertion_results if item["status"] == "pass")
failed = sum(1 for item in assertion_results if item["status"] == "fail")
report["summary"] = {
"cycles": cycles,
"assertions": {
"total": len(assertion_results),
"passed": passed,
"failed": failed,
},
}
report["mismatches"] = mismatch_lines
report["assertion_results"] = assertion_results
report["coverage"] = coverage
if failed == 0:
report["result"] = "pass"
print(
f"{LOG_PREFIX} PASS: "
f"cycles={cycles}, assertions={passed}/{len(assertion_results)}, assert_fail=0"
)
_emit_reports(args, report, assertion_results)
return 0
report["result"] = "fail"
print(
f"{LOG_PREFIX} FAIL: "
f"cycles={cycles}, assert_pass={passed}, assert_fail={failed}"
)
for line in mismatch_lines:
print(line)
_emit_reports(args, report, assertion_results)
return 1
finally:
if temp_dir_ctx is not None:
temp_dir_ctx.cleanup()
def write_report(path: str | None, report: dict[str, Any]) -> None:
"""输出 JSON 总报告。"""
if not path:
return
out_path = Path(path).resolve()
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps(report, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"{LOG_PREFIX} Report written: {out_path}")
def write_junit_report(
path: str | None,
report: dict[str, Any],
testcases: list[dict[str, Any]],
) -> None:
"""输出 JUnit XML 报告,便于 CI 平台直接展示测试结果。"""
if not path:
return
cases = testcases
if not cases:
cases = [
{
"name": "slwchipverify_run",
"status": "pass" if report.get("result") == "pass" else "fail",
"message": str(report.get("result", "unknown")),
"details": "",
"mode": "meta",
"cycle": None,
"within": None,
"matched_cycle": None,
}
]
failures = sum(1 for case in cases if case.get("status") == "fail")
errors = sum(1 for case in cases if case.get("status") == "error")
suite = ET.Element(
"testsuite",
{
"name": f"slwchipverify.{report.get('kind', 'unknown')}",
"tests": str(len(cases)),
"failures": str(failures),
"errors": str(errors),
},
)
classname = f"{report.get('top', 'dut')}.{report.get('kind', 'unknown')}"
for case in cases:
testcase = ET.SubElement(
suite,
"testcase",
{
"classname": classname,
"name": str(case.get("name", "unnamed")),
},
)
status = str(case.get("status", "pass"))
message = str(case.get("message", ""))
details = str(case.get("details", ""))
if status == "fail":
failure = ET.SubElement(testcase, "failure", {"message": message})
failure.text = details
elif status == "error":
error = ET.SubElement(testcase, "error", {"message": message})
error.text = details
out_path = Path(path).resolve()
out_path.parent.mkdir(parents=True, exist_ok=True)
tree = ET.ElementTree(suite)
tree.write(out_path, encoding="utf-8", xml_declaration=True)
print(f"{LOG_PREFIX} JUnit report written: {out_path}")
def write_csv_report(
path: str | None,
report: dict[str, Any],
testcases: list[dict[str, Any]],
) -> None:
"""输出逐用例 CSV 报告,便于后处理与统计。"""
if not path:
return
out_path = Path(path).resolve()
out_path.parent.mkdir(parents=True, exist_ok=True)
fieldnames = [
"kind",
"top",
"name",
"status",
"message",
"details",
"mode",
"cycle",
"within",
"matched_cycle",
]
with out_path.open("w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
rows = testcases
if not rows:
rows = [
{
"name": "slwchipverify_run",
"status": "pass" if report.get("result") == "pass" else "fail",
"message": str(report.get("result", "unknown")),
"details": "",
"mode": "meta",
"cycle": None,
"within": None,
"matched_cycle": None,
}
]
for row in rows:
writer.writerow(
{
"kind": report.get("kind", "unknown"),
"top": report.get("top", "dut"),
"name": row.get("name", ""),
"status": row.get("status", ""),
"message": row.get("message", ""),
"details": row.get("details", ""),
"mode": row.get("mode", ""),
"cycle": row.get("cycle", ""),
"within": row.get("within", ""),
"matched_cycle": row.get("matched_cycle", ""),
}
)
print(f"{LOG_PREFIX} CSV report written: {out_path}")
def main() -> int:
"""CLI 主入口。"""
args = parse_args()
try:
if args.command == "run":
return verify_run(args)
raise ChipVerifyError(f"Unknown command: {args.command}")
except ChipVerifyError as exc:
print(f"{LOG_PREFIX} ERROR: {exc}", file=sys.stderr)
return 2
except subprocess.TimeoutExpired as exc:
print(f"{LOG_PREFIX} ERROR: command timeout: {exc}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())