chipgraph 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chipgraph/__init__.py +7 -0
- chipgraph/adapters/__init__.py +1 -0
- chipgraph/adapters/format/__init__.py +6 -0
- chipgraph/adapters/format/__main__.py +16 -0
- chipgraph/adapters/format/_yaml_lines.py +54 -0
- chipgraph/adapters/format/chip_yaml.py +323 -0
- chipgraph/adapters/format/qsoc_contract.py +246 -0
- chipgraph/adapters/llm/__init__.py +58 -0
- chipgraph/adapters/llm/_budget.py +173 -0
- chipgraph/adapters/llm/_errors.py +50 -0
- chipgraph/adapters/llm/_nda.py +30 -0
- chipgraph/adapters/llm/_retry.py +85 -0
- chipgraph/adapters/llm/anthropic.py +247 -0
- chipgraph/adapters/llm/decide_backend.py +179 -0
- chipgraph/adapters/llm/fake.py +87 -0
- chipgraph/adapters/llm/guard.py +68 -0
- chipgraph/adapters/parser/__init__.py +22 -0
- chipgraph/adapters/parser/cocotb.py +304 -0
- chipgraph/adapters/parser/generic_regex.py +75 -0
- chipgraph/adapters/parser/verible.py +55 -0
- chipgraph/adapters/parser/verilator.py +46 -0
- chipgraph/adapters/review/__init__.py +7 -0
- chipgraph/adapters/review/file.py +123 -0
- chipgraph/adapters/review/github.py +886 -0
- chipgraph/adapters/runner/__init__.py +7 -0
- chipgraph/adapters/runner/local.py +122 -0
- chipgraph/adapters/runtime/__init__.py +5 -0
- chipgraph/adapters/runtime/claude_code/__init__.py +24 -0
- chipgraph/adapters/runtime/claude_code/agents.py +158 -0
- chipgraph/adapters/runtime/claude_code/decisions.py +280 -0
- chipgraph/adapters/runtime/claude_code/guarded.py +310 -0
- chipgraph/adapters/runtime/claude_code/isolation.py +482 -0
- chipgraph/adapters/runtime/claude_code/loop.py +342 -0
- chipgraph/adapters/runtime/claude_code/previous.py +419 -0
- chipgraph/adapters/runtime/claude_code/review.py +492 -0
- chipgraph/adapters/runtime/claude_code/runtime.py +192 -0
- chipgraph/adapters/runtime/claude_code/service.py +2285 -0
- chipgraph/adapters/tool/__init__.py +7 -0
- chipgraph/adapters/tool/cmd.py +229 -0
- chipgraph/adapters/tool/edalize.py +773 -0
- chipgraph/adapters/tool/filelist.py +259 -0
- chipgraph/adapters/tool/pyslang.py +485 -0
- chipgraph/adapters/vcs/__init__.py +1 -0
- chipgraph/adapters/vcs/git.py +521 -0
- chipgraph/app/__init__.py +10 -0
- chipgraph/app/baseline.py +339 -0
- chipgraph/app/build.py +332 -0
- chipgraph/app/checks.py +202 -0
- chipgraph/app/context.py +191 -0
- chipgraph/app/errors.py +11 -0
- chipgraph/app/executors.py +79 -0
- chipgraph/app/findings.py +203 -0
- chipgraph/app/ingest.py +523 -0
- chipgraph/checks/__init__.py +52 -0
- chipgraph/checks/_cdc_analyze.py +272 -0
- chipgraph/checks/_cdc_elaborate.py +455 -0
- chipgraph/checks/_common.py +268 -0
- chipgraph/checks/_cross.py +96 -0
- chipgraph/checks/_model.py +64 -0
- chipgraph/checks/_naming_pyslang.py +235 -0
- chipgraph/checks/_naming_rules.py +127 -0
- chipgraph/checks/_xref_syntax.py +260 -0
- chipgraph/checks/cdc_struct.py +235 -0
- chipgraph/checks/connect.py +353 -0
- chipgraph/checks/cross_chip.py +454 -0
- chipgraph/checks/duplicate.py +153 -0
- chipgraph/checks/filelist.py +323 -0
- chipgraph/checks/format.py +268 -0
- chipgraph/checks/generated.py +151 -0
- chipgraph/checks/hardcode.py +233 -0
- chipgraph/checks/layout.py +155 -0
- chipgraph/checks/naming.py +311 -0
- chipgraph/checks/plan_check.py +901 -0
- chipgraph/checks/ports_diff.py +203 -0
- chipgraph/checks/spec_schema.py +277 -0
- chipgraph/checks/style.py +910 -0
- chipgraph/checks/tb_static.py +599 -0
- chipgraph/checks/trace.py +224 -0
- chipgraph/checks/vendor_guard.py +369 -0
- chipgraph/checks/wrap_check.py +486 -0
- chipgraph/cli/__init__.py +1688 -0
- chipgraph/cli/next_steps.py +137 -0
- chipgraph/core/__init__.py +1 -0
- chipgraph/core/config/__init__.py +93 -0
- chipgraph/core/config/defaults.py +14 -0
- chipgraph/core/config/errors.py +32 -0
- chipgraph/core/config/loader.py +766 -0
- chipgraph/core/config/models.py +845 -0
- chipgraph/core/config/templates.py +340 -0
- chipgraph/core/contracts/__init__.py +75 -0
- chipgraph/core/contracts/_paths.py +26 -0
- chipgraph/core/contracts/agent.py +49 -0
- chipgraph/core/contracts/artifact.py +68 -0
- chipgraph/core/contracts/check.py +76 -0
- chipgraph/core/contracts/event.py +70 -0
- chipgraph/core/contracts/export.py +128 -0
- chipgraph/core/contracts/finding.py +171 -0
- chipgraph/core/contracts/gate.py +50 -0
- chipgraph/core/contracts/rule.py +177 -0
- chipgraph/core/contracts/types.py +45 -0
- chipgraph/core/engine/__init__.py +70 -0
- chipgraph/core/engine/agent_rule.py +1362 -0
- chipgraph/core/engine/baseline.py +283 -0
- chipgraph/core/engine/decide.py +462 -0
- chipgraph/core/engine/fanout.py +1358 -0
- chipgraph/core/engine/gate.py +170 -0
- chipgraph/core/engine/graph.py +647 -0
- chipgraph/core/engine/records.py +60 -0
- chipgraph/core/engine/rules.py +92 -0
- chipgraph/core/engine/scheduler.py +637 -0
- chipgraph/core/engine/select.py +1239 -0
- chipgraph/core/engine/targets.py +342 -0
- chipgraph/core/model/__init__.py +98 -0
- chipgraph/core/model/entities.py +244 -0
- chipgraph/core/model/export.py +16 -0
- chipgraph/core/model/ingest.py +693 -0
- chipgraph/core/model/json_value.py +4 -0
- chipgraph/core/model/keys.py +95 -0
- chipgraph/core/model/model.py +153 -0
- chipgraph/core/model/provenance.py +27 -0
- chipgraph/core/model/query.py +583 -0
- chipgraph/core/model/registry.py +80 -0
- chipgraph/core/model/relations.py +61 -0
- chipgraph/core/model/store.py +279 -0
- chipgraph/core/plugin_api/__init__.py +81 -0
- chipgraph/core/plugin_api/local.py +167 -0
- chipgraph/core/plugin_api/pack.py +159 -0
- chipgraph/core/plugin_api/protocols.py +177 -0
- chipgraph/core/plugin_api/registry.py +111 -0
- chipgraph/core/plugin_api/types.py +94 -0
- chipgraph/core/runtime/__init__.py +41 -0
- chipgraph/core/runtime/executor.py +79 -0
- chipgraph/core/runtime/queue.py +565 -0
- chipgraph/core/runtime/roles/__init__.py +91 -0
- chipgraph/core/runtime/roles/data/author.md +41 -0
- chipgraph/core/runtime/roles/data/critic.md +56 -0
- chipgraph/core/runtime/roles/data/planner.md +38 -0
- chipgraph/core/runtime/roles/data/researcher.md +37 -0
- chipgraph/core/runtime/roles/data/rtl-author.md +64 -0
- chipgraph/core/runtime/roles/data/tb-author.md +48 -0
- chipgraph/core/runtime/roles/data/triage.md +34 -0
- chipgraph/core/runtime/roles/policy.py +299 -0
- chipgraph/core/runtime/roles/registry.py +52 -0
- chipgraph/core/runtime/roles/review.py +275 -0
- chipgraph/core/runtime/roles/schema.py +82 -0
- chipgraph/core/runtime/roles/skills.py +134 -0
- chipgraph/core/runtime/roles/spec.py +232 -0
- chipgraph/core/state/__init__.py +1 -0
- chipgraph/core/state/artifacts.py +156 -0
- chipgraph/core/state/backend.py +117 -0
- chipgraph/core/state/findings.py +151 -0
- chipgraph/core/state/handoff.py +411 -0
- chipgraph/core/state/idempotency.py +59 -0
- chipgraph/core/state/journal.py +238 -0
- chipgraph/core/state/layout.py +82 -0
- chipgraph/core/state/lock.py +195 -0
- chipgraph/core/state/trace.py +225 -0
- chipgraph/learn/__init__.py +96 -0
- chipgraph/learn/draft.py +187 -0
- chipgraph/learn/infer.py +580 -0
- chipgraph/learn/models.py +145 -0
- chipgraph/learn/try_run.py +228 -0
- chipgraph/mcp/__init__.py +12 -0
- chipgraph/mcp/model_tools.py +167 -0
- chipgraph/mcp/server.py +787 -0
- chipgraph/orgs/qnsc/naming-v1.yml +113 -0
- chipgraph/orgs/qnsc/profile.yml +5 -0
- chipgraph/packs/__init__.py +8 -0
- chipgraph/packs/assist/__init__.py +16 -0
- chipgraph/packs/assist/ask/__init__.py +58 -0
- chipgraph/packs/assist/ask/_db.py +57 -0
- chipgraph/packs/assist/ask/_project.py +230 -0
- chipgraph/packs/assist/ask/answer.py +266 -0
- chipgraph/packs/assist/ask/check.py +182 -0
- chipgraph/packs/assist/ask/contract.py +115 -0
- chipgraph/packs/assist/ask/documents.py +157 -0
- chipgraph/packs/assist/ask/fts.py +80 -0
- chipgraph/packs/assist/ask/retrieve.py +331 -0
- chipgraph/packs/assist/audit/__init__.py +383 -0
- chipgraph/packs/assist/pack.yml +13 -0
- chipgraph/packs/assist/triage/__init__.py +64 -0
- chipgraph/packs/assist/triage/contract.py +94 -0
- chipgraph/packs/assist/triage/parse.py +371 -0
- chipgraph/packs/assist/triage/question.py +384 -0
- chipgraph/packs/assist/triage/report.py +160 -0
- chipgraph/packs/assist/triage/rules.py +447 -0
- chipgraph/packs/assist/triage/run.py +224 -0
- chipgraph/packs/digital_rtl/__init__.py +19 -0
- chipgraph/packs/digital_rtl/layout.py +144 -0
- chipgraph/packs/digital_rtl/pack.yml +25 -0
- chipgraph/packs/digital_rtl/plan/__init__.py +34 -0
- chipgraph/packs/digital_rtl/plan/__main__.py +16 -0
- chipgraph/packs/digital_rtl/plan/context.py +82 -0
- chipgraph/packs/digital_rtl/plan/expand.py +94 -0
- chipgraph/packs/digital_rtl/plan/model.py +150 -0
- chipgraph/packs/digital_rtl/plan/resolver.py +400 -0
- chipgraph/packs/digital_rtl/plan/show.py +176 -0
- chipgraph/packs/digital_rtl/pr/__init__.py +11 -0
- chipgraph/packs/digital_rtl/pr/body.py +165 -0
- chipgraph/packs/digital_rtl/pr/gate.py +110 -0
- chipgraph/packs/digital_rtl/pr/open.py +397 -0
- chipgraph/packs/digital_rtl/rtl/__init__.py +9 -0
- chipgraph/packs/digital_rtl/rtl/context.py +272 -0
- chipgraph/packs/digital_rtl/rules/open_pr.yml +30 -0
- chipgraph/packs/digital_rtl/rules/plan.yml +28 -0
- chipgraph/packs/digital_rtl/rules/plan_expand.yml +29 -0
- chipgraph/packs/digital_rtl/rules/pr_gate.yml +27 -0
- chipgraph/packs/digital_rtl/rules/review.yml +31 -0
- chipgraph/packs/digital_rtl/rules/rtl_module.yml +53 -0
- chipgraph/packs/digital_rtl/skills/lang-sv-rtl.md +58 -0
- chipgraph/packs/digital_rtl/skills/org-naming.md +29 -0
- chipgraph/packs/digital_rtl/skills/plan-modules.md +63 -0
- chipgraph/packs/digital_rtl/skills/review-diff.md +41 -0
- chipgraph/packs/digital_rtl/targets/plan.yml +11 -0
- chipgraph/packs/digital_rtl/targets/rtl.yml +20 -0
- chipgraph/packs/digital_rtl/wrapper/__init__.py +24 -0
- chipgraph/packs/digital_rtl/wrapper/common.py +171 -0
- chipgraph/packs/digital_rtl/wrapper/context.py +222 -0
- chipgraph/packs/digital_rtl/wrapper/emacs.py +626 -0
- chipgraph/packs/digital_rtl/wrapper/expansion.py +304 -0
- chipgraph/packs/digital_rtl/wrapper/lisp.py +504 -0
- chipgraph/packs/digital_rtl/wrapper/resolver.py +60 -0
- chipgraph/packs/digital_rtl/wrapper/rules/wrapper.yml +41 -0
- chipgraph/packs/digital_rtl/wrapper/rules/wrapper_gen.yml +35 -0
- chipgraph/packs/digital_rtl/wrapper/skills/wrapper-auto.md +120 -0
- chipgraph/packs/dv/__init__.py +15 -0
- chipgraph/packs/dv/declaration.py +392 -0
- chipgraph/packs/dv/interface.py +596 -0
- chipgraph/packs/dv/pack.yml +14 -0
- chipgraph/packs/dv/rules/tb_module.yml +42 -0
- chipgraph/packs/dv/skills/dv/cocotb.md +72 -0
- chipgraph/packs/spec_core/__init__.py +9 -0
- chipgraph/packs/spec_core/extract/__init__.py +12 -0
- chipgraph/packs/spec_core/extract/_markdown.py +371 -0
- chipgraph/packs/spec_core/extract/_text.py +116 -0
- chipgraph/packs/spec_core/extract/mas.py +975 -0
- chipgraph/packs/spec_core/gen/__init__.py +11 -0
- chipgraph/packs/spec_core/gen/agents_md/__init__.py +148 -0
- chipgraph/packs/spec_core/gen/agents_md/_context.py +239 -0
- chipgraph/packs/spec_core/gen/agents_md/_markers.py +131 -0
- chipgraph/packs/spec_core/gen/agents_md/agents_md.md.j2 +178 -0
- chipgraph/packs/spec_core/gen/diagram/__init__.py +110 -0
- chipgraph/packs/spec_core/gen/diagram/__main__.py +44 -0
- chipgraph/packs/spec_core/gen/diagram/_diagrams.py +403 -0
- chipgraph/packs/spec_core/gen/diagram/_scene.py +163 -0
- chipgraph/packs/spec_core/pack.yml +6 -0
- chipgraph/py.typed +0 -0
- chipgraph-0.1.0.dist-info/METADATA +53 -0
- chipgraph-0.1.0.dist-info/RECORD +252 -0
- chipgraph-0.1.0.dist-info/WHEEL +4 -0
- chipgraph-0.1.0.dist-info/entry_points.txt +61 -0
- chipgraph-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
"""`TbStaticCheck`: a cocotb test read without a simulator and without the RTL (M2-06).
|
|
2
|
+
|
|
3
|
+
The check the `dv/tb_module` rule runs on the testbench Author's test file. It is
|
|
4
|
+
deterministic and never runs a simulator (unless the profile asks for one, see `sim`
|
|
5
|
+
below); it reads only the test file, the Design Model and, when the model lacks a
|
|
6
|
+
module's ports, that module's declaration (`chipgraph.packs.dv.interface`, the same
|
|
7
|
+
interface the Author's context shows). Issues are on the test file, as `file:line`:
|
|
8
|
+
|
|
9
|
+
- `tb_static.missing`: the test file does not exist;
|
|
10
|
+
- `tb_static.syntax`: it does not compile (`ast.parse`);
|
|
11
|
+
- `tb_static.no_cocotb`: it does not import `cocotb`;
|
|
12
|
+
- `tb_static.no_test`: no `@cocotb.test()` coroutine (`tb_static.not_async`: a
|
|
13
|
+
`@cocotb.test()` on a plain `def`);
|
|
14
|
+
- `tb_static.unknown_port`: a `dut.<name>` access (or `getattr(dut, "<name>")`) whose
|
|
15
|
+
name is not a port of the interface nor an allowed handle attribute (`_log`, `_name`,
|
|
16
|
+
`_path`, and the `allow` arg's names, for a documented clock/reset convention);
|
|
17
|
+
`tb_static.dynamic_access`: `getattr(dut, <not a literal>)`, `dut._id(...)` and other
|
|
18
|
+
lookups by a computed name. When no interface is known the port check is skipped
|
|
19
|
+
with one `warning`;
|
|
20
|
+
- `tb_static.forbidden`: anything that could read the RTL or run a program: `open()`,
|
|
21
|
+
`exec`/`eval`/`compile`/`__import__`, `os.system`/`os.popen`/`os.exec*`/`os.spawn*`/
|
|
22
|
+
`os.listdir`/`os.scandir`/`os.walk`, `.read_text()`/`.read_bytes()`/`.open()`,
|
|
23
|
+
importing `subprocess`, `glob`, `pathlib`, `shutil`, `io`, `fileinput`, `linecache`,
|
|
24
|
+
`importlib`, `codecs`, `builtins`, `pty`, and string literals naming an RTL file
|
|
25
|
+
(`.sv`, `.svh`, `.v`, `.vh`) or an `rtl/` path;
|
|
26
|
+
- requirements (from the Design Model, the block's): every id must appear in a
|
|
27
|
+
`# verifies: <ID>[, <ID>...]` comment or in a `@cocotb.test()` docstring at least once
|
|
28
|
+
(`tb_static.req_missing`, a `warning`); an id cited there that is not one of the
|
|
29
|
+
model's requirements is an `error` (`tb_static.req_unknown`).
|
|
30
|
+
|
|
31
|
+
A `fail` has at least one `error` issue; warnings alone pass.
|
|
32
|
+
|
|
33
|
+
Profile example (`.chipgraph.yml`)::
|
|
34
|
+
|
|
35
|
+
adapters:
|
|
36
|
+
tb_static:
|
|
37
|
+
use: tb_static
|
|
38
|
+
test: "dv/{block}/test_{block}.py" # the default
|
|
39
|
+
top: "tiny_{block}" # the module, when the model has none
|
|
40
|
+
filelist: "filelists/{block}.f" # where its declaration is (default: layout)
|
|
41
|
+
allow: [clk_i] # extra dut names (clock/reset conventions)
|
|
42
|
+
sim: # optional: run the test on the RTL too
|
|
43
|
+
use: edalize
|
|
44
|
+
simulator: verilator
|
|
45
|
+
top: "tiny_{block}"
|
|
46
|
+
filelist: "filelists/{block}.f"
|
|
47
|
+
test_module: "dv/{block}/test_{block}.py"
|
|
48
|
+
|
|
49
|
+
`sim` (optional, per profile): an inline adapter config. Only when every static rule
|
|
50
|
+
passed, the named tool adapter (`edalize`, or any check/tool adapter) runs with these
|
|
51
|
+
args and its result becomes this check's: its status, and its issues after the static
|
|
52
|
+
ones. The engine shows a role that must not see RTL only what it may of that result
|
|
53
|
+
(`FeedbackFilter`).
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
from __future__ import annotations
|
|
57
|
+
|
|
58
|
+
import ast
|
|
59
|
+
import io
|
|
60
|
+
import re
|
|
61
|
+
import time
|
|
62
|
+
import tokenize
|
|
63
|
+
from collections.abc import Iterator, Mapping
|
|
64
|
+
from pathlib import Path
|
|
65
|
+
from typing import Any
|
|
66
|
+
|
|
67
|
+
from chipgraph.checks._common import (
|
|
68
|
+
ArgError,
|
|
69
|
+
compute_idempotency_key,
|
|
70
|
+
error_result,
|
|
71
|
+
optional_list_of_str,
|
|
72
|
+
optional_str,
|
|
73
|
+
sha256_file,
|
|
74
|
+
)
|
|
75
|
+
from chipgraph.checks._model import ModelUnavailable, load_model
|
|
76
|
+
from chipgraph.core.contracts import CheckResult, CheckSpec, Issue
|
|
77
|
+
from chipgraph.core.contracts.types import CheckStatus, Severity
|
|
78
|
+
from chipgraph.core.model.model import DesignModel
|
|
79
|
+
from chipgraph.core.plugin_api.registry import PluginError, Registry
|
|
80
|
+
from chipgraph.core.plugin_api.types import ToolContext
|
|
81
|
+
from chipgraph.packs.dv.interface import Interface, block_requirements, project_interface
|
|
82
|
+
|
|
83
|
+
DEFAULT_TEST = "dv/{block}/test_{block}.py"
|
|
84
|
+
"""Where `dv/tb_module` writes a block's test."""
|
|
85
|
+
|
|
86
|
+
HANDLE_ATTRS = frozenset({"_log", "_name", "_path"})
|
|
87
|
+
"""Attributes of a cocotb handle a test may use besides the ports."""
|
|
88
|
+
|
|
89
|
+
FORBIDDEN_MODULES = frozenset(
|
|
90
|
+
{
|
|
91
|
+
"subprocess",
|
|
92
|
+
"glob",
|
|
93
|
+
"pathlib",
|
|
94
|
+
"shutil",
|
|
95
|
+
"io",
|
|
96
|
+
"fileinput",
|
|
97
|
+
"linecache",
|
|
98
|
+
"importlib",
|
|
99
|
+
"codecs",
|
|
100
|
+
"builtins",
|
|
101
|
+
"pty",
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
FORBIDDEN_CALLS = frozenset({"open", "exec", "eval", "compile", "__import__"})
|
|
105
|
+
FORBIDDEN_OS = frozenset({"system", "popen", "listdir", "scandir", "walk", "startfile"})
|
|
106
|
+
FORBIDDEN_OS_PREFIXES = ("exec", "spawn", "posix_spawn")
|
|
107
|
+
FORBIDDEN_METHODS = frozenset({"read_text", "read_bytes", "open", "iterdir", "glob", "rglob"})
|
|
108
|
+
_RTL_LITERAL = re.compile(r"(?:\.(?:sv|svh|v|vh)\b|(?:^|[/\\])rtl[/\\])", re.IGNORECASE)
|
|
109
|
+
_VERIFIES = re.compile(r"#\s*verifies\s*:\s*(?P<ids>.+)$", re.IGNORECASE)
|
|
110
|
+
_ID_TOKEN = re.compile(r"[A-Za-z][A-Za-z0-9_]*(?:-[A-Za-z0-9_]+)+")
|
|
111
|
+
_ID_LIKE = re.compile(r"^[A-Z][A-Z0-9]*(?:[-_][A-Z0-9]+)*-\d+$")
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _fill(template: str, params: Mapping[str, str]) -> str:
|
|
115
|
+
out = template
|
|
116
|
+
for key, value in params.items():
|
|
117
|
+
out = out.replace("{" + key + "}", value).replace("{" + key.upper() + "}", value.upper())
|
|
118
|
+
return out
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _issue(file: str, line: int | None, rule: str, msg: str, severity: Severity = "error") -> Issue:
|
|
122
|
+
return Issue(file=file, line=line, rule=rule, severity=severity, msg=msg)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# --- what the test file does -------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _is_cocotb_test(decorator: ast.expr) -> bool:
|
|
129
|
+
target = decorator.func if isinstance(decorator, ast.Call) else decorator
|
|
130
|
+
return (
|
|
131
|
+
isinstance(target, ast.Attribute)
|
|
132
|
+
and target.attr == "test"
|
|
133
|
+
and isinstance(target.value, ast.Name)
|
|
134
|
+
and target.value.id == "cocotb"
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _tests(tree: ast.Module) -> Iterator[ast.FunctionDef | ast.AsyncFunctionDef]:
|
|
139
|
+
for node in ast.walk(tree):
|
|
140
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and any(
|
|
141
|
+
_is_cocotb_test(d) for d in node.decorator_list
|
|
142
|
+
):
|
|
143
|
+
yield node
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _imports_cocotb(tree: ast.Module) -> bool:
|
|
147
|
+
for node in ast.walk(tree):
|
|
148
|
+
if isinstance(node, ast.Import) and any(
|
|
149
|
+
a.name == "cocotb" or a.name.startswith("cocotb.") for a in node.names
|
|
150
|
+
):
|
|
151
|
+
return True
|
|
152
|
+
if isinstance(node, ast.ImportFrom) and (node.module or "").split(".")[0] == "cocotb":
|
|
153
|
+
return True
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _dotted(node: ast.expr) -> str | None:
|
|
158
|
+
if isinstance(node, ast.Name):
|
|
159
|
+
return node.id
|
|
160
|
+
if isinstance(node, ast.Attribute):
|
|
161
|
+
base = _dotted(node.value)
|
|
162
|
+
return f"{base}.{node.attr}" if base is not None else None
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _forbidden(tree: ast.Module, rel: str) -> list[Issue]:
|
|
167
|
+
issues: list[Issue] = []
|
|
168
|
+
os_names = {"os"}
|
|
169
|
+
for node in ast.walk(tree):
|
|
170
|
+
if isinstance(node, ast.Import):
|
|
171
|
+
for alias in node.names:
|
|
172
|
+
top = alias.name.split(".")[0]
|
|
173
|
+
if top in FORBIDDEN_MODULES:
|
|
174
|
+
issues.append(
|
|
175
|
+
_issue(rel, node.lineno, "tb_static.forbidden", f"import of {alias.name!r}")
|
|
176
|
+
)
|
|
177
|
+
if top == "os" and alias.asname:
|
|
178
|
+
os_names.add(alias.asname)
|
|
179
|
+
elif isinstance(node, ast.ImportFrom):
|
|
180
|
+
module = node.module or ""
|
|
181
|
+
top = module.split(".")[0]
|
|
182
|
+
if top in FORBIDDEN_MODULES:
|
|
183
|
+
issues.append(
|
|
184
|
+
_issue(rel, node.lineno, "tb_static.forbidden", f"import from {module!r}")
|
|
185
|
+
)
|
|
186
|
+
if top == "os":
|
|
187
|
+
for alias in node.names:
|
|
188
|
+
if alias.name in FORBIDDEN_OS or alias.name.startswith(FORBIDDEN_OS_PREFIXES):
|
|
189
|
+
issues.append(
|
|
190
|
+
_issue(
|
|
191
|
+
rel,
|
|
192
|
+
node.lineno,
|
|
193
|
+
"tb_static.forbidden",
|
|
194
|
+
f"import of os.{alias.name}",
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
elif isinstance(node, ast.Call):
|
|
198
|
+
func = node.func
|
|
199
|
+
name = _dotted(func)
|
|
200
|
+
if isinstance(func, ast.Name) and func.id in FORBIDDEN_CALLS:
|
|
201
|
+
issues.append(
|
|
202
|
+
_issue(rel, node.lineno, "tb_static.forbidden", f"call of {func.id}()")
|
|
203
|
+
)
|
|
204
|
+
elif isinstance(func, ast.Attribute):
|
|
205
|
+
base = _dotted(func.value)
|
|
206
|
+
if base in os_names and (
|
|
207
|
+
func.attr in FORBIDDEN_OS or func.attr.startswith(FORBIDDEN_OS_PREFIXES)
|
|
208
|
+
):
|
|
209
|
+
issues.append(
|
|
210
|
+
_issue(rel, node.lineno, "tb_static.forbidden", f"call of {name}()")
|
|
211
|
+
)
|
|
212
|
+
elif func.attr in FORBIDDEN_METHODS:
|
|
213
|
+
issues.append(
|
|
214
|
+
_issue(
|
|
215
|
+
rel,
|
|
216
|
+
node.lineno,
|
|
217
|
+
"tb_static.forbidden",
|
|
218
|
+
f"call of .{func.attr}(): a test reads no file",
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
elif (
|
|
222
|
+
isinstance(node, ast.Constant)
|
|
223
|
+
and isinstance(node.value, str)
|
|
224
|
+
and _RTL_LITERAL.search(node.value)
|
|
225
|
+
):
|
|
226
|
+
issues.append(
|
|
227
|
+
_issue(
|
|
228
|
+
rel,
|
|
229
|
+
node.lineno,
|
|
230
|
+
"tb_static.forbidden",
|
|
231
|
+
"a string naming an RTL file or an rtl/ path: a test never reads the RTL",
|
|
232
|
+
)
|
|
233
|
+
)
|
|
234
|
+
return issues
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _dut_names(tree: ast.Module) -> set[str]:
|
|
238
|
+
"""The names a test calls the DUT handle: `dut`, and the first argument of each
|
|
239
|
+
`@cocotb.test()` coroutine."""
|
|
240
|
+
names = {"dut"}
|
|
241
|
+
for node in _tests(tree):
|
|
242
|
+
if node.args.args:
|
|
243
|
+
names.add(node.args.args[0].arg)
|
|
244
|
+
return names
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _port_issues(tree: ast.Module, rel: str, allowed: frozenset[str]) -> list[Issue]:
|
|
248
|
+
issues: list[Issue] = []
|
|
249
|
+
duts = _dut_names(tree)
|
|
250
|
+
for node in ast.walk(tree):
|
|
251
|
+
if (
|
|
252
|
+
isinstance(node, ast.Attribute)
|
|
253
|
+
and isinstance(node.value, ast.Name)
|
|
254
|
+
and node.value.id in duts
|
|
255
|
+
):
|
|
256
|
+
name = node.attr
|
|
257
|
+
if name in allowed or name in HANDLE_ATTRS:
|
|
258
|
+
continue
|
|
259
|
+
if name.startswith("_"):
|
|
260
|
+
issues.append(
|
|
261
|
+
_issue(
|
|
262
|
+
rel,
|
|
263
|
+
node.lineno,
|
|
264
|
+
"tb_static.dynamic_access",
|
|
265
|
+
f"{node.value.id}.{name}: a lookup into the design's internals; use "
|
|
266
|
+
"only the interface's ports",
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
continue
|
|
270
|
+
issues.append(
|
|
271
|
+
_issue(
|
|
272
|
+
rel,
|
|
273
|
+
node.lineno,
|
|
274
|
+
"tb_static.unknown_port",
|
|
275
|
+
f"{node.value.id}.{name} is not a port of the interface "
|
|
276
|
+
f"(ports: {', '.join(sorted(allowed)) or 'none'}); a test uses only the "
|
|
277
|
+
"interface, never the design's internals",
|
|
278
|
+
)
|
|
279
|
+
)
|
|
280
|
+
elif (
|
|
281
|
+
isinstance(node, ast.Call)
|
|
282
|
+
and isinstance(node.func, ast.Name)
|
|
283
|
+
and node.func.id in ("getattr", "hasattr", "setattr")
|
|
284
|
+
and node.args
|
|
285
|
+
and isinstance(node.args[0], ast.Name)
|
|
286
|
+
and node.args[0].id in duts
|
|
287
|
+
):
|
|
288
|
+
key = node.args[1] if len(node.args) > 1 else None
|
|
289
|
+
if isinstance(key, ast.Constant) and isinstance(key.value, str):
|
|
290
|
+
if key.value not in allowed and key.value not in HANDLE_ATTRS:
|
|
291
|
+
issues.append(
|
|
292
|
+
_issue(
|
|
293
|
+
rel,
|
|
294
|
+
node.lineno,
|
|
295
|
+
"tb_static.unknown_port",
|
|
296
|
+
f"{node.func.id}(dut, {key.value!r}): not a port of the interface",
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
else:
|
|
300
|
+
issues.append(
|
|
301
|
+
_issue(
|
|
302
|
+
rel,
|
|
303
|
+
node.lineno,
|
|
304
|
+
"tb_static.dynamic_access",
|
|
305
|
+
f"{node.func.id}(dut, <computed name>): name the interface's ports "
|
|
306
|
+
"directly",
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
return issues
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
# --- cocotb 2.x types ------------------------------------------------------------------------
|
|
313
|
+
|
|
314
|
+
_ARRAY_ONLY = frozenset(
|
|
315
|
+
{"to_unsigned", "to_signed", "to_bytes", "binstr", "integer", "signed_integer"}
|
|
316
|
+
)
|
|
317
|
+
"""Methods of `LogicArray` that a one-bit signal's value (a `Logic`) does not have."""
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _logic_issues(
|
|
321
|
+
tree: ast.Module, rel: str, widths: Mapping[str, int | str | None]
|
|
322
|
+
) -> list[Issue]:
|
|
323
|
+
"""`dut.<port>.value.to_unsigned()` on a one-bit port: its value is a `Logic`, which has no
|
|
324
|
+
such method (cocotb 2.x), so the test would die with an AttributeError in the simulator."""
|
|
325
|
+
issues: list[Issue] = []
|
|
326
|
+
duts = _dut_names(tree)
|
|
327
|
+
for node in ast.walk(tree):
|
|
328
|
+
if not (isinstance(node, ast.Attribute) and node.attr in _ARRAY_ONLY):
|
|
329
|
+
continue
|
|
330
|
+
value = node.value
|
|
331
|
+
if not (isinstance(value, ast.Attribute) and value.attr == "value"):
|
|
332
|
+
continue
|
|
333
|
+
port = value.value
|
|
334
|
+
if (
|
|
335
|
+
isinstance(port, ast.Attribute)
|
|
336
|
+
and isinstance(port.value, ast.Name)
|
|
337
|
+
and port.value.id in duts
|
|
338
|
+
and widths.get(port.attr) == 1
|
|
339
|
+
):
|
|
340
|
+
issues.append(
|
|
341
|
+
_issue(
|
|
342
|
+
rel,
|
|
343
|
+
node.lineno,
|
|
344
|
+
"tb_static.logic_method",
|
|
345
|
+
f"{port.value.id}.{port.attr}.value.{node.attr}: {port.attr} is one bit wide, "
|
|
346
|
+
f"so its value is a `Logic`, which has no `{node.attr}`; use "
|
|
347
|
+
f"`int({port.value.id}.{port.attr}.value)`",
|
|
348
|
+
)
|
|
349
|
+
)
|
|
350
|
+
return issues
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
# --- requirements --------------------------------------------------------------------------
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _citations(text: str, tree: ast.Module) -> list[tuple[int, str]]:
|
|
357
|
+
"""Every (line, id) a test cites: in `# verifies:` comments, and id-like tokens in the
|
|
358
|
+
docstrings of `@cocotb.test()` coroutines."""
|
|
359
|
+
found: list[tuple[int, str]] = []
|
|
360
|
+
try:
|
|
361
|
+
for tok in tokenize.generate_tokens(io.StringIO(text).readline):
|
|
362
|
+
if tok.type == tokenize.COMMENT:
|
|
363
|
+
match = _VERIFIES.search(tok.string)
|
|
364
|
+
if match:
|
|
365
|
+
for id_ in re.split(r"[\s,;]+", match.group("ids").strip()):
|
|
366
|
+
if id_:
|
|
367
|
+
found.append((tok.start[0], id_.strip(".")))
|
|
368
|
+
except (tokenize.TokenError, SyntaxError):
|
|
369
|
+
pass
|
|
370
|
+
for node in _tests(tree):
|
|
371
|
+
doc = ast.get_docstring(node, clean=False)
|
|
372
|
+
if not doc or not node.body:
|
|
373
|
+
continue
|
|
374
|
+
line = node.body[0].lineno
|
|
375
|
+
for match in _ID_TOKEN.finditer(doc):
|
|
376
|
+
if _ID_LIKE.match(match.group(0)):
|
|
377
|
+
found.append((line, match.group(0)))
|
|
378
|
+
return found
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _requirement_issues(
|
|
382
|
+
rel: str, text: str, tree: ast.Module, model: DesignModel | None, block: str | None
|
|
383
|
+
) -> list[Issue]:
|
|
384
|
+
if model is None or block is None:
|
|
385
|
+
return [
|
|
386
|
+
_issue(
|
|
387
|
+
rel,
|
|
388
|
+
None,
|
|
389
|
+
"tb_static.req_unchecked",
|
|
390
|
+
"no Design Model (run `chipgraph ingest`): requirement citations not checked",
|
|
391
|
+
"warning",
|
|
392
|
+
)
|
|
393
|
+
]
|
|
394
|
+
ids = [r["id"] for r in block_requirements(model, block)]
|
|
395
|
+
known = {
|
|
396
|
+
r.name for r in model.by_kind("requirement")
|
|
397
|
+
} # another block's id is not unknown, only stale ones are
|
|
398
|
+
cited = _citations(text, tree)
|
|
399
|
+
cited_ids = {c for _, c in cited}
|
|
400
|
+
issues = [
|
|
401
|
+
_issue(
|
|
402
|
+
rel,
|
|
403
|
+
line,
|
|
404
|
+
"tb_static.req_unknown",
|
|
405
|
+
f"{id_} is cited but is not a requirement of the Design Model",
|
|
406
|
+
)
|
|
407
|
+
for line, id_ in cited
|
|
408
|
+
if id_ not in known
|
|
409
|
+
]
|
|
410
|
+
issues.extend(
|
|
411
|
+
_issue(
|
|
412
|
+
rel,
|
|
413
|
+
None,
|
|
414
|
+
"tb_static.req_missing",
|
|
415
|
+
f"requirement {id_} of block {block!r} is cited by no `# verifies:` comment or "
|
|
416
|
+
"`@cocotb.test()` docstring",
|
|
417
|
+
"warning",
|
|
418
|
+
)
|
|
419
|
+
for id_ in ids
|
|
420
|
+
if id_ not in cited_ids
|
|
421
|
+
)
|
|
422
|
+
return issues
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
# --- the check -------------------------------------------------------------------------------
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def _project_parts(root: Path, block: str | None) -> dict[str, Any]:
|
|
429
|
+
"""The profile's layout for `block` ({} when there is no readable profile)."""
|
|
430
|
+
from chipgraph.core.config.errors import ConfigError
|
|
431
|
+
from chipgraph.core.config.loader import load
|
|
432
|
+
|
|
433
|
+
try:
|
|
434
|
+
resolved = load(root)
|
|
435
|
+
except (ConfigError, OSError, ValueError):
|
|
436
|
+
return {}
|
|
437
|
+
if resolved is None:
|
|
438
|
+
return {}
|
|
439
|
+
profile = resolved.for_block(block) if block is not None else resolved.profile
|
|
440
|
+
return dict(profile.layout)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def static_issues(
|
|
444
|
+
root: Path,
|
|
445
|
+
rel: str,
|
|
446
|
+
*,
|
|
447
|
+
interface: Interface | None,
|
|
448
|
+
model: DesignModel | None,
|
|
449
|
+
block: str | None,
|
|
450
|
+
allow: frozenset[str] = frozenset(),
|
|
451
|
+
) -> list[Issue]:
|
|
452
|
+
"""Every static issue of the test file `rel` (see the module docstring)."""
|
|
453
|
+
path = root / rel
|
|
454
|
+
if not path.is_file():
|
|
455
|
+
return [_issue(rel, None, "tb_static.missing", f"test file {rel} does not exist")]
|
|
456
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
457
|
+
try:
|
|
458
|
+
tree = ast.parse(text, filename=rel)
|
|
459
|
+
except SyntaxError as exc:
|
|
460
|
+
return [_issue(rel, exc.lineno, "tb_static.syntax", f"does not compile: {exc.msg}")]
|
|
461
|
+
issues: list[Issue] = []
|
|
462
|
+
if not _imports_cocotb(tree):
|
|
463
|
+
issues.append(_issue(rel, None, "tb_static.no_cocotb", "the test does not import cocotb"))
|
|
464
|
+
tests = list(_tests(tree))
|
|
465
|
+
if not tests:
|
|
466
|
+
issues.append(
|
|
467
|
+
_issue(rel, None, "tb_static.no_test", "no `@cocotb.test()` coroutine in the file")
|
|
468
|
+
)
|
|
469
|
+
issues.extend(
|
|
470
|
+
_issue(
|
|
471
|
+
rel,
|
|
472
|
+
t.lineno,
|
|
473
|
+
"tb_static.not_async",
|
|
474
|
+
f"{t.name}: a `@cocotb.test()` must be an `async def`",
|
|
475
|
+
)
|
|
476
|
+
for t in tests
|
|
477
|
+
if not isinstance(t, ast.AsyncFunctionDef)
|
|
478
|
+
)
|
|
479
|
+
issues.extend(_forbidden(tree, rel))
|
|
480
|
+
if interface is None or interface.source is None:
|
|
481
|
+
issues.append(
|
|
482
|
+
_issue(
|
|
483
|
+
rel,
|
|
484
|
+
None,
|
|
485
|
+
"tb_static.no_interface",
|
|
486
|
+
"no interface known for the module: port use not checked",
|
|
487
|
+
"warning",
|
|
488
|
+
)
|
|
489
|
+
)
|
|
490
|
+
else:
|
|
491
|
+
names = {p.name for p in interface.ports} | {
|
|
492
|
+
n for n in (interface.clock, interface.reset) if n
|
|
493
|
+
}
|
|
494
|
+
issues.extend(_port_issues(tree, rel, frozenset(names) | allow))
|
|
495
|
+
issues.extend(_logic_issues(tree, rel, {p.name: p.width for p in interface.ports}))
|
|
496
|
+
issues.extend(_requirement_issues(rel, text, tree, model, block))
|
|
497
|
+
return sorted(issues, key=lambda i: (i.line or 0, i.rule, i.msg))
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
class TbStaticCheck:
|
|
501
|
+
"""Reads a cocotb test file without a simulator and without the RTL."""
|
|
502
|
+
|
|
503
|
+
id = "tb_static"
|
|
504
|
+
name = "Testbench static check"
|
|
505
|
+
|
|
506
|
+
async def run(self, spec: CheckSpec, ctx: ToolContext) -> CheckResult:
|
|
507
|
+
start = time.monotonic()
|
|
508
|
+
args = spec.args
|
|
509
|
+
try:
|
|
510
|
+
test_template = optional_str(args, "test", DEFAULT_TEST)
|
|
511
|
+
allow = frozenset(optional_list_of_str(args, "allow"))
|
|
512
|
+
sim = args.get("sim")
|
|
513
|
+
if sim is not None and not (
|
|
514
|
+
isinstance(sim, Mapping) and isinstance(sim.get("use"), str)
|
|
515
|
+
):
|
|
516
|
+
raise ArgError("'sim' must be an adapter config with a 'use' key")
|
|
517
|
+
except ArgError as exc:
|
|
518
|
+
return error_result(spec, str(exc), start)
|
|
519
|
+
|
|
520
|
+
params = dict(ctx.params)
|
|
521
|
+
block = params.get("block") or None
|
|
522
|
+
rel = _fill(test_template, params)
|
|
523
|
+
if "{" in rel:
|
|
524
|
+
return error_result(spec, f"test path {rel!r} has an unfilled placeholder", start)
|
|
525
|
+
model: DesignModel | None = None
|
|
526
|
+
model_hash = ""
|
|
527
|
+
try:
|
|
528
|
+
loaded = load_model(ctx.repo_root)
|
|
529
|
+
model, model_hash = loaded.model, loaded.build_inputs_hash or ""
|
|
530
|
+
except ModelUnavailable:
|
|
531
|
+
model = None
|
|
532
|
+
interface = project_interface(
|
|
533
|
+
ctx.repo_root,
|
|
534
|
+
model=model,
|
|
535
|
+
block=block,
|
|
536
|
+
params=params,
|
|
537
|
+
args=args,
|
|
538
|
+
layout=_project_parts(ctx.repo_root, block),
|
|
539
|
+
)
|
|
540
|
+
issues = static_issues(
|
|
541
|
+
ctx.repo_root, rel, interface=interface, model=model, block=block, allow=allow
|
|
542
|
+
)
|
|
543
|
+
status: CheckStatus = "fail" if any(i.severity == "error" for i in issues) else "pass"
|
|
544
|
+
log_tail = (
|
|
545
|
+
f"tb_static: {rel}: {sum(i.severity == 'error' for i in issues)} error(s), "
|
|
546
|
+
f"{sum(i.severity == 'warning' for i in issues)} warning(s); interface "
|
|
547
|
+
f"{interface.module or '?'} from {interface.source or 'nowhere'}"
|
|
548
|
+
)
|
|
549
|
+
if status == "pass" and sim is not None:
|
|
550
|
+
sim_result = await _run_sim(dict(sim), ctx, spec.id)
|
|
551
|
+
status = sim_result.status
|
|
552
|
+
issues = [*issues, *sim_result.issues]
|
|
553
|
+
log_tail = f"{log_tail}\n{sim_result.log_tail}".strip()
|
|
554
|
+
path = ctx.repo_root / rel
|
|
555
|
+
files = [(rel, sha256_file(path))] if path.is_file() else []
|
|
556
|
+
files.append(("<model>", model_hash))
|
|
557
|
+
return CheckResult(
|
|
558
|
+
check_id=spec.id,
|
|
559
|
+
status=status,
|
|
560
|
+
issues=tuple(issues),
|
|
561
|
+
log_tail=log_tail[-4000:],
|
|
562
|
+
duration_s=time.monotonic() - start,
|
|
563
|
+
idempotency_key=compute_idempotency_key(spec.id, args, files),
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
async def _run_sim(cfg: dict[str, Any], ctx: ToolContext, check_id: str) -> CheckResult:
|
|
568
|
+
"""Run the inline `sim` adapter config with this check's context."""
|
|
569
|
+
use = str(cfg.pop("use"))
|
|
570
|
+
registry = Registry()
|
|
571
|
+
try:
|
|
572
|
+
registry.discover()
|
|
573
|
+
except PluginError as exc:
|
|
574
|
+
return _sim_error(check_id, f"cannot load the adapters: {exc}")
|
|
575
|
+
plugin = None
|
|
576
|
+
for kind in ("check", "tool"):
|
|
577
|
+
try:
|
|
578
|
+
plugin = registry.get(kind, use)
|
|
579
|
+
break
|
|
580
|
+
except PluginError:
|
|
581
|
+
continue
|
|
582
|
+
if plugin is None:
|
|
583
|
+
return _sim_error(check_id, f"no check or tool adapter named {use!r} (tb_static.sim)")
|
|
584
|
+
sim_spec = CheckSpec(id=f"{check_id}.sim", capability="sim", adapter=use, args=cfg)
|
|
585
|
+
result: CheckResult = await plugin.run(sim_spec, ctx)
|
|
586
|
+
return result
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
def _sim_error(check_id: str, msg: str) -> CheckResult:
|
|
590
|
+
return CheckResult(
|
|
591
|
+
check_id=f"{check_id}.sim",
|
|
592
|
+
status="error",
|
|
593
|
+
issues=(Issue(rule="tb_static.sim", msg=msg),),
|
|
594
|
+
duration_s=0.0,
|
|
595
|
+
idempotency_key=compute_idempotency_key(check_id, {"sim": msg}, ()),
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
__all__ = ["DEFAULT_TEST", "TbStaticCheck", "static_issues"]
|