fix(profiles): station hardware must follow the active profile
Every setting is per profile, amplifiers included — that is the whole point of one profile per station. But the amplifier clients were built once at startup and again only when Settings was saved, so switching profile left them on the previous profile's port. An operator with an SPE on COM9 for HF and another on COM10 for 6 m had to open Settings and press Save after every switch. Save is not a connect button; switching profile is what asks for that hardware. The amplifier is what was reported, but it was never alone: the motorized antenna, the Antenna Genius and the Tuner Genius are configured the same way and were started in the same two places. All four now restart on a profile switch, asynchronously, exactly as a save does. Rotators were already correct — their client is built per command rather than held open, so they read the active profile every time. Unconditional, like a save: comparing each device's configuration across profiles to skip a reconnect would trade a second of downtime for the chance of missing one. A test now keeps the two lists in lockstep — anything started at boot must be re-applied on a profile switch or be named, with its reason, as something that must not be.
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Every setting in OpsLog is per profile, station hardware included. A device
|
||||
// started at boot and never again therefore stays on the PREVIOUS profile's
|
||||
// port until Settings is opened and saved — which is how an operator running an
|
||||
// SPE on COM9 for HF and another on COM10 for 6 m, one per profile, found the
|
||||
// amplifier still on the old port after switching. Save is not a connect
|
||||
// button.
|
||||
//
|
||||
// This test keeps startup and reloadAfterProfileSwitch in lockstep: anything
|
||||
// started at boot must either be re-applied on a profile switch or be listed
|
||||
// below with the reason it must not be. Adding a device makes the choice
|
||||
// explicit instead of leaving the fifth one to be found by a user.
|
||||
func TestProfileSwitchReappliesEveryStartupDevice(t *testing.T) {
|
||||
src, err := os.ReadFile("app.go")
|
||||
if err != nil {
|
||||
t.Fatalf("read app.go: %v", err)
|
||||
}
|
||||
|
||||
// Started at boot but deliberately NOT re-run on a profile switch:
|
||||
notPerProfile := map[string]string{
|
||||
"startAllEnabledClusters": "the cluster panel reconnects itself; its servers are a global list",
|
||||
"startGridCache": "a shared on-disk grid cache, not a profile's",
|
||||
"startBandOpenFeed": "PSK Reporter, keyed on the operator grid it re-reads itself",
|
||||
}
|
||||
|
||||
startup := body(t, string(src), "func (a *App) startup(ctx context.Context) {")
|
||||
reload := body(t, string(src), "func (a *App) reloadAfterProfileSwitch() {")
|
||||
|
||||
call := regexp.MustCompile(`a\.(start[A-Z][A-Za-z]*)\b`)
|
||||
seen := map[string]bool{}
|
||||
for _, m := range call.FindAllStringSubmatch(startup, -1) {
|
||||
name := m[1]
|
||||
if seen[name] || notPerProfile[name] != "" {
|
||||
continue
|
||||
}
|
||||
seen[name] = true
|
||||
if !strings.Contains(reload, "a."+name) {
|
||||
t.Errorf("%s runs at startup but not on a profile switch — the device stays on the previous profile's settings.\n"+
|
||||
"Add it to reloadAfterProfileSwitch, or to notPerProfile here with the reason it must not follow the profile.", name)
|
||||
}
|
||||
}
|
||||
if len(seen) == 0 {
|
||||
t.Fatal("no startup device starters found — this test has stopped checking anything")
|
||||
}
|
||||
}
|
||||
|
||||
// body returns the source of the function opening with the given signature,
|
||||
// up to the closing brace in column 0.
|
||||
func body(t *testing.T, src, signature string) string {
|
||||
t.Helper()
|
||||
i := strings.Index(src, signature)
|
||||
if i < 0 {
|
||||
t.Fatalf("%q not found in app.go", signature)
|
||||
}
|
||||
rest := src[i+len(signature):]
|
||||
if j := strings.Index(rest, "\n}"); j >= 0 {
|
||||
return rest[:j]
|
||||
}
|
||||
return rest
|
||||
}
|
||||
Reference in New Issue
Block a user