205 lines
5.7 KiB
Go
205 lines
5.7 KiB
Go
//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)
|
|
}
|