package main import ( "context" "errors" "strconv" "testing" "time" ) func TestTheLoadMustBeSustainedToBoostAndToRelaxAndBetweenTheLinesNothingMoves(t *testing.T) { var p Policy at := time.Now() mains := Source{OnAC: true, Reason: "ACAD (Mains) is online"} for i := 0; i < SustainSamples-1; i++ { p.Observe(90, at) } if p.Decide(mains).Profile != ProfileOnAC { t.Fatal("boosted before the load was sustained") } p.Observe(35, at) // between the lines breaks the run p.Observe(90, at) if p.Boosted { t.Fatal("a broken run still counted") } for i := 0; i < SustainSamples; i++ { p.Observe(CPUHighPercent, at) } if d := p.Decide(mains); d.Profile != ProfileUnderLoad { t.Fatalf("%+v", d) } p.Observe(35, at) if !p.Boosted { t.Fatal("load between the lines relaxed the boost") } for i := 0; i < RelaxSamples; i++ { p.Observe(5, at) } if p.Decide(mains).Profile != ProfileOnAC { t.Fatal("did not relax after a sustained low") } p.Boosted = true if d := p.Decide(Source{OnAC: false, Reason: "BAT1 is discharging"}); d.Profile != ProfileOnBattery { t.Fatalf("battery: %+v", d) } } func TestIOWaitIsIdle(t *testing.T) { a, err := ParseProcStat("cpu 100 0 100 700 100 0 0 0 0 0\ncpu0 1 2 3\n") if err != nil { t.Fatal(err) } b, _ := ParseProcStat("cpu 150 0 150 700 200 0 0 0 0 0\n") busy, ok := Busy(a, b) if !ok || busy != 50 { t.Fatalf("%v %v", busy, ok) } if _, ok := Busy(b, b); ok { t.Fatal("no time passed and a load was answered") } if _, err := ParseProcStat("intr 1 2"); err == nil { t.Fatal("a file without the cpu line was read") } } func switcherOn(t *testing.T) (*fake, *Switcher, *[]map[string]any) { f := newFake(t) f.onMains() f.file("/proc/stat", "cpu 0 0 0 0 0 0 0 0\n") var emitted []map[string]any sw := NewSwitcher(f.machine(), func(_ string, body any) error { emitted = append(emitted, body.(map[string]any)) return nil }) return f, sw, &emitted } func TestStartingIsNotAReasonToSwitch(t *testing.T) { f, sw, emitted := switcherOn(t) sw.Evaluate(context.Background(), false) if calls := f.callsLike("asusctl profile set"); len(calls) != 0 || len(*emitted) != 0 { t.Fatalf("the first decision acted: %v %v", calls, *emitted) } } func TestAChangeOfPowerSourceSwitchesOnceAndPublishesIt(t *testing.T) { f, sw, emitted := switcherOn(t) ctx := context.Background() sw.Evaluate(ctx, false) f.onBattery() sw.Evaluate(ctx, false) sw.Evaluate(ctx, true) // nothing changed: nothing done, and on battery nothing sampled if calls := f.callsLike("asusctl profile set"); len(calls) != 1 || calls[0] != "asusctl profile set Quiet" { t.Fatalf("%v", calls) } if len(*emitted) != 1 || (*emitted)[0]["profile"] != "Quiet" || (*emitted)[0]["from"] != "Balanced" { t.Fatalf("%v", *emitted) } f.onMains() sw.Evaluate(ctx, false) if calls := f.callsLike("asusctl profile set"); len(calls) != 2 || calls[1] != "asusctl profile set Balanced" { t.Fatalf("%v", calls) } } func TestSustainedLoadOnMainsBoostsFromSamples(t *testing.T) { f, sw, _ := switcherOn(t) ctx := context.Background() sw.Evaluate(ctx, false) var user int for i := 1; i <= SustainSamples; i++ { user += 90 f.file("/proc/stat", "cpu "+itoa(user)+" 0 0 "+itoa(i*10)+" 0 0 0 0\n") sw.Evaluate(ctx, true) } if !f.called("asusctl profile set Performance") { t.Fatalf("%v", f.calls) } } func TestAHoldKeepsTheProfileUntilItEndsAndAFailureIsTriedAgain(t *testing.T) { f, sw, _ := switcherOn(t) ctx := context.Background() now := time.Now() sw.now = func() time.Time { return now } sw.Evaluate(ctx, false) f.onBattery() sw.Evaluate(ctx, false) // source change ends any hold; switches to Quiet sw.Hold("Performance", time.Hour) f.fails["asusctl profile set Balanced"] = errors.New("asusd is restarting") f.onMains() sw.Evaluate(ctx, false) // a change of source: the hold ends, the switch is attempted and fails if r := sw.Report(); r.LastError == "" || r.Held != "" { t.Fatalf("%+v", r) } delete(f.fails, "asusctl profile set Balanced") sw.Evaluate(ctx, false) if r := sw.Report(); r.LastApplied != "Balanced" || r.LastError != "" { t.Fatalf("not tried again: %+v", r) } // A hold on the same source keeps a new decision from acting until it ends. sw.Hold("Quiet", time.Hour) sw.policy.Boosted = true sw.Evaluate(ctx, false) if f.called("asusctl profile set Performance") { t.Fatal("switched during a hold") } now = now.Add(2 * time.Hour) sw.Evaluate(ctx, false) if !f.called("asusctl profile set Performance") { t.Fatal("did not act once the hold ended") } } func TestAsusdsSettingsAreSetOnlyWhereTheyDiffer(t *testing.T) { f := newFake(t) f.answers["asusctl battery info"] = "Current battery charge limit: 100%\n" f.answers["asusctl profile get"] = "Active profile: Balanced\nAC profile Performance\nBattery profile Quiet\n" sw := NewSwitcher(f.machine(), nil) sw.AssertVendorSettings(context.Background()) if !f.called("asusctl battery limit 80") || !f.called("asusctl profile set -a Balanced") || f.called("asusctl profile set -b Quiet") { t.Fatalf("%v", f.calls) } } func TestTheSwitcherDoesNotActOnAnotherModel(t *testing.T) { f := newFake(t) f.file("/sys/class/dmi/id/product_family", "ROG Strix\n") sw := NewSwitcher(f.machine(), nil) done := make(chan struct{}) go func() { sw.Run(context.Background()); close(done) }() select { case <-done: case <-time.After(2 * time.Second): t.Fatal("the switcher ran on another model") } if r := sw.Report(); r.Running || r.Disabled == "" || len(f.calls) != 0 { t.Fatalf("%+v %v", r, f.calls) } } func TestOnlyPowerSupplyUeventsWake(t *testing.T) { yes := []byte("change@/devices/LNXSYSTM:00/ACPI0003:00/power_supply/ACAD\x00ACTION=change\x00SUBSYSTEM=power_supply\x00POWER_SUPPLY_ONLINE=0\x00") no := []byte("change@/devices/virtual/net/wlan0\x00ACTION=change\x00SUBSYSTEM=net\x00") if !powerSupplyEvent(yes) || powerSupplyEvent(no) { t.Fatal("the uevent filter") } } func itoa(n int) string { return strconv.Itoa(n) }