Refactor: fixtures in its own files.

Added readme for creating tests.
Found and fixed interesting bugs in destroy_db when there is no world.
renamed the testfile to testrunner to make it clear it is actually running the texts. Also, made testrunner more focused on the actual running of the tests.
Added debug target to test makefile.
This commit is contained in:
welcor 2024-06-27 00:31:54 +02:00
parent 9399b68f26
commit c8fc70bf43
11 changed files with 318 additions and 181 deletions

View file

@ -13,5 +13,9 @@ Read more in the doc/ folder
## To run the tests
1. clone the munit library into src/munit. It is registered as a submodule in git `git submodule init`
1. clone the munit library into src/munit. It is registered as a submodule in git
`git submodule init && git submodule update`
2. install the cmocka-library: `sudo apt install libcmocka-dev`
3. `./config.status && cd src && make test`

223
src/db.c
View file

@ -44,26 +44,26 @@
struct config_data config_info; /* Game configuration list. */
struct room_data *world = NULL; /* array of rooms */
room_rnum top_of_world = 0; /* ref to top element of world */
room_rnum top_of_world = NOWHERE; /* ref to top element of world */
struct char_data *character_list = NULL; /* global linked list of chars */
struct index_data *mob_index; /* index table for mobile file */
struct char_data *mob_proto; /* prototypes for mobs */
mob_rnum top_of_mobt = 0; /* top of mobile index table */
mob_rnum top_of_mobt = NOBODY; /* top of mobile index table */
struct obj_data *object_list = NULL; /* global linked list of objs */
struct index_data *obj_index; /* index table for object file */
struct obj_data *obj_proto; /* prototypes for objs */
obj_rnum top_of_objt = 0; /* top of object index table */
obj_rnum top_of_objt = NOTHING; /* top of object index table */
struct zone_data *zone_table; /* zone table */
zone_rnum top_of_zone_table = 0;/* top element of zone tab */
zone_rnum top_of_zone_table = NOTHING;/* top element of zone tab */
/* begin previously located in players.c */
struct player_index_element *player_table = NULL; /* index to plr file */
int top_of_p_table = 0; /* ref to top of table */
int top_of_p_file = 0; /* ref of size of p file */
long top_idnum = 0; /* highest idnum in use */
int top_of_p_table = NOBODY; /* ref to top of table */
int top_of_p_file = NOBODY; /* ref of size of p file */
long top_idnum = NOBODY; /* highest idnum in use */
/* end previously located in players.c */
struct message_list fight_messages[MAX_MESSAGES]; /* fighting messages */
@ -528,84 +528,91 @@ void destroy_db(void)
}
/* Rooms */
for (cnt = 0; cnt <= top_of_world; cnt++) {
if (world[cnt].name)
free(world[cnt].name);
if (world[cnt].description)
free(world[cnt].description);
free_extra_descriptions(world[cnt].ex_description);
if (top_of_world != NOWHERE) {
for (cnt = 0; cnt <= top_of_world; cnt++) {
if (world[cnt].name)
free(world[cnt].name);
if (world[cnt].description)
free(world[cnt].description);
free_extra_descriptions(world[cnt].ex_description);
if (world[cnt].events != NULL) {
if (world[cnt].events->iSize > 0) {
struct event * pEvent;
if (world[cnt].events != NULL) {
if (world[cnt].events->iSize > 0) {
struct event * pEvent;
while ((pEvent = simple_list(world[cnt].events)) != NULL)
event_cancel(pEvent);
}
free_list(world[cnt].events);
world[cnt].events = NULL;
}
while ((pEvent = simple_list(world[cnt].events)) != NULL)
event_cancel(pEvent);
}
free_list(world[cnt].events);
world[cnt].events = NULL;
}
/* free any assigned scripts */
if (SCRIPT(&world[cnt]))
extract_script(&world[cnt], WLD_TRIGGER);
/* free script proto list */
free_proto_script(&world[cnt], WLD_TRIGGER);
/* free any assigned scripts */
if (SCRIPT(&world[cnt]))
extract_script(&world[cnt], WLD_TRIGGER);
/* free script proto list */
free_proto_script(&world[cnt], WLD_TRIGGER);
for (itr = 0; itr < NUM_OF_DIRS; itr++) { /* NUM_OF_DIRS here, not DIR_COUNT */
if (!world[cnt].dir_option[itr])
continue;
for (itr = 0; itr < NUM_OF_DIRS; itr++) { /* NUM_OF_DIRS here, not DIR_COUNT */
if (world[cnt].dir_option[itr] == NULL)
continue;
if (world[cnt].dir_option[itr]->general_description)
free(world[cnt].dir_option[itr]->general_description);
if (world[cnt].dir_option[itr]->keyword)
free(world[cnt].dir_option[itr]->keyword);
free(world[cnt].dir_option[itr]);
if (world[cnt].dir_option[itr]->general_description)
free(world[cnt].dir_option[itr]->general_description);
if (world[cnt].dir_option[itr]->keyword)
free(world[cnt].dir_option[itr]->keyword);
free(world[cnt].dir_option[itr]);
}
}
free(world);
top_of_world = NOWHERE;
}
free(world);
top_of_world = 0;
/* Objects */
for (cnt = 0; cnt <= top_of_objt; cnt++) {
if (obj_proto[cnt].name)
free(obj_proto[cnt].name);
if (obj_proto[cnt].description)
free(obj_proto[cnt].description);
if (obj_proto[cnt].short_description)
free(obj_proto[cnt].short_description);
if (obj_proto[cnt].action_description)
free(obj_proto[cnt].action_description);
free_extra_descriptions(obj_proto[cnt].ex_description);
/* Objects */
if (top_of_objt != NOTHING) {
for (cnt = 0; cnt <= top_of_objt; cnt++) {
if (obj_proto[cnt].name)
free(obj_proto[cnt].name);
if (obj_proto[cnt].description)
free(obj_proto[cnt].description);
if (obj_proto[cnt].short_description)
free(obj_proto[cnt].short_description);
if (obj_proto[cnt].action_description)
free(obj_proto[cnt].action_description);
free_extra_descriptions(obj_proto[cnt].ex_description);
/* free script proto list */
free_proto_script(&obj_proto[cnt], OBJ_TRIGGER);
/* free script proto list */
free_proto_script(&obj_proto[cnt], OBJ_TRIGGER);
}
free(obj_proto);
free(obj_index);
top_of_objt = NOTHING;
}
free(obj_proto);
free(obj_index);
/* Mobiles */
for (cnt = 0; cnt <= top_of_mobt; cnt++) {
if (mob_proto[cnt].player.name)
free(mob_proto[cnt].player.name);
if (mob_proto[cnt].player.title)
free(mob_proto[cnt].player.title);
if (mob_proto[cnt].player.short_descr)
free(mob_proto[cnt].player.short_descr);
if (mob_proto[cnt].player.long_descr)
free(mob_proto[cnt].player.long_descr);
if (mob_proto[cnt].player.description)
free(mob_proto[cnt].player.description);
if (top_of_mobt != NOBODY) {
for (cnt = 0; cnt <= top_of_mobt; cnt++) {
if (mob_proto[cnt].player.name)
free(mob_proto[cnt].player.name);
if (mob_proto[cnt].player.title)
free(mob_proto[cnt].player.title);
if (mob_proto[cnt].player.short_descr)
free(mob_proto[cnt].player.short_descr);
if (mob_proto[cnt].player.long_descr)
free(mob_proto[cnt].player.long_descr);
if (mob_proto[cnt].player.description)
free(mob_proto[cnt].player.description);
/* free script proto list */
free_proto_script(&mob_proto[cnt], MOB_TRIGGER);
/* free script proto list */
free_proto_script(&mob_proto[cnt], MOB_TRIGGER);
while (mob_proto[cnt].affected)
affect_remove(&mob_proto[cnt], mob_proto[cnt].affected);
while (mob_proto[cnt].affected)
affect_remove(&mob_proto[cnt], mob_proto[cnt].affected);
}
free(mob_proto);
free(mob_index);
top_of_mobt = NOBODY;
}
free(mob_proto);
free(mob_index);
/* Shops */
destroy_shops();
@ -615,26 +622,29 @@ void destroy_db(void)
/* Zones */
#define THIS_CMD zone_table[cnt].cmd[itr]
for (cnt = 0; cnt <= top_of_zone_table; cnt++) {
if (zone_table[cnt].name)
free(zone_table[cnt].name);
if (zone_table[cnt].builders)
free(zone_table[cnt].builders);
if (zone_table[cnt].cmd) {
/* first see if any vars were defined in this zone */
for (itr = 0;THIS_CMD.command != 'S';itr++)
if (THIS_CMD.command == 'V') {
if (THIS_CMD.sarg1)
free(THIS_CMD.sarg1);
if (THIS_CMD.sarg2)
free(THIS_CMD.sarg2);
}
/* then free the command list */
free(zone_table[cnt].cmd);
if (top_of_zone_table != NOWHERE)
{
for (cnt = 0; cnt <= top_of_zone_table; cnt++) {
if (zone_table[cnt].name)
free(zone_table[cnt].name);
if (zone_table[cnt].builders)
free(zone_table[cnt].builders);
if (zone_table[cnt].cmd) {
/* first see if any vars were defined in this zone */
for (itr = 0;THIS_CMD.command != 'S';itr++)
if (THIS_CMD.command == 'V') {
if (THIS_CMD.sarg1)
free(THIS_CMD.sarg1);
if (THIS_CMD.sarg2)
free(THIS_CMD.sarg2);
}
/* then free the command list */
free(zone_table[cnt].cmd);
}
}
free(zone_table);
top_of_zone_table = NOWHERE;
}
free(zone_table);
#undef THIS_CMD
/* zone table reset queue */
@ -648,27 +658,30 @@ void destroy_db(void)
}
/* Triggers */
for (cnt=0; cnt < top_of_trigt; cnt++) {
if (trig_index[cnt]->proto) {
/* make sure to nuke the command list (memory leak) */
/* free_trigger() doesn't free the command list */
if (trig_index[cnt]->proto->cmdlist) {
struct cmdlist_element *i, *j;
i = trig_index[cnt]->proto->cmdlist;
while (i) {
j = i->next;
if (i->cmd)
free(i->cmd);
free(i);
i = j;
if (top_of_trigt != NOTHING)
{
for (cnt=0; cnt < top_of_trigt; cnt++) {
if (trig_index[cnt]->proto) {
/* make sure to nuke the command list (memory leak) */
/* free_trigger() doesn't free the command list */
if (trig_index[cnt]->proto->cmdlist) {
struct cmdlist_element *i, *j;
i = trig_index[cnt]->proto->cmdlist;
while (i) {
j = i->next;
if (i->cmd)
free(i->cmd);
free(i);
i = j;
}
}
free_trigger(trig_index[cnt]->proto);
}
free_trigger(trig_index[cnt]->proto);
free(trig_index[cnt]);
}
free(trig_index[cnt]);
free(trig_index);
top_of_trigt = NOTHING;
}
free(trig_index);
/* Events */
event_free_all();

View file

@ -8,7 +8,7 @@ ASAN:=n
UBSAN:=n
EXTENSION:=
TEST_ENV:=
CFLAGS:=-Wall -Wno-char-subscripts -Wno-unused-but-set-variable
CFLAGS:=-Wall -Wno-char-subscripts -Wno-unused-but-set-variable -g
CFLAGS+=-Wl,--wrap=send_to_char,--wrap=vwrite_to_output
AGGRESSIVE_WARNINGS=n
LIBS:=-lcrypt
@ -45,27 +45,31 @@ ifneq ($(CC),pgcc)
endif
endif
MUNIT_FILES := ../munit/munit.h ../munit/munit.c
#MUNIT_FILES := ../munit/munit.h ../munit/munit.c
TEST_FILES := $(ls *.c)
# exclude main.c to avoid having multiple entrypoints.
OBJ_FILES_FROM_MUD_CODE != ls ../*.c | grep -v main.c | sed 's/\.c$$/.o/g' | xargs
OBJ_FILES_FORM_MUNIT:= ../munit/munit.o
OBJ_FILES_FROM_TESTS!= ls *.c | sed 's/\$$.c/.o/g' | xargs
OBJ_FILES_FROM_MUNIT:= ../munit/munit.o
OBJ_FILES_FROM_TESTS!= ls *.c | sed 's/\.c/.o/g' | xargs
OBJ_FILES := $(OBJ_FILES_FROM_MUD_CODE) $(OBJ_FILES_FORM_MUNIT) $(OBJ_FILES_FROM_TESTS) $(LIBS)
OBJ_FILES := $(OBJ_FILES_FROM_MUD_CODE) $(OBJ_FILES_FROM_MUNIT) $(OBJ_FILES_FROM_TESTS) $(LIBS)
testfile: $(OBJ_FILES)
$(CC) $(CFLAGS) -o testfile $(OBJ_FILES)
testrunner: $(OBJ_FILES)
$(CC) $(CFLAGS) -o testrunner $(OBJ_FILES)
test: testrunner
$(TEST_ENV) ./testrunner
debug: testrunner
$(TEST_ENV) gdb -q --args ./testrunner --no-fork
test: testfile
$(TEST_ENV) ./testfile
$%.o: %.c
$(CC) $< $(CFLAGS) -c -o $@
clean:
rm -f *.o testfile depend
rm -f *.o testrunner depend
all: test
default: all

47
src/test/README.md Normal file
View file

@ -0,0 +1,47 @@
# unit and integration tests for tbamud
## how do I add a new test?
Open the .c file of your choosing and add a `UNIT_TEST` function.
The function will have access to all the global variables and all non-static
functions in the code, but there will be no data loaded.
The name of the function will be listed when the tests are run.
The [munit website](https://nemequ.github.io/munit/#getting-started) may be useful for more details.
## how do I add new files with tests?
First, create your test file. As a general rule, keep unit tests in files named
after the files containing the functions you are testing. For instance, if you're
testing the `do_simple_move()` function, create a file called `test.act.movement.c`.
You can use the example file `test.example.c` as a template.
The `.c`-file needs a couple of boilerplate parts:
- An import statement to include the `.h`-file.
- UNIT_TEST-functions. See above.
- An array of `MunitTest`s for inclusion in the runner app.
The name in these are concatenated between the name in testrunner and the name of the tests in the output.
This is useful for grouping.
Next, create a header file for your tests. It's a good idea to keep the same name,
with a .h postfix. So in the example, it'll be `test.act.movement.h`.
You can use the `test.example.h` file as a template. It needs a little boilerplate, too.
- It needs to include the testrunner.h for the prototype of UNIT_TEST and access to munit-structs.
- It needs a guard to only be loaded once (the `#ifndef/#define/#endif` incantation at the start and end)
- It needs a prototype of all tests in the .c-file.
- It needs a prototype of the array of tests.
Finally, add the array to the suites array in `testrunner.c` to actually run the tests.
- Add the .h file to the list of imported files.
- Add a row to the suites array. The name in this list is prepended to every test in the given
file when listing the results.
Now, having all the bits and pieces ready, you can add you unit tests, and run them with `make test`

View file

@ -15,12 +15,12 @@ UNIT_TEST(test_do_remove_should_remove_second_item_by_number) {
char_data *ch = get_test_char();
obj_data *ring1 = create_obj();
ring1->name = "ring";
ring1->short_description = "ring1";
ring1->name = strdup("ring");
ring1->short_description = strdup("ring1");
obj_data *ring2 = create_obj();
ring2->name = "ring";
ring2->short_description = "ring2";
ring2->name = strdup("ring");
ring2->short_description = strdup("ring2");
equip_char(ch, ring1, WEAR_FINGER_R);
equip_char(ch, ring2, WEAR_FINGER_L);
@ -33,9 +33,19 @@ UNIT_TEST(test_do_remove_should_remove_second_item_by_number) {
return MUNIT_OK;
}
static void* before_each(const MunitParameter params[], void* user_data) {
simple_world();
add_test_char(0);
return NULL;
}
static void after_each(void* fixture) {
destroy_db();
}
MunitTest act_item_c_tests[] = {
STD_TEST("/do_remove/item_not_found", test_do_remove_should_give_message_on_removing_of_unknown_item),
STD_TEST("/do_remove/remove_second_item", test_do_remove_should_remove_second_item_by_number),
EXT_TEST("/do_remove/item_not_found", test_do_remove_should_give_message_on_removing_of_unknown_item, before_each, after_each),
EXT_TEST("/do_remove/remove_second_item", test_do_remove_should_remove_second_item_by_number, before_each, after_each),
// end of array marker
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }

14
src/test/test.example.c Normal file
View file

@ -0,0 +1,14 @@
#include "test.example.h"
UNIT_TEST(example_test)
{
return MUNIT_OK;
}
MunitTest test_example_c_tests[] = {
STD_TEST("/example/example_test", example_test),
// end of array marker
{ NULL, NULL, NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }
};

10
src/test/test.example.h Normal file
View file

@ -0,0 +1,10 @@
#include "testrunner.h"
#ifndef TEST_EXAMPLE_H
#define TEST_EXAMPLE_H
extern MunitTest test_example_c_tests[];
UNIT_TEST(example_test);
#endif

46
src/test/test.fixtures.c Normal file
View file

@ -0,0 +1,46 @@
#include "test.fixtures.h"
/*
* test-fixtures common for many tests
*/
static char_data* test_char;
void simple_world()
{
int i;
CREATE(world, struct room_data, 1);
top_of_world = 0;
world[0].func = NULL;
world[0].contents = NULL;
world[0].people = NULL;
world[0].light = 0;
SCRIPT(&world[0]) = NULL;
for (i = 0; i < NUM_OF_DIRS; i++)
world[0].dir_option[i] = NULL;
world[0].ex_description = NULL;
}
char_data *get_test_char() {
return test_char;
}
void add_test_char(room_rnum target_room_rnum)
{
if (top_of_world < 0) {
fprintf(stderr, "World not created, nowhere to put character in add_test_char");
exit(-1);
}
char_data *ch = create_char();
CREATE(ch->player_specials, struct player_special_data, 1);
ch->char_specials.position = POS_STANDING;
CREATE(ch->desc, struct descriptor_data, 1);
char_to_room(ch, target_room_rnum);
test_char = ch;
}

34
src/test/test.fixtures.h Normal file
View file

@ -0,0 +1,34 @@
#ifndef TEST_FIXTURES_H
#define TEST_FIXTURES_H
#include "../conf.h"
#include "../sysdep.h"
#include "../structs.h"
#include "../utils.h"
#include "../comm.h"
#include "../db.h"
#include "../handler.h"
#include "../screen.h"
#include "../interpreter.h"
#include "../spells.h"
#include "../dg_scripts.h"
#include "../act.h"
#include "../class.h"
#include "../fight.h"
#include "../quest.h"
#include "../mud_event.h"
#include "../munit/munit.h"
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
/*
* test fixtures
*/
char_data *get_test_char();
void simple_world();
void add_test_char(room_rnum target_room);
#endif //TEST_FIXTURES_H

View file

@ -1,15 +1,13 @@
#include "testrunner.h"
#include "test.handler.h"
#include "test.act.item.h"
#include "test.example.h"
static void simple_world();
static void add_char();
static char_data* test_char;
static MunitSuite suites[] = {
static MunitSuite suites[] = {
{ "/handler.c", handler_c_tests, NULL, 1, MUNIT_SUITE_OPTION_NONE },
{ "/act.item.c", act_item_c_tests, NULL, 1, MUNIT_SUITE_OPTION_NONE },
{ "/act.item.c", act_item_c_tests, NULL, 1, MUNIT_SUITE_OPTION_NONE },
{ "/test.example.c", test_example_c_tests, NULL, 1, MUNIT_SUITE_OPTION_NONE },
{ NULL, NULL, NULL, 0, MUNIT_SUITE_OPTION_NONE }
};
@ -24,39 +22,10 @@ static const MunitSuite test_suite = {
int main(int argc, char* argv[MUNIT_ARRAY_PARAM(argc + 1)]) {
logfile = stderr;
simple_world();
add_char();
return munit_suite_main(&test_suite, (void*) "µnit", argc, argv);
}
/*
* test-fixtures common for many tests
*/
static void simple_world()
{
CREATE(world, struct room_data, 1);
top_of_world = 1;
}
char_data *get_test_char() {
return test_char;
}
static void add_char()
{
char_data *ch = create_char();
CREATE(ch->player_specials, struct player_special_data, 1);
new_mobile_data(ch);
ch->char_specials.position = POS_STANDING;
CREATE(ch->desc, struct descriptor_data, 1);
char_to_room(ch, 0);
test_char = ch;
}
static char testbuf[MAX_OUTPUT_BUFFER];
static int testbuf_size = 0;

View file

@ -1,27 +1,7 @@
#ifndef TESTRUNNER_H
#define TESTRUNNER_H
#include "../conf.h"
#include "../sysdep.h"
#include "../structs.h"
#include "../utils.h"
#include "../comm.h"
#include "../db.h"
#include "../handler.h"
#include "../screen.h"
#include "../interpreter.h"
#include "../spells.h"
#include "../dg_scripts.h"
#include "../act.h"
#include "../class.h"
#include "../fight.h"
#include "../quest.h"
#include "../mud_event.h"
#include "../munit/munit.h"
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include "test.fixtures.h"
/**
* Utility macro for defining tests.
@ -35,9 +15,15 @@
#define STD_TEST(test_name, test_fun) { (char *)(test_name), (test_fun), NULL, NULL, MUNIT_TEST_OPTION_NONE, NULL }
/*
* test fixtures
* An "extended test" has setup or teardown but doesn't take any parameters.
* This is a utility macro for the test suite listing.
*/
#define EXT_TEST(test_name, test_fun, setup_fun, teardown_fun) { (char *)(test_name), (test_fun), (setup_fun), (teardown_fun), MUNIT_TEST_OPTION_NONE, NULL }
/*
* Returns the latest messages sent through send_to_char() or act()
*/
char_data *get_test_char();
char *get_last_messages();
#endif