mirror of
https://github.com/vgstation-coders/vgstation13.git
synced 2025-12-10 02:16:05 +00:00
@@ -9,8 +9,12 @@ env:
|
||||
- BYOND_MAJOR="512"
|
||||
- BYOND_MINOR="1413"
|
||||
- ALL_MAPS="tgstation metaclub defficiency packedstation test_box test_tiny"
|
||||
- PROJECT_NAME="vgstation13"
|
||||
- RUST_BACKTRACE="1"
|
||||
- RUST_TEST_THREADS=1
|
||||
matrix:
|
||||
- DM_UNIT_TESTS="1"
|
||||
- DM_UNIT_TESTS="0"
|
||||
|
||||
cache:
|
||||
directories:
|
||||
@@ -42,7 +46,9 @@ script:
|
||||
# --jobs 1 to prevent threading problems with the BYOND crate.
|
||||
- cargo test --jobs 1 --verbose
|
||||
- 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:
|
||||
irc:
|
||||
|
||||
@@ -121,6 +121,8 @@ var/CURRENT_TICKLIMIT = TICK_LIMIT_RUNNING
|
||||
// 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()
|
||||
/datum/controller/master/proc/Setup()
|
||||
set waitfor = FALSE
|
||||
sleep(1 SECONDS)
|
||||
to_chat(world, "<span class='boldannounce'>Initializing subsystems...</span>")
|
||||
|
||||
// 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.
|
||||
sortTim(subsystems, /proc/cmp_subsystem_display)
|
||||
// Set world options.
|
||||
world.sleep_offline = 1
|
||||
world.tick_lag = config.Ticklag
|
||||
sleep(1)
|
||||
// Loop.
|
||||
|
||||
@@ -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]"]`")
|
||||
|
||||
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
|
||||
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.")
|
||||
@@ -184,6 +188,10 @@ var/datum/controller/gameticker/ticker
|
||||
//here to initialize the random events nicely at round start
|
||||
setup_economy()
|
||||
|
||||
#ifdef UNIT_TESTS
|
||||
run_unit_tests()
|
||||
#endif
|
||||
|
||||
spawn(0)//Forking here so we dont have to wait for this to finish
|
||||
mode.post_setup()
|
||||
//Cleanup some stuff
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
desclines += " (This error will now be silenced for [configured_error_silence_time / 600] minutes)"
|
||||
|
||||
// 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)
|
||||
world.log << line
|
||||
@@ -109,4 +110,9 @@
|
||||
if (global.error_cache)
|
||||
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
|
||||
|
||||
4
code/modules/unit_tests/_unit_tests.dm
Normal file
4
code/modules/unit_tests/_unit_tests.dm
Normal file
@@ -0,0 +1,4 @@
|
||||
#ifdef UNIT_TESTS
|
||||
#include "unit_test.dm"
|
||||
#include "reagent_recipe_collisions.dm"
|
||||
#endif
|
||||
67
code/modules/unit_tests/reagent_recipe_collisions.dm
Normal file
67
code/modules/unit_tests/reagent_recipe_collisions.dm
Normal 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
|
||||
72
code/modules/unit_tests/unit_test.dm
Normal file
72
code/modules/unit_tests/unit_test.dm
Normal 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)
|
||||
@@ -140,13 +140,12 @@ var/savefile/panicfile
|
||||
|
||||
src.update_status()
|
||||
|
||||
sleep_offline = 1
|
||||
sleep_offline = 0
|
||||
|
||||
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]**")
|
||||
|
||||
spawn(10)
|
||||
Master.Setup()
|
||||
Master.Setup()
|
||||
|
||||
process_teleport_locs() //Sets up the wizard teleport locations
|
||||
process_ghost_teleport_locs() //Sets up ghost teleport locations.
|
||||
@@ -160,7 +159,6 @@ var/savefile/panicfile
|
||||
KickInactiveClients()*/
|
||||
|
||||
#undef RECOMMENDED_VERSION
|
||||
|
||||
return ..()
|
||||
|
||||
//world/Topic(href, href_list[])
|
||||
|
||||
@@ -1,43 +1,41 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import asyncio
|
||||
import distutils.spawn
|
||||
import re
|
||||
import sys
|
||||
import os
|
||||
|
||||
import travis_utils
|
||||
|
||||
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():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("dme", help="The DME file to compile.")
|
||||
parser.add_argument("-M", "--mapfile", nargs="*", help="Extra map files to replace the regular map file in the DME with.")
|
||||
args = parser.parse_args()
|
||||
dme = os.environ.get("PROJECT_NAME") # The DME file to compile.
|
||||
if not dme:
|
||||
print("No project name specified.")
|
||||
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 args.mapfile is not None:
|
||||
# Handle map file replacement.
|
||||
with open(dme, "r") as f:
|
||||
if build_tests is True and mapfiles 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"
|
||||
mapfiles = "test_tiny"
|
||||
|
||||
if build_tests is True:
|
||||
with open(dme, "r+") as f:
|
||||
content = f.read()
|
||||
f.seek(0, 0)
|
||||
f.write("#define UNIT_TESTS\n" + content)
|
||||
|
||||
# Make string to replace the map include with.
|
||||
includes = ""
|
||||
for arg in args.mapfile:
|
||||
includes += "#include \"maps\\\\{}.dm\"\n".format(arg)
|
||||
|
||||
content = MAP_INCLUDE_RE.sub(includes, content, count=1)
|
||||
dme = "{}.mdme".format(dme)
|
||||
with open(dme, "w") as f:
|
||||
if mapfiles is not None:
|
||||
with open(dme, "r+") as f:
|
||||
content = f.read()
|
||||
includes = ""
|
||||
for arg in mapfiles.split():
|
||||
includes += "#include \"maps\\\\{}.dm\"\n".format(arg)
|
||||
content = MAP_INCLUDE_RE.sub(includes, content, count=1)
|
||||
f.seek(0, 0)
|
||||
f.write(content)
|
||||
|
||||
compiler = "DreamMaker"
|
||||
@@ -49,28 +47,10 @@ def main():
|
||||
print("Unable to find DM compiler.")
|
||||
exit(1)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
code = loop.run_until_complete(run_compiler([compiler, dme]))
|
||||
loop = travis_utils.get_platform_event_loop()
|
||||
code = loop.run_until_complete(travis_utils.run_with_timeout_guards([compiler, dme]))
|
||||
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__":
|
||||
main()
|
||||
|
||||
17
tools/travis/config/config.txt
Normal file
17
tools/travis/config/config.txt
Normal 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
30
tools/travis/run_tests.py
Executable 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()
|
||||
40
tools/travis/travis_utils.py
Normal file
40
tools/travis/travis_utils.py
Normal 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()
|
||||
@@ -2221,6 +2221,7 @@
|
||||
#include "code\modules\telesci\telepad.dm"
|
||||
#include "code\modules\telesci\telesci_computer.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\antibodies.dm"
|
||||
#include "code\modules\virus2\centrifuge.dm"
|
||||
|
||||
Reference in New Issue
Block a user