Unit testing (#18329)

* Unit testing

* Fixes
This commit is contained in:
DamianX
2018-05-31 13:32:18 +02:00
committed by jknpj
parent c10e0b89e5
commit 191b694b95
13 changed files with 286 additions and 56 deletions

View File

@@ -9,8 +9,12 @@ env:
- BYOND_MAJOR="512" - BYOND_MAJOR="512"
- BYOND_MINOR="1413" - BYOND_MINOR="1413"
- ALL_MAPS="tgstation metaclub defficiency packedstation test_box test_tiny" - ALL_MAPS="tgstation metaclub defficiency packedstation test_box test_tiny"
- PROJECT_NAME="vgstation13"
- RUST_BACKTRACE="1" - RUST_BACKTRACE="1"
- RUST_TEST_THREADS=1 - RUST_TEST_THREADS=1
matrix:
- DM_UNIT_TESTS="1"
- DM_UNIT_TESTS="0"
cache: cache:
directories: directories:
@@ -42,7 +46,9 @@ script:
# --jobs 1 to prevent threading problems with the BYOND crate. # --jobs 1 to prevent threading problems with the BYOND crate.
- cargo test --jobs 1 --verbose - cargo test --jobs 1 --verbose
- cd - - cd -
- echo ${ALL_MAPS} | xargs tools/travis/build.py vgstation13.dme -M - tools/travis/build.py
- cp tools/travis/config/config.txt config/
- (! tools/travis/run_tests.py | grep -zq "UNIT TEST FAIL")
notifications: notifications:
irc: irc:

View File

@@ -121,6 +121,8 @@ var/CURRENT_TICKLIMIT = TICK_LIMIT_RUNNING
// Please don't stuff random bullshit here, // Please don't stuff random bullshit here,
// Make a subsystem, give it the SS_NO_FIRE flag, and do your work in it's Initialize() // Make a subsystem, give it the SS_NO_FIRE flag, and do your work in it's Initialize()
/datum/controller/master/proc/Setup() /datum/controller/master/proc/Setup()
set waitfor = FALSE
sleep(1 SECONDS)
to_chat(world, "<span class='boldannounce'>Initializing subsystems...</span>") to_chat(world, "<span class='boldannounce'>Initializing subsystems...</span>")
// Sort subsystems by init_order, so they initialize in the correct order. // Sort subsystems by init_order, so they initialize in the correct order.
@@ -141,7 +143,6 @@ var/CURRENT_TICKLIMIT = TICK_LIMIT_RUNNING
// Sort subsystems by display setting for easy access. // Sort subsystems by display setting for easy access.
sortTim(subsystems, /proc/cmp_subsystem_display) sortTim(subsystems, /proc/cmp_subsystem_display)
// Set world options. // Set world options.
world.sleep_offline = 1
world.tick_lag = config.Ticklag world.tick_lag = config.Ticklag
sleep(1) sleep(1)
// Loop. // Loop.

View File

@@ -74,7 +74,11 @@ var/datum/controller/gameticker/ticker
send2maindiscord("**Server is loaded** and in pre-game lobby at `[config.server? "byond://[config.server]" : "byond://[world.address]:[world.port]"]`") send2maindiscord("**Server is loaded** and in pre-game lobby at `[config.server? "byond://[config.server]" : "byond://[world.address]:[world.port]"]`")
do do
var/delay_timetotal = 3000 //actually 5 minutes or incase this is changed from 3000, (time_in_seconds * 10) #ifdef UNIT_TESTS
var/delay_timetotal = 2 SECONDS
#else
var/delay_timetotal = 5 MINUTES
#endif
pregame_timeleft = world.timeofday + delay_timetotal pregame_timeleft = world.timeofday + delay_timetotal
to_chat(world, "<B><FONT color='blue'>Welcome to the pre-game lobby!</FONT></B>") to_chat(world, "<B><FONT color='blue'>Welcome to the pre-game lobby!</FONT></B>")
to_chat(world, "Please, setup your character and select ready. Game will start in [(pregame_timeleft - world.timeofday) / 10] seconds.") to_chat(world, "Please, setup your character and select ready. Game will start in [(pregame_timeleft - world.timeofday) / 10] seconds.")
@@ -184,6 +188,10 @@ var/datum/controller/gameticker/ticker
//here to initialize the random events nicely at round start //here to initialize the random events nicely at round start
setup_economy() setup_economy()
#ifdef UNIT_TESTS
run_unit_tests()
#endif
spawn(0)//Forking here so we dont have to wait for this to finish spawn(0)//Forking here so we dont have to wait for this to finish
mode.post_setup() mode.post_setup()
//Cleanup some stuff //Cleanup some stuff

View File

@@ -101,7 +101,8 @@
desclines += " (This error will now be silenced for [configured_error_silence_time / 600] minutes)" desclines += " (This error will now be silenced for [configured_error_silence_time / 600] minutes)"
// Now to actually output the error info... // Now to actually output the error info...
world.log << "\[[time_stamp()]] Runtime in [e.file],[e.line]: [e]" var/main_line = "\[[time_stamp()]] Runtime in [e.file],[e.line]: [e]"
world.log << main_line
for (var/line in desclines) for (var/line in desclines)
world.log << line world.log << line
@@ -109,4 +110,9 @@
if (global.error_cache) if (global.error_cache)
global.error_cache.log_error(e, desclines) global.error_cache.log_error(e, desclines)
#ifdef UNIT_TESTS
if(global.current_test)
global.current_test.fail("[main_line]\n[desclines.Join("\n")]")
#endif
#endif #endif

View File

@@ -0,0 +1,4 @@
#ifdef UNIT_TESTS
#include "unit_test.dm"
#include "reagent_recipe_collisions.dm"
#endif

View File

@@ -0,0 +1,67 @@
/datum/unit_test/reagent_recipe_collisions
/datum/unit_test/reagent_recipe_collisions/start()
var/datum/reagents/r = new // Builds chemical_reactions_list
qdel(r)
r = null
var/list/reactions = list()
for(var/V in global.chemical_reactions_list)
reactions += global.chemical_reactions_list[V]
for(var/i in 1 to (reactions.len-1))
for(var/i2 in (i+1) to reactions.len)
var/datum/chemical_reaction/r1 = reactions[i]
var/datum/chemical_reaction/r2 = reactions[i2]
if(recipes_do_conflict(r1, r2))
fail("Chemical recipe conflict between [r1.type] and [r2.type]")
/datum/unit_test/reagent_recipe_collisions/proc/recipes_do_conflict(datum/chemical_reaction/r1, datum/chemical_reaction/r2)
//do the non-list tests first, because they are cheaper
if(r1.required_container != r2.required_container)
return FALSE
if(r1.is_cold_recipe == r2.is_cold_recipe)
if(r1.required_temp != r2.required_temp)
//one reaction requires a more extreme temperature than the other, so there is no conflict
return FALSE
else
var/datum/chemical_reaction/cold_one = r1.is_cold_recipe ? r1 : r2
var/datum/chemical_reaction/warm_one = r1.is_cold_recipe ? r2 : r1
if(warm_one.required_temp == 0 || cold_one.required_temp < warm_one.required_temp)
// the warm reaction doesn't require any particular temperature or the range of temperatures does not overlap, so there is no conflict
return FALSE
//find the reactions with the shorter and longer required_reagents list
var/datum/chemical_reaction/long_req
var/datum/chemical_reaction/short_req
if(r1.required_reagents.len > r2.required_reagents.len)
long_req = r1
short_req = r2
else if(r1.required_reagents.len < r2.required_reagents.len)
long_req = r2
short_req = r1
else
//if they are the same length, sort instead by the length of the catalyst list
//this is important if the required_reagents lists are the same
if(r1.required_catalysts.len > r2.required_catalysts.len)
long_req = r1
short_req = r2
else
long_req = r2
short_req = r1
//check if the shorter reaction list is a subset of the longer one
var/list/overlap = r1.required_reagents & r2.required_reagents
if(overlap.len != short_req.required_reagents.len)
//there is at least one reagent in the short list that is not in the long list, so there is no conflict
return FALSE
//check to see if the shorter reaction's catalyst list is also a subset of the longer reaction's catalyst list
//if the longer reaction's catalyst list is a subset of the shorter ones, that is fine
//if the reaction lists are the same, the short reaction will have the shorter required_catalysts list, so it will register as a conflict
var/list/short_minus_long_catalysts = short_req.required_catalysts - long_req.required_catalysts
if(short_minus_long_catalysts.len)
//there is at least one unique catalyst for the short reaction, so there is no conflict
return FALSE
//if we got this far, the longer reaction will be impossible to create if the shorter one is earlier in global.chemical_reactions_list, and will require the reagents to be added in a particular order otherwise
return TRUE

View File

@@ -0,0 +1,72 @@
/*
Usage:
Override /run() to run your test code.
Call fail() to fail the test (You should specify a reason).
Use /New() and Destroy() for setup/teardown, respectively.
You can use the run_loc_bottom_left and run_loc_top_right if your tests require turfs.
*/
var/datum/unit_test/current_test
var/failed_any_test = FALSE
/datum/unit_test
var/list/procs_tested
//usable vars
var/turf/run_loc_bottom_left
var/turf/run_loc_top_right
//internal vars
var/succeeded = TRUE
var/list/fail_reasons
/datum/unit_test/New()
run_loc_bottom_left = locate(1, 1, 1)
run_loc_top_right = locate(5, 5, 1)
/datum/unit_test/Destroy()
//clear the test area
for(var/atom/movable/AM in block(run_loc_bottom_left, run_loc_top_right))
qdel(AM)
..()
/datum/unit_test/proc/start()
fail("run() called parent or not implemented")
/datum/unit_test/proc/fail(var/reason = "No reason provided")
succeeded = FALSE
if(!istext(reason))
reason = "FORMATTED: [isnull(reason) ? "NULL" : "reason"]"
if(!fail_reasons)
fail_reasons = list()
fail_reasons.Add(reason)
/proc/run_unit_tests()
CHECK_TICK
for(var/I in subtypesof(/datum/unit_test))
var/datum/unit_test/test = new I
global.current_test = test
var/duration = world.timeofday
test.start()
duration = world.timeofday - duration
global.current_test = null
global.failed_any_test |= !test.succeeded
var/list/log_entry = list("UNIT TEST [test.succeeded ? "PASS" : "FAIL"]: [I] [duration / 10]s")
var/list/fail_reasons = test.fail_reasons
qdel(test)
for(var/J in 1 to length(fail_reasons))
log_entry.Add("\tREASON #[J]: [fail_reasons[J]]")
world.log << log_entry.Join("\n")
CHECK_TICK
del(world)

View File

@@ -140,13 +140,12 @@ var/savefile/panicfile
src.update_status() src.update_status()
sleep_offline = 1 sleep_offline = 0
send2mainirc("Server starting up on [config.server? "byond://[config.server]" : "byond://[world.address]:[world.port]"]") send2mainirc("Server starting up on [config.server? "byond://[config.server]" : "byond://[world.address]:[world.port]"]")
send2maindiscord("**Server starting up** on `[config.server? "byond://[config.server]" : "byond://[world.address]:[world.port]"]`. Map is **[map.nameLong]**") send2maindiscord("**Server starting up** on `[config.server? "byond://[config.server]" : "byond://[world.address]:[world.port]"]`. Map is **[map.nameLong]**")
spawn(10) Master.Setup()
Master.Setup()
process_teleport_locs() //Sets up the wizard teleport locations process_teleport_locs() //Sets up the wizard teleport locations
process_ghost_teleport_locs() //Sets up ghost teleport locations. process_ghost_teleport_locs() //Sets up ghost teleport locations.
@@ -160,7 +159,6 @@ var/savefile/panicfile
KickInactiveClients()*/ KickInactiveClients()*/
#undef RECOMMENDED_VERSION #undef RECOMMENDED_VERSION
return ..() return ..()
//world/Topic(href, href_list[]) //world/Topic(href, href_list[])

View File

@@ -1,43 +1,41 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import argparse
import asyncio import asyncio
import distutils.spawn import distutils.spawn
import re import re
import sys import sys
import os
import travis_utils
MAP_INCLUDE_RE = re.compile(r"#include \"maps\\[a-zA-Z0-9][a-zA-Z0-9_]*\.dm\"") MAP_INCLUDE_RE = re.compile(r"#include \"maps\\[a-zA-Z0-9][a-zA-Z0-9_]*\.dm\"")
ensure_future = None
# ensure_future is new in 3.4.4, previously it was asyncio.async.
try:
ensure_future = asyncio.ensure_future
except AttributeError:
# Can't directly do asyncio.async because async is a keyword now,
# and that'd parse error on newer versions.
ensure_future = getattr(asyncio, "async")
def main(): def main():
parser = argparse.ArgumentParser() dme = os.environ.get("PROJECT_NAME") # The DME file to compile.
parser.add_argument("dme", help="The DME file to compile.") if not dme:
parser.add_argument("-M", "--mapfile", nargs="*", help="Extra map files to replace the regular map file in the DME with.") print("No project name specified.")
args = parser.parse_args() exit(1)
dme += ".dme"
mapfiles = os.environ.get("ALL_MAPS") # Extra map files to replace the regular map file in the DME with.
build_tests = os.environ.get("DM_UNIT_TESTS") == "1" # Whether to build unit tests or not.
dme = args.dme if build_tests is True and mapfiles is not None:
if args.mapfile is not None: print("Cannot run tests AND change maps at the same time, overriding ALL_MAPS.") # Because BYOND will cry "corrupt map data in world file"
# Handle map file replacement. mapfiles = "test_tiny"
with open(dme, "r") as f:
if build_tests is True:
with open(dme, "r+") as f:
content = f.read() content = f.read()
f.seek(0, 0)
f.write("#define UNIT_TESTS\n" + content)
# Make string to replace the map include with. if mapfiles is not None:
includes = "" with open(dme, "r+") as f:
for arg in args.mapfile: content = f.read()
includes += "#include \"maps\\\\{}.dm\"\n".format(arg) includes = ""
for arg in mapfiles.split():
content = MAP_INCLUDE_RE.sub(includes, content, count=1) includes += "#include \"maps\\\\{}.dm\"\n".format(arg)
dme = "{}.mdme".format(dme) content = MAP_INCLUDE_RE.sub(includes, content, count=1)
with open(dme, "w") as f: f.seek(0, 0)
f.write(content) f.write(content)
compiler = "DreamMaker" compiler = "DreamMaker"
@@ -49,28 +47,10 @@ def main():
print("Unable to find DM compiler.") print("Unable to find DM compiler.")
exit(1) exit(1)
loop = asyncio.get_event_loop() loop = travis_utils.get_platform_event_loop()
code = loop.run_until_complete(run_compiler([compiler, dme])) code = loop.run_until_complete(travis_utils.run_with_timeout_guards([compiler, dme]))
exit(code) exit(code)
# DM SOMEHOW manages to go 10 minutes without logging anything nowadays.
# So... Travis kills it.
# Thanks DM.
# This repeats messages like travis_wait (which I couldn't get working) does to prevent that.
@asyncio.coroutine
def run_compiler(args):
compiler_process = yield from asyncio.create_subprocess_exec(*args)
task = ensure_future(print_timeout_guards())
ret = yield from compiler_process.wait()
task.cancel()
return ret
@asyncio.coroutine
def print_timeout_guards():
while True:
yield from asyncio.sleep(8*60)
print("Keeping Travis alive. Ignore this!")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -0,0 +1,17 @@
ADMIN_LEGACY_SYSTEM
BAN_LEGACY_SYSTEM
PROBABILITY EXTENDED 5
ALLOW_HOLIDAYS
TICKLAG 0.33
TICKCOMP 0
SKIP_MINIMAP_GENERATION
SKIP_VAULT_GENERATION
SHUT_UP_AUTOMATIC_DIAGNOSTIC_AND_ANNOUNCEMENT_SYSTEM
ENABLE_WAGES

30
tools/travis/run_tests.py Executable file
View File

@@ -0,0 +1,30 @@
#!/usr/bin/env python3
import os
import distutils.spawn
import asyncio
import travis_utils
def main():
if os.environ.get("DM_UNIT_TESTS") != "1":
print("DM_UNIT_TESTS is not set, not running tests.")
return
dmb = os.environ.get("PROJECT_NAME") # The DMB file to run.
if not dmb:
print("No project name specified.")
exit(1)
dmb += ".dmb"
executable = "DreamDaemon"
dreamdaemon = distutils.spawn.find_executable(executable)
if not dreamdaemon:
print("Unable to find {}.".format(executable))
exit(1)
loop = travis_utils.get_platform_event_loop()
code = loop.run_until_complete(travis_utils.run_with_timeout_guards([dreamdaemon, dmb, "-close", "-trusted"]))
exit(code)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,40 @@
import asyncio
import sys
ensure_future = None
# ensure_future is new in 3.4.4, previously it was asyncio.async.
try:
ensure_future = asyncio.ensure_future
except AttributeError:
# Can't directly do asyncio.async because async is a keyword now,
# and that'd parse error on newer versions.
ensure_future = getattr(asyncio, "async")
# DM SOMEHOW manages to go 10 minutes without logging anything nowadays.
# So... Travis kills it.
# Thanks DM.
# This repeats messages like travis_wait (which I couldn't get working) does to prevent that.
@asyncio.coroutine
def run_with_timeout_guards(args):
target_process = yield from asyncio.create_subprocess_exec(*args, stderr=asyncio.subprocess.STDOUT)
task = ensure_future(print_timeout_guards())
ret = yield from target_process.wait()
task.cancel()
return ret
@asyncio.coroutine
def print_timeout_guards():
while True:
yield from asyncio.sleep(8*60)
print("Keeping Travis alive. Ignore this!")
# Windows needs a different event loop to manage subprocesses
def get_platform_event_loop():
if sys.platform == "win32" or sys.platform == "cygwin":
loop = asyncio.ProactorEventLoop()
asyncio.set_event_loop(loop)
return loop
else:
return asyncio.get_event_loop()

View File

@@ -2221,6 +2221,7 @@
#include "code\modules\telesci\telepad.dm" #include "code\modules\telesci\telepad.dm"
#include "code\modules\telesci\telesci_computer.dm" #include "code\modules\telesci\telesci_computer.dm"
#include "code\modules\tooltip\tooltip.dm" #include "code\modules\tooltip\tooltip.dm"
#include "code\modules\unit_tests\_unit_tests.dm"
#include "code\modules\virus2\analyser.dm" #include "code\modules\virus2\analyser.dm"
#include "code\modules\virus2\antibodies.dm" #include "code\modules\virus2\antibodies.dm"
#include "code\modules\virus2\centrifuge.dm" #include "code\modules\virus2\centrifuge.dm"