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
29 changes: 29 additions & 0 deletions internal/app/session/readouts.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ func (s *Sim) refreshReadouts(w *state.World, index map[string]int) {
w.Counts = s.eventCounts()
w.Scores = s.scores()
w.Stats = s.nodeStats(w.Events)
s.syncRegions(w)
if s.history == nil {
s.history = newNodeHistory()
}
Expand All @@ -70,3 +71,31 @@ func (s *Sim) refreshReadouts(w *state.World, index map[string]int) {
}
}
}

// syncRegions re-projects the scenario's regions and default scope onto the
// snapshot rows, the way the scores and trails above are re-projected from the
// engine.
//
// The scenario is authoritative: it is what nodes.regions and infer.apply
// write and what the firmware was provisioned from, so a mesh that relays
// scoped traffic holds its regions there. The row is a published copy, and a
// change applied while the mesh runs reaches the scenario but was seen to be
// lost from the row - nodes.list, the one place the manual says regions can be
// read, then answered "no regions" on a mesh where every node held one. Copied
// here every readout, the row cannot drift from the scenario for longer than a
// readout, whatever rebuilt it.
func (s *Sim) syncRegions(w *state.World) {
if len(s.nodes) == 0 || len(w.Nodes) == 0 {
return
}
byName := make(map[string]int, len(w.Nodes))
for i := range w.Nodes {
byName[w.Nodes[i].Name] = i
}
for i := range s.nodes {
if j, ok := byName[s.nodes[i].Name]; ok {
w.Nodes[j].Regions = s.nodes[i].Regions
w.Nodes[j].DefaultScope = s.nodes[i].DefaultScope
}
}
}
42 changes: 42 additions & 0 deletions internal/app/session/syncregions_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package session

import (
"testing"

"github.com/MeshBench/meshbench/internal/app/state"
"github.com/MeshBench/meshbench/internal/world/scenario"
)

// The snapshot's region columns are re-projected from the scenario every
// readout, so a region applied while the mesh runs cannot vanish from
// nodes.list even if whatever rebuilds the rows drops it (#737). The scenario
// is authoritative: it is what the firmware was provisioned from.
func TestSyncRegionsMakesTheSnapshotFollowTheScenario(t *testing.T) {
s := &Sim{nodes: []scenario.Node{
{Name: "A", Regions: []string{"fif"}, DefaultScope: "fif"},
{Name: "B"},
}}
// The snapshot has drifted: A holds the regions in the scenario but the
// row does not, which is the state the bug leaves.
w := &state.World{Nodes: []state.Node{{Name: "A"}, {Name: "B"}}}

s.syncRegions(w)

if got := w.Nodes[0].Regions; len(got) != 1 || got[0] != "fif" {
t.Errorf("A's row has regions %v, want [fif] from the scenario", got)
}
if w.Nodes[0].DefaultScope != "fif" {
t.Errorf("A's row default scope is %q, want fif", w.Nodes[0].DefaultScope)
}
if len(w.Nodes[1].Regions) != 0 {
t.Errorf("B holds no region in the scenario, so its row should hold none: %v", w.Nodes[1].Regions)
}

// A row with no scenario node of that name is left alone rather than
// cleared: the scenario decides what exists.
w2 := &state.World{Nodes: []state.Node{{Name: "ghost", Regions: []string{"x"}}}}
s.syncRegions(w2)
if len(w2.Nodes[0].Regions) != 1 {
t.Error("a row with no matching scenario node was cleared")
}
}
Loading