Skip to content
Closed
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
21 changes: 14 additions & 7 deletions graphify/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,13 +161,18 @@ def _real_count(nodes) -> int:
non_empty = {cid: nodes for cid, nodes in communities.items()
if any(not _ifn(G, n) for n in nodes)}
# One predicate for every figure the report prints about itself (#3148):
# "thin" is 0 < real < min_community_size, and "shown" is what the render
# loop below actually renders (real >= min_community_size) - previously
# shown was total-thin, which also counted communities with ZERO real
# nodes that the loop skips, overstating the count (#2129's residual).
# "thin" is real < min_community_size (ZERO real nodes included), and
# "shown" is what the render loop below actually renders (real >=
# min_community_size) - previously shown was total-thin, which also
# counted communities with ZERO real nodes that the loop skips,
# overstating the count (#2129's residual). A subsequent `0 <` guard on
# "thin" fixed that overcount but introduced the opposite gap: an
# all-file-node community (real == 0) is skipped by the SAME render loop
# yet was excluded from "thin" too, so it went uncounted anywhere and
# `shown + thin` silently undercounted `len(communities)` (#3548).
thin_count_summary = sum(
1 for nodes in communities.values()
if 0 < _real_count(nodes) < min_community_size
if _real_count(nodes) < min_community_size
)
shown_count = sum(
1 for nodes in communities.values()
Expand Down Expand Up @@ -314,10 +319,12 @@ def _real_count(nodes) -> int:
# Same threshold the Summary and Communities headers used (#3148): this
# was a hardcoded 3, so with --min-community-size anything else the count
# here disagreed with the label text beside it, which already printed
# min_community_size.
# min_community_size. No `0 <` guard, matching thin_count_summary above
# (#3548): an all-file-node community is skipped by the same render loop
# as a genuinely thin one, so it belongs in this count too.
thin_communities = {
cid: nodes for cid, nodes in communities.items()
if 0 < sum(1 for n in nodes if not _is_file_node(G, n)) < min_community_size
if sum(1 for n in nodes if not _is_file_node(G, n)) < min_community_size
}
gap_count = len(isolated) + len(thin_communities)

Expand Down
48 changes: 38 additions & 10 deletions tests/test_report_gap_thresholds.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""GRAPH_REPORT's headline numbers must agree with themselves (#3148).
"""GRAPH_REPORT's headline numbers must agree with themselves (#3148, #3548).

The Summary and Communities headers counted thin communities against the
caller's --min-community-size, while the Knowledge Gaps section counted
Expand All @@ -8,6 +8,14 @@
from graph.json never matched it. Also the #2129 residual: "shown" was
total-minus-thin, which counted zero-real-node communities the render loop
skips.

#3548: fixing the #2129 residual by computing "shown" directly (instead of
total-minus-thin) then left "thin" itself undercounting - a `0 <` guard
excluded a zero-real-node community from "thin" too, so a community the
render loop also skips went uncounted anywhere. `shown + thin` no longer
summed to the total. Both thresholds below now count the all-file-node
community ("onlyfile") as thin at every positive min_community_size, since
0 is always less than it.
"""
from __future__ import annotations

Expand Down Expand Up @@ -48,32 +56,52 @@ def _report(min_size):


def test_summary_and_gaps_count_thin_with_the_same_threshold():
# At min=5: "mid" (4 real) and "onlyfile" (0 real) are both thin; "big"
# (6 real) is shown.
text = _report(5)
assert "(1 shown, 1 thin omitted)" in text
assert "(1 shown, 2 thin omitted)" in text
m = re.search(r"\*\*(\d+) thin communit\w+ \(<(\d+) nodes\) omitted", text)
assert m, text
assert m.group(1) == "1" and m.group(2) == "5", m.group(0)
assert m.group(1) == "2" and m.group(2) == "5", m.group(0)


def test_at_the_default_threshold_nothing_is_thin():
def test_at_the_default_threshold_only_the_zero_real_community_is_thin():
# At min=3: "mid" (4 real) clears the threshold and is shown; "onlyfile"
# (0 real) is still thin at any positive threshold (#3548).
text = _report(3)
assert "0 thin omitted" in text
assert "1 thin omitted" in text
if "## Knowledge Gaps" in text:
gaps = text.split("## Knowledge Gaps")[-1].split("## ")[0]
assert "thin communit" not in gaps
assert "1 thin communit" in gaps


def test_shown_counts_only_what_the_render_loop_renders():
"""The zero-real-node community is neither shown nor thin (#2129 residual):
shown must be 1 (the big community), not total-minus-thin = 2."""
"""`shown + thin` must sum to the total community count (#3548): the
zero-real-node community is never shown (the render loop skips it), so
it must be counted as thin rather than going uncounted anywhere."""
text = _report(5)
header = re.search(r"## Communities \((\d+) total, (\d+) thin omitted\)", text)
assert header and header.group(2) == "1"
assert "(1 shown, 1 thin omitted)" in text
assert header and header.group(1) == "3" and header.group(2) == "2"
assert "(1 shown, 2 thin omitted)" in text
rendered = len(re.findall(r"### Community ", text))
assert rendered <= 1 or rendered == int(re.search(r"\((\d+) shown", text).group(1))


def test_shown_plus_thin_equals_total_communities_and_matches_render_count():
# The two invariants #3548's own report suggested as a regression check,
# checked at both thresholds used elsewhere in this file.
for min_size in (3, 5):
text = _report(min_size)
summary = re.search(
r"\((\d+) shown, (\d+) thin omitted\)", text
)
assert summary, text
shown, thin = int(summary.group(1)), int(summary.group(2))
rendered = len(re.findall(r"### Community \d+ - ", text))
assert shown == rendered, (min_size, shown, rendered)
assert shown + thin == 3, (min_size, shown, thin) # 3 communities total


def test_isolated_count_is_auditable_against_the_raw_graph():
text = _report(3)
assert "isolated node(s):" in text
Expand Down
Loading