The laptop model's hardware module and a memory-pressure module for any machine (hq research 027/03, 026/05, to-be 42 phase 3). The predecessor's polling auto-profile and mem-guard user scripts become each module's own Go code launched by the node runtime (ADR 0198): a profile switcher woken by the kernel's power-supply uevents, and a guard that warns on RAM, swap or PSI before systemd-oomd acts, on the desktop over the account's bus and always as an event. supergfxctl and triggerhappy are kept as found (research 027 Q1).
191 lines
5.9 KiB
Go
191 lines
5.9 KiB
Go
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) }
|