package main import ( "errors" "sync" "testing" "time" ) // The coupling is a SET, not a global switch. // // A station can run a combiner pair AND a third amplifier that has nothing to // do with it — two SPE on the combiner, a PowerGenius on another antenna. A // global flag would send that third one into OPERATE alongside them. func TestAmpTargetsFollowTheGroup(t *testing.T) { a := &App{} a.ampInsts = map[string]*ampInst{"spe1": {}, "spe2": {}, "pgxl": {}} group := []string{"spe1", "spe2"} // A member commands the whole group, itself first. got := a.ampTargets("spe2", group) if len(got) != 2 || got[0] != "spe2" || got[1] != "spe1" { t.Errorf("member = %v, want [spe2 spe1] — the clicked one first", got) } // The amplifier OUTSIDE the group keeps its buttons to itself. if got := a.ampTargets("pgxl", group); len(got) != 1 || got[0] != "pgxl" { t.Errorf("outsider = %v, want just [pgxl]", got) } // No group at all: everyone is on their own. if got := a.ampTargets("spe1", nil); len(got) != 1 || got[0] != "spe1" { t.Errorf("no group = %v, want just [spe1]", got) } // A group remembering an amplifier that is gone must not carry it: it would // fail the command for a member that no longer exists. if got := a.ampTargets("spe1", []string{"spe1", "deleted"}); len(got) != 1 || got[0] != "spe1" { t.Errorf("stale member = %v, want it dropped", got) } } // One amplifier coupled to itself is not a group — storing it would make every // command fan out to a single member for ever, which is just noise. func TestLinkedAmpsNeedsTwo(t *testing.T) { for _, tc := range []struct { in []string want int }{ {[]string{"a", "b"}, 2}, {[]string{"a"}, 0}, {[]string{" ", "a"}, 0}, // blanks are not members {nil, 0}, } { clean := make([]string, 0, len(tc.in)) for _, id := range tc.in { if id != "" && id != " " { clean = append(clean, id) } } if len(clean) < 2 { clean = nil } if len(clean) != tc.want { t.Errorf("SetLinkedAmps(%v) would keep %d, want %d", tc.in, len(clean), tc.want) } } } // Two combined amplifiers must be commanded AT THE SAME TIME, not one after the // other. // // Sequentially, the second was commanded only once the first had answered — and // an SPE answers over its own link, in its own time. The combiner heard power // appear on one input before the other and beeped about it, on every OFF and // every ON. This is what an operator hears, so it is worth a test that would // hear it too. func TestLinkedAmpCommandsLeaveTogether(t *testing.T) { a := &App{} const slow = 150 * time.Millisecond var mu sync.Mutex starts := map[string]time.Time{} err := a.ampFanOut([]string{"one", "two"}, func(id string) error { mu.Lock() starts[id] = time.Now() mu.Unlock() time.Sleep(slow) // an amplifier taking its time to answer return nil }) if err != nil { t.Fatalf("fan-out: %v", err) } if len(starts) != 2 { t.Fatalf("%d amplifiers were commanded, want both", len(starts)) } // Both goroutines wait on one channel and are released by closing it, so the // gap is scheduling noise. Sequential execution would put a full command // between them. gap := starts["one"].Sub(starts["two"]) if gap < 0 { gap = -gap } if gap > slow/3 { t.Errorf("the two amplifiers were commanded %v apart — the combiner hears that as one input arriving late", gap) } } // The amplifier the operator clicked comes first, and its failure is the one // reported: "the amp I pressed did not respond" beats the same message about // its silent partner. func TestLinkedAmpErrorNamesTheOneClicked(t *testing.T) { a := &App{} clicked := errors.New("the one clicked") other := errors.New("the other one") err := a.ampFanOut([]string{"clicked", "other"}, func(id string) error { if id == "clicked" { return clicked } return other }) if !errors.Is(err, clicked) { t.Errorf("fan-out reported %v, want the amplifier the operator pressed", err) } } // One amplifier is the ordinary case and must not change: run inline, no // goroutine, no barrier, and the error straight back. func TestSingleAmpRunsInline(t *testing.T) { a := &App{} boom := errors.New("not running") if err := a.ampFanOut([]string{"solo"}, func(string) error { return boom }); !errors.Is(err, boom) { t.Errorf("a single amplifier reported %v, want the error itself", err) } if err := a.ampFanOut(nil, func(string) error { return boom }); err != nil { t.Errorf("an empty group reported %v, want nothing to do", err) } }