- external list now accepts serial: and size: prefixes; desc: matches serial too. Fixes docked-mode deadlock where Hyprland's synthetic FALLBACK output (created when the built-in is disabled by a stale docked config) was treated as a real external monitor, leaving the daemon stuck in docked after undocking. - modes name resolution gains serial: prefix (resolveSerial). - add -log-level flag for observability. - add unit tests for external matching and name resolution. - update README and example config.
136 lines
4.3 KiB
Go
136 lines
4.3 KiB
Go
package config
|
|
|
|
import "testing"
|
|
|
|
func TestMatchesExternalAutoDetect(t *testing.T) {
|
|
c := &Config{} // empty external list → auto-detect
|
|
|
|
cases := []struct {
|
|
name string
|
|
m ExternalMonitor
|
|
want bool
|
|
}{
|
|
{"internal eDP", ExternalMonitor{Name: "eDP-1"}, false},
|
|
{"internal LVDS", ExternalMonitor{Name: "LVDS-1"}, false},
|
|
{"internal DSI", ExternalMonitor{Name: "DSI-1"}, false},
|
|
{"synthetic fallback upper", ExternalMonitor{Name: "FALLBACK"}, false},
|
|
{"synthetic fallback lower", ExternalMonitor{Name: "fallback"}, false},
|
|
{"external DP", ExternalMonitor{Name: "DP-3"}, true},
|
|
{"external HDMI", ExternalMonitor{Name: "HDMI-A-1"}, true},
|
|
{"empty name", ExternalMonitor{Name: ""}, true},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
if got := c.MatchesExternal(tc.m); got != tc.want {
|
|
t.Errorf("MatchesExternal(%+v) = %v, want %v", tc.m, got, tc.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMatchesExternalPlainName(t *testing.T) {
|
|
c := &Config{External: []string{"DP-1", "DP-2"}}
|
|
|
|
if !c.MatchesExternal(ExternalMonitor{Name: "DP-1"}) {
|
|
t.Error("exact DP-1 should match")
|
|
}
|
|
if c.MatchesExternal(ExternalMonitor{Name: "DP-3"}) {
|
|
t.Error("DP-3 should not match when external lists DP-1/DP-2")
|
|
}
|
|
if c.MatchesExternal(ExternalMonitor{Name: "eDP-1"}) {
|
|
t.Error("eDP-1 should not match explicit external list")
|
|
}
|
|
}
|
|
|
|
func TestMatchesExternalDescMatchesDescriptionAndSerial(t *testing.T) {
|
|
c := &Config{External: []string{"desc:Xiaomi"}}
|
|
|
|
m := ExternalMonitor{
|
|
Name: "DP-12",
|
|
Description: "Xiaomi Corporation Mi Monitor 3342300033911",
|
|
Serial: "3342300033911",
|
|
}
|
|
if !c.MatchesExternal(m) {
|
|
t.Error("desc should match via description")
|
|
}
|
|
|
|
m.Serial = ""
|
|
m.Description = "Generic Monitor"
|
|
if c.MatchesExternal(m) {
|
|
t.Error("desc should not match when neither description nor serial contains needle")
|
|
}
|
|
|
|
m.Description = "Generic Monitor"
|
|
m.Serial = "Xiaomi 3342300033911"
|
|
if !c.MatchesExternal(m) {
|
|
t.Error("desc should match via serial even when description lacks the needle")
|
|
}
|
|
}
|
|
|
|
func TestMatchesExternalSerial(t *testing.T) {
|
|
c := &Config{External: []string{"serial:3342300033911"}}
|
|
|
|
if !c.MatchesExternal(ExternalMonitor{Name: "DP-12", Serial: "3342300033911"}) {
|
|
t.Error("serial exact should match")
|
|
}
|
|
if !c.MatchesExternal(ExternalMonitor{Name: "DP-12", Serial: "WXYZ3342300033911ABC"}) {
|
|
t.Error("serial substring should match")
|
|
}
|
|
if c.MatchesExternal(ExternalMonitor{Name: "DP-12", Serial: ""}) {
|
|
t.Error("empty serial should not match serial prefix")
|
|
}
|
|
if c.MatchesExternal(ExternalMonitor{Name: "DP-12", Serial: "3342300033912"}) {
|
|
t.Error("different serial should not match")
|
|
}
|
|
}
|
|
|
|
func TestMatchesExternalSize(t *testing.T) {
|
|
c := &Config{External: []string{"size:2560x1440"}}
|
|
|
|
if !c.MatchesExternal(ExternalMonitor{Name: "DP-12", Width: 2560, Height: 1440}) {
|
|
t.Error("size exact should match")
|
|
}
|
|
if c.MatchesExternal(ExternalMonitor{Name: "DP-12", Width: 1920, Height: 1200}) {
|
|
t.Error("different size should not match")
|
|
}
|
|
if c.MatchesExternal(ExternalMonitor{Name: "DP-12", Width: 2560, Height: 1080}) {
|
|
t.Error("mixed dimensions should not match")
|
|
}
|
|
}
|
|
|
|
func TestMatchesExternalSizeWithRefresh(t *testing.T) {
|
|
c := &Config{External: []string{"size:2560x1440@165"}}
|
|
|
|
if !c.MatchesExternal(ExternalMonitor{Name: "DP-12", Width: 2560, Height: 1440, RefreshRate: 165}) {
|
|
t.Error("size+refresh exact should match")
|
|
}
|
|
if !c.MatchesExternal(ExternalMonitor{Name: "DP-12", Width: 2560, Height: 1440, RefreshRate: 164.999}) {
|
|
t.Error("size+refresh within tolerance should match")
|
|
}
|
|
if c.MatchesExternal(ExternalMonitor{Name: "DP-12", Width: 2560, Height: 1440, RefreshRate: 120}) {
|
|
t.Error("different refresh should not match")
|
|
}
|
|
}
|
|
|
|
func TestMatchesExternalPrefersAnyMatchingEntry(t *testing.T) {
|
|
c := &Config{External: []string{"DP-1", "serial:3342300033911"}}
|
|
|
|
if !c.MatchesExternal(ExternalMonitor{Name: "DP-1"}) {
|
|
t.Error("plain name entry should match")
|
|
}
|
|
if !c.MatchesExternal(ExternalMonitor{Name: "DP-9", Serial: "3342300033911"}) {
|
|
t.Error("serial entry should match via second list item")
|
|
}
|
|
}
|
|
|
|
func TestMatchSizeInvalid(t *testing.T) {
|
|
if matchSize("size:not-a-size", 2560, 1440, 0) {
|
|
t.Error("garbage size spec should not match")
|
|
}
|
|
if matchSize("size:2560", 2560, 0, 0) {
|
|
t.Error("malformed dimensions should not match")
|
|
}
|
|
if matchSize("size:2560x1440@bogus", 2560, 1440, 165) {
|
|
t.Error("malformed refresh rate should not match")
|
|
}
|
|
}
|