2021-05-26 10:45:49 -06:00
|
|
|
-- tests the quickfort ecosystem end-to-end via:
|
2021-06-07 07:11:06 -06:00
|
|
|
-- .csv -> quickfort/buildingplan/dig-now -> blueprint -> .csv
|
2021-05-26 10:45:49 -06:00
|
|
|
--
|
|
|
|
-- test cases are sets of .csv files in the
|
|
|
|
-- blueprints/library/test/ecosystem/in directory
|
|
|
|
--
|
|
|
|
-- test metadata is stored in an associated #notes blueprint:
|
|
|
|
-- description (required)
|
|
|
|
-- width (required)
|
|
|
|
-- height (required)
|
|
|
|
-- depth (default is 1)
|
|
|
|
--
|
2021-06-07 07:11:06 -06:00
|
|
|
-- depends on blueprint, buildingplan, and dig-now plugins (as well as the
|
|
|
|
-- quickfort script, of course)
|
2021-05-26 10:45:49 -06:00
|
|
|
--
|
|
|
|
-- note that this test harness cannot (yet) test #query blueprints that define
|
|
|
|
-- rooms since furniture is not actually built during the test. It also cannot
|
|
|
|
-- test blueprints that #build flooring and then #build a workshop on top, again
|
2022-02-12 19:11:33 -07:00
|
|
|
-- since the flooring is never actually built. We can support these features
|
|
|
|
-- once we figure out how to programmatically deconstruct buildlings without
|
|
|
|
-- crashing the game.
|
2021-05-26 10:45:49 -06:00
|
|
|
|
2021-09-09 01:46:33 -06:00
|
|
|
config.mode = 'fortress'
|
2021-05-26 10:45:49 -06:00
|
|
|
|
2021-06-07 06:58:51 -06:00
|
|
|
local blueprint = require('plugins.blueprint')
|
2021-06-08 23:09:16 -06:00
|
|
|
local quickfort_list = reqscript('internal/quickfort/list')
|
|
|
|
local quickfort_command = reqscript('internal/quickfort/command')
|
2021-10-04 12:14:05 -06:00
|
|
|
local utils = require('utils')
|
2021-06-07 06:58:51 -06:00
|
|
|
|
2021-05-26 10:45:49 -06:00
|
|
|
local blueprints_dir = 'blueprints/'
|
|
|
|
local input_dir = 'library/test/ecosystem/in/'
|
2022-02-12 19:11:33 -07:00
|
|
|
local golden_dir = 'library/test/ecosystem/golden/'
|
2021-05-26 10:45:49 -06:00
|
|
|
local output_dir = 'library/test/ecosystem/out/'
|
|
|
|
|
2021-10-04 12:14:05 -06:00
|
|
|
local phase_names = utils.invert(blueprint.valid_phases)
|
2021-05-26 10:45:49 -06:00
|
|
|
|
2021-09-09 01:46:33 -06:00
|
|
|
-- clear the output dir before each test run (but not after -- to allow
|
2021-10-04 12:14:05 -06:00
|
|
|
-- inspection of failed results)
|
2021-09-09 01:46:33 -06:00
|
|
|
local function test_wrapper(test_fn)
|
|
|
|
local outdir = blueprints_dir .. output_dir
|
|
|
|
for _, v in ipairs(dfhack.filesystem.listdir_recursive(outdir)) do
|
|
|
|
if not v.isdir then
|
|
|
|
os.remove(v.path)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
test_fn()
|
|
|
|
end
|
|
|
|
config.wrapper = test_wrapper
|
|
|
|
|
2021-05-26 10:45:49 -06:00
|
|
|
local function bad_spec(expected, varname, basename, bad_value)
|
|
|
|
qerror(('expected %s for %s in "%s" test spec; got "%s"'):
|
|
|
|
format(expected, varname, basename, bad_value))
|
|
|
|
end
|
|
|
|
|
|
|
|
local function get_positive_int(numstr, varname, basename)
|
|
|
|
local num = tonumber(numstr)
|
|
|
|
if not num or num <= 0 or num ~= math.floor(num) then
|
|
|
|
bad_spec('positive integer', varname, basename, numstr)
|
|
|
|
end
|
|
|
|
return num
|
|
|
|
end
|
|
|
|
|
2022-02-12 19:11:33 -07:00
|
|
|
local function os_exists(path)
|
|
|
|
return os.rename(path, path) and true or false
|
|
|
|
end
|
|
|
|
|
2021-05-26 10:45:49 -06:00
|
|
|
local function get_blueprint_sets()
|
|
|
|
-- find test blueprints with `quickfort list`
|
|
|
|
local mock_print = mock.func()
|
|
|
|
mock.patch(quickfort_list, 'print', mock_print,
|
|
|
|
function()
|
|
|
|
dfhack.run_script('quickfort', 'list', '-l', input_dir)
|
|
|
|
end)
|
|
|
|
|
|
|
|
-- group blueprint sets
|
|
|
|
local sets = {}
|
|
|
|
for _,args in ipairs(mock_print.call_args) do
|
|
|
|
local line = args[1]
|
2021-10-04 12:14:05 -06:00
|
|
|
-- phase is the label or, if the label doesn't exist, the mode
|
|
|
|
local _,_,listnum,fname,phase = line:find('(%d+)%) (%S+).-[/(]([^ )]+)')
|
2021-05-26 10:45:49 -06:00
|
|
|
if listnum then
|
|
|
|
local _,_,file_part = fname:find('/([^/]+)$')
|
|
|
|
local _,_,basename = file_part:find('^([^-.]+)')
|
2021-10-04 12:14:05 -06:00
|
|
|
if not sets[basename] then sets[basename] = {spec={}, phases={}} end
|
2022-02-12 19:11:33 -07:00
|
|
|
local golden_path = blueprints_dir..golden_dir..file_part
|
|
|
|
if not os_exists(golden_path) then
|
|
|
|
golden_path = blueprints_dir..fname
|
|
|
|
end
|
2021-10-04 12:14:05 -06:00
|
|
|
sets[basename].phases[phase] = {
|
2021-05-26 10:45:49 -06:00
|
|
|
listnum=listnum,
|
2022-02-12 19:11:33 -07:00
|
|
|
golden_filepath=golden_path,
|
2021-05-26 10:45:49 -06:00
|
|
|
output_filepath=blueprints_dir..output_dir..file_part}
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- load test specs
|
|
|
|
for basename,set in pairs(sets) do
|
2021-10-04 12:14:05 -06:00
|
|
|
local spec, notes = set.spec, set.phases.notes
|
2021-05-26 10:45:49 -06:00
|
|
|
|
|
|
|
-- set defaults
|
|
|
|
spec.depth = '1'
|
|
|
|
|
|
|
|
-- read spec
|
|
|
|
mock.patch(quickfort_command, 'print',
|
|
|
|
function(text)
|
|
|
|
for line in text:gmatch('[^\n]*') do
|
|
|
|
local _,_,var,val = line:find('%*?%s*([^=]+)=(.*)')
|
|
|
|
if var then spec[var] = val end
|
|
|
|
end
|
|
|
|
end,
|
|
|
|
function()
|
|
|
|
dfhack.run_script('quickfort', 'run', '-q', notes.listnum)
|
|
|
|
end)
|
|
|
|
|
2022-02-12 19:11:33 -07:00
|
|
|
-- validate spec and convert number strings to numeric vars
|
2021-05-26 10:45:49 -06:00
|
|
|
if not spec.description or spec.description == '' then
|
|
|
|
qerror(('missing description in test spec for "%s"'):
|
|
|
|
format(basename))
|
|
|
|
end
|
|
|
|
spec.width = get_positive_int(spec.width, 'width', basename)
|
|
|
|
spec.height = get_positive_int(spec.height, 'height', basename)
|
|
|
|
spec.depth = get_positive_int(spec.depth, 'depth', basename)
|
|
|
|
end
|
|
|
|
|
|
|
|
return sets
|
|
|
|
end
|
|
|
|
|
|
|
|
local function is_usable_test_tile(pos)
|
|
|
|
local tiletype = dfhack.maps.getTileType(pos)
|
|
|
|
local tileattrs = df.tiletype.attrs[tiletype]
|
|
|
|
local good_material = tileattrs.material == df.tiletype_material.STONE or
|
|
|
|
tileattrs.material == df.tiletype_material.FEATURE or
|
|
|
|
tileattrs.material == df.tiletype_material.MINERAL
|
|
|
|
local good_shape = tileattrs.shape == df.tiletype_shape.WALL
|
|
|
|
return good_material and good_shape
|
|
|
|
end
|
|
|
|
|
|
|
|
local function get_test_area(area, spec)
|
|
|
|
-- return with success if our current area meets or exceeds requirements
|
|
|
|
if area.width >= spec.width and area.height >= spec.height and
|
|
|
|
area.depth >= spec.depth then
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
-- return with failure if the test requirements cannot ever be satisfied by
|
|
|
|
-- the current map
|
|
|
|
if spec.width > df.global.world.map.x_count - 2 or
|
|
|
|
spec.height > df.global.world.map.y_count - 2 or
|
|
|
|
spec.depth > df.global.world.map.z_count then
|
2021-10-01 22:34:04 -06:00
|
|
|
print('map too small to accomodate test')
|
2021-05-26 10:45:49 -06:00
|
|
|
return false
|
|
|
|
end
|
|
|
|
|
|
|
|
-- keep this simple for now. just go down the layers and check the region
|
|
|
|
-- starting at the upper left corner of each level.
|
2021-06-07 07:06:49 -06:00
|
|
|
local startz = area.pos and area.pos.z or df.global.world.map.z_count-1
|
|
|
|
for z_start = startz,0,-1 do
|
2021-05-26 10:45:49 -06:00
|
|
|
local z_end = z_start - spec.depth + 1
|
|
|
|
if z_end < 1 then return false end
|
|
|
|
for z = z_start,z_end,-1 do
|
|
|
|
for y = 1,spec.height do
|
|
|
|
for x = 1,spec.width do
|
|
|
|
if not is_usable_test_tile(xyz2pos(x, y, z)) then
|
|
|
|
-- next check should start on the z-level below this one
|
|
|
|
z_start = z
|
|
|
|
goto continue
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
do
|
|
|
|
area.width, area.height, area.depth =
|
|
|
|
spec.width, spec.height, spec.depth
|
|
|
|
area.pos = {x=1, y=1, z=z_start}
|
2021-09-10 16:01:07 -06:00
|
|
|
area.endpos = {x=area.width, y=area.height, z=z_start-area.depth+1}
|
2021-05-26 10:45:49 -06:00
|
|
|
return true
|
|
|
|
end
|
|
|
|
::continue::
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local function get_cursor_arg(pos)
|
|
|
|
return ('--cursor=%d,%d,%d'):format(pos.x, pos.y, pos.z)
|
|
|
|
end
|
|
|
|
|
|
|
|
local function quickfort_cmd(cmd, listnum, pos)
|
|
|
|
dfhack.run_script('quickfort', cmd, '-q', listnum, get_cursor_arg(pos))
|
|
|
|
end
|
|
|
|
|
|
|
|
local function quickfort_run(listnum, pos)
|
|
|
|
quickfort_cmd('run', listnum, pos)
|
|
|
|
end
|
|
|
|
|
|
|
|
local function quickfort_undo(listnum, pos)
|
|
|
|
quickfort_cmd('undo', listnum, pos)
|
|
|
|
end
|
|
|
|
|
|
|
|
local function designate_area(pos, spec)
|
|
|
|
local endx, endy, endz = pos.x + spec.width - 1, pos.y + spec.height - 1,
|
|
|
|
pos.z - spec.depth + 1
|
|
|
|
for z = pos.z,endz,-1 do for y = pos.y,endy do for x = pos.x,endx do
|
|
|
|
dfhack.maps.getTileFlags(xyz2pos(x, y, z)).dig =
|
|
|
|
df.tile_dig_designation.Default
|
|
|
|
end end end
|
|
|
|
end
|
|
|
|
|
2021-10-04 12:14:05 -06:00
|
|
|
local function format_pos(pos)
|
|
|
|
return ('%s,%s,%s'):format(pos.x, pos.y, pos.z)
|
|
|
|
end
|
|
|
|
|
|
|
|
local function run_dig_now(area)
|
|
|
|
dfhack.run_command('dig-now', format_pos(area.pos),
|
|
|
|
format_pos(area.endpos), '--clean')
|
|
|
|
end
|
|
|
|
|
2021-05-26 10:45:49 -06:00
|
|
|
local function run_blueprint(basename, set, pos)
|
2021-10-02 13:52:43 -06:00
|
|
|
blueprint.run(tostring(set.spec.width), tostring(set.spec.height),
|
|
|
|
tostring(-set.spec.depth), output_dir..basename,
|
|
|
|
get_cursor_arg(pos), '-tphase')
|
2021-05-26 10:45:49 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
local function reset_area(area, spec)
|
2021-06-07 06:51:10 -06:00
|
|
|
-- include the area border tiles that can get unhidden
|
|
|
|
local width, height, depth = spec.width+2, spec.height+2, spec.depth
|
2021-06-07 06:22:39 -06:00
|
|
|
local commands = {
|
|
|
|
'f', 'any', ';',
|
|
|
|
'p', 'any', ';',
|
|
|
|
'p', 's', 'wall', ';',
|
|
|
|
'p', 'sp', 'normal', ';',
|
|
|
|
'p', 'h', '1', ';',
|
2021-06-07 06:51:10 -06:00
|
|
|
'r', tostring(width), tostring(height), tostring(depth)}
|
2021-06-07 06:22:39 -06:00
|
|
|
dfhack.run_command('tiletypes-command', table.unpack(commands))
|
|
|
|
|
|
|
|
local pos = copyall(area.pos)
|
2021-06-07 06:51:10 -06:00
|
|
|
-- include the border tiles
|
|
|
|
pos.x = pos.x - 1
|
|
|
|
pos.y = pos.y - 1
|
|
|
|
-- tiletypes goes up z's, so adjust starting zlevel accordingly
|
2021-06-07 06:22:39 -06:00
|
|
|
pos.z = pos.z - spec.depth + 1
|
|
|
|
|
|
|
|
dfhack.run_command('tiletypes-here', '--quiet', get_cursor_arg(pos))
|
2021-05-26 10:45:49 -06:00
|
|
|
|
2021-10-04 12:14:05 -06:00
|
|
|
-- patch up tiles where the material couldn't be automatically determined
|
|
|
|
-- we don't just set all tiles to 'stone' so we don't obliterate veins
|
|
|
|
commands = {
|
|
|
|
'f', 's', 'empty', ';',
|
|
|
|
'p', 'm', 'stone'}
|
|
|
|
dfhack.run_command('tiletypes-command', table.unpack(commands))
|
|
|
|
dfhack.run_command('tiletypes-here', '--quiet', get_cursor_arg(pos))
|
|
|
|
|
|
|
|
commands = {'f', 's', 'ramp_top'}
|
|
|
|
dfhack.run_command('tiletypes-command', table.unpack(commands))
|
|
|
|
dfhack.run_command('tiletypes-here', '--quiet', get_cursor_arg(pos))
|
2021-09-10 16:01:07 -06:00
|
|
|
end
|
|
|
|
|
2021-05-26 10:45:49 -06:00
|
|
|
function test.end_to_end()
|
|
|
|
-- read in test plan
|
|
|
|
local sets = get_blueprint_sets()
|
|
|
|
|
|
|
|
local area = {width=0, height=0, depth=0}
|
|
|
|
for basename,set in pairs(sets) do
|
2021-10-04 12:14:05 -06:00
|
|
|
print(('running quickfort ecosystem test: "%s": %s'):
|
2021-05-26 10:45:49 -06:00
|
|
|
format(basename, set.spec.description))
|
|
|
|
|
|
|
|
-- find an unused area of the map that meets requirements, else skip
|
|
|
|
if not get_test_area(area, set.spec) then
|
|
|
|
print(('cannot find unused map area to test set "%s"; skipping'):
|
|
|
|
format(basename))
|
|
|
|
goto continue
|
|
|
|
end
|
|
|
|
|
|
|
|
-- quickfort run #dig blueprint (or just designate the whole block if
|
|
|
|
-- there is no #dig blueprint)
|
2021-10-04 12:14:05 -06:00
|
|
|
local phases = set.phases
|
|
|
|
if phases.dig then
|
|
|
|
quickfort_run(phases.dig.listnum, area.pos)
|
2021-05-26 10:45:49 -06:00
|
|
|
else
|
|
|
|
designate_area(area.pos, set.spec)
|
|
|
|
end
|
|
|
|
|
2021-06-07 06:22:39 -06:00
|
|
|
-- run dig-now to dig out designated tiles
|
2021-10-04 12:14:05 -06:00
|
|
|
run_dig_now(area)
|
2021-05-26 10:45:49 -06:00
|
|
|
|
|
|
|
-- quickfort run remaining blueprints
|
2021-10-04 12:14:05 -06:00
|
|
|
for _,phase_name in ipairs(phase_names) do
|
|
|
|
if phase_name ~= 'dig' and phases[phase_name] then
|
|
|
|
quickfort_run(phases[phase_name].listnum, area.pos)
|
|
|
|
if phase_name == 'track' then
|
|
|
|
run_dig_now(area)
|
|
|
|
end
|
2021-05-26 10:45:49 -06:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- run blueprint to generate files in output dir
|
|
|
|
run_blueprint(basename, set, area.pos)
|
|
|
|
|
|
|
|
-- quickfort undo blueprints
|
2021-10-04 12:14:05 -06:00
|
|
|
for _,phase_name in ipairs(phase_names) do
|
|
|
|
if phases[phase_name] then
|
|
|
|
quickfort_undo(phases[phase_name].listnum, area.pos)
|
2021-05-26 10:45:49 -06:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- run tiletypes to reset tiles in area to hidden walls
|
|
|
|
reset_area(area, set.spec)
|
|
|
|
|
|
|
|
-- compare md5sum of input and output files
|
|
|
|
local md5File = dfhack.internal.md5File
|
2021-10-04 12:14:05 -06:00
|
|
|
for phase,phase_data in pairs(phases) do
|
|
|
|
if phase == 'notes' then goto continue end
|
|
|
|
print((' verifying phase: %s'):format(phase))
|
2022-02-12 19:11:33 -07:00
|
|
|
local golden_filepath = phase_data.golden_filepath
|
2021-10-04 12:14:05 -06:00
|
|
|
local output_filepath = phase_data.output_filepath
|
2022-02-12 19:11:33 -07:00
|
|
|
local input_hash, input_size = md5File(golden_filepath)
|
2021-05-26 10:45:49 -06:00
|
|
|
local output_hash, output_size = md5File(output_filepath)
|
|
|
|
expect.eq(input_hash, output_hash,
|
|
|
|
'compare blueprint contents to input: '..output_filepath)
|
|
|
|
expect.eq(input_size, output_size,
|
|
|
|
'compare blueprint length to input: '..output_filepath)
|
|
|
|
if not output_hash then goto continue end
|
|
|
|
if input_hash ~= output_hash or input_size ~= output_size then
|
|
|
|
-- show diff
|
|
|
|
local input, output =
|
2022-02-12 19:11:33 -07:00
|
|
|
io.open(golden_filepath, 'r'), io.open(output_filepath, 'r')
|
2021-05-26 10:45:49 -06:00
|
|
|
local input_lines, output_lines = {}, {}
|
|
|
|
for l in input:lines() do table.insert(input_lines, l) end
|
|
|
|
for l in output:lines() do table.insert(output_lines, l) end
|
|
|
|
input:close()
|
|
|
|
output:close()
|
|
|
|
expect.table_eq(input_lines, output_lines)
|
|
|
|
end
|
|
|
|
::continue::
|
|
|
|
end
|
|
|
|
|
|
|
|
::continue::
|
|
|
|
end
|
|
|
|
end
|