2424
2525from typing import Dict , Tuple
2626
27+ import os
28+
2729from lobster .common .report import Report
2830from lobster .common .location import (
2931 Void_Reference ,
3234 Codebeamer_Reference ,
3335)
3436from lobster .common .items import Item , Requirement , Implementation , Activity
37+ from .graphviz_utils import is_dot_available
3538
3639
3740class RstUtils :
@@ -176,6 +179,8 @@ def location_link(location, source_root: str = "") -> str:
176179 # lobster-trace: rst_req.RST_Source_Root_Prefix
177180 if isinstance (location , File_Reference ):
178181 href = source_root + location .filename if source_root else location .filename
182+ if location .line :
183+ href += f"#L{ location .line } "
179184 return f"`{ e (location .to_string ())} <{ href } >`__"
180185
181186 # lobster-trace: UseCases.Item_GitHub_Source
@@ -339,9 +344,38 @@ def dot_escape(text: str) -> str:
339344 """
340345 return text .replace ("\\ " , "\\ \\ " ).replace ('"' , '\\ "' )
341346
347+ @classmethod
348+ def _build_dot_lines (cls , report : Report ) -> list :
349+ """Return the raw DOT diagram lines (without the RST directive wrapper)."""
350+ lines = []
351+ lines .append ("digraph tracing_policy {" )
352+ lines .append (" rankdir=TB;" )
353+ lines .append (
354+ ' node [shape=box, style=filled, fontname="Helvetica", margin="0.3,0.1"];'
355+ )
356+ lines .append (" edge [arrowhead=open];" )
357+ lines .append ("" )
358+ for level_name , level in report .config .items ():
359+ fill , font = cls .KIND_COLORS .get (level .kind , ("#9E9E9E" , "white" ))
360+ safe = cls .dot_escape (level_name )
361+ lines .append (f' "{ safe } " [fillcolor="{ fill } ", fontcolor="{ font } "];' )
362+ for level_name , level in report .config .items ():
363+ for trace_target in level .traces :
364+ src = cls .dot_escape (level_name )
365+ dst = cls .dot_escape (trace_target )
366+ lines .append (f' "{ src } " -> "{ dst } ";' )
367+ lines .append ("}" )
368+ return lines
369+
342370 @classmethod
343371 def build (cls , report : Report , indent : int = 0 ) -> list :
344- """Return RST lines for a ``.. graphviz::`` tracing-policy diagram.
372+ """Return RST lines for the tracing-policy diagram.
373+
374+ When Graphviz ``dot`` is available, emits a ``.. graphviz::`` directive
375+ that Sphinx renders as an image. When ``dot`` is not found, emits a
376+ ``.. note::`` explaining how to install Graphviz together with the raw
377+ DOT source in a ``.. code-block::`` so the diagram can still be
378+ visualised manually.
345379
346380 Args:
347381 report: The loaded LOBSTER report whose ``config`` provides level
@@ -356,28 +390,34 @@ def build(cls, report: Report, indent: int = 0) -> list:
356390 # lobster-trace: rst_req.RST_Report_Tracing_Policy_Diagram
357391 indent_str = " " * indent
358392 nested_indent = indent_str + " "
393+ dot_lines = cls ._build_dot_lines (report )
394+
395+ # Respect an explicit GRAPHVIZ_DOT env var set by the build system
396+ # (e.g. via --action_env=GRAPHVIZ_DOT=/usr/bin/dot in CI).
397+ dot_bin = os .environ .get ("GRAPHVIZ_DOT" ) or None
398+ if is_dot_available (dot_bin ):
399+ out = []
400+ out .append (f"{ indent_str } .. graphviz::" )
401+ out .append ("" )
402+ for dot_line in dot_lines :
403+ out .append (f"{ nested_indent } { dot_line } " )
404+ out .append ("" )
405+ return out
359406 out = []
360- out .append (f"{ indent_str } .. graphviz ::" )
407+ out .append (f"{ indent_str } .. note ::" )
361408 out .append ("" )
362- out .append (f"{ nested_indent } digraph tracing_policy {{" )
363- out .append (f"{ nested_indent } rankdir=TB;" )
364409 out .append (
365- f"{ nested_indent } node [shape=box, style=filled, "
366- f'fontname="Helvetica", margin="0.3,0.1"];'
410+ f"{ nested_indent } The tracing-policy diagram below could not be rendered "
411+ f"because the Graphviz ``dot`` utility was not found."
412+ )
413+ out .append (
414+ f"{ nested_indent } Install `Graphviz <https://graphviz.org>`__ and rebuild "
415+ f"to see the diagram as an image."
367416 )
368- out .append (f"{ nested_indent } edge [arrowhead=open];" )
369417 out .append ("" )
370- for level_name , level in report .config .items ():
371- fill , font = cls .KIND_COLORS .get (level .kind , ("#9E9E9E" , "white" ))
372- safe = cls .dot_escape (level_name )
373- out .append (
374- f'{ nested_indent } "{ safe } " [fillcolor="{ fill } ", fontcolor="{ font } "];'
375- )
376- for level_name , level in report .config .items ():
377- for trace_target in level .traces :
378- src = cls .dot_escape (level_name )
379- dst = cls .dot_escape (trace_target )
380- out .append (f'{ nested_indent } "{ src } " -> "{ dst } ";' )
381- out .append (f"{ nested_indent } }}" )
418+ out .append (f"{ nested_indent } .. code-block:: dot" )
419+ out .append ("" )
420+ for dot_line in dot_lines :
421+ out .append (f"{ nested_indent } { dot_line } " )
382422 out .append ("" )
383423 return out
0 commit comments