diff --git a/docs/changelog.txt b/docs/changelog.txt index be0b51c69e..007def690a 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -75,6 +75,7 @@ Template for new versions: - `buildingplan`: fix "Unlink all" only unlinking some mechanisms (or crashing) when freeing mechanisms from the building - `tiletypes`: remove plants (including trees) rooted on a tile that is painted into something that cannot host them, so cleared ground no longer regrows floating trees - `timestream`: deal properly with units who have breathing difficulties +- `timestream`: pass the validated numeric value to the fps setter so ``set fps`` with unusual numeric strings (e.g. ``60.9``) can't be silently misapplied - `stocks`: overlay now resets scroll position when collapsing categories so the item list is no longer left blank and unscrollable - Fixed persistent site data API (``dfhack.persistent.saveSiteData``/``getSiteData``) failing on newly reclaimed fortresses until the first save diff --git a/plugins/lua/timestream.lua b/plugins/lua/timestream.lua index 6f5b68ddf6..91398e355d 100644 --- a/plugins/lua/timestream.lua +++ b/plugins/lua/timestream.lua @@ -15,7 +15,7 @@ local function do_set(setting_name, arg) if setting_name ~= 'fps' or not numarg then qerror('must specify setting and value') end - timestream_setFps(arg) + timestream_setFps(numarg) print(('set %s to %s'):format(setting_name, timestream_getFps())) end diff --git a/test/plugins/autonestbox.lua b/test/plugins/autonestbox.lua new file mode 100644 index 0000000000..997feb054b --- /dev/null +++ b/test/plugins/autonestbox.lua @@ -0,0 +1,37 @@ +config.mode = 'fortress' +config.target = 'autonestbox' + +local autonestbox = require('plugins.autonestbox') + +local function parse(args) + local opts = {} + autonestbox.parse_commandline(opts, args) + return opts +end + +function test.now_flag() + expect.true_(parse({'now'}).now) +end + +function test.help_flag() + expect.true_(parse({'help'}).help) + expect.true_(parse({'--help'}).help) + expect.true_(parse({'-h'}).help) +end + +function test.help_does_not_set_now() + local opts = parse({'help', 'now'}) + expect.true_(opts.help) + expect.nil_(opts.now) +end + +function test.unknown_positionals_ignored() + local opts = parse({'bogus'}) + expect.nil_(opts.now) + expect.nil_(opts.help) +end + +function test.mixed_positionals() + local opts = parse({'bogus', 'now', 'other'}) + expect.true_(opts.now) +end diff --git a/test/plugins/plant.lua b/test/plugins/plant.lua new file mode 100644 index 0000000000..2079713505 --- /dev/null +++ b/test/plugins/plant.lua @@ -0,0 +1,118 @@ +config.mode = 'fortress' +config.target = 'plant' + +local plant = require('plugins.plant') + +-- parse_commandline takes C++-allocated outputs: an options table, two +-- coords, and a vector filter. stl vectors can't be created from +-- Lua, so borrow an int32 vector field from a temporary burrow. +local function parse(args) + local opts = {} + local pos1, pos2 = xyz2pos(0, 0, -30000), xyz2pos(0, 0, -30000) + local b = df.burrow:new() + local ok, err = pcall(plant.parse_commandline, opts, pos1, pos2, b.block_x, args) + -- copy out before deleting the burrow; the vector dangles with it + local filter = {} + for i = 0, #b.block_x - 1 do filter[i + 1] = b.block_x[i] end + local result = {opts=opts, pos1=pos1, pos2=pos2, filter=filter} + b:delete() + if not ok then error(err, 0) end + return result +end + +local function find_valid_plant() + for _,p in ipairs(df.global.world.raws.plants.bushes) do + if p.id and #p.id > 0 then return p end + end +end + +function test.modes_set_flags() + expect.eq(-2, parse({'list'}).opts.plant_idx) + expect.true_(parse({'grow'}).opts.grow) + expect.true_(parse({'remove'}).opts.del) +end + +function test.create_requires_plant_id() + expect.error_match('Must specify plant_id', + function() parse({'create'}) end) +end + +function test.create_resolves_plant_by_name_and_index() + local p = find_valid_plant() + if not p then return end + local by_name = parse({'create', p.id}) + expect.true_(by_name.opts.create) + expect.eq(p.index, by_name.opts.plant_idx) + expect.eq(p.index, parse({'create', tostring(p.index)}).opts.plant_idx) +end + +function test.create_unknown_plant_errors() + expect.error_match('Plant raw not found', + function() parse({'create', 'DFHACK_NO_SUCH_PLANT'}) end) +end + +function test.invalid_and_missing_modes_error() + expect.error_match('Specify mode', function() parse({}) end) + expect.error_match('Invalid mode', function() parse({'bogus'}) end) +end + +function test.too_many_positionals_error() + expect.error_match('Too many positionals', + function() parse({'grow', '1,1,100', '2,2,100', 'extra'}) end) +end + +function test.age_option_parsing() + expect.eq(40320*3-1, parse({'grow', '--age', 'tree'}).opts.age) + expect.eq(40320*5-1, parse({'grow', '--age', '5'}).opts.age) + expect.eq(40320*1250-1, parse({'grow', '--age', '9999'}).opts.age) + expect.error_match('Invalid age', + function() parse({'grow', '--age', 'bogus'}) end) +end + +function test.filter_and_exclude_populate_vector() + local p = find_valid_plant() + if not p then return end + local res = parse({'remove', '--filter', p.id}) + expect.eq(1, #res.filter) + expect.eq(p.index, res.filter[1]) + res = parse({'remove', '--exclude', p.id}) + expect.true_(res.opts.filter_ex) +end + +function test.double_filter_errors() + expect.error_match('Filter already defined', + function() + local p = find_valid_plant() + if not p then error('skip') end + parse({'remove', '--filter', p.id, '--exclude', p.id}) + end) +end + +function test.option_flags() + local opts = parse({'remove', '-s', '-p', '-t', '-d', '-z', '-n', '-c'}).opts + expect.true_(opts.shrubs) + expect.true_(opts.saplings) + expect.true_(opts.trees) + expect.true_(opts.dead) + expect.true_(opts.zlevel) + expect.true_(opts.dry_run) + expect.true_(opts.force) +end + +function test.coords_assigned_to_positions() + local res = parse({'grow', '3,4,100', '7,8,101'}) + expect.eq(3, res.pos1.x) + expect.eq(4, res.pos1.y) + expect.eq(100, res.pos1.z) + expect.eq(7, res.pos2.x) + expect.eq(8, res.pos2.y) + expect.eq(101, res.pos2.z) +end + +function test.create_uses_third_positional_as_pos1() + local p = find_valid_plant() + if not p then return end + local res = parse({'create', p.id, '9,10,102'}) + expect.eq(9, res.pos1.x) + expect.eq(102, res.pos1.z) +end diff --git a/test/plugins/prospector.lua b/test/plugins/prospector.lua new file mode 100644 index 0000000000..42b0016f60 --- /dev/null +++ b/test/plugins/prospector.lua @@ -0,0 +1,55 @@ +config.mode = 'fortress' +config.target = 'prospector' + +local prospector = require('plugins.prospector') + +local function parse(args) + local opts = {} + prospector.parse_commandline(opts, args) + return opts +end + +function test.help_flag() + expect.true_(parse({'--help'}).help) + expect.true_(parse({'-h'}).help) +end + +function test.all_and_hell_set_hidden() + expect.true_(parse({'all'}).hidden) + expect.nil_(parse({'all'}).tube) + local opts = parse({'hell'}) + expect.true_(opts.hidden) + expect.true_(opts.tube) +end + +function test.unknown_keyword_errors() + expect.error_match('unknown keyword', + function() prospector.parse_commandline({}, {'bogus'}) end) +end + +function test.show_sections_set_flags() + local opts = parse({'-s', 'gems,ores'}) + expect.true_(opts.gems) + expect.true_(opts.ores) + -- selecting any section disables the rest + expect.false_(opts.summary) + expect.false_(opts.layers) +end + +function test.show_all_sections() + local opts = parse({'-s', 'summary,liquids,layers,features,ores,gems,veins,shrubs,trees'}) + for _,s in ipairs{'summary', 'liquids', 'layers', 'features', 'ores', + 'gems', 'veins', 'shrubs', 'trees'} do + expect.true_(opts[s], 'expected section flag set: '..s) + end +end + +function test.unknown_show_section_errors() + expect.error_match('unknown report section', + function() prospector.parse_commandline({}, {'-s', 'bogus'}) end) +end + +function test.values_flag() + expect.true_(parse({'-v'}).value) + expect.true_(parse({'--values'}).value) +end diff --git a/test/plugins/tiletypes.lua b/test/plugins/tiletypes.lua index a10990179f..88c8aa6494 100644 --- a/test/plugins/tiletypes.lua +++ b/test/plugins/tiletypes.lua @@ -1,6 +1,8 @@ config.mode = 'fortress' config.target = 'tiletypes' +local tiletypes = require('plugins.tiletypes') + local function set_cursor(pos) df.global.cursor.x = pos.x df.global.cursor.y = pos.y @@ -116,3 +118,48 @@ function test.here_point_bad_option_is_failure() local _, status = dfhack.run_command_silent('tiletypes-here-point --bogus') expect.eq(CR_FAILURE, status) end + +local function parse(args) + local opts = {cursor = xyz2pos(0, 0, 0)} + tiletypes.parse_commandline(opts, args) + return opts +end + +function test.help_flag() + expect.true_(parse({'--help'}).help) + expect.true_(parse({'-h'}).help) + expect.true_(parse({'help'}).help) + expect.true_(parse({'?'}).help) +end + +function test.quiet_flag() + expect.true_(parse({'-q'}).quiet) + expect.true_(parse({'--quiet'}).quiet) +end + +function test.cursor_option() + local opts = parse({'-c', '5,7,99'}) + expect.eq(5, opts.cursor.x) + expect.eq(7, opts.cursor.y) + expect.eq(99, opts.cursor.z) +end + +function test.cursor_option_long_form() + local opts = parse({'--cursor', '1,2,3'}) + expect.eq(1, opts.cursor.x) + expect.eq(2, opts.cursor.y) + expect.eq(3, opts.cursor.z) +end + +function test.bad_cursor_errors() + expect.error(function() + tiletypes.parse_commandline({cursor = xyz2pos(0, 0, 0)}, + {'-c', 'not-a-coord'}) + end) +end + +function test.positionals_do_not_set_flags() + local opts = parse({'paint', 'stone', 'microcline'}) + expect.nil_(opts.help) + expect.nil_(opts.quiet) +end diff --git a/test/plugins/timestream.lua b/test/plugins/timestream.lua new file mode 100644 index 0000000000..b3594dee8e --- /dev/null +++ b/test/plugins/timestream.lua @@ -0,0 +1,47 @@ +config.mode = 'fortress' +config.target = 'timestream' + +local timestream = require('plugins.timestream') + +local saved_fps = timestream.timestream_getFps() + +config.wrapper = function(test_fn) + return dfhack.with_finalize(function() + timestream.timestream_setFps(saved_fps) + end, test_fn) +end + +function test.status_and_unknown_command() + expect.true_(timestream.parse_commandline({})) + expect.true_(timestream.parse_commandline({'status'})) + expect.false_(timestream.parse_commandline({'help'})) + expect.false_(timestream.parse_commandline({'bogus'})) +end + +function test.set_fps_roundtrip() + expect.true_(timestream.parse_commandline({'set', 'fps', '60'})) + expect.eq(60, timestream.timestream_getFps()) +end + +function test.set_fps_clamps_to_minimum() + -- the C++ layer clamps the target to at least 10 fps + expect.true_(timestream.parse_commandline({'set', 'fps', '5'})) + expect.eq(10, timestream.timestream_getFps()) +end + +function test.set_fps_rejects_bad_args() + expect.error_match('must specify setting and value', + function() timestream.parse_commandline({'set'}) end) + expect.error_match('must specify setting and value', + function() timestream.parse_commandline({'set', 'fps'}) end) + expect.error_match('must specify setting and value', + function() timestream.parse_commandline({'set', 'bogus', '60'}) end) + expect.error_match('must specify setting and value', + function() timestream.parse_commandline({'set', 'fps', 'fast'}) end) +end + +function test.reset_restores_default() + timestream.timestream_setFps(43) + expect.true_(timestream.parse_commandline({'reset'})) + expect.ne(43, timestream.timestream_getFps()) +end