mirror of
https://github.com/safishamsi/graphify.git
synced 2026-09-02 19:55:59 +00:00
fix(watch): keep a visualization when the graph outgrows the viz cap
Crossing MAX_NODES_FOR_VIZ left the project with no graph.html at all: _rebuild_code unlinked the existing file and wrote nothing in its place. The delete on its own is defensible — a kept graph.html would describe an older, smaller graph — but the file is gone before the user reads the message, and the next incremental rebuild silently removes it again, so a repo that grows past the threshold just loses its visualization with no way to keep one. The export path already solved this (#1019): over the cap it re-renders the community-aggregation view rather than going without. Do the same here, so the artifact is current AND present instead of current OR present. GRAPHIFY_VIZ_NODE_LIMIT=0 still means "no HTML viz" (CI runners) rather than "aggregate", and if the aggregated render also fails the old skip-and-remove behaviour stands.
This commit is contained in:
+28
-6
@@ -1359,18 +1359,40 @@ def _rebuild_code(
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# to_html raises ValueError for graphs > MAX_NODES_FOR_VIZ (5000).
|
||||
# to_html raises ValueError for graphs > the viz node limit.
|
||||
# Wrap so core outputs (graph.json + GRAPH_REPORT.md) always land.
|
||||
html_written = False
|
||||
if not no_change:
|
||||
html_target = out / "graph.html"
|
||||
try:
|
||||
to_html(G, communities, str(out / "graph.html"), community_labels=labels or None)
|
||||
to_html(G, communities, str(html_target), community_labels=labels or None)
|
||||
html_written = True
|
||||
except ValueError as viz_err:
|
||||
print(f"[graphify watch] Skipped graph.html: {viz_err}")
|
||||
stale = out / "graph.html"
|
||||
if stale.exists():
|
||||
stale.unlink()
|
||||
# Over the cap. Deleting was defensible on its own — a kept
|
||||
# graph.html would describe an older, smaller graph — but it
|
||||
# leaves a project that crossed the threshold with no
|
||||
# visualization at all, and the file is gone before the user
|
||||
# sees the message. The export path (#1019) already re-renders
|
||||
# the community-aggregation view in exactly this case, so do
|
||||
# the same here: current AND present beats current OR present.
|
||||
from graphify.exporters.html import _viz_node_limit
|
||||
if html_target.exists():
|
||||
html_target.unlink()
|
||||
limit = _viz_node_limit()
|
||||
if limit <= 0:
|
||||
# GRAPHIFY_VIZ_NODE_LIMIT=0 means "no HTML viz" (CI runners),
|
||||
# so honour it rather than aggregating around it.
|
||||
print(f"[graphify watch] Skipped graph.html: {viz_err}")
|
||||
else:
|
||||
try:
|
||||
to_html(G, communities, str(html_target),
|
||||
community_labels=labels or None, node_limit=limit)
|
||||
# The aggregator declines to write a single-community
|
||||
# graph, so trust the file rather than the call.
|
||||
html_written = html_target.exists()
|
||||
except Exception as fallback_err:
|
||||
print(f"[graphify watch] Skipped graph.html: {viz_err} "
|
||||
f"(aggregated view also failed: {fallback_err})")
|
||||
|
||||
# Regenerate callflow HTML if the user previously generated one —
|
||||
# opt-in by existence so users who never ran callflow-html aren't affected.
|
||||
|
||||
@@ -267,6 +267,59 @@ def test_rebuild_code_drops_labels_whose_community_changed(tmp_path):
|
||||
)
|
||||
|
||||
|
||||
def test_rebuild_code_keeps_a_visualization_when_over_the_viz_cap(tmp_path, monkeypatch):
|
||||
"""Crossing the viz node limit must not leave the project with no graph.html.
|
||||
_rebuild_code used to unlink the existing file and write nothing, so a repo
|
||||
that grew past the cap silently lost its visualization — and the file was
|
||||
already gone by the time the user read the message. The export path falls
|
||||
back to the community-aggregation view in exactly this case; the incremental
|
||||
path should too, so the artifact stays both current and present."""
|
||||
import json
|
||||
from graphify.watch import _rebuild_code
|
||||
|
||||
corpus = tmp_path / "corpus"
|
||||
corpus.mkdir()
|
||||
# Several *disconnected* clusters: the aggregator declines to render a
|
||||
# single-community meta-graph, so a ring of mutually-importing modules
|
||||
# would collapse to one community and exercise the wrong path.
|
||||
for g in range(4):
|
||||
for i in range(3):
|
||||
other = (i + 1) % 3
|
||||
(corpus / f"g{g}_m{i}.py").write_text(
|
||||
f"import g{g}_m{other}\n\n"
|
||||
+ "".join(f"def g{g}_f{i}_{j}():\n return {j}\n\n" for j in range(4)),
|
||||
encoding="utf-8",
|
||||
)
|
||||
assert _rebuild_code(corpus, acquire_lock=False) is True
|
||||
html = corpus / "graphify-out" / "graph.html"
|
||||
assert html.exists(), "expected a normal (under-cap) rebuild to write graph.html"
|
||||
before = html.read_text(encoding="utf-8")
|
||||
|
||||
# Drop the cap below the graph's size but above its community count — the
|
||||
# real shape of this bug. (A cap under the community count would make the
|
||||
# aggregated meta-graph breach it too, which is a different situation.)
|
||||
graph = json.loads((corpus / "graphify-out" / "graph.json").read_text(encoding="utf-8"))
|
||||
communities = {n.get("community") for n in graph["nodes"] if n.get("community") is not None}
|
||||
cap = (len(communities) + len(graph["nodes"])) // 2
|
||||
assert len(communities) < cap < len(graph["nodes"]), "test corpus cannot exercise the cap"
|
||||
monkeypatch.setenv("GRAPHIFY_VIZ_NODE_LIMIT", str(cap))
|
||||
(corpus / "g9_extra.py").write_text("def extra():\n return 1\n", encoding="utf-8")
|
||||
assert _rebuild_code(corpus, acquire_lock=False) is True
|
||||
|
||||
assert html.exists(), (
|
||||
"graph.html was deleted when the graph exceeded the viz cap — the "
|
||||
"incremental rebuild must fall back to the aggregated view like export does"
|
||||
)
|
||||
after = html.read_text(encoding="utf-8")
|
||||
assert after != before, "graph.html must be re-rendered, not left stale"
|
||||
|
||||
# And the documented kill switch still means "no viz", not "aggregate".
|
||||
monkeypatch.setenv("GRAPHIFY_VIZ_NODE_LIMIT", "0")
|
||||
(corpus / "g9_extra2.py").write_text("def extra2():\n return 2\n", encoding="utf-8")
|
||||
assert _rebuild_code(corpus, acquire_lock=False) is True
|
||||
assert not html.exists(), "GRAPHIFY_VIZ_NODE_LIMIT=0 must disable the HTML viz outright"
|
||||
|
||||
|
||||
def test_update_rebuilds_with_nested_star_gitignore(tmp_path):
|
||||
"""#1880: `graphify update` must not emit 0 nodes (and then refuse to
|
||||
overwrite) just because the source tree has a nested `.gitignore` with a
|
||||
|
||||
Reference in New Issue
Block a user