Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions bazel/rules/rules_score/private/dependable_element.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -1026,9 +1026,12 @@ def _dependable_element_index_impl(ctx):
lobster_rst_dir = ctx.actions.declare_directory(
ctx.label.name + "/traceability_report",
)
package = ctx.label.package
package_depth = len(package.split("/")) if package else 0
source_root = "/".join([".." for _ in range(package_depth + 2)]) + "/"

source_root = ctx.var.get("LOBSTER_SOURCE_ROOT", "")
if not source_root:
package = ctx.label.package
package_depth = len(package.split("/")) if package else 0
source_root = "/".join([".." for _ in range(package_depth + 2)]) + "/"
rst_args = ctx.actions.args()
rst_args.add(lobster_report_file.path)
rst_args.add_all(["--out-dir", lobster_rst_dir.path])
Expand Down
12 changes: 12 additions & 0 deletions bazel/rules/rules_score/private/sphinx_module.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,18 @@ def _score_html_impl(ctx):
merge_inputs.append(dep_html_dir)
merge_args.extend(["--dep", dep_name + ":" + dep_html_dir.path])

# Auto-detect static files from srcs: any file whose short_path contains
# '/_static/' is a static asset that Sphinx may not copy correctly in the
# Bazel sandbox (confdir != srcdir prevents html_static_path from resolving).
# Copy them explicitly into output/_static/ via the merge step.
for orig_file in ctx.files.srcs:
path = orig_file.short_path
static_marker = "/_static/"
if static_marker in path:
subpath = path[path.index(static_marker) + len(static_marker):]
merge_args.extend(["--extra-static", orig_file.path + ":" + subpath])
merge_inputs.append(orig_file)

# Merging html files
ctx.actions.run(
inputs = merge_inputs,
Expand Down
32 changes: 30 additions & 2 deletions bazel/rules/rules_score/src/sphinx_html_merge.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,20 +138,30 @@ def copy_tree(src, dst, rel_path):
copy_tree(src_path, dst_path, Path("."))


def merge_html_dirs(output_dir, main_html_dir, dependencies):
def merge_html_dirs(output_dir, main_html_dir, dependencies, extra_static=None):
"""Merge HTML directories.

Args:
output_dir: Target output directory
main_html_dir: Main module's HTML directory to copy as-is
dependencies: List of (name, path) tuples for dependency modules
extra_static: List of (src_file, dest_subpath) tuples for extra files to
place in output/_static/. These are copied AFTER the main
HTML so they overwrite any theme-provided files if needed.
"""
output_path = Path(output_dir)

# First, copy the main HTML directory
logging.info("Copying main HTML from %s to %s", main_html_dir, output_dir)
copy_html_files(main_html_dir, output_dir)

# Copy any extra static files into output/_static/
for src_file, dest_subpath in extra_static or []:
dst = output_path / "_static" / dest_subpath
dst.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(src_file, dst)
logging.info("Copied extra static %s → _static/%s", src_file, dest_subpath)

# Collect all dependency names for link fixing and exclusion
dep_names = [name for name, _ in dependencies]

Expand Down Expand Up @@ -187,6 +197,13 @@ def main():
metavar="NAME:PATH",
help="Dependency HTML directory in format NAME:PATH",
)
parser.add_argument(
"--extra-static",
action="append",
default=[],
metavar="SRC:SUBPATH",
help="Extra file to place in output/_static/. Format: SRC_FILE:DEST_SUBPATH",
)
parser.add_argument(
"--log-level",
choices=["error", "warn", "info", "debug"],
Expand All @@ -212,8 +229,19 @@ def main():
name, path = dep_spec.split(":", 1)
dependencies.append((name, path))

# Parse extra static files
extra_static = []
for spec in args.extra_static:
if ":" not in spec:
logging.error(
"Invalid --extra-static format '%s', expected SRC:SUBPATH", spec
)
return 1
src, subpath = spec.split(":", 1)
extra_static.append((src, subpath))

# Merge the HTML directories
merge_html_dirs(args.output, args.main, dependencies)
merge_html_dirs(args.output, args.main, dependencies, extra_static=extra_static)

logging.info("Successfully merged HTML into %s", args.output)
return 0
Expand Down
4 changes: 4 additions & 0 deletions bazel/rules/rules_score/templates/conf.template.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,10 @@
plantuml = f"{plantuml_path} -Playout=smetana"
plantuml_output_format = "svg_obj"

import shutil as _shutil

graphviz_dot = os.environ.get("GRAPHVIZ_DOT") or _shutil.which("dot") or "dot"

# HTML theme
html_theme = "sphinx_rtd_theme"

Expand Down
78 changes: 59 additions & 19 deletions tools/lobster_rst_report/_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@

from typing import Dict, Tuple

import os

from lobster.common.report import Report
from lobster.common.location import (
Void_Reference,
Expand All @@ -32,6 +34,7 @@
Codebeamer_Reference,
)
from lobster.common.items import Item, Requirement, Implementation, Activity
from .graphviz_utils import is_dot_available


class RstUtils:
Expand Down Expand Up @@ -176,6 +179,8 @@ def location_link(location, source_root: str = "") -> str:
# lobster-trace: rst_req.RST_Source_Root_Prefix
if isinstance(location, File_Reference):
href = source_root + location.filename if source_root else location.filename
if location.line:
href += f"#L{location.line}"
return f"`{e(location.to_string())} <{href}>`__"

# lobster-trace: UseCases.Item_GitHub_Source
Expand Down Expand Up @@ -339,9 +344,38 @@ def dot_escape(text: str) -> str:
"""
return text.replace("\\", "\\\\").replace('"', '\\"')

@classmethod
def _build_dot_lines(cls, report: Report) -> list:
"""Return the raw DOT diagram lines (without the RST directive wrapper)."""
lines = []
lines.append("digraph tracing_policy {")
lines.append(" rankdir=TB;")
lines.append(
' node [shape=box, style=filled, fontname="Helvetica", margin="0.3,0.1"];'
)
lines.append(" edge [arrowhead=open];")
lines.append("")
for level_name, level in report.config.items():
fill, font = cls.KIND_COLORS.get(level.kind, ("#9E9E9E", "white"))
safe = cls.dot_escape(level_name)
lines.append(f' "{safe}" [fillcolor="{fill}", fontcolor="{font}"];')
for level_name, level in report.config.items():
for trace_target in level.traces:
src = cls.dot_escape(level_name)
dst = cls.dot_escape(trace_target)
lines.append(f' "{src}" -> "{dst}";')
lines.append("}")
return lines

@classmethod
def build(cls, report: Report, indent: int = 0) -> list:
"""Return RST lines for a ``.. graphviz::`` tracing-policy diagram.
"""Return RST lines for the tracing-policy diagram.

When Graphviz ``dot`` is available, emits a ``.. graphviz::`` directive
that Sphinx renders as an image. When ``dot`` is not found, emits a
``.. note::`` explaining how to install Graphviz together with the raw
DOT source in a ``.. code-block::`` so the diagram can still be
visualised manually.

Args:
report: The loaded LOBSTER report whose ``config`` provides level
Expand All @@ -356,28 +390,34 @@ def build(cls, report: Report, indent: int = 0) -> list:
# lobster-trace: rst_req.RST_Report_Tracing_Policy_Diagram
indent_str = " " * indent
nested_indent = indent_str + " "
dot_lines = cls._build_dot_lines(report)

# Respect an explicit GRAPHVIZ_DOT env var set by the build system
# (e.g. via --action_env=GRAPHVIZ_DOT=/usr/bin/dot in CI).
dot_bin = os.environ.get("GRAPHVIZ_DOT") or None
if is_dot_available(dot_bin):
out = []
out.append(f"{indent_str}.. graphviz::")
out.append("")
for dot_line in dot_lines:
out.append(f"{nested_indent}{dot_line}")
out.append("")
return out
out = []
out.append(f"{indent_str}.. graphviz::")
out.append(f"{indent_str}.. note::")
out.append("")
out.append(f"{nested_indent}digraph tracing_policy {{")
out.append(f"{nested_indent} rankdir=TB;")
out.append(
f"{nested_indent} node [shape=box, style=filled, "
f'fontname="Helvetica", margin="0.3,0.1"];'
f"{nested_indent}The tracing-policy diagram below could not be rendered "
f"because the Graphviz ``dot`` utility was not found."
)
out.append(
f"{nested_indent}Install `Graphviz <https://graphviz.org>`__ and rebuild "
f"to see the diagram as an image."
)
out.append(f"{nested_indent} edge [arrowhead=open];")
out.append("")
for level_name, level in report.config.items():
fill, font = cls.KIND_COLORS.get(level.kind, ("#9E9E9E", "white"))
safe = cls.dot_escape(level_name)
out.append(
f'{nested_indent} "{safe}" [fillcolor="{fill}", fontcolor="{font}"];'
)
for level_name, level in report.config.items():
for trace_target in level.traces:
src = cls.dot_escape(level_name)
dst = cls.dot_escape(trace_target)
out.append(f'{nested_indent} "{src}" -> "{dst}";')
out.append(f"{nested_indent}}}")
out.append(f"{nested_indent}.. code-block:: dot")
out.append("")
for dot_line in dot_lines:
out.append(f"{nested_indent} {dot_line}")
out.append("")
return out
39 changes: 16 additions & 23 deletions tools/lobster_rst_report/_renderers.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,14 +328,11 @@ def build(self) -> list:


class CoverageGridBuilder:
"""Build the coverage summary section (table + policy diagram side by side).
"""Build the coverage summary table.

Uses a sphinx-design ``.. grid:: 1 1 2 2`` layout:

* Left column (7/12 width on desktop) -- a ``.. list-table::`` with
per-level coverage statistics and links to each level.
* Right column (5/12 width on desktop) -- the :class:`PolicyDiagramBuilder`
graphviz diagram showing level kinds and tracing relationships.
Emits a plain ``.. list-table::`` with per-level coverage statistics and
links to each level. The tracing-policy diagram is emitted separately
(above this table) by the caller via :class:`PolicyDiagramBuilder`.

Usage::

Expand All @@ -352,7 +349,7 @@ def __init__(self, report: Report):
self._report = report

def build(self, ref_fn) -> list:
"""Return RST lines for the coverage grid.
"""Return RST lines for the coverage table.

Args:
ref_fn: A callable ``(level_name: str) -> str`` returning an RST
Expand All @@ -365,29 +362,25 @@ def build(self, ref_fn) -> list:
# lobster-trace: UseCases.Item_Coverage
# lobster-trace: rst_req.RST_Report_Coverage_Table
lines = []
lines += [".. grid:: 1 1 2 2", " :gutter: 3", ""]
lines += [" .. grid-item::", " :columns: 12 12 7 7", ""]
lines += [
" .. list-table::",
" :header-rows: 1",
" :widths: 35 15 15 15",
".. list-table::",
" :header-rows: 1",
" :widths: 35 15 15 15",
"",
]
lines += [
" * - Category",
" - Coverage",
" - OK Items",
" - Total Items",
" * - Category",
" - Coverage",
" - OK Items",
" - Total Items",
]
for level_name in self._report.config:
data = self._report.coverage[level_name]
lines.append(f" * - {ref_fn(level_name)}")
lines.append(f" - {data.coverage:.1f}%")
lines.append(f" - {data.ok}")
lines.append(f" - {data.items}")
lines.append(f" * - {ref_fn(level_name)}")
lines.append(f" - {data.coverage:.1f}%")
lines.append(f" - {data.ok}")
lines.append(f" - {data.items}")
lines.append("")
lines += [" .. grid-item::", " :columns: 12 12 5 5", ""]
lines += PolicyDiagramBuilder.build(self._report, indent=6)
return lines


Expand Down
23 changes: 20 additions & 3 deletions tools/lobster_rst_report/rst_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
from lobster.common.errors import LOBSTER_Error
from .graphviz_utils import is_dot_available

from ._helpers import RstUtils, ItemNaming
from ._helpers import RstUtils, ItemNaming, PolicyDiagramBuilder
from ._renderers import (
_KIND_ORDER,
_build_page_map,
Expand Down Expand Up @@ -89,7 +89,12 @@ def write_rst(report: Report, source_root: str = "") -> str:
lines.append(f"| LOBSTER Version: {LOBSTER_VERSION}")
lines.append("")

# Coverage table + tracing-policy diagram (rubric = not a TOC entry)
# Tracing-policy diagram (rubric = not a TOC entry)
lines.append(".. rubric:: Tracing Policy")
lines.append("")
lines += PolicyDiagramBuilder.build(report)

# Coverage table (rubric = not a TOC entry)
lines.append(".. rubric:: Coverage Summary")
lines.append("")

Expand Down Expand Up @@ -222,7 +227,12 @@ def write_rst_pages(report: Report, source_root: str = "") -> Dict[str, str]:
lines.append(f"| LOBSTER Version: {LOBSTER_VERSION}")
lines.append("")

# Coverage table + policy diagram (rubric = not a TOC entry)
# Tracing-policy diagram (rubric = not a TOC entry)
lines.append(".. rubric:: Tracing Policy")
lines.append("")
lines += PolicyDiagramBuilder.build(report)

# Coverage table (rubric = not a TOC entry)
lines.append(".. rubric:: Coverage Summary")
lines.append("")

Expand All @@ -239,14 +249,21 @@ def ref_fn(n):

# Per-kind toctrees -- :caption: shows in sidebar but doesn't create a
# heading node, so clicking a level goes straight to that level's page
# The first toctree includes 'self' so the overview page appears as a
# navigation item alongside the per-level sub-pages.
first = True
for kind, kind_title in _KIND_ORDER:
levels_of_kind = [lv for lv in report.config.values() if lv.kind == kind]
if not levels_of_kind:
continue
lines.append(".. toctree::")
lines.append(f" :caption: {kind_title}")
lines.append(" :maxdepth: 1")
lines.append(" :hidden:")
lines.append("")
if first:
lines.append(" Overview <self>")
first = False
for lv in levels_of_kind:
lines.append(f" {page_map[lv.name]}")
lines.append("")
Expand Down
Loading