diff --git a/code/__HELPERS/_logging.dm b/code/__HELPERS/_logging.dm index 8b7cdc0c2e6..18dd85fa5bb 100644 --- a/code/__HELPERS/_logging.dm +++ b/code/__HELPERS/_logging.dm @@ -24,6 +24,12 @@ #define testing(msg) #endif +#ifdef UNIT_TESTS +/proc/log_test(text) + WRITE_FILE(GLOB.test_log, "\[[time_stamp()]]: [text]") + SEND_TEXT(world.log, text) +#endif + /proc/log_admin(text) GLOB.admin_log.Add(text) if (CONFIG_GET(flag/log_admin)) diff --git a/code/_compile_options.dm b/code/_compile_options.dm index ac4eedc7e02..c47c0699d27 100644 --- a/code/_compile_options.dm +++ b/code/_compile_options.dm @@ -13,6 +13,8 @@ //#define VISUALIZE_ACTIVE_TURFS //Highlights atmos active turfs in green #endif +//#define UNIT_TESTS //Enables unit tests via TEST_RUN_PARAMETER + #ifndef PRELOAD_RSC //set to: #define PRELOAD_RSC 0 // 0 to allow using external resources or on-demand behaviour; #endif // 1 to use the default behaviour; @@ -39,6 +41,10 @@ #define FIND_REF_NO_CHECK_TICK #endif +#ifdef TRAVISBUILDING +#define UNIT_TESTS +#endif + #ifdef TRAVISTESTING #define TESTING #endif diff --git a/code/game/world.dm b/code/game/world.dm index a4249d6b45f..227b50493a9 100644 --- a/code/game/world.dm +++ b/code/game/world.dm @@ -52,7 +52,11 @@ GLOBAL_PROTECT(security_mode) Master.sleep_offline_after_initializations = FALSE SSticker.start_immediately = TRUE CONFIG_SET(number/round_end_countdown, 0) +#ifdef UNIT_TESTS + SSticker.OnRoundstart(CALLBACK(GLOBAL_PROC, /proc/RunUnitTests)) +#else SSticker.force_ending = TRUE +#endif /world/proc/SetupExternalRSC() #if (PRELOAD_RSC == 0) @@ -83,6 +87,10 @@ GLOBAL_PROTECT(security_mode) GLOB.world_pda_log = file("[GLOB.log_directory]/pda.log") GLOB.sql_error_log = file("[GLOB.log_directory]/sql.log") GLOB.manifest_log = file("[GLOB.log_directory]/manifest.log") +#ifdef UNIT_TESTS + GLOB.test_log = file("[GLOB.log_directory]/tests.log") + WRITE_FILE(GLOB.test_log, "\n\nStarting up round ID [GLOB.round_id]. [time_stamp()]\n---------------------") +#endif WRITE_FILE(GLOB.world_game_log, "\n\nStarting up round ID [GLOB.round_id]. [time_stamp()]\n---------------------") WRITE_FILE(GLOB.world_attack_log, "\n\nStarting up round ID [GLOB.round_id]. [time_stamp()]\n---------------------") WRITE_FILE(GLOB.world_runtime_log, "\n\nStarting up round ID [GLOB.round_id]. [time_stamp()]\n---------------------") @@ -152,6 +160,10 @@ GLOBAL_PROTECT(security_mode) if(GLOB) if(GLOB.total_runtimes != 0) fail_reasons = list("Total runtimes: [GLOB.total_runtimes]") +#ifdef UNIT_TESTS + if(GLOB.failed_any_test) + LAZYADD(fail_reasons, "Unit Tests failed!") +#endif if(!GLOB.log_directory) LAZYADD(fail_reasons, "Missing GLOB.log_directory!") else diff --git a/code/modules/error_handler/error_handler.dm b/code/modules/error_handler/error_handler.dm index 3febc519390..a9988102ba6 100644 --- a/code/modules/error_handler/error_handler.dm +++ b/code/modules/error_handler/error_handler.dm @@ -121,6 +121,12 @@ GLOBAL_VAR_INIT(total_runtimes_skipped, 0) for(var/line in desclines) SEND_TEXT(world.log, line) +#ifdef UNIT_TESTS + if(GLOB.current_test) + //good day, sir + GLOB.current_test.Fail("[main_line]\n[desclines.Join("\n")]") +#endif + /* This logs the runtime in the old format */ E.name = "\n\[[time2text(world.timeofday,"hh:mm:ss")]\][E.name]" diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm new file mode 100644 index 00000000000..ad2ba4048cb --- /dev/null +++ b/code/modules/unit_tests/_unit_tests.dm @@ -0,0 +1,5 @@ +//include unit test files in this module in this ifdef + +#ifdef UNIT_TESTS +#include "unit_test.dm" +#endif diff --git a/code/modules/unit_tests/unit_test.dm b/code/modules/unit_tests/unit_test.dm new file mode 100644 index 00000000000..6d05c340b30 --- /dev/null +++ b/code/modules/unit_tests/unit_test.dm @@ -0,0 +1,77 @@ +/* + +Usage: +Override /Run() to run your test code + +Call Fail() to fail the test (You should specify a reason) + +You may use /New() and /Destroy() for setup/teardown respectively + +You can use the run_loc_bottom_left and run_loc_top_right to get turfs for testing + +*/ + +GLOBAL_DATUM(current_test, /datum/unit_test) +GLOBAL_VAR_INIT(failed_any_test, FALSE) +GLOBAL_VAR(test_log) + +/datum/unit_test + //Bit of metadata for the future maybe + var/list/procs_tested + + //usable vars + var/turf/run_loc_bottom_left + var/turf/run_loc_top_right + + //internal shit + 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) + return ..() + +/datum/unit_test/proc/Run() + Fail("Run() called parent or not implemented") + +/datum/unit_test/proc/Fail(reason = "No reason") + succeeded = FALSE + + if(!istext(reason)) + reason = "FORMATTED: [reason != null ? reason : "NULL"]" + + LAZYADD(fail_reasons, reason) + +/proc/RunUnitTests() + CHECK_TICK + + for(var/I in subtypesof(/datum/unit_test)) + var/datum/unit_test/test = new I + + GLOB.current_test = test + var/duration = REALTIMEOFDAY + + test.Run() + + duration = REALTIMEOFDAY - duration + GLOB.current_test = null + GLOB.failed_any_test |= !test.succeeded + + var/list/log_entry = list("[test.succeeded ? "PASS" : "FAIL"]: [I] [duration / 10]s") + var/list/fail_reasons = test.fail_reasons + + qdel(test) + + for(var/J in 1 to LAZYLEN(fail_reasons)) + log_entry += "\tREASON #[J]: [fail_reasons[J]]" + log_test(log_entry.Join("\n")) + + CHECK_TICK + + SSticker.force_ending = TRUE diff --git a/tgstation.dme b/tgstation.dme index 1f4a189e0ea..c444407d624 100755 --- a/tgstation.dme +++ b/tgstation.dme @@ -2378,6 +2378,7 @@ #include "code\modules\tgui\states\self.dm" #include "code\modules\tgui\states\zlevel.dm" #include "code\modules\tooltip\tooltip.dm" +#include "code\modules\unit_tests\_unit_tests.dm" #include "code\modules\uplink\uplink.dm" #include "code\modules\uplink\uplink_devices.dm" #include "code\modules\uplink\uplink_items.dm"