booley-rtl 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.
Potentially problematic release.
This version of booley-rtl might be problematic. Click here for more details.
- booley/__init__.py +13 -0
- booley/adapterlib/__init__.py +89 -0
- booley/adapterlib/parsers.py +169 -0
- booley/adapterlib/proc.py +181 -0
- booley/adapterlib/request.py +184 -0
- booley/adapterlib/responses.py +265 -0
- booley/adapterlib/runner.py +135 -0
- booley/bootstrap_mode.py +60 -0
- booley/core/__init__.py +9 -0
- booley/core/boundary.py +248 -0
- booley/core/btool_adapter_contract.py +259 -0
- booley/core/btool_adapter_validation.py +411 -0
- booley/core/config_paths.py +36 -0
- booley/core/models.py +114 -0
- booley/core/run_tool.py +168 -0
- booley/core_security.py +401 -0
- booley/data/__init__.py +0 -0
- booley/data/__pycache__/__init__.cpython-314.pyc +0 -0
- booley/data/cheatsheet.md +180 -0
- booley/data/criteria.toml +164 -0
- booley/data/docker/Dockerfile +227 -0
- booley/data/docker/Dockerfile.egress-proxy +7 -0
- booley/data/docker/Dockerfile.reaper +9 -0
- booley/data/docker/Dockerfile.riscv +140 -0
- booley/data/docker/build-riscv.sh +29 -0
- booley/data/docker/build.sh +27 -0
- booley/data/docker/pdk/NangateOpenCellLibrary_typical_ccs.lib +133314 -0
- booley/data/docker/pdk/nangate45/Nangate45.rc +13 -0
- booley/data/docker/pdk/nangate45/Nangate45_stdcell.lef +11552 -0
- booley/data/docker/pdk/nangate45/Nangate45_tech.lef +779 -0
- booley/data/docker/pdk/nangate45/README.md +6 -0
- booley/data/refs/booley_vcd_dump.sv +19 -0
- booley/data/refs/code_review/rtl/functional.md +62 -0
- booley/data/refs/code_review/rtl/ifdef.md +57 -0
- booley/data/refs/code_review/rtl/optimization.md +71 -0
- booley/data/refs/code_review/rtl/protocol-cdc.md +60 -0
- booley/data/refs/code_review/rtl/quality.md +22 -0
- booley/data/refs/code_review/rtl/security.md +56 -0
- booley/data/refs/code_review/testbench/tb-review.md +69 -0
- booley/data/refs/rtl-mutation-testing.md +192 -0
- booley/data/refs/rtl_style_guide.md +48 -0
- booley/data/refs/sim_result_sentinel.sv +23 -0
- booley/data/refs/tb_style_guide.md +312 -0
- booley/data/skills/booley-setup/AGENTS_TEMPLATE.md +25 -0
- booley/data/skills/booley-setup/BOOLEY_TEMPLATE.toml +35 -0
- booley/data/skills/booley-setup/CORE_TEMPLATE.yaml +56 -0
- booley/data/skills/booley-setup/SKILL.md +125 -0
- booley/data/skills/booley-setup/TESTS_TEMPLATE.toml +12 -0
- booley/data/skills/booley-setup/agents/openai.yaml +4 -0
- booley/data/skills/booley-setup/steps/2-project-config.md +399 -0
- booley/data/skills/booley-setup/steps/3-agents-md.md +95 -0
- booley/data/skills/booley-setup/steps/4-simulate.md +79 -0
- booley/data/skills/booley-setup/steps/5-lint.md +57 -0
- booley/data/skills/booley-setup/steps/6-asic-synthesize.md +111 -0
- booley/data/skills/booley-setup/steps/7-doctor.md +58 -0
- booley/data/skills/booley-setup/steps/_adapter-common.md +246 -0
- booley/data/skills/booley-ticket-create/SKILL.md +105 -0
- booley/data/skills/booley-ticket-create/TICKET_TEMPLATE.md +173 -0
- booley/data/skills/booley-ticket-create/ticket-creation.md +306 -0
- booley/data/skills/booley-ticket-triage/SKILL.md +50 -0
- booley/data/skills/booley-ticket-triage/steps/01-board-orphans.md +16 -0
- booley/data/skills/booley-ticket-triage/steps/02-blocked.md +76 -0
- booley/data/skills/booley-ticket-triage/steps/03-review.md +77 -0
- booley/data/skills/booley-ticket-triage/steps/04-summary.md +22 -0
- booley/data/skills/booley-ticket-triage/tool-reference.md +35 -0
- booley/docker/__init__.py +0 -0
- booley/docker/egress_proxy.py +388 -0
- booley/docker/proxy_entry.py +86 -0
- booley/docker/reaper.py +261 -0
- booley/filelock.py +86 -0
- booley/filesystem_utils.py +74 -0
- booley/find_good_commit.py +650 -0
- booley/fusesoc_registry.py +1587 -0
- booley/fusesoc_trace_overlay.py +450 -0
- booley/guidance_links.py +130 -0
- booley/harness/__init__.py +9 -0
- booley/harness/__main__.py +151 -0
- booley/harness/_backend_config.py +492 -0
- booley/harness/_claude_backend.py +884 -0
- booley/harness/_claude_transcript_md.py +117 -0
- booley/harness/_codex_backend.py +699 -0
- booley/harness/_codex_transcript_md.py +224 -0
- booley/harness/_cost.py +48 -0
- booley/harness/_editor_config.py +38 -0
- booley/harness/_limits.py +22 -0
- booley/harness/_retry.py +81 -0
- booley/harness/_ticket_ops.py +484 -0
- booley/harness/agent.py +102 -0
- booley/harness/agent_backend.py +68 -0
- booley/harness/backends/__init__.py +16 -0
- booley/harness/backends/sim_base.py +47 -0
- booley/harness/blocking.py +159 -0
- booley/harness/booley.py +1089 -0
- booley/harness/booley_status_display.py +222 -0
- booley/harness/colors.py +126 -0
- booley/harness/config.py +175 -0
- booley/harness/console/__init__.py +9 -0
- booley/harness/console/app.py +342 -0
- booley/harness/console/console.tcss +42 -0
- booley/harness/console/criteria_format.py +101 -0
- booley/harness/console/events.py +86 -0
- booley/harness/console/links.py +582 -0
- booley/harness/console/path_backtick.py +66 -0
- booley/harness/console/widgets.py +784 -0
- booley/harness/criteria_acceptance.py +545 -0
- booley/harness/debug_journal.py +100 -0
- booley/harness/devcontainer.py +464 -0
- booley/harness/doctor.py +3745 -0
- booley/harness/git_utils.py +476 -0
- booley/harness/init_cmd.py +1352 -0
- booley/harness/init_common.py +70 -0
- booley/harness/init_docker_image.py +408 -0
- booley/harness/init_git_hooks.py +347 -0
- booley/harness/init_skills.py +164 -0
- booley/harness/interactive_docker.py +346 -0
- booley/harness/logging_utils.py +97 -0
- booley/harness/mcp_config.py +121 -0
- booley/harness/models.py +213 -0
- booley/harness/orchestrator.py +1633 -0
- booley/harness/orchestrator_display.py +406 -0
- booley/harness/orchestrator_guardrails.py +90 -0
- booley/harness/orchestrator_probe.py +142 -0
- booley/harness/orchestrator_prompt.py +571 -0
- booley/harness/orphan_handler.py +177 -0
- booley/harness/preflight.py +438 -0
- booley/harness/project_image.py +340 -0
- booley/harness/prompt_artifacts.py +121 -0
- booley/harness/render_md.py +390 -0
- booley/harness/sandbox.py +327 -0
- booley/harness/sim_utils.py +169 -0
- booley/harness/steps/__init__.py +13 -0
- booley/harness/steps/step_00_parse_validate.py +477 -0
- booley/harness/steps/step_01_setup.py +608 -0
- booley/harness/steps/worktree_lock_gc.py +126 -0
- booley/harness/subscription_limit.py +220 -0
- booley/harness/terminal.py +319 -0
- booley/harness/ticket_cli.py +193 -0
- booley/harness/worktree_health.py +105 -0
- booley/heartbeat.py +121 -0
- booley/host_tools/__init__.py +1 -0
- booley/host_tools/__main__.py +214 -0
- booley/host_tools/exceptions.py +43 -0
- booley/host_tools/fixtures/vivado_impl_pass.log +89 -0
- booley/host_tools/fixtures/vivado_routed_real.rpt +41 -0
- booley/host_tools/fixtures/vivado_routed_real_2025_2.rpt +49 -0
- booley/host_tools/fixtures/vivado_timing.rpt +53 -0
- booley/host_tools/job_manager.py +300 -0
- booley/host_tools/job_state.py +98 -0
- booley/host_tools/mcp_proxy.py +250 -0
- booley/host_tools/path_mapper.py +80 -0
- booley/host_tools/result_extractor.py +92 -0
- booley/host_tools/server.py +331 -0
- booley/host_tools/subprocess_runner.py +247 -0
- booley/host_tools/template_loader.py +92 -0
- booley/host_tools/template_renderer.py +181 -0
- booley/host_tools/template_schema.py +90 -0
- booley/host_tools/templates/run_host_command.sh +11 -0
- booley/host_tools/templates/run_host_command.yaml +52 -0
- booley/host_tools/templates/spyglass_lint.tcl +29 -0
- booley/host_tools/templates/spyglass_lint.yaml +37 -0
- booley/host_tools/templates/vivado_synth.tcl +44 -0
- booley/host_tools/templates/vivado_synth.yaml +52 -0
- booley/host_tools/templates/xcelium_sim.sh +29 -0
- booley/host_tools/templates/xcelium_sim.yaml +42 -0
- booley/incontainer_register.py +325 -0
- booley/job_slots.py +617 -0
- booley/mcp_server.py +2411 -0
- booley/nested_mcp_capabilities.py +67 -0
- booley/paths.py +69 -0
- booley/platform_paths.py +149 -0
- booley/project_config.py +406 -0
- booley/project_dir.py +128 -0
- booley/run_mutmut.py +528 -0
- booley/shared_infra.py +418 -0
- booley/sim/bwave_fifo.py +260 -0
- booley/sim/iverilog_run.py +456 -0
- booley/sim/run_guard.py +149 -0
- booley/sim/sim_result.py +285 -0
- booley/sim/trace_session.py +710 -0
- booley/sim/vcs_run.py +269 -0
- booley/sim/verilator_run.py +513 -0
- booley/sim/xcelium_run.py +261 -0
- booley/sim_links.py +142 -0
- booley/ticket_board/__init__.py +272 -0
- booley/ticket_board/__main__.py +7 -0
- booley/ticket_board/analytics.py +413 -0
- booley/ticket_board/archive.py +136 -0
- booley/ticket_board/cli.py +394 -0
- booley/ticket_board/cli_handlers.py +676 -0
- booley/ticket_board/constants.py +99 -0
- booley/ticket_board/criteria_markdown.py +295 -0
- booley/ticket_board/evidence.py +101 -0
- booley/ticket_board/execution.py +248 -0
- booley/ticket_board/frontmatter.py +533 -0
- booley/ticket_board/git_ops.py +151 -0
- booley/ticket_board/helpers.py +225 -0
- booley/ticket_board/io.py +718 -0
- booley/ticket_board/lifecycle.py +177 -0
- booley/ticket_board/logs.py +209 -0
- booley/ticket_board/notifications.py +139 -0
- booley/ticket_board/operations.py +736 -0
- booley/ticket_board/paths.py +145 -0
- booley/ticket_board/reporting.py +382 -0
- booley/ticket_board/run_metrics.py +177 -0
- booley/ticket_board/scanner.py +188 -0
- booley/ticket_board/validation.py +752 -0
- booley/ticket_board/validation_logs.py +163 -0
- booley/tool_lock.py +140 -0
- booley/tools/__init__.py +104 -0
- booley/tools/_baseline_worktree.py +103 -0
- booley/tools/_diff_classify.py +203 -0
- booley/tools/_job_records.py +243 -0
- booley/tools/_run_lock.py +67 -0
- booley/tools/_tool_events.py +176 -0
- booley/tools/asic_synthesize.py +1595 -0
- booley/tools/base.py +801 -0
- booley/tools/bwave.py +892 -0
- booley/tools/bwave_sessions.py +269 -0
- booley/tools/clock_timing.py +167 -0
- booley/tools/commit_git_io.py +150 -0
- booley/tools/commit_message_format.py +130 -0
- booley/tools/commit_msg_hook.py +107 -0
- booley/tools/commit_msg_utils.py +159 -0
- booley/tools/compare_vcd.py +562 -0
- booley/tools/coverage_analyst.py +2663 -0
- booley/tools/coverage_verilog_utils.py +291 -0
- booley/tools/criteria.py +712 -0
- booley/tools/criteria_reference.py +298 -0
- booley/tools/deploy_skills.py +274 -0
- booley/tools/edam.py +413 -0
- booley/tools/elaborate.py +388 -0
- booley/tools/fpga_edam.py +356 -0
- booley/tools/fpga_impl.py +847 -0
- booley/tools/fpga_metrics.py +154 -0
- booley/tools/interface_spec.py +185 -0
- booley/tools/lint.py +693 -0
- booley/tools/mechanical_tool.py +131 -0
- booley/tools/mut_harness_inject.py +257 -0
- booley/tools/mutation_lock.py +523 -0
- booley/tools/mutation_tester.py +2153 -0
- booley/tools/orchestration_state.py +920 -0
- booley/tools/pre-commit-ruff.sh +26 -0
- booley/tools/project_native_backend.py +374 -0
- booley/tools/registry.py +359 -0
- booley/tools/reviewer.py +1763 -0
- booley/tools/rewrite_commits.py +25 -0
- booley/tools/schema_extractor.py +107 -0
- booley/tools/scope_precommit_hook.py +97 -0
- booley/tools/sim_edam.py +393 -0
- booley/tools/simulate.py +2163 -0
- booley/tools/source_fingerprint.py +147 -0
- booley/tools/specialist.py +646 -0
- booley/tools/submit_run_report.py +317 -0
- booley/tools/tb_coder.py +746 -0
- booley/tools/threshold_eval.py +152 -0
- booley/tools/tools_reference.py +175 -0
- booley/tools/validate_commit_msg.py +198 -0
- booley/tools/workspace_isolation.py +696 -0
- booley/tools/worktree_create.sh +508 -0
- booley/venue.py +71 -0
- booley/vivado/xdc/timing.xdc +7 -0
- booley/yosys/abc_config.json +5 -0
- booley/yosys/abc_scripts/abc_aggressive.script +21 -0
- booley/yosys/abc_scripts/abc_balanced.script +15 -0
- booley/yosys/abc_scripts/abc_fast.script +10 -0
- booley/yosys/openroad_timing.py +351 -0
- booley/yosys/run_yosys_syn.py +494 -0
- booley/yosys/sdc/abc_simple.sdc +5 -0
- booley/yosys/syn_config.py +33 -0
- booley/yosys/syn_core.py +1065 -0
- booley/yosys/syn_discovery.py +52 -0
- booley/yosys/syn_parse.py +60 -0
- booley/yosys/syn_subprocess.py +199 -0
- booley/yosys/synthesis_watchdog.py +565 -0
- booley/zombie_cleanup.py +183 -0
- booley_rtl-0.1.0.dist-info/METADATA +251 -0
- booley_rtl-0.1.0.dist-info/RECORD +288 -0
- booley_rtl-0.1.0.dist-info/WHEEL +5 -0
- booley_rtl-0.1.0.dist-info/entry_points.txt +5 -0
- booley_rtl-0.1.0.dist-info/licenses/LICENSE +201 -0
- booley_rtl-0.1.0.dist-info/top_level.txt +2 -0
- bwave/tests/fixtures/real_cvdp/generate.py +599 -0
- bwave/tests/gen_test_vcds.py +418 -0
- bwave/tests/gen_xcelium_dialect_vcd.py +152 -0
- bwave/tests/simulator_ground_truth_test.py +1723 -0
- bwave/tests/test_real_cvdp_fixtures.py +140 -0
- bwave/tests/test_xcelium_dialect.py +148 -0
- bwave/tools/check_schema.py +147 -0
booley/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Booley — orchestrated RTL development harness."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
# Distribution name on PyPI is "booley-rtl"; the import package is "booley".
|
|
7
|
+
__version__ = version("booley-rtl")
|
|
8
|
+
except PackageNotFoundError:
|
|
9
|
+
try:
|
|
10
|
+
# Pre-rename dev/editable installs registered the old distribution name.
|
|
11
|
+
__version__ = version("booley")
|
|
12
|
+
except PackageNotFoundError:
|
|
13
|
+
__version__ = "0.0.0-dev"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Public helper library for Project-native B-Tool adapters.
|
|
2
|
+
|
|
3
|
+
This package is the ONE sanctioned Booley import for project adapters
|
|
4
|
+
(``.booley_project/adapters/*.py``). Everything else under ``booley.*`` stays
|
|
5
|
+
private to adapters: the JSON request/response files remain the only contract
|
|
6
|
+
(docs/BTOOL-ADAPTER-CONTRACTS.md), and this library only makes satisfying that
|
|
7
|
+
contract mechanical. Adapters work without it; with it, the recurring blocks —
|
|
8
|
+
request parsing, response envelopes, identity echoes, stale-artifact guards,
|
|
9
|
+
budgeted subprocess runs, finding/metric normalization, and pre-write
|
|
10
|
+
self-validation against Booley's own validator — come from tested code instead
|
|
11
|
+
of being re-authored per project.
|
|
12
|
+
|
|
13
|
+
Import surface is guaranteed available wherever adapters run: Booley invokes
|
|
14
|
+
adapters with its own interpreter (``sys.executable``), both in the sandbox
|
|
15
|
+
and on the host, so the installed ``booley`` package is importable by
|
|
16
|
+
construction and can never version-skew against the invoking side.
|
|
17
|
+
|
|
18
|
+
Typical adapter::
|
|
19
|
+
|
|
20
|
+
from booley import adapterlib as al
|
|
21
|
+
|
|
22
|
+
def build(req: al.AdapterRequest) -> dict:
|
|
23
|
+
command = ["make", "sim", f"CONFIG={req.target_name}", f"TEST={req.test}"]
|
|
24
|
+
if req.dry_run:
|
|
25
|
+
return al.dry_run_response(req, " ".join(command))
|
|
26
|
+
report = al.fresh_artifact(req.work_dir / "build" / "run_case.report")
|
|
27
|
+
run = al.run_command(command, req)
|
|
28
|
+
verdict = al.derive_sim_verdict(returncode=run.returncode,
|
|
29
|
+
log_text=run.output, timed_out=run.timed_out)
|
|
30
|
+
return al.response(req, verdict, command=run.command,
|
|
31
|
+
log_path=run.log_path, elapsed_s=run.elapsed_s)
|
|
32
|
+
|
|
33
|
+
if __name__ == "__main__":
|
|
34
|
+
raise SystemExit(al.main(build))
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from .parsers import (
|
|
38
|
+
SIM_RESULT_FAILED,
|
|
39
|
+
SIM_RESULT_PASSED,
|
|
40
|
+
VERILATOR_ERROR_RE,
|
|
41
|
+
VERILATOR_WARNING_RE,
|
|
42
|
+
area_to_kge,
|
|
43
|
+
count_sva_errors,
|
|
44
|
+
derive_sim_verdict,
|
|
45
|
+
extract_error_gist,
|
|
46
|
+
first_error_line,
|
|
47
|
+
parse_area_from_stat,
|
|
48
|
+
parse_sim_verdict,
|
|
49
|
+
parse_verilator_findings,
|
|
50
|
+
)
|
|
51
|
+
from .proc import CommandResult, fresh_artifact, run_command
|
|
52
|
+
from .request import AdapterRequest
|
|
53
|
+
from .responses import (
|
|
54
|
+
AdapterError,
|
|
55
|
+
clock_timing,
|
|
56
|
+
dry_run_response,
|
|
57
|
+
finding,
|
|
58
|
+
normalize_severity,
|
|
59
|
+
response,
|
|
60
|
+
synthesis_metrics,
|
|
61
|
+
)
|
|
62
|
+
from .runner import main
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
"SIM_RESULT_FAILED",
|
|
66
|
+
"SIM_RESULT_PASSED",
|
|
67
|
+
"VERILATOR_ERROR_RE",
|
|
68
|
+
"VERILATOR_WARNING_RE",
|
|
69
|
+
"AdapterError",
|
|
70
|
+
"AdapterRequest",
|
|
71
|
+
"CommandResult",
|
|
72
|
+
"area_to_kge",
|
|
73
|
+
"clock_timing",
|
|
74
|
+
"count_sva_errors",
|
|
75
|
+
"derive_sim_verdict",
|
|
76
|
+
"dry_run_response",
|
|
77
|
+
"extract_error_gist",
|
|
78
|
+
"finding",
|
|
79
|
+
"first_error_line",
|
|
80
|
+
"fresh_artifact",
|
|
81
|
+
"main",
|
|
82
|
+
"normalize_severity",
|
|
83
|
+
"parse_area_from_stat",
|
|
84
|
+
"parse_sim_verdict",
|
|
85
|
+
"parse_verilator_findings",
|
|
86
|
+
"response",
|
|
87
|
+
"run_command",
|
|
88
|
+
"synthesis_metrics",
|
|
89
|
+
]
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Log/report parsers shared by the built-in flows and Project-native adapters.
|
|
2
|
+
|
|
3
|
+
WHAT: The pure text-extraction blocks of the built-in tools, importable by
|
|
4
|
+
adapters: sim sentinel verdicts (re-exported from ``booley.sim.sim_result``),
|
|
5
|
+
Verilator warning/error parsing (single source of truth — ``booley.tools.lint``
|
|
6
|
+
imports these regexes), compiler-error gists (shared with
|
|
7
|
+
``booley.tools.elaborate``), and Yosys area extraction (re-exported from
|
|
8
|
+
``booley.yosys.syn_parse``).
|
|
9
|
+
|
|
10
|
+
WHY: An adapter wrapping a Verilator/Icarus/Yosys-based project flow should
|
|
11
|
+
not re-derive parsing the built-in flows already hardened (full-scan sentinel
|
|
12
|
+
rule, QA-7 %Error-means-not-clean, location-less-finding dropping).
|
|
13
|
+
|
|
14
|
+
CONSUMERS: project adapters via ``booley.adapterlib``; ``booley.tools.lint``
|
|
15
|
+
(warning/error regexes) and ``booley.tools.elaborate`` (error gist).
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
# Re-exports: sentinel-based sim verdict + SVA counting (leaf module).
|
|
24
|
+
from booley.sim.sim_result import ( # noqa: F401 — public adapterlib surface
|
|
25
|
+
SIM_RESULT_FAILED,
|
|
26
|
+
SIM_RESULT_PASSED,
|
|
27
|
+
count_sva_errors,
|
|
28
|
+
parse_sim_verdict,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# Re-exports: Yosys stat-file area extraction + kGE conversion (leaf module).
|
|
32
|
+
from booley.yosys.syn_parse import ( # noqa: F401 — public adapterlib surface
|
|
33
|
+
area_to_kge,
|
|
34
|
+
parse_area_from_stat,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
from .responses import finding
|
|
38
|
+
|
|
39
|
+
# Verilator warning lines (also the built-in lint parser's regex):
|
|
40
|
+
# %Warning-UNUSEDSIGNAL: module_a.sv:42:5: Signal is not used: 'foo'
|
|
41
|
+
VERILATOR_WARNING_RE = re.compile(
|
|
42
|
+
r"%Warning-(?P<rule>[A-Z0-9_]+):\s+"
|
|
43
|
+
r"(?P<file>[^:]+):(?P<line>\d+):(?P<col>\d+):\s+"
|
|
44
|
+
r"(?P<message>.+)"
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Verilator emits ``%Error`` (and the summary ``%Error: Exiting due to N
|
|
48
|
+
# error(s)``) for hard failures — undeclared signals, parse errors — and exits
|
|
49
|
+
# non-zero. A warnings-only parser yields zero findings on such a run and
|
|
50
|
+
# would score it as a clean PASS (QA-7); scan for errors separately.
|
|
51
|
+
VERILATOR_ERROR_RE = re.compile(r"^%Error.*", re.MULTILINE)
|
|
52
|
+
|
|
53
|
+
# Located %Error lines — the ones that may become findings. The rule suffix is
|
|
54
|
+
# optional (``%Error-BLKANDNBLK: ...`` vs plain ``%Error: file:line:col: ...``);
|
|
55
|
+
# summary/epilogue errors carry no location and must NOT become findings.
|
|
56
|
+
_VERILATOR_LOCATED_ERROR_RE = re.compile(
|
|
57
|
+
r"%Error(?:-(?P<rule>[A-Z0-9_]+))?:\s+"
|
|
58
|
+
r"(?P<file>[^\s:][^:]*):(?P<line>\d+):(?:(?P<col>\d+):)?\s+"
|
|
59
|
+
r"(?P<message>.+)"
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def first_error_line(output: str) -> str | None:
|
|
64
|
+
"""Return the first Verilator ``%Error`` line in *output*, if any."""
|
|
65
|
+
match = VERILATOR_ERROR_RE.search(output)
|
|
66
|
+
return match.group(0).strip() if match else None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def parse_verilator_findings(output: str) -> list[dict[str, Any]]:
|
|
70
|
+
"""Parse Verilator output into contract-shaped lint findings.
|
|
71
|
+
|
|
72
|
+
``%Warning-RULE: file:line:col: msg`` → ``warning`` findings;
|
|
73
|
+
``%Error[-RULE]: file:line[:col]: msg`` → ``error`` findings. Location-less
|
|
74
|
+
error/summary lines (``%Error: Exiting due to N error(s)``) are dropped —
|
|
75
|
+
Booley rejects findings without a ``file``. Presence of any ``%Error`` line
|
|
76
|
+
should still flip the verdict to ``fail``/``tool_error``; check
|
|
77
|
+
:func:`first_error_line` for that, independently of the findings list.
|
|
78
|
+
"""
|
|
79
|
+
findings: list[dict[str, Any]] = []
|
|
80
|
+
for match in VERILATOR_WARNING_RE.finditer(output):
|
|
81
|
+
findings.append(finding(
|
|
82
|
+
rule=match.group("rule"),
|
|
83
|
+
severity="warning",
|
|
84
|
+
file=match.group("file"),
|
|
85
|
+
line=int(match.group("line")),
|
|
86
|
+
col=int(match.group("col")),
|
|
87
|
+
message=match.group("message"),
|
|
88
|
+
))
|
|
89
|
+
for match in _VERILATOR_LOCATED_ERROR_RE.finditer(output):
|
|
90
|
+
col = match.group("col")
|
|
91
|
+
findings.append(finding(
|
|
92
|
+
rule=match.group("rule") or "ERROR",
|
|
93
|
+
severity="error",
|
|
94
|
+
file=match.group("file"),
|
|
95
|
+
line=int(match.group("line")),
|
|
96
|
+
col=int(col) if col else None,
|
|
97
|
+
message=match.group("message"),
|
|
98
|
+
))
|
|
99
|
+
return findings
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def extract_error_gist(error_output: str) -> str:
|
|
103
|
+
"""Extract a one-line error gist from compiler output.
|
|
104
|
+
|
|
105
|
+
Looks for common error patterns (Verilator, Icarus, sv2v) and returns the
|
|
106
|
+
first meaningful error line, truncated for display — a ready-made
|
|
107
|
+
``first_error`` for simulate responses. (Also the built-in elaborate
|
|
108
|
+
tool's extractor.)
|
|
109
|
+
"""
|
|
110
|
+
if not error_output:
|
|
111
|
+
return ""
|
|
112
|
+
error_re = re.compile(
|
|
113
|
+
r"(?:%Error[^:]*:\s*\S+:\d+:\s*(.+)" # Verilator
|
|
114
|
+
r"|(?:^|\n)\s*(?:error|Error):\s*(.+)" # generic
|
|
115
|
+
r"|(?:^|\n)\s*(\S+:\d+:\s*(?:error|syntax error).+))", # Icarus/sv2v
|
|
116
|
+
)
|
|
117
|
+
m = error_re.search(error_output)
|
|
118
|
+
if m:
|
|
119
|
+
msg = next(g for g in m.groups() if g)
|
|
120
|
+
return msg.strip()[:80]
|
|
121
|
+
# Fallback: last non-empty line.
|
|
122
|
+
for line in reversed(error_output.splitlines()):
|
|
123
|
+
stripped = line.strip()
|
|
124
|
+
if stripped and not stripped.startswith(("-", "=")):
|
|
125
|
+
return stripped[:80]
|
|
126
|
+
return ""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def derive_sim_verdict(
|
|
130
|
+
*,
|
|
131
|
+
returncode: int | None,
|
|
132
|
+
log_text: str,
|
|
133
|
+
timed_out: bool = False,
|
|
134
|
+
pass_sentinels: list[str] | None = None,
|
|
135
|
+
fail_sentinels: list[str] | None = None,
|
|
136
|
+
) -> str:
|
|
137
|
+
"""Derive a simulate verdict — exit code first, sentinels second.
|
|
138
|
+
|
|
139
|
+
Encodes the contract's "Verdict integrity" ordering for the common
|
|
140
|
+
self-checking-testbench case:
|
|
141
|
+
|
|
142
|
+
- ``timed_out`` → ``timeout``;
|
|
143
|
+
- any FAIL sentinel in the log → ``fail`` (a TB that prints FAIL failed,
|
|
144
|
+
whatever the exit code);
|
|
145
|
+
- non-zero exit with no fail sentinel → ``elab_error`` (the build/elab
|
|
146
|
+
died before the test could judge itself — never a functional ``fail``,
|
|
147
|
+
never a ``pass`` inherited from a stale artifact);
|
|
148
|
+
- clean exit + PASS sentinel → ``pass``;
|
|
149
|
+
- clean exit, no sentinel → ``inconclusive`` (a silent run is not
|
|
150
|
+
evidence).
|
|
151
|
+
|
|
152
|
+
Sentinels default to the built-in ``[SIM_RESULT] PASSED/FAILED`` markers;
|
|
153
|
+
pass the project's own wording (booley.toml ``[tools.simulate]``
|
|
154
|
+
pass_sentinels/fail_sentinels) to keep the testbench untouched. Flows with
|
|
155
|
+
richer semantics (SVA counting, per-test reports) should derive their own
|
|
156
|
+
verdict from the lower-level pieces instead.
|
|
157
|
+
"""
|
|
158
|
+
if timed_out:
|
|
159
|
+
return "timeout"
|
|
160
|
+
sentinel = parse_sim_verdict(
|
|
161
|
+
log_text, pass_sentinels=pass_sentinels, fail_sentinels=fail_sentinels
|
|
162
|
+
)
|
|
163
|
+
if sentinel is False:
|
|
164
|
+
return "fail"
|
|
165
|
+
if returncode != 0:
|
|
166
|
+
return "elab_error"
|
|
167
|
+
if sentinel is True:
|
|
168
|
+
return "pass"
|
|
169
|
+
return "inconclusive"
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"""Command execution for Project-native adapters: log capture, timeout, freshness.
|
|
2
|
+
|
|
3
|
+
WHAT: ``run_command`` runs one project flow command with its combined output
|
|
4
|
+
streamed straight to a log file (never buffered whole in adapter memory), a
|
|
5
|
+
timeout derived from the request budget, and process-tree kill on expiry.
|
|
6
|
+
``fresh_artifact`` deletes result files a build/run rewrites, so a verdict can
|
|
7
|
+
never be inherited from a previous run's artifact.
|
|
8
|
+
|
|
9
|
+
WHY: These two blocks are where hand-rolled adapters produced false PASSes
|
|
10
|
+
(contract, "Verdict integrity: never trust a stale artifact") and unstructured
|
|
11
|
+
timeouts (Booley's hard kill leaves no evidence; a budget-aware command timeout
|
|
12
|
+
returns a structured ``timeout`` verdict with a log instead).
|
|
13
|
+
|
|
14
|
+
CONSUMERS: project adapters via ``booley.adapterlib``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import shlex
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any
|
|
26
|
+
|
|
27
|
+
from booley.platform_paths import kill_process_tree, popen_new_group_kwargs
|
|
28
|
+
|
|
29
|
+
from .request import AdapterRequest
|
|
30
|
+
|
|
31
|
+
# How much of the log tail to retain in CommandResult.output (and echo on
|
|
32
|
+
# request) — enough for sentinel scans and error extraction without holding a
|
|
33
|
+
# multi-GB simulation log in memory.
|
|
34
|
+
_KEEP_OUTPUT_BYTES = 2_000_000
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class CommandResult:
|
|
39
|
+
"""Outcome of one ``run_command`` invocation."""
|
|
40
|
+
|
|
41
|
+
command: str
|
|
42
|
+
returncode: int | None
|
|
43
|
+
timed_out: bool
|
|
44
|
+
elapsed_s: float
|
|
45
|
+
log_path: Path
|
|
46
|
+
# Tail of the combined stdout+stderr (capped; full text is in log_path).
|
|
47
|
+
output: str
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def ok(self) -> bool:
|
|
51
|
+
"""True when the command exited 0 within its budget."""
|
|
52
|
+
return self.returncode == 0 and not self.timed_out
|
|
53
|
+
|
|
54
|
+
def tail(self, lines: int = 50) -> str:
|
|
55
|
+
"""Last *lines* lines of the captured output — for ``error_tail`` fields."""
|
|
56
|
+
return "\n".join(self.output.splitlines()[-lines:])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def run_command(
|
|
60
|
+
command: list[str] | str,
|
|
61
|
+
req: AdapterRequest | None = None,
|
|
62
|
+
*,
|
|
63
|
+
log_path: str | Path | None = None,
|
|
64
|
+
cwd: str | Path | None = None,
|
|
65
|
+
env: dict[str, str] | None = None,
|
|
66
|
+
timeout_s: float | None = None,
|
|
67
|
+
echo_tail: bool = True,
|
|
68
|
+
) -> CommandResult:
|
|
69
|
+
"""Run one project flow command with contract-friendly defaults.
|
|
70
|
+
|
|
71
|
+
- *command*: an argv list (exec'd directly), or a string (run through the
|
|
72
|
+
shell — needed for Makefiles that assume bash, cf. contract "Common
|
|
73
|
+
Pitfalls").
|
|
74
|
+
- *log_path*: combined stdout+stderr destination. Defaults to the request's
|
|
75
|
+
``expected_log_path`` (where Booley looks for evidence), else
|
|
76
|
+
``<artifact_dir>/<tool>.log``.
|
|
77
|
+
- *cwd*: defaults to the request's ``work_dir``.
|
|
78
|
+
- *env*: overlaid on the process environment after the request's ``env``.
|
|
79
|
+
- *timeout_s*: defaults to the remaining request budget minus a margin
|
|
80
|
+
(``req.remaining_s()``), so the adapter still has time to parse results
|
|
81
|
+
and write a structured ``timeout`` response before Booley's hard kill.
|
|
82
|
+
On expiry the whole process tree is killed.
|
|
83
|
+
- *echo_tail*: reprint the log tail to stdout afterwards, so the
|
|
84
|
+
``adapter.stdout.log`` Booley archives shows what happened.
|
|
85
|
+
"""
|
|
86
|
+
if log_path is None:
|
|
87
|
+
if req is None:
|
|
88
|
+
raise ValueError("run_command needs log_path when no request is given")
|
|
89
|
+
log_path = req.expected_log_path or (req.artifact_dir / f"{req.tool}.log")
|
|
90
|
+
log = Path(log_path)
|
|
91
|
+
log.parent.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
|
|
93
|
+
shell = isinstance(command, str)
|
|
94
|
+
command_str = command if shell else shlex.join(command)
|
|
95
|
+
if timeout_s is None and req is not None:
|
|
96
|
+
timeout_s = req.remaining_s()
|
|
97
|
+
|
|
98
|
+
run_env = _merged_env(req, env)
|
|
99
|
+
start = time.monotonic()
|
|
100
|
+
# Stream output straight to the log file: no pipe to deadlock on, no
|
|
101
|
+
# multi-GB simulation log held in adapter memory.
|
|
102
|
+
with log.open("w", encoding="utf-8") as sink:
|
|
103
|
+
proc = subprocess.Popen(
|
|
104
|
+
command,
|
|
105
|
+
shell=shell,
|
|
106
|
+
cwd=str(cwd) if cwd is not None else (str(req.work_dir) if req else None),
|
|
107
|
+
stdout=sink,
|
|
108
|
+
stderr=subprocess.STDOUT,
|
|
109
|
+
env=run_env,
|
|
110
|
+
**popen_new_group_kwargs(),
|
|
111
|
+
)
|
|
112
|
+
timed_out = _wait(proc, timeout_s)
|
|
113
|
+
elapsed = time.monotonic() - start
|
|
114
|
+
|
|
115
|
+
output = _read_tail(log)
|
|
116
|
+
if timed_out:
|
|
117
|
+
output += f"\n[adapterlib] command timed out after {timeout_s:.0f}s: {command_str}\n"
|
|
118
|
+
if echo_tail:
|
|
119
|
+
print(f"[adapterlib] $ {command_str}\n{output}", flush=True)
|
|
120
|
+
return CommandResult(
|
|
121
|
+
command=command_str,
|
|
122
|
+
returncode=proc.returncode,
|
|
123
|
+
timed_out=timed_out,
|
|
124
|
+
elapsed_s=elapsed,
|
|
125
|
+
log_path=log,
|
|
126
|
+
output=output,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def fresh_artifact(*paths: str | Path) -> Path | tuple[Path, ...]:
|
|
131
|
+
"""Delete result files the upcoming build/run will (re)write.
|
|
132
|
+
|
|
133
|
+
Call BEFORE launching the flow for every artifact the verdict is read
|
|
134
|
+
from (a ``run_case.report``, a JUnit XML, a log sentinel file): anything
|
|
135
|
+
present afterwards then belongs to *this* invocation only, so a build that
|
|
136
|
+
fails to compile can never inherit the previous run's PASS (contract,
|
|
137
|
+
"Verdict integrity"). Returns the Path(s) for the post-run read-back.
|
|
138
|
+
"""
|
|
139
|
+
cleared = tuple(Path(p) for p in paths)
|
|
140
|
+
for path in cleared:
|
|
141
|
+
path.unlink(missing_ok=True)
|
|
142
|
+
return cleared[0] if len(cleared) == 1 else cleared
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _merged_env(req: AdapterRequest | None, env: dict[str, str] | None) -> dict[str, str] | None:
|
|
146
|
+
"""Process env + request env + explicit overrides (None = inherit as-is)."""
|
|
147
|
+
if req is None and env is None:
|
|
148
|
+
return None
|
|
149
|
+
import os
|
|
150
|
+
|
|
151
|
+
merged = os.environ.copy()
|
|
152
|
+
if req is not None:
|
|
153
|
+
merged.update(req.env)
|
|
154
|
+
if env is not None:
|
|
155
|
+
merged.update({str(k): str(v) for k, v in env.items()})
|
|
156
|
+
return merged
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _wait(proc: subprocess.Popen[Any], timeout_s: float | None) -> bool:
|
|
160
|
+
try:
|
|
161
|
+
proc.wait(timeout=timeout_s)
|
|
162
|
+
except subprocess.TimeoutExpired:
|
|
163
|
+
kill_process_tree(proc)
|
|
164
|
+
proc.wait()
|
|
165
|
+
return True
|
|
166
|
+
except KeyboardInterrupt:
|
|
167
|
+
kill_process_tree(proc)
|
|
168
|
+
raise
|
|
169
|
+
return False
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _read_tail(log: Path, keep_bytes: int = _KEEP_OUTPUT_BYTES) -> str:
|
|
173
|
+
try:
|
|
174
|
+
size = log.stat().st_size
|
|
175
|
+
with log.open("rb") as handle:
|
|
176
|
+
if size > keep_bytes:
|
|
177
|
+
handle.seek(size - keep_bytes)
|
|
178
|
+
return handle.read().decode("utf-8", errors="replace")
|
|
179
|
+
except OSError as exc: # log unreadable — surface why instead of crashing
|
|
180
|
+
print(f"[adapterlib] could not read log tail {log}: {exc}", file=sys.stderr)
|
|
181
|
+
return ""
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Typed read-only view of a Project-native adapter request.
|
|
2
|
+
|
|
3
|
+
WHAT: Wraps the ``request.json`` a B-Tool writes for a Project-native adapter
|
|
4
|
+
(see docs/BTOOL-ADAPTER-CONTRACTS.md "Common Request") behind named accessors,
|
|
5
|
+
so adapter code reads ``req.target_name`` instead of chasing raw dict keys —
|
|
6
|
+
and gets the renamed-key mistakes (``config`` vs ``target``) for free.
|
|
7
|
+
|
|
8
|
+
WHY: Every adapter used to hand-parse the request dict; typos in nested keys
|
|
9
|
+
surfaced only as downstream ``contract_error``. A single typed view makes the
|
|
10
|
+
request shape discoverable and keeps adapters resilient to additive schema
|
|
11
|
+
growth (unknown keys are simply not exposed).
|
|
12
|
+
|
|
13
|
+
CONSUMERS: project adapters via ``booley.adapterlib``; ``runner.main`` builds
|
|
14
|
+
one per invocation.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import time
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Any
|
|
24
|
+
|
|
25
|
+
# The tool-specific request section is not always named after the tool:
|
|
26
|
+
# asic_synthesize nests its extras under "synthesis" (contract, "Common Request").
|
|
27
|
+
_TOOL_SECTION_KEY = {
|
|
28
|
+
"simulate": "simulate",
|
|
29
|
+
"lint": "lint",
|
|
30
|
+
"asic_synthesize": "synthesis",
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class AdapterRequest:
|
|
36
|
+
"""One adapter invocation's request, plus where the response must go."""
|
|
37
|
+
|
|
38
|
+
raw: dict[str, Any]
|
|
39
|
+
request_path: Path
|
|
40
|
+
response_path: Path
|
|
41
|
+
# Monotonic clock at load time — anchors ``remaining_s`` so command
|
|
42
|
+
# timeouts leave room to write the response before Booley's hard kill.
|
|
43
|
+
loaded_at: float = field(default_factory=time.monotonic)
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def load(cls, request_path: str | Path, response_path: str | Path) -> AdapterRequest:
|
|
47
|
+
"""Read ``request.json`` and wrap it (raises on missing/invalid JSON)."""
|
|
48
|
+
path = Path(request_path)
|
|
49
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
50
|
+
if not isinstance(raw, dict):
|
|
51
|
+
raise ValueError(f"adapter request must be a JSON object: {path}")
|
|
52
|
+
return cls(raw=raw, request_path=path, response_path=Path(response_path))
|
|
53
|
+
|
|
54
|
+
# -- common envelope ----------------------------------------------------
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def schema_version(self) -> int:
|
|
58
|
+
return int(self.raw.get("schema_version", 1))
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def tool(self) -> str:
|
|
62
|
+
return str(self.raw.get("tool", ""))
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def work_dir(self) -> Path:
|
|
66
|
+
return Path(str(self.raw.get("work_dir", ".")))
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def artifact_dir(self) -> Path:
|
|
70
|
+
return Path(str(self.raw.get("artifact_dir", ".")))
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def timeout_s(self) -> float:
|
|
74
|
+
try:
|
|
75
|
+
return float(self.raw.get("timeout_s", 600))
|
|
76
|
+
except (TypeError, ValueError):
|
|
77
|
+
return 600.0
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def dry_run(self) -> bool:
|
|
81
|
+
return bool(self.raw.get("dry_run", False))
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def env(self) -> dict[str, str]:
|
|
85
|
+
"""Explicit environment additions supplied by Booley."""
|
|
86
|
+
raw = self.raw.get("env")
|
|
87
|
+
return {str(k): str(v) for k, v in raw.items()} if isinstance(raw, dict) else {}
|
|
88
|
+
|
|
89
|
+
# -- resolved Target ----------------------------------------------------
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def target(self) -> dict[str, Any]:
|
|
93
|
+
"""The Booley-resolved Target object (``name``, ``defines``, ...)."""
|
|
94
|
+
raw = self.raw.get("target")
|
|
95
|
+
return dict(raw) if isinstance(raw, dict) else {}
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def target_name(self) -> str:
|
|
99
|
+
"""The requested config name — echo back verbatim as ``resolved_target``."""
|
|
100
|
+
return str(self.target.get("name", ""))
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def top_module(self) -> str:
|
|
104
|
+
return str(self.raw.get("top_module", self.target.get("top_module", "")))
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def tb_top(self) -> str:
|
|
108
|
+
return str(self.raw.get("tb_top", self.target.get("tb_top", "")))
|
|
109
|
+
|
|
110
|
+
@property
|
|
111
|
+
def defines(self) -> list[str]:
|
|
112
|
+
raw = self.raw.get("defines", self.target.get("defines", []))
|
|
113
|
+
return [str(d) for d in raw] if isinstance(raw, list) else []
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def parameters(self) -> dict[str, Any]:
|
|
117
|
+
raw = self.raw.get("parameters", self.target.get("parameters", {}))
|
|
118
|
+
return dict(raw) if isinstance(raw, dict) else {}
|
|
119
|
+
|
|
120
|
+
@property
|
|
121
|
+
def tests(self) -> list[str]:
|
|
122
|
+
raw = self.raw.get("tests", self.target.get("tests", []))
|
|
123
|
+
return [str(t) for t in raw] if isinstance(raw, list) else []
|
|
124
|
+
|
|
125
|
+
# -- tool-specific section ----------------------------------------------
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def tool_section(self) -> dict[str, Any]:
|
|
129
|
+
"""The tool-specific request block (``simulate``/``lint``/``synthesis``)."""
|
|
130
|
+
key = _TOOL_SECTION_KEY.get(self.tool, self.tool)
|
|
131
|
+
raw = self.raw.get(key)
|
|
132
|
+
return dict(raw) if isinstance(raw, dict) else {}
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def test(self) -> str | None:
|
|
136
|
+
"""simulate: the resolved test name, or ``None`` when no test was selected.
|
|
137
|
+
|
|
138
|
+
When ``None``, the response must omit ``resolved_test`` (or return it as
|
|
139
|
+
null) — the contract forbids inventing one. ``responses.response`` obeys
|
|
140
|
+
this automatically.
|
|
141
|
+
"""
|
|
142
|
+
value = self.tool_section.get("test")
|
|
143
|
+
return None if value is None else str(value)
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def trace(self) -> bool:
|
|
147
|
+
"""simulate: whether a waveform trace was requested."""
|
|
148
|
+
return bool(self.tool_section.get("trace", False))
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def scope(self) -> str:
|
|
152
|
+
"""lint: comma-separated file filter from ``--scope`` (empty = all files)."""
|
|
153
|
+
return str(self.tool_section.get("scope", ""))
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def expected_log_path(self) -> Path | None:
|
|
157
|
+
"""Where Booley expects the tool log; a good default for ``run_command``."""
|
|
158
|
+
value = self.tool_section.get("expected_log_path")
|
|
159
|
+
return Path(str(value)) if value else None
|
|
160
|
+
|
|
161
|
+
@property
|
|
162
|
+
def expected_trace_path(self) -> Path | None:
|
|
163
|
+
"""simulate: where Booley expects the trace when ``trace`` is true."""
|
|
164
|
+
value = self.tool_section.get("expected_trace_path")
|
|
165
|
+
return Path(str(value)) if value else None
|
|
166
|
+
|
|
167
|
+
@property
|
|
168
|
+
def expected_report_dir(self) -> Path | None:
|
|
169
|
+
"""asic_synthesize: where Booley expects report files."""
|
|
170
|
+
value = self.tool_section.get("expected_report_dir")
|
|
171
|
+
return Path(str(value)) if value else None
|
|
172
|
+
|
|
173
|
+
# -- budget ---------------------------------------------------------------
|
|
174
|
+
|
|
175
|
+
def remaining_s(self, *, margin_s: float = 10.0, floor_s: float = 5.0) -> float:
|
|
176
|
+
"""Seconds left of the request budget, minus *margin_s* to finish up.
|
|
177
|
+
|
|
178
|
+
Booley hard-kills the adapter process tree at ``timeout_s``; commands
|
|
179
|
+
launched with this budget leave the adapter time to parse results and
|
|
180
|
+
write the response, so a slow tool becomes a structured ``timeout``
|
|
181
|
+
verdict instead of a synthetic one with no evidence.
|
|
182
|
+
"""
|
|
183
|
+
elapsed = time.monotonic() - self.loaded_at
|
|
184
|
+
return max(floor_s, self.timeout_s - elapsed - margin_s)
|