Add fake-memory-reader tests for the scanner/poller

process.go's boundary functions (readMemory, findModuleBase,
productVersion, findProcessID, openProcess, closeProcessHandle) become
swappable package variables, same pattern as testExeDir in totals.go.
process_test.go builds a fakeProcess (an in-memory buffer addressed
like real process memory) to exercise signature scanning, scanModule's
chunk-overlap logic, PlayerIns confirmation by memory read (the path
Linux always takes), the save-slot read, the double-read name
confirmation rule, and rejection of raw-pointer-as-UTF16 garbage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
emmatherockandClaude Sonnet 5 committed 2026-09-18 01:48:38 -03:00
1 parent 2ba312a833
commit 66aba2fff2
3 files changed
+321 -25

No files matched your search

+28 -15
View File
@@ -31,6 +31,19 @@ const processName = "eldenring.exe"
// process_linux.go).
type procHandle uintptr
// Boundary functions as package variables, defaulting to the platform
// implementation compiled into process_windows.go/process_linux.go.
// Tests (process_test.go) swap these for a fake in-memory reader — same
// pattern as testExeDir in totals.go, not a new interface.
var (
findProcessIDFn = findProcessID
openProcessFn = openProcess
closeProcessHandleFn = closeProcessHandle
readMemoryFn = readMemory
findModuleBaseFn = findModuleBase
productVersionFn = productVersion
)
// ------------------------- signature scans -------------------------
//
// Two signatures, both the same shape: a 7-byte "mov reg,[rip+disp32]"
@@ -111,7 +124,7 @@ func scanModule(h procHandle, base uintptr, size uint32, patterns [][]patByte) [
if rem := int(size - pos); readSize > rem {
readSize = rem
}
buf, ok := readMemory(h, base+uintptr(pos), readSize)
buf, ok := readMemoryFn(h, base+uintptr(pos), readSize)
if ok {
for i := 0; i < len(buf); i++ {
for p := range patterns {
@@ -140,7 +153,7 @@ func ripSlot(h procHandle, matchAddr uintptr) (uintptr, error) {
return 0, fmt.Errorf("pattern not found")
}
codeLocation := matchAddr + 3 // the first 3 bytes are the opcode
dispBytes, ok := readMemory(h, codeLocation, 4)
dispBytes, ok := readMemoryFn(h, codeLocation, 4)
if !ok {
return 0, fmt.Errorf("couldn't read the RIP-relative displacement")
}
@@ -178,12 +191,12 @@ type gamePointers struct {
func resolvePointers(h procHandle, pid uint32) (gamePointers, error) {
var gp gamePointers
base, size, exePath, err := findModuleBase(pid, processName)
base, size, exePath, err := findModuleBaseFn(pid, processName)
if err != nil {
return gp, err
}
major, minor, label, okVer := productVersion(exePath)
major, minor, label, okVer := productVersionFn(exePath)
gp.playerInsTried = playerInsCandidates(major, minor, okVer)
gp.playerInsOffset = gp.playerInsTried[0]
gp.versionLabel = label
@@ -216,7 +229,7 @@ func derefPointer(h procHandle, slot uintptr) (uintptr, bool) {
if slot == 0 {
return 0, false
}
buf, ok := readMemory(h, slot, 8)
buf, ok := readMemoryFn(h, slot, 8)
if !ok {
return 0, false
}
@@ -257,7 +270,7 @@ func readCharName(h procHandle, gameDataMan uintptr, c nameCandidate) (string, b
}
base = p
}
buf, ok := readMemory(h, base+c.nameOffset, charNameMaxChars*2)
buf, ok := readMemoryFn(h, base+c.nameOffset, charNameMaxChars*2)
if !ok {
return "", false
}
@@ -316,7 +329,7 @@ func readSaveSlot(h procHandle, gp gamePointers) int {
if !ok || gameMan == 0 {
return -1
}
buf, ok := readMemory(h, gameMan+saveSlotOffset, 1)
buf, ok := readMemoryFn(h, gameMan+saveSlotOffset, 1)
if !ok {
return -1
}
@@ -365,7 +378,7 @@ func isPlayerLoaded(h procHandle, gp *gamePointers) (loaded bool, known bool) {
}
// A real pointer points at mapped memory; a garbage one almost
// never survives this read.
if _, ok := readMemory(h, playerIns, 8); !ok {
if _, ok := readMemoryFn(h, playerIns, 8); !ok {
continue
}
gp.playerInsOffset = cand
@@ -436,7 +449,7 @@ func pollLoop() {
closeHandle := func() {
if handle != 0 {
closeProcessHandle(handle)
closeProcessHandleFn(handle)
handle = 0
}
pid = 0
@@ -449,13 +462,13 @@ func pollLoop() {
for {
if handle == 0 {
newPid, err := findProcessID(processName)
newPid, err := findProcessIDFn(processName)
if err != nil {
st.setDisconnected("waiting for eldenring.exe")
time.Sleep(3 * time.Second)
continue
}
h, err := openProcess(newPid)
h, err := openProcessFn(newPid)
if err != nil {
st.setDisconnected("couldn't open the process (permissions?): " + err.Error())
time.Sleep(3 * time.Second)
@@ -472,7 +485,7 @@ func pollLoop() {
st.setDisconnected(err.Error())
time.Sleep(2 * time.Second)
// if the process died, release the handle to retry from scratch
if _, ferr := findProcessID(processName); ferr != nil {
if _, ferr := findProcessIDFn(processName); ferr != nil {
closeHandle()
}
continue
@@ -510,7 +523,7 @@ func pollLoop() {
// and our detection was lying. Unlike a timeout, this can't
// fire just from leaving the game sitting at the menu a while.
if gdm, ok := derefPointer(handle, gp.gameDataManSlot); ok && gdm != 0 {
if buf, ok := readMemory(handle, gdm+0x94, 4); ok {
if buf, ok := readMemoryFn(handle, gdm+0x94, 4); ok {
raw := int32(binary.LittleEndian.Uint32(buf))
if raw >= 0 && raw < 1_000_000 {
if haveUnloadedRaw {
@@ -562,8 +575,8 @@ func pollLoop() {
continue
}
deathsBuf, ok1 := readMemory(handle, gameDataMan+0x94, 4)
bossBuf, ok2 := readMemory(handle, gameDataMan+0xC0, 1)
deathsBuf, ok1 := readMemoryFn(handle, gameDataMan+0x94, 4)
bossBuf, ok2 := readMemoryFn(handle, gameDataMan+0xC0, 1)
if !ok1 {
st.setDisconnected("lost the memory reading (the game closed or restarted)")
closeHandle()