From 741c84ada2ec7fdd0083744afab294d9a1b6e370 Mon Sep 17 00:00:00 2001 From: Alan Date: Fri, 27 May 2022 16:32:42 -0400 Subject: [PATCH] `startdwarf`: migrate from Ruby to Lua (#385) 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 --- startdwarf.lua | 31 +++++++++++++ startdwarf.rb | 19 -------- test/startdwarf.lua | 104 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+), 19 deletions(-) create mode 100644 startdwarf.lua delete mode 100644 startdwarf.rb create mode 100644 test/startdwarf.lua diff --git a/startdwarf.lua b/startdwarf.lua new file mode 100644 index 0000000000..49165b06f7 --- /dev/null +++ b/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) diff --git a/startdwarf.rb b/startdwarf.rb deleted file mode 100644 index c73b4931e9..0000000000 --- a/startdwarf.rb +++ /dev/null @@ -1,19 +0,0 @@ -# patch start dwarf count -=begin - -startdwarf -========== -Use at the embark screen to embark with the specified number of dwarves. Eg. -``startdwarf 500`` would lead to a severe food shortage and FPS issues, while -``startdwarf 10`` would just allow a few more warm bodies to dig in. -The number must be 7 or greater. - -=end - -nr = $script_args[0].to_i - -raise 'too low' if nr < 7 - -addr = df.get_global_address('start_dwarf_count') -raise 'patch address not available' if addr == 0 -df.memory_patch(addr, [nr].pack('L')) diff --git a/test/startdwarf.lua b/test/startdwarf.lua new file mode 100644 index 0000000000..af58ce9198 --- /dev/null +++ b/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