mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-07-03 16:31:39 +00:00
3efa37c46c
Adds Coverage (harmonic reach) + Redundancy (Tarjan articulation) axes + composite & grade. Closes #672. **TDD note (BLOCKER-1):** Community PR delivered as a single squashed commit, so there is no separate pre-fix failing-test commit — please accept as a community-PR exemption. The tests are *gating*, not just thorough: each axis test pins a specific topology outcome (coverage on line/star/disconnected/weight-sensitive; redundancy online/triangle/star/bridged-cliques), and an end-to-end `/api/nodes` surface test drives the whole pipeline and asserts the composite diverges from the Traffic axis. Inverting the `1/weight` distance, dropping the NaN/Inf reject, removing the `redundancyMinWeight` floor, or aliasing `usefulness_score` back onto `traffic_share_score` each break a specific assertion. The axis functions are pure (no hidden state), so the suite fully characterises the behavior without the red anchor. Co-authored-by: Waydroid Builder <build@waydroid.local>
132 lines
4.1 KiB
Go
132 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"math"
|
|
"testing"
|
|
)
|
|
|
|
// TestComputeCoverageScores_Empty: empty edge list yields a non-nil empty
|
|
// map (the recomputer swaps this in before the first graph lands).
|
|
func TestComputeCoverageScores_Empty(t *testing.T) {
|
|
scores := ComputeCoverageScores(nil)
|
|
if scores == nil {
|
|
t.Fatal("want non-nil empty map, got nil")
|
|
}
|
|
if len(scores) != 0 {
|
|
t.Errorf("want empty map, got %d entries", len(scores))
|
|
}
|
|
}
|
|
|
|
// TestComputeCoverageScores_LineGraph: on a 4-node line A-B-C-D the two
|
|
// middle nodes reach the rest more cheaply (harmonic reach) than the
|
|
// leaves, so B and C tie for the top (1.0 after normalization) and the
|
|
// leaves A, D tie below them.
|
|
func TestComputeCoverageScores_LineGraph(t *testing.T) {
|
|
edges := []BridgeEdge{
|
|
{A: "a", B: "b", Weight: 1.0},
|
|
{A: "b", B: "c", Weight: 1.0},
|
|
{A: "c", B: "d", Weight: 1.0},
|
|
}
|
|
s := ComputeCoverageScores(edges)
|
|
assertInUnit(t, s)
|
|
|
|
if math.Abs(s["b"]-s["c"]) > 1e-9 {
|
|
t.Errorf("symmetry: b and c should tie, got b=%v c=%v", s["b"], s["c"])
|
|
}
|
|
if math.Abs(s["a"]-s["d"]) > 1e-9 {
|
|
t.Errorf("symmetry: a and d should tie, got a=%v d=%v", s["a"], s["d"])
|
|
}
|
|
if !(s["b"] > s["a"]) {
|
|
t.Errorf("middle b should out-cover leaf a: b=%v a=%v", s["b"], s["a"])
|
|
}
|
|
if math.Abs(maxScoreValue(s)-1.0) > 1e-9 {
|
|
t.Errorf("normalization: max should be 1.0, got %v", maxScoreValue(s))
|
|
}
|
|
}
|
|
|
|
// TestComputeCoverageScores_Star: the hub of a star reaches every leaf in
|
|
// one hop and is the unique top scorer; the leaves tie below it (each
|
|
// reaches the hub directly and every other leaf via the hub).
|
|
func TestComputeCoverageScores_Star(t *testing.T) {
|
|
edges := []BridgeEdge{
|
|
{A: "s", B: "l1", Weight: 1.0},
|
|
{A: "s", B: "l2", Weight: 1.0},
|
|
{A: "s", B: "l3", Weight: 1.0},
|
|
}
|
|
s := ComputeCoverageScores(edges)
|
|
assertInUnit(t, s)
|
|
|
|
if math.Abs(s["s"]-1.0) > 1e-9 {
|
|
t.Errorf("hub should score 1.0, got %v", s["s"])
|
|
}
|
|
for _, leaf := range []string{"l1", "l2", "l3"} {
|
|
if !(s[leaf] < s["s"]) {
|
|
t.Errorf("leaf %q should cover less than the hub: %v vs %v", leaf, s[leaf], s["s"])
|
|
}
|
|
}
|
|
if math.Abs(s["l1"]-s["l2"]) > 1e-9 || math.Abs(s["l2"]-s["l3"]) > 1e-9 {
|
|
t.Errorf("leaves should tie: %v %v %v", s["l1"], s["l2"], s["l3"])
|
|
}
|
|
}
|
|
|
|
// TestComputeCoverageScores_Disconnected: harmonic reach must treat
|
|
// unreachable nodes as 0 contribution. With two separate 2-node
|
|
// components every node reaches exactly one peer at distance 1, so all
|
|
// four tie (and normalize to 1.0). If unreachable nodes leaked in, the
|
|
// symmetry would break.
|
|
func TestComputeCoverageScores_Disconnected(t *testing.T) {
|
|
edges := []BridgeEdge{
|
|
{A: "a", B: "b", Weight: 1.0},
|
|
{A: "c", B: "d", Weight: 1.0},
|
|
}
|
|
s := ComputeCoverageScores(edges)
|
|
assertInUnit(t, s)
|
|
if len(s) != 4 {
|
|
t.Fatalf("want 4 nodes, got %d", len(s))
|
|
}
|
|
for _, n := range []string{"a", "b", "c", "d"} {
|
|
if math.Abs(s[n]-1.0) > 1e-9 {
|
|
t.Errorf("node %q: want 1.0 (each reaches one peer), got %v", n, s[n])
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestComputeCoverageScores_WeightSensitive: a stronger edge is a shorter
|
|
// distance, so the node bridging both a strong and a weak edge reaches the
|
|
// most. A-B weight 1.0 (near), A-C weight 0.1 (far) ⇒ A out-covers B
|
|
// out-covers C. Flip the 1/w distance convention and this inverts.
|
|
func TestComputeCoverageScores_WeightSensitive(t *testing.T) {
|
|
edges := []BridgeEdge{
|
|
{A: "a", B: "b", Weight: 1.0},
|
|
{A: "a", B: "c", Weight: 0.1},
|
|
}
|
|
s := ComputeCoverageScores(edges)
|
|
assertInUnit(t, s)
|
|
if !(s["a"] > s["b"] && s["b"] > s["c"]) {
|
|
t.Errorf("want a > b > c, got a=%v b=%v c=%v", s["a"], s["b"], s["c"])
|
|
}
|
|
}
|
|
|
|
// --- shared test helpers; assertInUnit and maxScoreValue are both used by
|
|
// redundancy_score_test.go as well as the coverage tests above. ---
|
|
|
|
func assertInUnit(t *testing.T, m map[string]float64) {
|
|
t.Helper()
|
|
for k, v := range m {
|
|
if v < 0 || v > 1 || math.IsNaN(v) || math.IsInf(v, 0) {
|
|
t.Errorf("score %q=%v out of [0,1]", k, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
// maxScoreValue avoids shadowing the Go 1.21 `max` builtin (#1762 nit).
|
|
func maxScoreValue(m map[string]float64) float64 {
|
|
largest := 0.0
|
|
for _, v := range m {
|
|
if v > largest {
|
|
largest = v
|
|
}
|
|
}
|
|
return largest
|
|
}
|