Skip to content

Commit

Permalink
startdwarf: migrate from Ruby to Lua (#385)
Browse files Browse the repository at this point in the history
DFHack/dfhack#2081

* Add tests for startdwarf

* Use mock.observe_func() to verify that patchMemory() has the intended effect

* Clarify docs and consolidate argument validation
  • Loading branch information
lethosor committed May 27, 2022
1 parent b1bfe27 commit 741c84a
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 19 deletions.
31 changes: 31 additions & 0 deletions startdwarf.lua
@@ -0,0 +1,31 @@
-- change number of dwarves on initial embark

--[====[
startdwarf
==========
Use before embarking (e.g. at the site selection screen or any time before) to
change the number of dwarves you embark with from the default of 7.
- ``startdwarf 10`` would just allow a few more warm bodies to dig in
- ``startdwarf 500`` would lead to a severe food shortage and FPS issues
The number must be 7 or greater.
]====]

local addr = dfhack.internal.getAddress('start_dwarf_count')
if not addr then
qerror('start_dwarf_count address not available - cannot patch')
end

local num = tonumber(({...})[1])
if not num or num < 7 then
qerror('argument must be a number no less than 7')
end

dfhack.with_temp_object(df.new('uint32_t'), function(temp)
temp.value = num
local temp_size, temp_addr = temp:sizeof()
dfhack.internal.patchMemory(addr, temp_addr, temp_size)
end)
19 changes: 0 additions & 19 deletions startdwarf.rb

This file was deleted.

104 changes: 104 additions & 0 deletions test/startdwarf.lua
@@ -0,0 +1,104 @@
local utils = require('utils')

local function with_patches(callback, custom_mocks)
dfhack.with_temp_object(df.new('uint32_t'), function(temp_out)
local originalPatchMemory = dfhack.internal.patchMemory
local function safePatchMemory(target, source, length)
-- only allow patching the expected address - otherwise a buggy
-- script could corrupt the test environment
if target ~= utils.addressof(temp_out) then
return expect.fail(('attempted to patch invalid address 0x%x: expected 0x%x'):format(target, utils.addressof(temp_out)))
end
return originalPatchMemory(target, source, length)
end
local mocks = {
getAddress = mock.func(utils.addressof(temp_out)),
patchMemory = mock.observe_func(safePatchMemory),
}
if custom_mocks then
for k, v in pairs(custom_mocks) do
mocks[k] = v
end
end
mock.patch({
{dfhack.internal, 'getAddress', mocks.getAddress},
{dfhack.internal, 'patchMemory', mocks.patchMemory},
}, function()
callback(mocks, temp_out)
end)
end)
end

local function run_startdwarf(...)
return dfhack.run_script('startdwarf', ...)
end

local function test_early_error(args, expected_message, custom_mocks)
with_patches(function(mocks, temp_out)
temp_out.value = 12345

expect.error_match(expected_message, function()
run_startdwarf(table.unpack(args))
end)

expect.eq(mocks.getAddress.call_count, 1, 'getAddress was not called')
expect.table_eq(mocks.getAddress.call_args[1], {'start_dwarf_count'})

expect.eq(mocks.patchMemory.call_count, 0, 'patchMemory was called unexpectedly')

-- make sure the script didn't attempt to write in some other way
expect.eq(temp_out.value, 12345, 'memory was changed unexpectedly')
end, custom_mocks)
end

local function test_invalid_args(args, expected_message)
test_early_error(args, expected_message)
end

local function test_patch_successful(expected_value)
with_patches(function(mocks, temp_out)
run_startdwarf(tostring(expected_value))
expect.eq(temp_out.value, expected_value)

expect.eq(mocks.getAddress.call_count, 1, 'getAddress was not called')
expect.table_eq(mocks.getAddress.call_args[1], {'start_dwarf_count'})

expect.eq(mocks.patchMemory.call_count, 1, 'patchMemory was not called')
expect.eq(mocks.patchMemory.call_args[1][1], utils.addressof(temp_out),
'patchMemory called with wrong destination')
-- skip checking source (arg 2) because it has already been freed by the script
expect.eq(mocks.patchMemory.call_args[1][3], df.sizeof(temp_out),
'patchMemory called with wrong length')
end)
end

function test.no_arg()
test_invalid_args({}, 'must be a number')
end

function test.not_number()
test_invalid_args({'a'}, 'must be a number')
end

function test.too_small()
test_invalid_args({'4'}, 'less than 7')
test_invalid_args({'6'}, 'less than 7')
test_invalid_args({'-1'}, 'less than 7')
end

function test.missing_address()
test_early_error({}, 'address not available', {getAddress = mock.func(nil)})
test_early_error({'8'}, 'address not available', {getAddress = mock.func(nil)})
end

function test.exactly_7()
test_patch_successful(7)
end

function test.above_7()
test_patch_successful(10)
end

function test.uint8_overflow()
test_patch_successful(257)
end

0 comments on commit 741c84a

Please sign in to comment.