//go:build windows package main import ( "errors" "fmt" "sort" "syscall" "testing" "time" ) // Only OS boundaries are simulated. The real production controller is exercised. type fakeProcess struct { info procInfo alive, readable, writable bool failSet, failRead bool coerced uint32 writes int } type fakeBackend struct { procs map[processKey]*fakeProcess handles map[syscall.Handle]*fakeProcess next syscall.Handle snapshotErr bool onRead func(*fakeProcess) snapshots int } func newFake() *fakeBackend { return &fakeBackend{procs: make(map[processKey]*fakeProcess), handles: make(map[syscall.Handle]*fakeProcess)} } func (f *fakeBackend) add(pid uint32, rss uint64, prio uint32) *fakeProcess { p := &fakeProcess{info: procInfo{PID: pid, Key: processKey{pid, uint64(pid)}, Name: fmt.Sprintf("Process%d.exe", pid), RSS: rss, Prio: prio}, alive: true, readable: true, writable: true} f.procs[p.info.Key] = p return p } func (f *fakeBackend) Snapshot() (processSnapshot, error) { f.snapshots++ if f.snapshotErr { return processSnapshot{}, errors.New("snapshot unavailable") } out := processSnapshot{PIDs: make(map[uint32]bool)} keys := make([]processKey, 0, len(f.procs)) for key := range f.procs { keys = append(keys, key) } sort.Slice(keys, func(i, j int) bool { return keys[i].PID < keys[j].PID }) for _, key := range keys { p := f.procs[key] if !p.alive { continue } out.PIDs[key.PID] = true if !p.readable { continue } info := p.info f.next++ info.Handle = f.next info.Waitable = true f.handles[info.Handle] = p out.Processes = append(out.Processes, info) } return out, nil } func (f *fakeBackend) Alive(p *procInfo) (bool, error) { v, ok := f.handles[p.Handle] if !ok { return false, errors.New("invalid handle") } return v.alive, nil } func (f *fakeBackend) Priority(p *procInfo) (uint32, error) { v, ok := f.handles[p.Handle] if !ok || !v.alive || v.failRead { return 0, errors.New("priority unavailable") } if f.onRead != nil { f.onRead(v) } return v.info.Prio, nil } func (f *fakeBackend) SetPriority(p *procInfo, target uint32) error { v, ok := f.handles[p.Handle] if !ok || !v.alive || !v.writable || v.failSet { return errors.New("access denied") } v.writes++ v.info.Prio = target if v.coerced != 0 { v.info.Prio = v.coerced } return nil } func (f *fakeBackend) Close(p *procInfo) { if p.Handle == 0 { panic("double close") } if _, ok := f.handles[p.Handle]; !ok { panic("invalid close") } delete(f.handles, p.Handle) p.Handle = 0 } func testController(f *fakeBackend) *controller { c := newController(f, 100, 200, false, time.Minute) c.now = func() time.Time { return time.Unix(1000, 0) } return c } func assertPriority(t *testing.T, p *fakeProcess, want uint32) { t.Helper() if p.info.Prio != want { t.Fatalf("%s: got %s; want %s", p.info.Name, prioName(p.info.Prio), prioName(want)) } } func (f *fakeBackend) child(pid, parent uint32, created uint64) *fakeProcess { p := f.add(pid, 50, PriorityClassIdle) delete(f.procs, p.info.Key) p.info.Key.Created = created p.info.ParentPID = parent f.procs[p.info.Key] = p return p } func TestControllerInheritedIdleDescendantsRecover(t *testing.T) { f := newFake() game := f.add(10, 250, PriorityClassNormal) parent := f.add(20, 50, PriorityClassBelowNormal) c := testController(f) c.Step() child := f.child(30, 20, 116444736000000000+1001*10000000) grandchild := f.child(40, 30, 116444736000000000+1002*10000000) oldIdle := f.child(50, 20, 116444736000000000+999*10000000) unrelated := f.child(60, 0, 116444736000000000+1001*10000000) c.now = func() time.Time { return time.Unix(1003, 0) } c.Step() game.alive = false c.Step() assertPriority(t, parent, PriorityClassBelowNormal) assertPriority(t, child, PriorityClassBelowNormal) assertPriority(t, grandchild, PriorityClassBelowNormal) assertPriority(t, oldIdle, PriorityClassIdle) assertPriority(t, unrelated, PriorityClassIdle) c.Shutdown() assertClosed(t, f) } func assertClosed(t *testing.T, f *fakeBackend) { t.Helper() if len(f.handles) != 0 { t.Fatalf("leaked %d owned handles", len(f.handles)) } } func TestControllerShutdownRestoresHeavyAndBackground(t *testing.T) { f := newFake() game := f.add(10, 250, PriorityClassNormal) background := f.add(20, 50, PriorityClassBelowNormal) c := testController(f) c.Step() assertPriority(t, game, PriorityClassHigh) assertPriority(t, background, PriorityClassIdle) report := c.Shutdown() if len(report.Errors) != 0 { t.Fatal(report.Errors) } assertPriority(t, game, PriorityClassNormal) assertPriority(t, background, PriorityClassBelowNormal) assertClosed(t, f) } func TestControllerGameExitRestoresBackground(t *testing.T) { f := newFake() game := f.add(10, 250, PriorityClassNormal) background := f.add(20, 50, PriorityClassBelowNormal) c := testController(f) c.Step() game.alive = false c.Step() assertPriority(t, background, PriorityClassBelowNormal) c.Shutdown() assertClosed(t, f) } func TestControllerRestoreFailureDoesNotBlockOtherProcesses(t *testing.T) { f := newFake() game := f.add(10, 250, PriorityClassNormal) blocked := f.add(20, 50, PriorityClassNormal) healthy := f.add(30, 50, PriorityClassBelowNormal) c := testController(f) c.Step() game.alive = false blocked.failSet = true newHeavy := f.add(40, 150, PriorityClassNormal) c.Step() assertPriority(t, healthy, PriorityClassBelowNormal) assertPriority(t, newHeavy, PriorityClassHigh) blocked.failSet = false r := c.Shutdown() if r.Unrestored != 1 { t.Fatal("blocked restoration incorrectly reported successful", r) } assertPriority(t, blocked, PriorityClassIdle) assertPriority(t, newHeavy, PriorityClassNormal) assertClosed(t, f) }