diff --git a/CMakeLists.txt b/CMakeLists.txt index 800f74e37..db4149630 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -503,6 +503,7 @@ if(BUILD_TESTS) endif() install(DIRECTORY ${dfhack_SOURCE_DIR}/test DESTINATION ${DFHACK_DATA_DESTINATION}/scripts) + install(FILES ci/test.lua DESTINATION ${DFHACK_DATA_DESTINATION}/scripts) endif() # Packaging with CPack! diff --git a/test/main.lua b/ci/test.lua similarity index 72% rename from test/main.lua rename to ci/test.lua index ac09e695e..91f39310b 100644 --- a/test/main.lua +++ b/ci/test.lua @@ -1,6 +1,7 @@ -- DFHack developer test harness --@ module = true +local expect = require 'test.expect' local json = require 'json' local script = require 'gui.script' local utils = require 'utils' @@ -8,14 +9,14 @@ local utils = require 'utils' local help_text = [====[ -test/main -========= +test +==== Run DFHack tests. Usage: - test/main [] [] + test [] [] If a done_command is specified, it will be run after the tests complete. @@ -35,14 +36,13 @@ Options: Examples: - test/main runs all tests - test/main -r runs all tests that haven't been run before - test/main -m none runs tests that don't need the game to be in a - specific mode - test/main -t quickfort runs quickfort tests - test/main -d /path/to/dfhack-scripts/repo/test - runs tests in your in-development branch of the - scripts repo + test runs all tests + test -r runs all tests that haven't been run before + test -m none runs tests that don't need the game to be in a + specific mode + test -t quickfort runs quickfort tests + test -d /path/to/dfhack-scripts/repo/test + runs tests in your dev scripts repo Default values for the options may be set in a file named test_config.json in your DF folder. Options with comma-separated values should be written as json @@ -67,127 +67,6 @@ local TestStatus = { local VALID_MODES = utils.invert{'none', 'title', 'fortress'} -expect = {} -function expect.true_(value, comment) - return not not value, comment, 'expected true, got ' .. tostring(value) -end -function expect.false_(value, comment) - return not value, comment, 'expected false, got ' .. tostring(value) -end -function expect.fail(comment) - return false, comment or 'check failed, no reason provided' -end -function expect.nil_(value, comment) - return value == nil, comment, 'expected nil, got ' .. tostring(value) -end -function expect.eq(a, b, comment) - return a == b, comment, ('%s ~= %s'):format(a, b) -end -function expect.ne(a, b, comment) - return a ~= b, comment, ('%s == %s'):format(a, b) -end -function expect.lt(a, b, comment) - return a < b, comment, ('%s >= %s'):format(a, b) -end -function expect.le(a, b, comment) - return a <= b, comment, ('%s > %s'):format(a, b) -end -function expect.gt(a, b, comment) - return a > b, comment, ('%s <= %s'):format(a, b) -end -function expect.ge(a, b, comment) - return a >= b, comment, ('%s < %s'):format(a, b) -end -local function table_eq_recurse(a, b, keys, known_eq) - if a == b then return true end - local checked = {} - for k,v in pairs(a) do - if type(a[k]) == 'table' then - if known_eq[a[k]] and known_eq[a[k]][b[k]] then goto skip end - table.insert(keys, tostring(k)) - if type(b[k]) ~= 'table' then - return false, keys, {tostring(a[k]), tostring(b[k])} - end - if not known_eq[a[k]] then known_eq[a[k]] = {} end - for eq_tab,_ in pairs(known_eq[a[k]]) do - known_eq[eq_tab][b[k]] = true - end - known_eq[a[k]][b[k]] = true - if not known_eq[b[k]] then known_eq[b[k]] = {} end - for eq_tab,_ in pairs(known_eq[b[k]]) do - known_eq[eq_tab][a[k]] = true - end - known_eq[b[k]][a[k]] = true - local matched, keys_at_diff, diff = - table_eq_recurse(a[k], b[k], keys, known_eq) - if not matched then return false, keys_at_diff, diff end - keys[#keys] = nil - elseif a[k] ~= b[k] then - table.insert(keys, tostring(k)) - return false, keys, {tostring(a[k]), tostring(b[k])} - end - ::skip:: - checked[k] = true - end - for k in pairs(b) do - if not checked[k] then - table.insert(keys, tostring(k)) - return false, keys, {tostring(a[k]), tostring(b[k])} - end - end - return true -end -function expect.table_eq(a, b, comment) - if (type(a) ~= 'table' and type(a) ~= 'userdata') or - (type(b) ~= 'table' and type(b) ~= 'userdata') then - return false, comment, 'operands to table_eq must be tables or userdata' - end - local keys, known_eq = {}, {} - local matched, keys_at_diff, diff = table_eq_recurse(a, b, keys, known_eq) - if matched then return true end - local keystr = '['..keys_at_diff[1]..']' - for i=2,#keys_at_diff do keystr = keystr..'['..keys_at_diff[i]..']' end - return false, comment, - ('key %s: "%s" ~= "%s"'):format(keystr, diff[1], diff[2]) -end -function expect.error(func, ...) - local ok, ret = pcall(func, ...) - if ok then - return false, 'no error raised by function call' - else - return true - end -end -function expect.error_match(func, matcher, ...) - local ok, err = pcall(func, ...) - if ok then - return false, 'no error raised by function call' - elseif type(matcher) == 'string' then - if not tostring(err):match(matcher) then - return false, ('error "%s" did not match "%s"'):format(err, matcher) - end - elseif not matcher(err) then - return false, ('error "%s" did not satisfy matcher'):format(err) - end - return true -end -function expect.pairs_contains(table, key, comment) - for k, v in pairs(table) do - if k == key then - return true - end - end - return false, comment, ('could not find key "%s" in table'):format(key) -end -function expect.not_pairs_contains(table, key, comment) - for k, v in pairs(table) do - if k == key then - return false, comment, ('found key "%s" in table'):format(key) - end - end - return true -end - local function delay(frames) frames = frames or 1 script.sleep(frames, 'frames') @@ -285,7 +164,7 @@ local function get_test_files(test_dir) local files = {} print('Loading tests from ' .. test_dir) for _, entry in ipairs(dfhack.filesystem.listdir_recursive(test_dir)) do - if not entry.isdir and not entry.path:match('main.lua') then + if not entry.isdir then table.insert(files, entry.path) end end diff --git a/docs/changelog.txt b/docs/changelog.txt index ee509e9df..fa862eecd 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -38,6 +38,7 @@ changelog.txt uses a syntax similar to RST, with a few special sequences: ## Internals - The DFHack test harness is now much easier to use for iterative development. Configuration can now be specified on the commandline, there are more test filter options, and the test harness can now easily rerun tests that have been run before. +- The ``test/main`` command to invoke the test harness has been renamed to just ``test`` # 0.47.05-r1 diff --git a/library/lua/test/expect.lua b/library/lua/test/expect.lua new file mode 100644 index 000000000..bdecdbafa --- /dev/null +++ b/library/lua/test/expect.lua @@ -0,0 +1,145 @@ +-- Internal implementations of expect.*() functions for tests + +-- NOTE: do not use this module in tests directly! The test wrapper (ci/test.lua) +-- wraps these functions to track which tests have passed/failed, and calling +-- these functions directly will not work as expected. + + +local expect = mkmodule('test.expect') + +function expect.true_(value, comment) + return not not value, comment, 'expected true, got ' .. tostring(value) +end + +function expect.false_(value, comment) + return not value, comment, 'expected false, got ' .. tostring(value) +end + +function expect.fail(comment) + return false, comment or 'check failed, no reason provided' +end + +function expect.nil_(value, comment) + return value == nil, comment, 'expected nil, got ' .. tostring(value) +end + +function expect.eq(a, b, comment) + return a == b, comment, ('%s ~= %s'):format(a, b) +end + +function expect.ne(a, b, comment) + return a ~= b, comment, ('%s == %s'):format(a, b) +end + +function expect.lt(a, b, comment) + return a < b, comment, ('%s >= %s'):format(a, b) +end + +function expect.le(a, b, comment) + return a <= b, comment, ('%s > %s'):format(a, b) +end + +function expect.gt(a, b, comment) + return a > b, comment, ('%s <= %s'):format(a, b) +end + +function expect.ge(a, b, comment) + return a >= b, comment, ('%s < %s'):format(a, b) +end + +local function table_eq_recurse(a, b, keys, known_eq) + if a == b then return true end + local checked = {} + for k,v in pairs(a) do + if type(a[k]) == 'table' then + if known_eq[a[k]] and known_eq[a[k]][b[k]] then goto skip end + table.insert(keys, tostring(k)) + if type(b[k]) ~= 'table' then + return false, keys, {tostring(a[k]), tostring(b[k])} + end + if not known_eq[a[k]] then known_eq[a[k]] = {} end + for eq_tab,_ in pairs(known_eq[a[k]]) do + known_eq[eq_tab][b[k]] = true + end + known_eq[a[k]][b[k]] = true + if not known_eq[b[k]] then known_eq[b[k]] = {} end + for eq_tab,_ in pairs(known_eq[b[k]]) do + known_eq[eq_tab][a[k]] = true + end + known_eq[b[k]][a[k]] = true + local matched, keys_at_diff, diff = + table_eq_recurse(a[k], b[k], keys, known_eq) + if not matched then return false, keys_at_diff, diff end + keys[#keys] = nil + elseif a[k] ~= b[k] then + table.insert(keys, tostring(k)) + return false, keys, {tostring(a[k]), tostring(b[k])} + end + ::skip:: + checked[k] = true + end + for k in pairs(b) do + if not checked[k] then + table.insert(keys, tostring(k)) + return false, keys, {tostring(a[k]), tostring(b[k])} + end + end + return true +end + +function expect.table_eq(a, b, comment) + if (type(a) ~= 'table' and type(a) ~= 'userdata') or + (type(b) ~= 'table' and type(b) ~= 'userdata') then + return false, comment, 'operands to table_eq must be tables or userdata' + end + local keys, known_eq = {}, {} + local matched, keys_at_diff, diff = table_eq_recurse(a, b, keys, known_eq) + if matched then return true end + local keystr = '['..keys_at_diff[1]..']' + for i=2,#keys_at_diff do keystr = keystr..'['..keys_at_diff[i]..']' end + return false, comment, + ('key %s: "%s" ~= "%s"'):format(keystr, diff[1], diff[2]) +end + +function expect.error(func, ...) + local ok, ret = pcall(func, ...) + if ok then + return false, 'no error raised by function call' + else + return true + end +end + +function expect.error_match(func, matcher, ...) + local ok, err = pcall(func, ...) + if ok then + return false, 'no error raised by function call' + elseif type(matcher) == 'string' then + if not tostring(err):match(matcher) then + return false, ('error "%s" did not match "%s"'):format(err, matcher) + end + elseif not matcher(err) then + return false, ('error "%s" did not satisfy matcher'):format(err) + end + return true +end + +function expect.pairs_contains(table, key, comment) + for k, v in pairs(table) do + if k == key then + return true + end + end + return false, comment, ('could not find key "%s" in table'):format(key) +end + +function expect.not_pairs_contains(table, key, comment) + for k, v in pairs(table) do + if k == key then + return false, comment, ('found key "%s" in table'):format(key) + end + end + return true +end + +return expect diff --git a/test/test_test.lua b/test/test.lua similarity index 96% rename from test/test_test.lua rename to test/test.lua index 4543375e3..c2b083d38 100644 --- a/test/test_test.lua +++ b/test/test.lua @@ -1,4 +1,4 @@ -local expect_raw = reqscript('test/main').expect +local expect_raw = require('test.expect') function test.internal_in_test() expect.true_(dfhack.internal.IN_TEST) diff --git a/travis/run-tests.py b/travis/run-tests.py index 8f20a8efa..400a7141d 100755 --- a/travis/run-tests.py +++ b/travis/run-tests.py @@ -73,7 +73,7 @@ with open(test_init_file, 'w') as f: f.write(''' devel/dump-rpc dfhack-rpc.txt :lua dfhack.internal.addScriptPath(dfhack.getHackPath()) - test/main --resume "lua scr.breakdown_level=df.interface_breakdown_types.%s" + test --resume "lua scr.breakdown_level=df.interface_breakdown_types.%s" ''' % ('NONE' if args.no_quit else 'QUIT')) test_config_file = 'test_config.json'