Hardcode Tests (#1243)

* Add basic hardcode test framework. Closes #1238

Now go forth and write all the tests!

* Rename all the softcode test case files.

testFOO.pl -> testFOO.t because they're not actually proper perl scripts
and that was bugging me and causing syntax highlighting problems in
editors.

* Fix a couple of typos/formatting bugs.

* Add some more test cases.

* Add some utf8 tests.

* Support ancient perl, and better error handling on failed tsort invocation.

* More tests and run the tests after loading the database.

* Add tests for map_file()
This commit is contained in:
shawnw 2018-09-16 15:10:11 -07:00 committed by GitHub
parent e0017a4b8b
commit 0a0a9ad453
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
44 changed files with 927 additions and 97 deletions

View file

@ -49,9 +49,9 @@ config.h: configure
@echo "to suppress this message and continue compiling."
@exit 1
autogen: hdrs/cmds.h hdrs/funs.h hdrs/switches.h hdrs/gitinfo.h
autogen: hdrs/cmds.h hdrs/funs.h hdrs/switches.h hdrs/gitinfo.h src/tests.inc
hdrs/cmds.h: src/cmds.c src/command.c src/cque.c src/help.c src/set.c src/sql.c Patchlevel
hdrs/cmds.h: src/cmds.c src/command.c src/cque.c src/extchat.c src/help.c src/set.c src/sql.c Patchlevel
@PERL@ utils/mkcmds.pl commands
hdrs/switches.h: src/SWITCHES Patchlevel
@ -63,6 +63,9 @@ src/switchinc.c: src/SWITCHES Patchlevel
hdrs/funs.h: src/fun*.c src/bsd.c src/conf.c src/connlog.c src/extmail.c src/help.c src/markup.c src/wiz.c src/sql.c Patchlevel src/cque.c
@PERL@ utils/mkcmds.pl functions
src/tests.inc: src/*.c
@PERL@ utils/mkcmds.pl tests
hdrs/gitinfo.h: $(wildcard .git/HEAD .git/index)
@if [ -f ".git/HEAD" ]; then \
echo "#define GIT_REVISION \"$(shell git rev-parse --short HEAD)\"" > $@; \

21
hdrs/tests.h Normal file
View file

@ -0,0 +1,21 @@
/** \file tests.h
*
* \brief Headers to support hardcode test framework.
*/
#pragma once
#include "log.h"
#define TEST_GROUP(name) void test_##name (int *success, int *failure)
#define TEST(name, t) \
do { \
if (t) { \
*success += 1; \
} else { \
*failure += 1; \
do_rawlog(LT_TRACE, "%s:%d: Test %s failed!", __FILE__, __LINE__, name); \
} \
} while (0)
bool run_tests(void);

View file

@ -29,9 +29,9 @@ C_FILES=access.c atr_tab.c attrib.c boolexp.c bsd.c bufferq.c \
pcg_basic.c player.c plyrlist.c predicat.c privtab.c \
info_master.c ptab.c remember.c rob.c services.c set.c sig.c \
sort.c speech.c spellfix.c sql.c sqlite3.c ssl_master.c \
strdup.c strtree.c strutil.c tables.c timer.c tz.c unparse.c \
utf_impl.c utils.c version.c wait.c warnings.c websock.c \
wild.c wiz.c
strdup.c strtree.c strutil.c tables.c testframework.c timer.c \
tz.c unparse.c utf_impl.c utils.c version.c wait.c warnings.c \
websock.c wild.c wiz.c
# .o versions of above - these are used in the build
@ -47,9 +47,9 @@ O_FILES=access.o atr_tab.o attrib.o boolexp.o bsd.o bufferq.o \
pcg_basic.o player.o plyrlist.o predicat.o privtab.o \
info_master.o ptab.o remember.o rob.o services.o set.o sig.o \
sort.o speech.o spellfix.o sql.o sqlite3.o ssl_master.o \
strdup.o strtree.o strutil.o tables.o timer.o tz.o unparse.o \
utf_impl.o utils.o version.o wait.o warnings.o websock.o \
wild.o wiz.o
strdup.o strtree.o strutil.o tables.o testframework.o timer.o \
tz.o unparse.o utf_impl.o utils.o version.o wait.o warnings.o \
websock.o wild.o wiz.o
# This is a dummy target, in case you type 'make' in the source
# directory (likely for emacs users who M-x compile.)
@ -375,6 +375,9 @@ bsd.o: ../hdrs/charconv.h
bsd.o: ../hdrs/myutf8.h
bsd.o: ../hdrs/connlog.h
bsd.o: ../hdrs/charclass.h
bsd.o: ../hdrs/memcheck.h
bsd.o: ../hdrs/map_file.h
bsd.o: ../hdrs/tests.h
bsd.o: ../hdrs/wait.h
bsd.o: ../hdrs/lookup.h
bsd.o: ../hdrs/ssl_slave.h
@ -448,7 +451,6 @@ cmdlocal.o: ../config.h
cmdlocal.o: ../confmagic.h
cmdlocal.o: ../options.h
cmdlocal.o: ../hdrs/copyrite.h
cmdlocal.o: ../config.h
cmdlocal.o: ../hdrs/cmds.h
cmdlocal.o: ../hdrs/command.h
cmdlocal.o: ../hdrs/boolexp.h
@ -469,7 +471,6 @@ cmdlocal.o: ../hdrs/mypcre.h
cmdlocal.o: ../hdrs/parse.h
cmdlocal.o: ../hdrs/mushsql.h
cmdlocal.o: ../hdrs/sqlite3.h
cmdlocal.o: ../confmagic.h
cmds.o: ../config.h
cmds.o: ../confmagic.h
cmds.o: ../options.h
@ -882,7 +883,6 @@ flaglocal.o: ../config.h
flaglocal.o: ../confmagic.h
flaglocal.o: ../options.h
flaglocal.o: ../hdrs/copyrite.h
flaglocal.o: ../config.h
flaglocal.o: ../hdrs/conf.h
flaglocal.o: ../hdrs/htab.h
flaglocal.o: ../hdrs/mushtype.h
@ -896,7 +896,6 @@ flaglocal.o: ../hdrs/dbio.h
flaglocal.o: ../hdrs/ptab.h
flaglocal.o: ../hdrs/chunk.h
flaglocal.o: ../hdrs/mypcre.h
flaglocal.o: ../confmagic.h
flags.o: ../config.h
flags.o: ../confmagic.h
flags.o: ../options.h
@ -1110,7 +1109,6 @@ funlocal.o: ../config.h
funlocal.o: ../confmagic.h
funlocal.o: ../options.h
funlocal.o: ../hdrs/copyrite.h
funlocal.o: ../config.h
funlocal.o: ../hdrs/conf.h
funlocal.o: ../hdrs/htab.h
funlocal.o: ../hdrs/mushtype.h
@ -1128,7 +1126,6 @@ funlocal.o: ../hdrs/function.h
funlocal.o: ../hdrs/parse.h
funlocal.o: ../hdrs/mushsql.h
funlocal.o: ../hdrs/sqlite3.h
funlocal.o: ../confmagic.h
funmath.o: ../config.h
funmath.o: ../confmagic.h
funmath.o: ../options.h
@ -1409,7 +1406,6 @@ local.o: ../config.h
local.o: ../confmagic.h
local.o: ../options.h
local.o: ../hdrs/copyrite.h
local.o: ../config.h
local.o: ../hdrs/command.h
local.o: ../hdrs/boolexp.h
local.o: ../hdrs/chunk.h
@ -1432,7 +1428,6 @@ local.o: ../hdrs/lock.h
local.o: ../hdrs/parse.h
local.o: ../hdrs/mushsql.h
local.o: ../hdrs/sqlite3.h
local.o: ../confmagic.h
lock.o: ../config.h
lock.o: ../confmagic.h
lock.o: ../options.h
@ -1594,10 +1589,10 @@ markup.o: ../hdrs/parse.h
markup.o: ../hdrs/mushsql.h
markup.o: ../hdrs/sqlite3.h
markup.o: ../hdrs/pueblo.h
markup.o: ../hdrs/map_file.h
markup.o: ../hdrs/strutil.h
markup.o: ../hdrs/charconv.h
markup.o: ../hdrs/myutf8.h
markup.o: ../hdrs/map_file.h
markup.o: htmltab.c
match.o: ../config.h
match.o: ../confmagic.h
@ -1836,6 +1831,7 @@ parse.o: ../hdrs/memcheck.h
parse.o: ../hdrs/mymalloc.h
parse.o: ../hdrs/notify.h
parse.o: ../hdrs/strutil.h
parse.o: ../hdrs/tests.h
pcg_basic.o: ../config.h
pcg_basic.o: ../confmagic.h
pcg_basic.o: ../options.h
@ -2230,6 +2226,17 @@ strutil.o: ../hdrs/charclass.h
tables.o: ../config.h
tables.o: ../confmagic.h
tables.o: ../options.h
testframework.o: ../config.h
testframework.o: ../confmagic.h
testframework.o: ../options.h
testframework.o: ../hdrs/sqlite3.h
testframework.o: ../hdrs/tests.h
testframework.o: ../hdrs/log.h
testframework.o: ../hdrs/bufferq.h
testframework.o: ../hdrs/mushtype.h
testframework.o: ../hdrs/copyrite.h
testframework.o: ../hdrs/cJSON.h
testframework.o: tests.inc
timer.o: ../config.h
timer.o: ../confmagic.h
timer.o: ../options.h
@ -2428,6 +2435,8 @@ websock.o: ../hdrs/sqlite3.h
websock.o: ../confmagic.h
websock.o: ../hdrs/strutil.h
websock.o: ../hdrs/notify.h
websock.o: ../hdrs/mymalloc.h
websock.o: ../hdrs/connlog.h
websock.o: ../hdrs/websock.h
websock.o: ../hdrs/function.h
wild.o: ../config.h

View file

@ -111,6 +111,7 @@
#include "cJSON.h"
#include "memcheck.h"
#include "map_file.h"
#include "tests.h"
#include "websock.h"
#ifndef WIN32
@ -497,6 +498,7 @@ main(int argc, char **argv)
{
FILE *newerr;
bool detach_session __attribute__((__unused__)) = 1;
bool enable_tests = 0, only_test = 0;
/* disallow running as root on unix.
* This is done as early as possible, before translation is initialized.
@ -564,8 +566,15 @@ main(int argc, char **argv)
}
} else if (strcmp(argv[n], "--no-pcre-jit") == 0) {
pcre_study_flags = 0;
} else
} else if (strcmp(argv[n], "--tests") == 0) {
enable_tests = 1;
} else if (strcmp(argv[n], "--only-tests") == 0) {
enable_tests = 1;
only_test = 1;
detach_session = 0;
} else {
fprintf(stderr, "%s: unknown option \"%s\"\n", argv[0], argv[n]);
}
} else {
mush_strncpy(confname, argv[n], BUFFER_LEN);
break;
@ -711,6 +720,18 @@ main(int argc, char **argv)
set_signals();
if (enable_tests) {
bool r = run_tests();
if (r) {
do_rawlog(LT_ERR, "Hardcode tests all passed!");
} else {
do_rawlog(LT_ERR, "Hardcode tests had failures!");
}
if (only_test || !r) {
exit(r ? 0 : 1);
}
}
#ifdef INFO_SLAVE
init_info_slave();
#endif
@ -1071,13 +1092,13 @@ handle_curl_msg(CURLMsg *msg)
if (is_utf8) {
latin1 = utf8_to_latin1(body, body_size, &len, 1, "string");
if (len >= BUFFER_LEN) {
resp->too_big = 1;
resp->too_big = 1;
latin1[BUFFER_LEN - 1] = '\0';
}
} else {
latin1 = body;
if (body_size >= BUFFER_LEN) {
resp->too_big = 1;
resp->too_big = 1;
body[BUFFER_LEN - 1] = '\0';
}
}
@ -1114,7 +1135,7 @@ handle_curl_msg(CURLMsg *msg)
static int
check_status()
{
/* Check signal handler flags */
/* Check signal handler flags */
#ifndef WIN32
if (dump_error) {
if (WIFSIGNALED(dump_status)) {
@ -1573,7 +1594,7 @@ gameloop()
struct timeval current_time;
while (!shutdown_flag) {
/** let's find out how long we should wait */
/** let's find out how long we should wait */
#define min_timeout(store, func) \
timeout_check = func; \
if (timeout_check < store) \
@ -1696,13 +1717,13 @@ new_connection(int oldsock, int *result, conn_source source)
int remote_uid = -1;
bool good_to_read = 1;
/* As soon as the SSL slave opens a new connection to the mush, it
writes a string of the format 'IP^HOSTNAME\r\n'. This will thus
not block unless somebody's being naughty. People obviously can
be. So we'll wait a short time for readable data, and use a
non-blocking socket read anyways. If the client doesn't send
the hostname string fast enough, oh well.
*/
/* As soon as the SSL slave opens a new connection to the mush, it
writes a string of the format 'IP^HOSTNAME\r\n'. This will thus
not block unless somebody's being naughty. People obviously can
be. So we'll wait a short time for readable data, and use a
non-blocking socket read anyways. If the client doesn't send
the hostname string fast enough, oh well.
*/
#ifdef HAVE_POLL
{
@ -1973,12 +1994,12 @@ fcache_read(FBLOCK *fb, const char *filename)
MAPPED_FILE *mf = map_file(filename, 0);
if (mf) {
/* Copy instead of using the mapped file directly because what
happens when a mapped file is edited, even if it's a private
map? There don't seem to be any promises. */
happens when a mapped file is edited, even if it's a private
map? There don't seem to be any promises. */
fb->buff = mush_malloc(mf->len + 1, "fcache_data");
if (!fb->buff) {
unmap_file(mf);
return -1;
unmap_file(mf);
return -1;
}
memcpy(fb->buff, mf->data, mf->len);
fb->buff[mf->len] = '\0';
@ -2053,10 +2074,9 @@ fcache_load(dbref player)
who = fcache_read(&fcache.who_fcache[i], options.who_file[i]);
if (player != NOTHING) {
notify_format(player,
T("%s sizes: NewUser...%d Connect...%d "
"Guest...%d Motd...%d Wizmotd...%d Quit...%d "
"Register...%d Down...%d Full...%d Who...%d"),
notify_format(player, T("%s sizes: NewUser...%d Connect...%d "
"Guest...%d Motd...%d Wizmotd...%d Quit...%d "
"Register...%d Down...%d Full...%d Who...%d"),
i ? "HTMLFile" : "File", new, conn, guest, motd, wiz, quit,
reg, down, full, who);
}
@ -3510,15 +3530,14 @@ http_bounce_mud_url(DESC *d)
char buf[BUFFER_LEN];
char *bp = buf;
bool has_url = strncmp(MUDURL, "http", 4) == 0;
safe_format(buf, &bp,
"HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset:iso-8859-1\r\n"
"Pragma: no-cache\r\n"
"Connection: Close\r\n"
"\r\n"
"<!DOCTYPE html>\r\n"
"<HTML><HEAD>"
"<TITLE>Welcome to %s!</TITLE>",
safe_format(buf, &bp, "HTTP/1.1 200 OK\r\n"
"Content-Type: text/html; charset:iso-8859-1\r\n"
"Pragma: no-cache\r\n"
"Connection: Close\r\n"
"\r\n"
"<!DOCTYPE html>\r\n"
"<HTML><HEAD>"
"<TITLE>Welcome to %s!</TITLE>",
MUDNAME);
if (has_url) {
safe_format(buf, &bp, "<meta http-equiv=\"refresh\" content=\"5; url=%s\">",
@ -3766,7 +3785,7 @@ process_http_input(DESC *d, char *buf, int len)
break;
}
/* Reset the timer, but only if there is one. */
/* Reset the timer, but only if there is one. */
waitmore:
d->conn_timer = sq_register_in(2, http_finished_wrapper, (void *) d, NULL);
}
@ -4400,9 +4419,8 @@ check_connect(DESC *d, const char *msg)
}
if (!options.create_allow) {
fcache_dump(d, fcache.register_fcache, NULL, NULL);
do_rawlog(LT_CONN,
"Refused registration (creation disabled) for %s from "
"%s on descriptor %d.\n",
do_rawlog(LT_CONN, "Refused registration (creation disabled) for %s from "
"%s on descriptor %d.\n",
user, d->addr, d->descriptor);
queue_event(SYSEVENT, "SOCKET`CREATEFAIL", "%d,%s,%d,%s,%s",
d->descriptor, d->ip, mark_failed(d->ip),
@ -7657,9 +7675,8 @@ file_watch_event_in(int fd)
do_rawlog(LT_TRACE, "Reindexing help file %s.", file);
WATCH(file);
} else {
do_rawlog(LT_ERR,
"Got status change for file '%s' but I don't "
"know what to do with it! Mask 0x%x",
do_rawlog(LT_ERR, "Got status change for file '%s' but I don't "
"know what to do with it! Mask 0x%x",
file, ev->mask);
}
lastwd = ev->wd;

View file

@ -26,6 +26,7 @@
#include "charconv.h"
#include "log.h"
#include "strutil.h"
#include "tests.h"
/**
* Convert a latin-1 encoded string to utf-8.
@ -57,6 +58,17 @@ latin1_to_utf8(const char *restrict latin1, int len, int *outlen,
return utf8;
}
TEST_GROUP(latin1_to_utf8) {
char *utf8;
int len;
utf8 = latin1_to_utf8("abcd", 4, &len, "string");
TEST("latin1_to_utf8.1", strcmp(utf8, "abcd") == 0 && len == 4);
mush_free(utf8, "string");
utf8 = latin1_to_utf8("\xE1 bc", 4, &len, "string");
TEST("latin1_to_utf8.2", strcmp(utf8, "\u00E1 bc") == 0 && len == 5);
mush_free(utf8, "string");
}
/**
* Convert a latin-1 encoded string to utf-8 optionally handling
* telnet escape sequences.
@ -612,6 +624,29 @@ utf8_to_latin1(const char *restrict utf8, int len, int *outlen, bool translit,
return latin1;
}
TEST_GROUP(utf8_to_latin1) {
char *latin1;
int len;
latin1 = utf8_to_latin1("abcd", 4, &len, 0, "string");
TEST("utf8_to_latin1.1", strcmp(latin1, "abcd") == 0 && len == 4);
mush_free(latin1, "string");
latin1 = utf8_to_latin1("\xC3\xA1qq", 4, &len, 0, "string");
TEST("utf8_to_latin1.2", strcmp(latin1, "\xE1qq") == 0 && len == 3);
mush_free(latin1, "string");
latin1 = utf8_to_latin1("\xC3qq", 3, &len, 0, "string");
TEST("utf8_to_latin1.3", strcmp(latin1, "?qq") == 0 && len == 3);
mush_free(latin1, "string");
latin1 = utf8_to_latin1("\xE2\x80test", 6, &len, 1, "string");
TEST("utf8_to_latin1.4", strcmp(latin1, "?test") == 0 && len == 5);
mush_free(latin1, "string");
latin1 = utf8_to_latin1("\xE2\x80\x9Ctest\xE2\x80\x9D", -1, &len, 0, "string");
TEST("utf8_to_latin1.5", strcmp(latin1, "?test?") == 0 && len == 6);
mush_free(latin1, "string");
latin1 = utf8_to_latin1("\xE2\x80\x9Ctest\xE2\x80\x9D", -1, &len, 1, "string");
TEST("utf8_to_latin1.6", strcmp(latin1, "\"test\"") == 0 && len == 6);
mush_free(latin1, "string");
}
/**
* Convert a well-formed UTF-8 encoded string to Latin-1
*
@ -671,6 +706,23 @@ utf8_to_latin1_us(const char *restrict utf8, int len, int *outlen,
return latin1;
}
TEST_GROUP(utf8_to_latin1_us) {
char *latin1;
int len;
latin1 = utf8_to_latin1_us("abcd", 4, &len, 0, "string");
TEST("utf8_to_latin1_us.1", strcmp(latin1, "abcd") == 0 && len == 4);
mush_free(latin1, "string");
latin1 = utf8_to_latin1_us("\xC3\xA1qq", 4, &len, 0, "string");
TEST("utf8_to_latin1_us.2", strcmp(latin1, "\xE1qq") == 0 && len == 3);
mush_free(latin1, "string");
latin1 = utf8_to_latin1_us("\xE2\x80\x9Ctest\xE2\x80\x9D", -1, &len, 0, "string");
TEST("utf8_to_latin1_us.3", strcmp(latin1, "?test?") == 0 && len == 6);
mush_free(latin1, "string");
latin1 = utf8_to_latin1_us("\xE2\x80\x9Ctest\xE2\x80\x9D", -1, &len, 1, "string");
TEST("utf8_to_latin1_us.4", strcmp(latin1, "\"test\"") == 0 && len == 6);
mush_free(latin1, "string");
}
/**
* Check to see if a string is valid utf-8 or not.
* \param utf8 string to validate
@ -693,6 +745,13 @@ valid_utf8(const char *utf8)
return 0;
}
TEST_GROUP(valid_utf8) {
TEST("valid_utf8.1", valid_utf8("abcd"));
TEST("valid_utf8.2", valid_utf8("\xE2\x80\x9Ctest\xE2\x80\x9D"));
TEST("valid_utf8.3", valid_utf8("test\xFFtest") == 0);
TEST("valid_utf8.4", valid_utf8("test\xE2\x80test") == 0);
}
/** Convert a well-formed UTF-16 encoded string to UTF-8.
*
* \parm utf16 the UTF-16 string
@ -1441,3 +1500,20 @@ sanitize_utf8(const char *restrict orig, int len, int *outlen, const char *name)
}
return san8;
}
TEST_GROUP(sanitize_utf8) {
char *s;
int len;
s = sanitize_utf8("abcd", 4, &len, "string");
TEST("sanitize_utf8.1", strcmp(s, "abcd") == 0 && len == 4);
mush_free(s, "string");
s = sanitize_utf8("\xE2\x80\x9Ctest\xE2\x80\x9D", -1, &len, "string");
TEST("valid_utf8.2", strcmp(s, "\xE2\x80\x9Ctest\xE2\x80\x9D") == 0 && len == 10);
mush_free(s, "string");
s = sanitize_utf8("test\xFFtest", 9, &len, "string");
TEST("valid_utf8.3", strcmp(s, "test\xEF\xBF\xBDtest") == 0 && len == 11);
mush_free(s, "string");
s = sanitize_utf8("test\xE2\x80test", 10, &len, "string");
TEST("valid_utf8.4", strcmp(s, "test\xEF\xBF\xBDtest") == 0 && len == 11);
mush_free(s, "string");
}

View file

@ -38,6 +38,7 @@
#include "strtree.h"
#include "strutil.h"
#include "version.h"
#include "tests.h"
PTAB ptab_command; /**< Prefix table for command names. */
PTAB ptab_command_perms; /**< Prefix table for command permissions */
@ -449,6 +450,18 @@ strccat(char *buff, char **bp, const char *from)
safe_str(from, buff, bp);
}
TEST_GROUP(strccat) {
char buff[BUFFER_LEN];
char *bp = buff;
*bp = '\0';
strccat(buff, &bp, "foo");
*bp = '\0';
TEST("strccat.1", strcmp(buff, "foo") == 0);
strccat(buff, &bp, "bar");
*bp = '\0';
TEST("strccat.2", strcmp(buff, "foo, bar") == 0);
}
/* Comparison function for bsearch() */
static int
switch_cmp(const void *a, const void *b)
@ -489,6 +502,11 @@ switch_find(COMMAND_INFO *cmd, const char *sw)
return 0;
}
TEST_GROUP(switch_find) {
TEST("switch_find.1", switch_find(NULL, "LIST") > 0);
TEST("switch_find.2", switch_find(NULL, "NOTASWITCHEVERTHISMEEANSYOU") == 0);
}
/** Test if a particular switch was given, using name
* \param sw the switch mask to test
* \param name the name of the switch to test for.
@ -503,6 +521,13 @@ SW_BY_NAME(switch_mask sw, const char *name)
return false;
}
TEST_GROUP(SW_BY_NAME) {
// TEST SW_BY_NAME REQUIRES switch_find switchmask
switch_mask mask = switchmask("NOEVAL LIST");
TEST("SW_BY_NAME.1", SW_BY_NAME(mask, "LIST"));
TEST("SW_BY_NAME.2", SW_BY_NAME(mask, "NOTASWITCHEVERTHISMEANSYOU") == false);
}
/** Allocate and populate a COMMAND_INFO structure.
* This function generates a new COMMAND_INFO structure, populates it
* with given values, and returns a pointer. It should not be used
@ -732,6 +757,14 @@ switchmask(const char *switches)
return sw;
}
TEST_GROUP(switchmask) {
// TEST switchmask REQUIRES switch_find split_token
switch_mask mask = switchmask("NOEVAL LIST");
TEST("switchmask.1", mask != NULL);
TEST("switchmask.2", mask && SW_ISSET(mask, SWITCH_LIST));
TEST("switchmask.3", mask && SW_ISSET(mask, SWITCH_SPOOF) == 0);
}
/** Add an alias to the table of reserved aliases.
* This function adds an alias to the table of reserved aliases, preventing
* it from being matched for standard commands. It's typically used to

View file

@ -23,10 +23,12 @@
#include <errno.h>
#include <string.h>
#include <stdlib.h>
#include <stdio.h>
#include "map_file.h"
#include "mymalloc.h"
#include "log.h"
#include "tests.h"
/** Memory map a file.
*
@ -166,3 +168,58 @@ unmap_file(MAPPED_FILE *mapped)
#endif
mush_free(mapped, "mapped_file");
}
TEST_GROUP(map_file)
{
FILE *f;
MAPPED_FILE *m;
const char *fname = "mapfiletestdata.txt";
char data[10];
size_t bytes;
int r;
// Create a file with some data.
f = fopen(fname, "w");
TEST("map_file.create_file.1", f != NULL);
if (!f) {
goto cleanup;
}
r = fputs("abcdefg", f);
TEST("map_file.create_file.2", r != EOF);
fclose(f);
// Read-only map
m = map_file(fname, 0);
TEST("map_file.readable.1", m != NULL);
if (!m) {
goto cleanup;
}
TEST("map_file.readable.2", m->len == 7);
TEST("map_file.readable.3", memcmp(m->data, "abcdefg", 7) == 0);
unmap_file(m);
// Read-Write map, test writing.
m = map_file(fname, 1);
TEST("map_file.writable.1", m != NULL);
if (!m) {
goto cleanup;
}
TEST("map_file.writable.2", m->len == 7);
TEST("map_file.writable.3", memcmp(m->data, "abcdefg", 7) == 0);
*(((char *) m->data) + 1) = 'B';
TEST("map_file.writable.4", memcmp(m->data, "aBcdefg", 7) == 0);
unmap_file(m);
f = fopen(fname, "r");
TEST("map_file.open_file.1", f != NULL);
if (!f) {
goto cleanup;
}
bytes = fread(data, 1, sizeof data, f);
fclose(f);
TEST("map_file.open_file.2", bytes == 7);
TEST("map_file.writable.5", memcmp(data, "aBcdefg", 7) == 0);
cleanup:
remove(fname);
}

View file

@ -39,6 +39,7 @@
#include "notify.h"
#include "strtree.h"
#include "strutil.h"
#include "tests.h"
extern char *absp[], *obj[], *poss[], *subj[]; /* fundb.c */
int global_fun_invocations;
@ -269,6 +270,21 @@ is_boolean(char const *str)
return 1;
}
TEST_GROUP(is_boolean)
{
// TEST is_boolean REQUIRES is_integer
int saved = TINY_BOOLEANS;
options.tiny_booleans = 1;
TEST("is_boolean.tiny.1", is_boolean("0") == 1);
TEST("is_boolean.tiny.2", is_boolean("") == 0);
TEST("is_boolean.tiny.3", is_boolean("foo") == 0);
options.tiny_booleans = 0;
TEST("is_boolean.penn.1", is_boolean("0") == 1);
TEST("is_boolean.penn.2", is_boolean("") == 1);
TEST("is_boolean.penn.3", is_boolean("foo") == 1);
options.tiny_booleans = saved;
}
/** Is a string a dbref?
* A dbref is a string starting with a #, optionally followed by a -,
* and then followed by at least one digit, and nothing else.
@ -279,16 +295,34 @@ is_boolean(char const *str)
bool
is_dbref(char const *str)
{
if (!str || (*str != NUMBER_TOKEN) || !*(str + 1))
if (!str || (*str != NUMBER_TOKEN) || !*(str + 1)) {
return 0;
if (*(str + 1) == '-') {
str++;
}
for (str++; isdigit(*str); str++) {
str += 1;
if (*str == '-') {
str += 1;
}
if (!*str) {
return 0;
}
for (; isdigit(*str); str += 1) {
}
return !*str;
}
TEST_GROUP(is_dbref)
{
TEST("is_dbref.1", is_dbref("") == 0);
TEST("is_dbref.2", is_dbref("foo") == 0);
TEST("is_dbref.3", is_dbref("#foo") == 0);
TEST("is_dbref.4", is_dbref("#-1") == 1);
TEST("is_dbref.5", is_dbref("#1234") == 1);
TEST("is_dbref.6", is_dbref("#12AB") == 0);
TEST("is_dbref.7", is_dbref("#") == 0);
TEST("is_dbref.8", is_dbref("#-") == 0);
TEST("is_dbref.9", is_dbref("#-A") == 0);
}
/** Is a string an objid?
* \verbatim
* An objid is a string starting with a #, optionally followed by a -,
@ -350,6 +384,32 @@ is_integer(char const *str)
return 1;
}
TEST_GROUP(is_integer)
{
int saved_math = TINY_MATH;
int saved_null = NULL_EQ_ZERO;
options.tiny_math = 1;
TEST("is_integer.tiny.1", is_integer("") == 1);
TEST("is_integer.tiny.2", is_integer("foo") == 1);
TEST("is_integer.tiny.3", is_integer("5") == 1);
TEST("is_integer.tiny.4", is_integer(NULL) == 1);
options.tiny_math = 0;
TEST("is_integer.penn.1", is_integer("foo") == 0);
TEST("is_integer.penn.2", is_integer("5") == 1);
TEST("is_integer.penn.3", is_integer(" 12") == 1);
TEST("is_integer.penn.4", is_integer(NULL) == 0);
TEST("is_integer.penn.5", is_integer("-5") == 1);
TEST("is_integer.penn.6", is_integer("2147483647") == 1); // INT_MAX
TEST("is_integer.penn.7", is_integer("2147483648") == 0); // INT_MAX + 1
TEST("is_integer.penn.8", is_integer("12foo") == 0);
options.null_eq_zero = 1;
TEST("is_integer.penn.9", is_integer("") == 1);
options.null_eq_zero = 0;
TEST("is_integer.penn.10", is_integer("") == 0);
options.tiny_math = saved_math;
options.null_eq_zero = saved_null;
}
/** Is string an unsigned integer?
* To TinyMUSH, any string is an uinteger. To PennMUSH, a string that
* passes parse_uint is an uinteger, and a blank string is an uinteger
@ -383,6 +443,33 @@ is_uinteger(char const *str)
return 1;
}
TEST_GROUP(is_uinteger)
{
int saved_math = TINY_MATH;
int saved_null = NULL_EQ_ZERO;
options.tiny_math = 1;
TEST("is_uinteger.tiny.1", is_uinteger("") == 1);
TEST("is_uinteger.tiny.2", is_uinteger("foo") == 1);
TEST("is_uinteger.tiny.3", is_uinteger("5") == 1);
TEST("is_uinteger.tiny.4", is_uinteger(NULL) == 1);
TEST("is_uinteger.tiny.5", is_uinteger("-5") == 1);
options.tiny_math = 0;
TEST("is_uinteger.penn.1", is_uinteger("foo") == 0);
TEST("is_uinteger.penn.2", is_uinteger("5") == 1);
TEST("is_uinteger.penn.3", is_uinteger(" 12") == 1);
TEST("is_uinteger.penn.4", is_uinteger(NULL) == 0);
TEST("is_uinteger.penn.5", is_uinteger("-5") == 0);
TEST("is_uinteger.penn.6", is_uinteger("4294967295") == 1); // UINT_MAX
TEST("is_uinteger.penn.7", is_uinteger("4294967296") == 0); // UINT_MAX + 1
TEST("is_uinteger.penn.8", is_uinteger("12foo") == 0);
options.null_eq_zero = 1;
TEST("is_uinteger.penn.9", is_uinteger("") == 1);
options.null_eq_zero = 0;
TEST("is_uinteger.penn.10", is_uinteger("") == 0);
options.tiny_math = saved_math;
options.null_eq_zero = saved_null;
}
/** Is string really an unsigned integer?
* \param str string to check.
* \retval 1 string is an uinteger.
@ -541,6 +628,31 @@ is_number(char const *str)
return is_strict_number(str);
}
TEST_GROUP(is_number)
{
int saved_tiny = TINY_MATH;
int saved_null = NULL_EQ_ZERO;
options.tiny_math = 1;
TEST("is_number.tiny.1", is_number("") == 1);
TEST("is_number.tiny.2", is_number("foo") == 1);
TEST("is_number.tiny.3", is_number("5") == 1);
TEST("is_number.tiny.4", is_number(NULL) == 1);
TEST("is_number.tiny.5", is_number("-5.5") == 1);
options.tiny_math = 0;
TEST("is_number.penn.1", is_number("foo") == 0);
TEST("is_number.penn.2", is_number("5") == 1);
TEST("is_number.penn.3", is_number(" 12.05") == 1);
TEST("is_number.penn.5", is_number("-5") == 1);
TEST("is_number.penn.7", is_number("2e6") == 1);
TEST("is_number.penn.8", is_number("12foo") == 0);
options.null_eq_zero = 1;
TEST("is_number.penn.9", is_number("") == 1);
options.null_eq_zero = 0;
TEST("is_number.penn.10", is_number("") == 0);
options.tiny_math = saved_tiny;
options.null_eq_zero = saved_null;
}
/** Convert a string containing a signed integer into an int.
* Does not do any format checking. Invalid strings will return 0.
* Use this instead of strtol() when storing to an int to avoid problems
@ -2107,9 +2219,9 @@ process_expression(char *buff, char **bp, char const **str, dbref executor,
*str += 16;
}
// fprintf(stderr, "Skipped over '%.*s' to '%c'\n", (int)(*str - pos),
// pos,
// **str);
// fprintf(stderr, "Skipped over '%.*s' to '%c'\n", (int)(*str - pos),
// pos,
// **str);
#else
/* Inlined strcspn() equivalent, to save on overhead and portability */
@ -2151,15 +2263,13 @@ process_expression(char *buff, char **bp, char const **str, dbref executor,
* added 17 Sep 2012. Remove when this behaviour is removed. */
else if (tflags & PT_NOT_COMMA) {
if (pe_info && pe_info->attrname)
notify_format(Owner(executor),
"Unescaped comma in final arg of %s "
"by #%d in %s. This behavior is "
"deprecated.",
notify_format(Owner(executor), "Unescaped comma in final arg of %s "
"by #%d in %s. This behavior is "
"deprecated.",
lca_func_name, executor, pe_info->attrname);
else
notify_format(Owner(executor),
"Unescaped comma in final arg of %s "
"by #%d. This behavior is deprecated.",
notify_format(Owner(executor), "Unescaped comma in final arg of %s "
"by #%d. This behavior is deprecated.",
lca_func_name, executor);
tflags &= ~PT_NOT_COMMA;
}

View file

@ -35,6 +35,7 @@
#include "parse.h"
#include "pueblo.h"
#include "charclass.h"
#include "tests.h"
/* TODO: Adding this prototype here is cheating, but it's easier for now. Clean
this up eventually... */
@ -137,13 +138,26 @@ chopstr(const char *str, size_t lim)
{
static char tbuf1[BUFFER_LEN];
if (strlen(str) <= lim)
return (char *) str;
return (char *) str; // Not what the documentation claims!
if (lim >= BUFFER_LEN)
lim = BUFFER_LEN;
mush_strncpy(tbuf1, str, lim);
return tbuf1;
}
TEST_GROUP(chopstr)
{
char test[BUFFER_LEN * 2];
char answer[BUFFER_LEN];
memset(test, 'A', sizeof test);
memset(answer, 'A', sizeof answer);
test[BUFFER_LEN * 2 - 1] = '\0';
answer[BUFFER_LEN - 1] = '\0';
TEST("chopstr.1", strcmp(chopstr("AAAA", 10), "AAAA") == 0);
TEST("chopstr.2", strcmp(chopstr("AAAABB", 5), "AAAA") == 0);
TEST("chopstr.3", strcmp(chopstr(test, BUFFER_LEN + 100), answer) == 0);
}
#if !defined(HAVE_STRCASECMP) && !defined(HAVE__STRICMP)
/** strcasecmp for systems without it.
* \param s1 one string to compare.
@ -201,6 +215,19 @@ string_prefix(const char *RESTRICT string, const char *RESTRICT prefix)
return *prefix == '\0';
}
TEST_GROUP(string_prefix)
{
TEST("string_prefix.1", string_prefix("AAAAB", "AAAA"));
TEST("string_prefix.2", string_prefix("AAAAB", "aaa"));
TEST("string_prefix.3", string_prefix("AABB", "AAA") == 0);
TEST("string_prefix.4", string_prefix("AABB", "aaa") == 0);
TEST("string_prefix.5", string_prefix(NULL, "AAA") == 0);
TEST("string_prefix.6", string_prefix("AAAA", NULL) == 0);
TEST("string_prefix.7", string_prefix("AAAAA", ""));
TEST("string_prefix.8", string_prefix("", "AA") == 0);
TEST("string_prefix.9", string_prefix("", ""));
}
/** Does string begin with prefix?
* This comparison is case-insensitive. An empty prefix always
* fails.
@ -219,6 +246,19 @@ string_prefixe(const char *RESTRICT string, const char *RESTRICT prefix)
return *prefix == '\0';
}
TEST_GROUP(string_prefixe)
{
TEST("string_prefixe.1", string_prefixe("AAAAB", "AAAA"));
TEST("string_prefixe.2", string_prefixe("AAAAB", "aaa"));
TEST("string_prefixe.3", string_prefixe("AABB", "AAA") == 0);
TEST("string_prefixe.4", string_prefixe("AABB", "aaa") == 0);
TEST("string_prefixe.5", string_prefixe(NULL, "AAA") == 0);
TEST("string_prefixe.6", string_prefixe("AAAA", NULL) == 0);
TEST("string_prefixe.7", string_prefixe("AAAAA", "") == 0);
TEST("string_prefixe.8", string_prefixe("", "AA") == 0);
TEST("string_prefixe.9", string_prefixe("", "") == 0);
}
/** Match a substring at the start of a word in a string, case-insensitively.
* \param src a string of words to match against.
* \param sub a prefix to match against the start of a word in string.
@ -1068,7 +1108,7 @@ safe_hexstr(uint8_t *bytes, int len, char *buff, char **bp)
/* skip_space and seek_char are essentially right out of the 2.0 code */
/** Return a pointer to the next non-space character in a string, or NULL.
* We return NULL if given a null string or a string with only spaces.
* We return NULL if given a null string.
* \param s string to search for non-spaces.
* \return pointer to next non-space character in s.
*/
@ -1081,6 +1121,14 @@ skip_space(const char *s)
return c;
}
TEST_GROUP(skip_space)
{
TEST("skip_space.1", skip_space(NULL) == NULL);
TEST("skip_space.2", *skip_space(" ") == '\0');
TEST("skip_space.3", strcmp(skip_space(" AA"), "AA") == 0);
TEST("skip_space.4", strcmp(skip_space("AAAA"), "AAAA") == 0);
}
/** Return a pointer to next char in s which matches c, or to the terminating
* nul at the end of s.
* \param s string to search.
@ -1102,6 +1150,12 @@ seek_char(const char *s, char c)
#endif /* HAVE_STRCHRNUL */
}
TEST_GROUP(seek_char)
{
TEST("seek_char.1", *seek_char("ABA", 'B') == 'B');
TEST("seek_char.2", *seek_char("AAA", 'B') == '\0');
}
/** Search for all copies of old in string, and replace each with newbit.
* The replaced string is returned, newly allocated.
* \param old string to find.
@ -1221,6 +1275,15 @@ copy_up_to(char *RESTRICT dest, const char *RESTRICT src, char c)
return dest;
}
TEST_GROUP(copy_up_to)
{
char dest[BUFFER_LEN];
TEST("copy_up_to.1", strcmp(copy_up_to(dest, "AAAA", 'B'), "AAAA") == 0);
TEST("copy_up_to.2", strcmp(copy_up_to(dest, "AABA", 'B'), "AA") == 0);
TEST("copy_up_to.3", *copy_up_to(dest, "", 'B') == '\0');
TEST("copy_up_to.4", *copy_up_to(dest, "B", 'B') == '\0');
}
/** Given a string and a separator, trim leading and trailing spaces
* if the separator is a space. This destructively modifies the string.
* \param str string to trim.
@ -1248,6 +1311,21 @@ trim_space_sep(char *str, char sep)
return str;
}
TEST_GROUP(trim_space_sep)
{
char buff[BUFFER_LEN];
strcpy(buff, " foo ");
TEST("trim_space_sep.1", strcmp(trim_space_sep(buff, ' '), "foo") == 0);
strcpy(buff, " foo ");
TEST("trim_space_sep.2", strcmp(trim_space_sep(buff, 'x'), " foo ") == 0);
strcpy(buff, "foo");
TEST("trim_space_sep.3", strcmp(trim_space_sep(buff, ' '), "foo") == 0);
strcpy(buff, " foo");
TEST("trim_space_sep.4", strcmp(trim_space_sep(buff, ' '), "foo") == 0);
strcpy(buff, "foo ");
TEST("trim_space_sep.5", strcmp(trim_space_sep(buff, ' '), "foo") == 0);
}
/** Find the start of the next token in a string.
* If the separator is a space, we magically skip multiple spaces.
* \param str the string.
@ -1285,6 +1363,19 @@ next_token(char *str, char sep)
return str;
}
TEST_GROUP(next_token) {
char *c;
c = next_token(" a b", ' ');
TEST("next_token.1", c && *c == 'a');
c = next_token("a|b", '|');
TEST("next_token.2", c && *c == 'b');
c = next_token("\x1B[0ma b", ' ');
TEST("next_token.3", c && *c == 'b');
c = next_token(" ", ' ');
TEST("next_token.4", c && *c == '\0');
TEST("next_token.5", next_token("", '|') == NULL);
}
/** Split out the next token from a string, destructively modifying it.
* As usually, if the separator is a space, we skip multiple spaces.
* The string's address is updated to be past the token, and the token
@ -1333,6 +1424,35 @@ split_token(char **sp, char sep)
return save;
}
TEST_GROUP(split_token) {
char *c, *t;
char buff[BUFFER_LEN];
t = NULL;
c = split_token(&t, ' ');
TEST("split_token.1", c == NULL && t == NULL);
strcpy(buff, " a b");
t = buff;
c = split_token(&t, ' ');
TEST("split_token.2", strcmp(c, "") == 0 && strcmp(t, "a b") == 0);
strcpy(buff, "a|b");
t = buff;
c = split_token(&t, '|');
TEST("split_token.3", strcmp(c, "a") == 0 && strcmp(t, "b") == 0);
strcpy(buff, "\x1B[0ma b");
t = buff;
c = split_token(&t, ' ');
TEST("split_token.4", strcmp(c, "\x1B[0ma") == 0 && strcmp(t, "b") == 0);
strcpy(buff, " ");
t = buff;
c = split_token(&t, ' ');
TEST("split_token.5", c && *c == '\0' && *t == '\0');
strcpy(buff, "");
t = buff;
c = split_token(&t, '|');
TEST("split_token.6", c && *c == '\0' && t == NULL);
}
/** Count the number of tokens in a string.
* \param str string to count.
* \param sep token separator.
@ -1350,6 +1470,19 @@ do_wordcount(char *str, char sep)
return n;
}
TEST_GROUP(do_wordcount)
{
// TEST do_wordcount REQUIRES next_token
char buff[] = "A B C D";
char buff2[] = "A|B|C|D";
char buff3[] = "A B C D";
TEST("do_wordcount.1", do_wordcount(buff, ' ') == 4);
TEST("do_wordcount.2", do_wordcount(buff2, '|') == 4);
TEST("do_wordcount.3", do_wordcount(buff3, ' ') == 4);
TEST("do_wordcount.4", do_wordcount(buff3, '|') == 1);
TEST("do_wordcount.5", do_wordcount("", ' ') == 0);
}
/** Given a string, a word, and a separator, remove first occurence
* of the word from the string. Destructive.
* \param list a string containing a separated list.
@ -1386,6 +1519,18 @@ remove_word(char *list, char *word, char sep)
return buff;
}
TEST_GROUP(remove_word) {
// TEST remove_word REQUIRES split_token
char buff[BUFFER_LEN];
char *c;
strcpy(buff, "adam boy charles");
c = remove_word(buff, "boy", ' ');
TEST("remove_word.1", strcmp(c, "adam charles") == 0);
strcpy(buff, "adam|boy|charles");
c = remove_word(buff, "charles", '|');
TEST("remove_word.2", strcmp(c, "adam|boy") == 0);
}
/** Return the next name in a list. A name may be a single word, or
* a quoted string. This is used by things like page/list. The list's
* pointer is advanced to the next name in the list.
@ -1422,6 +1567,20 @@ next_in_list(const char **head)
return buf;
}
TEST_GROUP(next_in_list) {
char buff[BUFFER_LEN];
char *c;
const char *t;
strcpy(buff, "adam boy charles");
t = buff;
c = next_in_list(&t);
TEST("next_in_list.1", strcmp(c, "adam") == 0 && strcmp(t, " boy charles") == 0);
strcpy(buff, "\"mr. t\" ba");
t = buff;
c = next_in_list(&t);
TEST("next_in_list.2", strcmp(c, "mr. t") == 0 && strcmp(t, " ba") == 0);
}
#ifndef HAVE_IMAXDIV_T
typedef struct imaxdiv_t {
intmax_t rem;
@ -1860,6 +2019,22 @@ remove_trailing_whitespace(char *buff, size_t len)
return len;
}
TEST_GROUP(remove_trailing_whitespace)
{
char buff[BUFFER_LEN];
strcpy(buff, "foo \t ");
TEST("remove_trailing_whitespace.1",
remove_trailing_whitespace(buff, strlen(buff)) == 3 &&
strcmp(buff, "foo") == 0);
strcpy(buff, "bar");
TEST("remove_trailing_whitespace.2",
remove_trailing_whitespace(buff, strlen(buff)) == 3 &&
strcmp(buff, "bar") == 0);
buff[0] = '\0';
TEST("remove_trailing_whitespace.3",
remove_trailing_whitespace(buff, 0) == 0);
}
int
safe_chr(char c, char *buff, char **bp)
{
@ -1893,6 +2068,17 @@ strchr_unescaped(char *s, int c)
return NULL;
}
TEST_GROUP(strchr_unescaped)
{
TEST("strchr_unescaped.1",
strcmp(strchr_unescaped("$foo\\:bar:there", ':'), ":there") == 0);
TEST("strchr_unescaped.2", strchr_unescaped("$foo\\:noescape", ':') == NULL);
TEST("strchr_unescaped.3",
strcmp(strchr_unescaped("$foo\\\\:noescape", ':'), ":noescape") == 0);
TEST("strchr_unescaped.4", strchr_unescaped(NULL, ':') == NULL);
TEST("strchr_unescaped.5", strchr_unescaped("nosuchthing", ':') == NULL);
}
/* keystr format:
*
* Either a single word, which is returned for any keyword Or one or
@ -1975,6 +2161,24 @@ glob_to_like(const char *orig, char esc, int *len)
return like;
}
TEST_GROUP(glob_to_like)
{
char *g;
int len;
g = glob_to_like("foo*", '$', &len);
TEST("glob_to_like.1", strcmp(g, "foo%") == 0 && len == 4);
mush_free(g, "string");
g = glob_to_like("f?o", '$', &len);
TEST("glob_to_like.2", strcmp(g, "f_o") == 0 && len == 3);
mush_free(g, "string");
g = glob_to_like("*foo%bar*", '$', &len);
TEST("glob_to_like.3", strcmp(g, "%foo$%bar%") == 0 && len == 10);
mush_free(g, "string");
g = glob_to_like("", '$', &len);
TEST("glob_to_like.4", g && *g == '\0' && len == 0);
mush_free(g, "string");
}
/** Escape SQL like wildcards from a string.
*
* \param orig the string to escape.
@ -2007,3 +2211,21 @@ escape_like(const char *orig, char esc, int *len)
return like;
}
TEST_GROUP(escape_like)
{
char *g;
int len;
g = escape_like("foo%", '$', &len);
TEST("escape_like.1", strcmp(g, "foo$%") == 0 && len == 5);
mush_free(g, "string");
g = escape_like("f_o", '$', &len);
TEST("escape_like.2", strcmp(g, "f$_o") == 0 && len == 4);
mush_free(g, "string");
g = escape_like("foobar", '$', &len);
TEST("escape_like.3", strcmp(g, "foobar") == 0 && len == 6);
mush_free(g, "string");
g = escape_like("", '$', &len);
TEST("escape_like.4", g && *g == '\0' && len == 0);
mush_free(g, "string");
}

74
src/testframework.c Normal file
View file

@ -0,0 +1,74 @@
/** \file testframework.c
*
* \brief Hardcode test framework
*/
#include <stdio.h>
#include <string.h>
#include "sqlite3.h"
#include "tests.h"
#include "tests.inc"
/** Run the hardcode tests.
* \return true if all tests passed, false if tests failed.
*/
bool
run_tests(void)
{
int total_success = 0, total_failure = 0, skipped = 0, total_tests = 0;
struct test_record *t;
sqlite3_str *logmsg;
char *logstr;
do_rawlog(LT_TRACE, "Starting tests.");
for (t = tests; t->name; t += 1) {
total_tests += 1;
if (t->status == TEST_NOT_RUN) {
int success = 0, failure = 0;
t->fun(&success, &failure);
if (failure == 0) {
t->status = TEST_PASSED;
} else {
struct test_record *deps;
char *testname = sqlite3_mprintf("|%s|", t->name);
for (deps = t + 1; deps->name; deps += 1) {
if (strstr(deps->depends, testname)) {
deps->status = TEST_SKIPME;
}
}
sqlite3_free(testname);
t->status = TEST_FAILED;
}
logmsg = sqlite3_str_new(NULL);
sqlite3_str_appendf(logmsg, "%s %s: %d/%d tests succeeded", t->name,
t->status == TEST_PASSED ? "PASSED" : "FAILED",
success, success + failure);
if (failure) {
sqlite3_str_appendf(logmsg, ", %d failed", failure);
}
logstr = sqlite3_str_finish(logmsg);
do_rawlog(LT_TRACE, "%s.", logstr);
sqlite3_free(logstr);
total_success += success;
total_failure += failure;
} else if (t->status == TEST_SKIPME) {
do_rawlog(LT_TRACE, "%s SKIPPED", t->name);
skipped += 1;
}
}
logmsg = sqlite3_str_new(NULL);
sqlite3_str_appendf(logmsg, "%d test groups, with %d/%d tests succeeding",
total_tests, total_success,
total_success + total_failure);
if (skipped) {
sqlite3_str_appendf(logmsg, ", %d test groups skipped", skipped);
}
if (total_failure) {
sqlite3_str_appendf(logmsg, ", and %d tests failed", total_failure);
}
logstr = sqlite3_str_finish(logmsg);
do_rawlog(LT_TRACE, "%s.", logstr);
sqlite3_free(logstr);
return total_failure == 0;
}

77
src/tests.inc Normal file
View file

@ -0,0 +1,77 @@
/* Auto-generated file. DO NOT EDIT */
void test_switch_find(int *, int *);
void test_split_token(int *, int *);
void test_is_integer(int *, int *);
void test_next_token(int *, int *);
void test_switchmask(int *, int *);
void test_remove_word(int *, int *);
void test_is_boolean(int *, int *);
void test_do_wordcount(int *, int *);
void test_SW_BY_NAME(int *, int *);
void test_chopstr(int *, int *);
void test_copy_up_to(int *, int *);
void test_escape_like(int *, int *);
void test_glob_to_like(int *, int *);
void test_is_dbref(int *, int *);
void test_is_number(int *, int *);
void test_is_uinteger(int *, int *);
void test_latin1_to_utf8(int *, int *);
void test_map_file(int *, int *);
void test_next_in_list(int *, int *);
void test_remove_trailing_whitespace(int *, int *);
void test_sanitize_utf8(int *, int *);
void test_seek_char(int *, int *);
void test_skip_space(int *, int *);
void test_strccat(int *, int *);
void test_strchr_unescaped(int *, int *);
void test_string_prefix(int *, int *);
void test_string_prefixe(int *, int *);
void test_trim_space_sep(int *, int *);
void test_utf8_to_latin1(int *, int *);
void test_utf8_to_latin1_us(int *, int *);
void test_valid_utf8(int *, int *);
struct test_record {
const char *name;
void (*fun)(int *, int *);
const char *depends;
int status;
};
#define TEST_NOT_RUN 0
#define TEST_PASSED 1
#define TEST_FAILED 2
#define TEST_SKIPME 3
static struct test_record tests[] = {
{"switch_find", test_switch_find, "||", TEST_NOT_RUN},
{"split_token", test_split_token, "||", TEST_NOT_RUN},
{"is_integer", test_is_integer, "||", TEST_NOT_RUN},
{"next_token", test_next_token, "||", TEST_NOT_RUN},
{"switchmask", test_switchmask, "|switch_find|split_token|", TEST_NOT_RUN},
{"remove_word", test_remove_word, "|split_token|", TEST_NOT_RUN},
{"is_boolean", test_is_boolean, "|is_integer|", TEST_NOT_RUN},
{"do_wordcount", test_do_wordcount, "|next_token|", TEST_NOT_RUN},
{"SW_BY_NAME", test_SW_BY_NAME, "|switch_find|switchmask|", TEST_NOT_RUN},
{"chopstr", test_chopstr, "||", TEST_NOT_RUN},
{"copy_up_to", test_copy_up_to, "||", TEST_NOT_RUN},
{"escape_like", test_escape_like, "||", TEST_NOT_RUN},
{"glob_to_like", test_glob_to_like, "||", TEST_NOT_RUN},
{"is_dbref", test_is_dbref, "||", TEST_NOT_RUN},
{"is_number", test_is_number, "||", TEST_NOT_RUN},
{"is_uinteger", test_is_uinteger, "||", TEST_NOT_RUN},
{"latin1_to_utf8", test_latin1_to_utf8, "||", TEST_NOT_RUN},
{"map_file", test_map_file, "||", TEST_NOT_RUN},
{"next_in_list", test_next_in_list, "||", TEST_NOT_RUN},
{"remove_trailing_whitespace", test_remove_trailing_whitespace, "||", TEST_NOT_RUN},
{"sanitize_utf8", test_sanitize_utf8, "||", TEST_NOT_RUN},
{"seek_char", test_seek_char, "||", TEST_NOT_RUN},
{"skip_space", test_skip_space, "||", TEST_NOT_RUN},
{"strccat", test_strccat, "||", TEST_NOT_RUN},
{"strchr_unescaped", test_strchr_unescaped, "||", TEST_NOT_RUN},
{"string_prefix", test_string_prefix, "||", TEST_NOT_RUN},
{"string_prefixe", test_string_prefixe, "||", TEST_NOT_RUN},
{"trim_space_sep", test_trim_space_sep, "||", TEST_NOT_RUN},
{"utf8_to_latin1", test_utf8_to_latin1, "||", TEST_NOT_RUN},
{"utf8_to_latin1_us", test_utf8_to_latin1_us, "||", TEST_NOT_RUN},
{"valid_utf8", test_valid_utf8, "||", TEST_NOT_RUN},
{NULL, NULL, NULL, TEST_NOT_RUN}
};

View file

@ -83,7 +83,7 @@ sub start {
die "Could not start game process properly; pid $child!\n";
} elsif (defined($child)) {
chdir("testgame");
my @execargs = ("./netmush", "--no-session", "--disable-socket-quota");
my @execargs = ("./netmush", "--no-session", "--disable-socket-quota", "--tests");
if ($self->{VALGRIND}) {
unshift @execargs, "valgrind", "--tool=memcheck", '--log-file=../valgrind-%p.log',
"--leak-check=full", "--track-origins=yes";

View file

@ -1,26 +0,0 @@
Regression tests for Penn functions and commands.
Usage:
perl runtest.pl [--valgrind] testFOO.pl ...
or
./alltests.sh [--valgrind]
The test*.pl files are frameworks for tests, not actual perl
scripts. The choice of extension was unfortunate.
The --valgrind option runs the test game under valgrind to help detect
memory issues.
Their format:
login mortal
expect N failures!
run tests:
perl code
All the lines above the 'run tests:' one are optional.
Look at existing files for how to write tests. Some hints: $god is
always available as a test connection. If 'login mortal' was given,
$mortal is too.

71
test/README.md Normal file
View file

@ -0,0 +1,71 @@
# Regression tests for Penn functions and commands.
Penn has two different test suites -- one for hardcode, one for softcode. This document describes how to run them and write tests for them.
# Hardcode Tests
These are run at startup, after configuration files are read and the database is loaded, but before the game starts accepting connections. It's intended for things that can't easily be tested in softcode and things softcode tests themselves depend on working right.
## Running tests
The `--tests` option to `netmush` runs the tests and logs the results to `log/trace.log`. The MUSH continues to finish starting up and running normally unless a test case fails, in which case it ends with exit code 1.
The `--only-tests` option also runs the test cases, but then exits instead of continuing to start up. An exit code of 0 means all tests passed, 1 means there were failures.
## Writing tests
All source files that define tests need to `#include "tests.h"`.
A *test group* is defined with the `TEST_GROUP()` macro like so:
TEST_GROUP(some_name) {
// test cases
}
Each test group can have one or more tests. Tests for the same hardcode function should all go in a single test group. The tests look like:
TEST("name", test expression);
`test expression` should evaluate to a true value to indicate success, and 0 or `false` for failure. Failures get logged.
If a *test group* should only be called after another *test group* has successfully run, add a comment like:
// TEST some_name REQUIRES other_test1 other_test2
# Softcode Tests
## Running tests
From the test subdirectory:
$ perl runtest.pl [--valgrind] testFOO.t ...
or
$ ./alltests.sh [--valgrind]
Note: The the hardcode tests are automatically run as well.
## Writing tests
The test*.t files in the **test** subdirectory define softcode test cases.
The `--valgrind` option runs the test game under valgrind to help detect memory issues.
Their format:
login mortal
expect N failures!
run tests:
test cases
All the lines above the 'run tests:' one are optional.
The *test cases* are perl code:
The `test()` function has four arguments -- the name of the test, the player to run it as (Either `$god` or `$mortal`, a softcode command, and a regular expression that should match the expected result (Or an array ref of REs).
The player objects have a `command()` method that runs its argument in the game without counting as a test: `$mortal->command("think not a test");`
Some hints: $god is always available as a test connection. If 'login mortal' was given, $mortal is too. See existing tests for examples of how to write new ones.

View file

@ -10,5 +10,5 @@ fi
export LC_ALL=${LOC}
export LC_COLLATE=C
exec @PERL@ runtest.pl $@ test*.pl
exec @PERL@ runtest.pl $@ test*.t

View file

@ -7,6 +7,8 @@ use warnings;
use File::Compare;
use File::Copy;
use File::Temp qw/tempfile/;
use IPC::Open2;
# use feature qw/say/; # People STILL sometimes use OSes that have really ancient perl versions that don't support say. Sigh.
my @tmpfiles;
@ -25,10 +27,13 @@ foreach my $command (@ARGV) {
make_cmds();
} elsif ($command eq "functions") {
make_funs();
} elsif ($command eq "tests") {
make_tests();
} elsif ($command eq "all") {
make_switches();
make_cmds();
make_funs();
make_tests();
} else {
warn "Unknown option '${command}'\n";
}
@ -173,3 +178,84 @@ EOH
maybemove $tempfile, "hdrs/funs.h";
}
sub make_tests {
my @tests = scan_files_for_pattern("src/*.c", qr/TEST_GROUP\((\w+)\)/);
my %depends;
for my $dep (scan_files_for_pattern("src/*.c", qr#// TEST (\w+ REQUIRES .*)#)) {
my @bits = split/\s+/, $dep;
my $test = shift @bits;
shift @bits;
$depends{$test} = \@bits;
}
my $err = eval {
# Remove all tests that depend on a specific order from @tests
my $orderedtests = tsort(\%depends);
my %t = map { $_ => 1 } @$orderedtests;
@tests = grep { not exists $t{$_} } @tests;
# And put them back in the list in the right order
unshift @tests, @$orderedtests;
};
if (not defined $err) {
warn "Unable to sort tests: $@\n";
return;
}
my ($HDR, $tmpfile) = tempfile("testXXXXX", SUFFIX => ".c", TMPDIR => 1);
push @tmpfiles, $tmpfile;
print $HDR "/* Auto-generated file. DO NOT EDIT */\n";
print $HDR "void test_$_(int *, int *);\n" for @tests;
print $HDR <<EOF;
struct test_record {
const char *name;
void (*fun)(int *, int *);
const char *depends;
int status;
};
#define TEST_NOT_RUN 0
#define TEST_PASSED 1
#define TEST_FAILED 2
#define TEST_SKIPME 3
static struct test_record tests[] = {
EOF
for my $test (@tests) {
local $" = "|";
$depends{$test} //= [];
print $HDR <<EOF;
{"$test", test_$test, "|@{$depends{$test}}|", TEST_NOT_RUN},
EOF
}
print $HDR <<EOF;
{NULL, NULL, NULL, TEST_NOT_RUN}
};
EOF
close $HDR;
maybemove($tmpfile, "src/tests.inc");
}
# There are topological sort modules for Perl, but none in the core list.
# So just pipe out to tsort(1) even though it makes me feel dirty. This is perl, not shell!
sub tsort {
my $deps = shift;
my ($to_tsort, $from_tsort);
my $pid = open2($from_tsort, $to_tsort, "tsort") or die "Unable to run tsort: $!\n";
while (my ($item, $deplist) = each %$deps) {
for my $d (@$deplist) {
print $to_tsort "$item $d\n";
}
}
close $to_tsort;
my $sorted;
while (<$from_tsort>) {
chomp;
unshift @$sorted, $_;
}
close $from_tsort;
waitpid $pid, 0;
return $sorted;
}