From e65f11731155dec69701d806ba00c6bd24ffc33c Mon Sep 17 00:00:00 2001 From: Darshan Shaligram Date: Thu, 24 Sep 2009 20:18:07 +0530 Subject: Set up a basic testing framework for LOS. ./crawl -test with a full debug build will run the test. --- crawl-ref/.gitignore | 2 +- crawl-ref/source/acr.cc | 19 +++++++ crawl-ref/source/chardump.cc | 9 ++- crawl-ref/source/clua.cc | 6 -- crawl-ref/source/ctest.cc | 121 +++++++++++++++++++++++++++++++++++++++++ crawl-ref/source/ctest.h | 15 +++++ crawl-ref/source/debug.cc | 3 +- crawl-ref/source/dungeon.cc | 6 ++ crawl-ref/source/dungeon.h | 2 + crawl-ref/source/files.cc | 12 +++- crawl-ref/source/files.h | 4 ++ crawl-ref/source/initfile.cc | 18 +++--- crawl-ref/source/luadgn.cc | 89 +++++++++++++++++++++++++++++- crawl-ref/source/makefile.obj | 1 + crawl-ref/source/makefile.unix | 3 + crawl-ref/source/state.h | 2 + crawl-ref/source/stuff.cc | 3 +- crawl-ref/source/test/los.lua | 68 +++++++++++++++++++++++ 18 files changed, 359 insertions(+), 24 deletions(-) create mode 100644 crawl-ref/source/ctest.cc create mode 100644 crawl-ref/source/ctest.h create mode 100644 crawl-ref/source/test/los.lua (limited to 'crawl-ref') diff --git a/crawl-ref/.gitignore b/crawl-ref/.gitignore index 112510f8e1..33683b3191 100644 --- a/crawl-ref/.gitignore +++ b/crawl-ref/.gitignore @@ -7,7 +7,7 @@ morgue *.rej *.swp core -debug.map +*.map err.txt map.dump mapgen.log diff --git a/crawl-ref/source/acr.cc b/crawl-ref/source/acr.cc index 2da1b34de9..ad5b968a30 100644 --- a/crawl-ref/source/acr.cc +++ b/crawl-ref/source/acr.cc @@ -53,6 +53,7 @@ REVISION("$Rev$"); #include "cloud.h" #include "clua.h" #include "command.h" +#include "ctest.h" #include "crash.h" #include "database.h" #include "debug.h" @@ -316,6 +317,10 @@ static void _show_commandline_options_help() puts(""); puts("Arena options: (Stage a tournament between various monsters.)"); puts(" -arena \" v arena:\""); +#if DEBUG_DIAGNOSTICS + puts(""); + puts(" -test run test cases in ./test"); +#endif } static void _wanderer_startup_message() @@ -3508,6 +3513,20 @@ static bool _initialise(void) } #endif + if (crawl_state.test) + { +#ifdef DEBUG_DIAGNOSTICS + crawl_tests::run_tests(true); + // Superfluous, just to make it clear that this is the end of + // the line. + end(0, false); +#else + end(1, false, "Non-debug Crawl cannot run tests. " + "Please use a debug build"); +#endif + } + + if (crawl_state.arena) { run_map_preludes(); diff --git a/crawl-ref/source/chardump.cc b/crawl-ref/source/chardump.cc index 097932a3c0..be0b4d43aa 100644 --- a/crawl-ref/source/chardump.cc +++ b/crawl-ref/source/chardump.cc @@ -1207,7 +1207,14 @@ void dump_map(FILE *fp, bool debug) for (int y = 0; y < GYM; ++y) { for (int x = 0; x < GXM; ++x) - fputc(grid_character_at(coord_def(x,y)), fp); + { + if (you.pos() == coord_def(x, y)) + fputc('@', fp); + else if (grd[x][y] == DNGN_FLOOR_SPECIAL) + fputc('?', fp); + else + fputc(grid_character_at(coord_def(x,y)), fp); + } fputc('\n', fp); } } diff --git a/crawl-ref/source/clua.cc b/crawl-ref/source/clua.cc index d14df76573..de3a3f534c 100644 --- a/crawl-ref/source/clua.cc +++ b/crawl-ref/source/clua.cc @@ -810,10 +810,6 @@ LUARET1(you_taking_stairs, boolean, current_delay_action() == DELAY_ASCENDING_STAIRS || current_delay_action() == DELAY_DESCENDING_STAIRS) LUARET1(you_turns, number, you.num_turns) -LUARET1(you_see_grid, boolean, - see_grid(luaL_checkint(ls, 1), luaL_checkint(ls, 2))) -LUARET1(you_see_grid_no_trans, boolean, - see_grid_no_trans(luaL_checkint(ls, 1), luaL_checkint(ls, 2))) LUARET1(you_can_smell, boolean, player_can_smell()) LUARET1(you_has_claws, number, you.has_claws(false)) @@ -928,8 +924,6 @@ static const struct luaL_reg you_lib[] = { "subdepth", you_subdepth }, { "absdepth", you_absdepth }, - { "see_grid", you_see_grid }, - { "see_grid_no_trans", you_see_grid_no_trans }, { "can_smell", you_can_smell }, { "has_claws", you_has_claws }, diff --git a/crawl-ref/source/ctest.cc b/crawl-ref/source/ctest.cc new file mode 100644 index 0000000000..48227254e2 --- /dev/null +++ b/crawl-ref/source/ctest.cc @@ -0,0 +1,121 @@ +/* + * File: ctest.cc + * Summary: Crawl Lua test cases + * Written by: Darshan Shaligram + * + * Modified for Crawl Reference by $Author$ on $Date$ + * + * ctest runs Lua tests found in the test directory. The intent here + * is to test parts of Crawl that can be easily tested from within Crawl + * itself (such as LOS). As a side-effect, writing Lua bindings to support + * tests will expand the available Lua bindings. :-) + * + * Tests will run only with Crawl built in its source tree without + * DATA_DIR_PATH set. + */ + +#include "AppHdr.h" + +#if DEBUG_DIAGNOSTICS + +#include "clua.h" +#include "files.h" +#include "luadgn.h" +#include "maps.h" +#include "stuff.h" + +#include +#include + +namespace crawl_tests +{ + const std::string test_dir = "test"; + const std::string test_player_name = "Superbug99"; + + int ntests = 0; + int nsuccess = 0; + + typedef std::pair file_error; + std::vector failures; + + void reset_test_data() + { + ntests = 0; + nsuccess = 0; + failures.clear(); + // XXX: Good grief, you.your_name is still not a C++ string?! + strncpy(you.your_name, test_player_name.c_str(), kNameLen); + you.your_name[kNameLen - 1] = 0; + } + + int crawl_begin_test(lua_State *ls) + { + mprf(MSGCH_PROMPT, "Starting tests: %s", luaL_checkstring(ls, 1)); + lua_pushnumber(ls, ++ntests); + return (1); + } + + int crawl_test_success(lua_State *ls) + { + mprf(MSGCH_PROMPT, "Test success: %s", luaL_checkstring(ls, 1)); + lua_pushnumber(ls, ++nsuccess); + return (1); + } + + static const struct luaL_reg crawl_test_lib[] = + { + { "begin_test", crawl_begin_test }, + { "test_success", crawl_test_success }, + { NULL, NULL } + }; + + void init_test_bindings() + { + lua_stack_cleaner clean(dlua); + luaL_openlib(dlua, "crawl", crawl_test_lib, 0); + } + + void run_test(const std::string &file) + { + ++ntests; + const std::string path(catpath(test_dir, file)); + dlua.execfile(path.c_str(), true, false); + if (dlua.error.empty()) + ++nsuccess; + else + failures.push_back(file_error(file, dlua.error)); + } + + // Assumes curses has already been initialized. + bool run_tests(bool exit_on_complete) + { + run_map_preludes(); + reset_test_data(); + + init_test_bindings(); + + // Get a list of Lua files in test. Order of execution of + // tests should be irrelevant. + const std::vector tests( + get_dir_files_ext(test_dir, ".lua")); + std::for_each(tests.begin(), tests.end(), + run_test); + + if (exit_on_complete) + { + cio_cleanup(); + for (int i = 0, size = failures.size(); i < size; ++i) + { + const file_error &fe(failures[i]); + fprintf(stderr, "Test error: %s\n", + fe.second.c_str()); + } + const int code = failures.empty() ? 0 : 1; + end(code, false, "%d tests, %d succeeded, %d failed", + ntests, nsuccess, failures.size()); + } + return (failures.empty()); + } +} + +#endif // DEBUG_DIAGNOSTICS diff --git a/crawl-ref/source/ctest.h b/crawl-ref/source/ctest.h new file mode 100644 index 0000000000..0cc1d1ad55 --- /dev/null +++ b/crawl-ref/source/ctest.h @@ -0,0 +1,15 @@ +#ifndef CTEST_H +#define CTEST_H + +#include "AppHdr.h" + +#ifdef DEBUG_DIAGNOSTICS + +namespace crawl_tests +{ + void run_tests(bool exit_on_complete); +} + +#endif + +#endif diff --git a/crawl-ref/source/debug.cc b/crawl-ref/source/debug.cc index 4b47b21c88..c008f5c1fa 100644 --- a/crawl-ref/source/debug.cc +++ b/crawl-ref/source/debug.cc @@ -819,7 +819,8 @@ static void _wizard_go_to_level(const level_pos &pos) #else UNUSED(newlevel); #endif - save_game_state(); + if (!crawl_state.test) + save_game_state(); new_level(); viewwindow(true, true); diff --git a/crawl-ref/source/dungeon.cc b/crawl-ref/source/dungeon.cc index 1abe8a5a99..30de547fe9 100644 --- a/crawl-ref/source/dungeon.cc +++ b/crawl-ref/source/dungeon.cc @@ -363,6 +363,12 @@ void level_clear_vault_memory() dgn_Map_Mask.init(0); } +void dgn_flush_map_memory() +{ + you.uniq_map_tags.clear(); + you.uniq_map_names.clear(); +} + static void _dgn_load_colour_grid() { dgn_colour_grid.reset(new dungeon_colour_grid); diff --git a/crawl-ref/source/dungeon.h b/crawl-ref/source/dungeon.h index 00d24662b6..cb9c4bdbda 100644 --- a/crawl-ref/source/dungeon.h +++ b/crawl-ref/source/dungeon.h @@ -332,6 +332,8 @@ void write_level_connectivity(writer &th); bool builder(int level_number, int level_type); +void dgn_flush_map_memory(); + coord_def dgn_find_feature_marker(dungeon_feature_type feat); // Set floor/wall colour based on the mons_alloc array. Used for diff --git a/crawl-ref/source/files.cc b/crawl-ref/source/files.cc index b5dc708b08..3650f8899d 100644 --- a/crawl-ref/source/files.cc +++ b/crawl-ref/source/files.cc @@ -225,7 +225,6 @@ bool get_dos_compatible_file_name(std::string *fname) } #endif - // Returns the names of all files in the given directory. Note that the // filenames returned are relative to the directory. std::vector get_dir_files(const std::string &dirname) @@ -268,6 +267,17 @@ std::vector get_dir_files(const std::string &dirname) return (files); } +std::vector get_dir_files_ext(const std::string &dir, + const std::string &ext) +{ + const std::vector allfiles(get_dir_files(dir)); + std::vector filtered; + for (int i = 0, size = allfiles.size(); i < size; ++i) + if (ends_with(allfiles[i], ext)) + filtered.push_back(allfiles[i]); + return (filtered); +} + std::string get_parent_directory(const std::string &filename) { std::string::size_type pos = filename.rfind(FILE_SEPARATOR); diff --git a/crawl-ref/source/files.h b/crawl-ref/source/files.h index e732e354ca..50da4cb0cc 100644 --- a/crawl-ref/source/files.h +++ b/crawl-ref/source/files.h @@ -39,6 +39,10 @@ bool is_absolute_path(const std::string &path); bool is_read_safe_path(const std::string &path); void assert_read_safe_path(const std::string &path) throw (std::string); +std::vector get_dir_files(const std::string &dir); +std::vector get_dir_files_ext(const std::string &dir, + const std::string &ext); + std::string datafile_path(std::string basename, bool croak_on_fail = true, bool test_base_path = false); diff --git a/crawl-ref/source/initfile.cc b/crawl-ref/source/initfile.cc index a342137e3b..247cde2076 100644 --- a/crawl-ref/source/initfile.cc +++ b/crawl-ref/source/initfile.cc @@ -3424,6 +3424,7 @@ enum commandline_option_type { CLO_MACRO, CLO_MAPSTAT, CLO_ARENA, + CLO_TEST, CLO_HELP, CLO_NOPS @@ -3432,7 +3433,7 @@ enum commandline_option_type { static const char *cmd_ops[] = { "scores", "name", "species", "job", "plain", "dir", "rc", "rcdir", "tscores", "vscores", "scorefile", "morgue", "macro", - "mapstat", "arena", "help" + "mapstat", "arena", "test", "help" }; const int num_cmd_ops = CLO_NOPS; @@ -3451,28 +3452,21 @@ std::string find_executable_path() tempPath[0] = 0; #if defined ( _MSC_VER ) - int retval = GetModuleFileName ( NULL, tempPath, sizeof(tempPath) ); - #elif defined ( __linux__ ) - int retval = readlink ( "/proc/self/exe", tempPath, sizeof(tempPath) ); - #elif defined ( __MACH__ ) - strncpy ( tempPath, NXArgv[0], sizeof(tempPath) ); - #else - // We don't know how to find the executable's path on this OS. - #endif - return std::string(tempPath); } bool parse_args( int argc, char **argv, bool rc_only ) { + COMPILE_CHECK(ARRAYSZ(cmd_ops) == CLO_NOPS, c1); + std::string exe_path = find_executable_path(); if (!exe_path.empty()) @@ -3615,6 +3609,10 @@ bool parse_args( int argc, char **argv, bool rc_only ) } break; + case CLO_TEST: + crawl_state.test = true; + break; + case CLO_MACRO: if (!next_is_param) return (false); diff --git a/crawl-ref/source/luadgn.cc b/crawl-ref/source/luadgn.cc index a0a965b738..27ae6fa4ae 100644 --- a/crawl-ref/source/luadgn.cc +++ b/crawl-ref/source/luadgn.cc @@ -29,9 +29,12 @@ REVISION("$Rev$"); #include "mapdef.h" #include "mapmark.h" #include "maps.h" +#include "message.h" #include "misc.h" #include "mon-util.h" #include "monplace.h" +#include "place.h" +#include "spells3.h" #include "spl-util.h" #include "state.h" #include "stuff.h" @@ -461,6 +464,42 @@ static int dgn_depth(lua_State *ls) return dgn_depth_proc(ls, map->depths, 2); } +// WARNING: This is a very low-level call. +LUAFN(dgn_dbg_goto_place) +{ + try + { + const level_id id = level_id::parse_level_id(luaL_checkstring(ls, 1)); + you.level_type = id.level_type; + if (id.level_type == LEVEL_DUNGEON) + { + you.where_are_you = static_cast(id.branch); + you.your_level = absdungeon_depth(id.branch, id.depth); + } + } + catch (const std::string &err) + { + luaL_error(ls, err.c_str()); + } + return (0); +} + +LUAFN(dgn_dbg_flush_map_memory) +{ + dgn_flush_map_memory(); + init_level_connectivity(); + return (0); +} + +LUAFN(dgn_dbg_generate_level) +{ + no_messages mx; + env.show.init(0); + env.map.init(map_cell()); + builder(you.your_level, you.level_type); + return (0); +} + static int dgn_place(lua_State *ls) { MAP(ls, 1, map); @@ -2934,7 +2973,7 @@ LUAFN(dgn_rtile) #endif } -LUAFN(dgn_debug_dump_map) +LUAFN(dgn_dbg_dump_map) { const int pos = lua_isuserdata(ls, 1) ? 2 : 1; if (lua_isstring(ls, pos)) @@ -2944,6 +2983,11 @@ LUAFN(dgn_debug_dump_map) static const struct luaL_reg dgn_lib[] = { + { "dbg_goto_place", dgn_dbg_goto_place }, + { "dbg_flush_map_memory", dgn_dbg_flush_map_memory }, + { "dbg_generate_level", dgn_dbg_generate_level }, + { "dbg_dump_map", dgn_dbg_dump_map }, + { "default_depth", dgn_default_depth }, { "name", dgn_name }, { "depth", dgn_depth }, @@ -3084,8 +3128,6 @@ static const struct luaL_reg dgn_lib[] = { "lev_floortile", dgn_lev_floortile }, { "lev_rocktile", dgn_lev_rocktile }, - { "debug_dump_map", dgn_debug_dump_map }, - { NULL, NULL } }; @@ -3103,6 +3145,12 @@ LUAFN(_crawl_milestone) return (0); } +LUAFN(_crawl_redraw_view) +{ + viewwindow(true, false); + return (0); +} + #ifdef UNIX LUAFN(_crawl_millis) { @@ -3122,6 +3170,7 @@ static const struct luaL_reg crawl_lib[] = { { "args", _crawl_args }, { "mark_milestone", _crawl_milestone }, + { "redraw_view", _crawl_redraw_view }, #ifdef UNIX { "millis", _crawl_millis }, #endif @@ -3266,12 +3315,46 @@ LUARET1(you_x_pos, number, you.pos().x) LUARET1(you_y_pos, number, you.pos().y) LUARET2(you_pos, number, you.pos().x, you.pos().y) +// see_grid should not be exposed to user scripts. The game should +// never disclose grid coordinates to the player. Therefore we load it +// only into the core Lua interpreter (dlua), never into the user +// script interpreter (clua). +LUARET1(you_see_grid, boolean, + see_grid(luaL_checkint(ls, 1), luaL_checkint(ls, 2))) +LUARET1(you_see_grid_no_trans, boolean, + see_grid_no_trans(luaL_checkint(ls, 1), luaL_checkint(ls, 2))) + +LUAFN(you_moveto) +{ + const coord_def place(luaL_checkint(ls, 1), luaL_checkint(ls, 2)); + ASSERT(map_bounds(place)); + you.moveto(place); + return (0); +} + +LUAFN(you_random_teleport) +{ + you_teleport_now(false, false); + return (0); +} + +LUAFN(you_losight) +{ + calc_show_los(); + return (0); +} + static const struct luaL_reg you_lib[] = { { "hear_pos", you_can_hear_pos }, { "x_pos", you_x_pos }, { "y_pos", you_y_pos }, { "pos", you_pos }, + { "moveto", you_moveto }, + { "see_grid", you_see_grid }, + { "see_grid_no_trans", you_see_grid_no_trans }, + { "random_teleport", you_random_teleport }, + { "losight", you_losight }, { NULL, NULL } }; diff --git a/crawl-ref/source/makefile.obj b/crawl-ref/source/makefile.obj index 62d3018ebf..6c3b2f7c72 100644 --- a/crawl-ref/source/makefile.obj +++ b/crawl-ref/source/makefile.obj @@ -12,6 +12,7 @@ chardump.o \ cio.o \ cloud.o \ command.o \ +ctest.o \ database.o \ debug.o \ decks.o \ diff --git a/crawl-ref/source/makefile.unix b/crawl-ref/source/makefile.unix index ab6135b042..9892a8a94a 100644 --- a/crawl-ref/source/makefile.unix +++ b/crawl-ref/source/makefile.unix @@ -151,6 +151,9 @@ PKG_EXCLUDES := $(PWD)/misc/src-pkg-excludes.lst all: $(GAME) +test: $(GAME) + $(PWD)/$(GAME) -test + ########################################################################## # Dependencies diff --git a/crawl-ref/source/state.h b/crawl-ref/source/state.h index 397bd510ef..d8cd566a29 100644 --- a/crawl-ref/source/state.h +++ b/crawl-ref/source/state.h @@ -59,6 +59,8 @@ struct game_state bool arena_suspended; // Set if the arena has been temporarily // suspended. + bool test; // Set if we want to run self-tests and exit. + bool unicode_ok; // Is unicode support available? std::string (*glyph2strfn)(unsigned glyph); diff --git a/crawl-ref/source/stuff.cc b/crawl-ref/source/stuff.cc index e405c2bf73..d825f8e5b4 100644 --- a/crawl-ref/source/stuff.cc +++ b/crawl-ref/source/stuff.cc @@ -790,7 +790,8 @@ void end(int exit_code, bool print_error, const char *format, ...) } #if defined(WIN32CONSOLE) || defined(DOS) || defined(DGL_PAUSE_AFTER_ERROR) - if (exit_code && !crawl_state.arena && !crawl_state.seen_hups) + if (exit_code && !crawl_state.arena + && !crawl_state.seen_hups && !crawl_state.test) { fprintf(stderr, "Hit Enter to continue...\n"); getchar(); diff --git a/crawl-ref/source/test/los.lua b/crawl-ref/source/test/los.lua new file mode 100644 index 0000000000..6ff3a092a4 --- /dev/null +++ b/crawl-ref/source/test/los.lua @@ -0,0 +1,68 @@ +-- Vet LOS for symmetry. + +local FAILMAP = 'losfail.map' +local checks = 0 + +local function test_los() + -- Send the player to a random spot on the level. + you.random_teleport() + + -- Forcibly redo LOS. + you.losight() + + -- And draw the view to keep the watcher entertained. + crawl.redraw_view() + + checks = checks + 1 + + local you_x, you_y = you.pos() + + local visible_spots = { } + for y = -8, 8 do + for x = -8, 8 do + if x ~= 0 or y ~= 0 then + local px, py = x + you_x, y + you_y + if you.see_grid(px, py) then + table.insert(visible_spots, { px, py }) + end + end + end + end + + -- For each position in LOS, jump to that position and make sure we + -- can see the original spot. + for _, spot in ipairs(visible_spots) do + local x, y = unpack(spot) + you.moveto(x, y) + you.losight() + if not you.see_grid(you_x, you_y) then + crawl.redraw_view() + local this_p = dgn.point(x, y) + local you_p = dgn.point(you_x, you_y) + dgn.grid(you_x, you_y, "floor_special") + dgn.dbg_dump_map(FAILMAP) + assert(false, + "LOS asymmetry detected (iter #" .. checks .. "): " .. you_p .. + " sees " .. this_p .. ", but not vice versa." .. + " Map saved to " .. FAILMAP) + end + end +end + +local function run_los_tests(depth, nlevels, tests_per_level) + local place = "D:" .. depth + crawl.mpr("Running LOS tests on " .. place) + dgn.dbg_goto_place(place) + + for lev_i = 1, nlevels do + dgn.dbg_flush_map_memory() + dgn.dbg_generate_level() + for t_i = 1, tests_per_level do + test_los() + end + end +end + +for depth = 1, 27 do + run_los_tests(depth, 10, 100) +end \ No newline at end of file -- cgit v1.2.3-54-g00ecf