diff --git a/code/datums/datum.dm b/code/datums/datum.dm index 5f9bf37040..fadc565202 100644 --- a/code/datums/datum.dm +++ b/code/datums/datum.dm @@ -110,6 +110,12 @@ continue qdel(timer) + #ifdef REFERENCE_TRACKING + #ifdef REFERENCE_TRACKING_DEBUG + found_refs = null + #endif + #endif + //BEGIN: ECS SHIT signal_enabled = FALSE diff --git a/code/modules/error_handler/error_handler.dm b/code/modules/error_handler/error_handler.dm index 6a3d2c2233..4c6c105eb7 100644 --- a/code/modules/error_handler/error_handler.dm +++ b/code/modules/error_handler/error_handler.dm @@ -128,7 +128,7 @@ GLOBAL_VAR_INIT(total_runtimes_skipped, 0) #ifdef UNIT_TESTS if(GLOB.current_test) //good day, sir - GLOB.current_test.Fail("[main_line]\n[desclines.Join("\n")]") + GLOB.current_test.Fail("[main_line]\n[desclines.Join("\n")]", file = E.file, line = E.line) #endif diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index df5d6703cf..5166673a1c 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -3,9 +3,18 @@ #if defined(UNIT_TESTS) || defined(SPACEMAN_DMM) +/// For advanced cases, fail unconditionally but don't return (so a test can return multiple results) +#define TEST_FAIL(reason) (Fail(reason || "No reason", __FILE__, __LINE__)) + /// Asserts that a condition is true /// If the condition is not true, fails the test -#define TEST_ASSERT(assertion, reason) if (!(assertion)) { return Fail("Assertion failed: [reason || "No reason"]") } +#define TEST_ASSERT(assertion, reason) if (!(assertion)) { return Fail("Assertion failed: [reason || "No reason"]", __FILE__, __LINE__) } + +/// Asserts that a parameter is not null +#define TEST_ASSERT_NOTNULL(a, reason) if (isnull(a)) { return Fail("Expected non-null value: [reason || "No reason"]", __FILE__, __LINE__) } + +/// Asserts that a parameter is null +#define TEST_ASSERT_NULL(a, reason) if (!isnull(a)) { return Fail("Expected null value but received [a]: [reason || "No reason"]", __FILE__, __LINE__) } /// Asserts that the two parameters passed are equal, fails otherwise /// Optionally allows an additional message in the case of a failure @@ -13,7 +22,7 @@ var/lhs = ##a; \ var/rhs = ##b; \ if (lhs != rhs) { \ - return Fail("Expected [isnull(lhs) ? "null" : lhs] to be equal to [isnull(rhs) ? "null" : rhs].[message ? " [message]" : ""]"); \ + return Fail("Expected [isnull(lhs) ? "null" : lhs] to be equal to [isnull(rhs) ? "null" : rhs].[message ? " [message]" : ""]", __FILE__, __LINE__); \ } \ } while (FALSE) @@ -23,7 +32,7 @@ var/lhs = ##a; \ var/rhs = ##b; \ if (lhs == rhs) { \ - return Fail("Expected [isnull(lhs) ? "null" : lhs] to not be equal to [isnull(rhs) ? "null" : rhs].[message ? " [message]" : ""]"); \ + return Fail("Expected [isnull(lhs) ? "null" : lhs] to not be equal to [isnull(rhs) ? "null" : rhs].[message ? " [message]" : ""]", __FILE__, __LINE__); \ } \ } while (FALSE) @@ -37,8 +46,25 @@ #define UNIT_TEST_FAILED 1 #define UNIT_TEST_SKIPPED 2 +#define TEST_PRE 0 #define TEST_DEFAULT 1 -#define TEST_DEL_WORLD INFINITY +/// After most test steps, used for tests that run long so shorter issues can be noticed faster +#define TEST_LONGER 10 +/// This must be the last test to run due to the inherent nature of the test iterating every single tangible atom in the game and qdeleting all of them (while taking long sleeps to make sure the garbage collector fires properly) taking a large amount of time. +#define TEST_CREATE_AND_DESTROY INFINITY + +/// Change color to red on ANSI terminal output, if enabled with -DANSICOLORS. +#ifdef ANSICOLORS +#define TEST_OUTPUT_RED(text) "\x1B\x5B1;31m[text]\x1B\x5B0m" +#else +#define TEST_OUTPUT_RED(text) (text) +#endif +/// Change color to green on ANSI terminal output, if enabled with -DANSICOLORS. +#ifdef ANSICOLORS +#define TEST_OUTPUT_GREEN(text) "\x1B\x5B1;32m[text]\x1B\x5B0m" +#else +#define TEST_OUTPUT_GREEN(text) (text) +#endif /// A trait source when adding traits through unit tests #define TRAIT_SOURCE_UNIT_TESTS "unit_tests" @@ -55,7 +81,7 @@ // #include "connect_loc.dm" // #include "confusion.dm" // #include "crayons.dm" -// #include "create_and_destroy.dm" +#include "create_and_destroy.dm" // #include "designs.dm" #include "dynamic_ruleset_sanity.dm" // #include "egg_glands.dm" @@ -105,12 +131,12 @@ /// SANDSTORM TESTS #include "interactions.dm" //No regrets -#ifdef REFERENCE_TRACKING //Don't try and parse this file if ref tracking isn't turned on. IE: don't parse ref tracking please mr linter +#ifdef REFERENCE_TRACKING_DEBUG //Don't try and parse this file if ref tracking isn't turned on. IE: don't parse ref tracking please mr linter #include "find_reference_sanity.dm" #endif #undef TEST_ASSERT #undef TEST_ASSERT_EQUAL #undef TEST_ASSERT_NOTEQUAL -#undef TEST_FOCUS +//#undef TEST_FOCUS - This define is used by vscode unit test extension to pick specific unit tests to run and appended later so needs to be used out of scope here #endif diff --git a/code/modules/unit_tests/anchored_mobs.dm b/code/modules/unit_tests/anchored_mobs.dm index 103b97e7a9..88487ea2b8 100644 --- a/code/modules/unit_tests/anchored_mobs.dm +++ b/code/modules/unit_tests/anchored_mobs.dm @@ -4,6 +4,4 @@ var/mob/M = i if(initial(M.anchored)) L += "[i]" - if(!L.len) - return //passed! - Fail("The following mobs are defined as anchored. This is incompatible with the new move force/resist system and needs to be revised.: [L.Join(" ")]") + TEST_ASSERT(!L.len, "The following mobs are defined as anchored. This is incompatible with the new move force/resist system and needs to be revised.: [L.Join(" ")]") diff --git a/code/modules/unit_tests/bespoke_id.dm b/code/modules/unit_tests/bespoke_id.dm index 06676c626c..e1356650de 100644 --- a/code/modules/unit_tests/bespoke_id.dm +++ b/code/modules/unit_tests/bespoke_id.dm @@ -5,4 +5,4 @@ for(var/i in subtypesof(/datum/element)) var/datum/element/faketype = i if((initial(faketype.element_flags) & ELEMENT_BESPOKE) && initial(faketype.id_arg_index) == base_index) - Fail("A bespoke element was not configured with a proper id_arg_index: [faketype]") + TEST_FAIL("A bespoke element was not configured with a proper id_arg_index: [faketype]") diff --git a/code/modules/unit_tests/card_mismatch.dm b/code/modules/unit_tests/card_mismatch.dm index 506e88f19c..90d8250ff0 100644 --- a/code/modules/unit_tests/card_mismatch.dm +++ b/code/modules/unit_tests/card_mismatch.dm @@ -1,7 +1,6 @@ /datum/unit_test/card_mismatch /datum/unit_test/card_mismatch/Run() - var/message = checkCardpacks(SStrading_card_game.card_packs) - message += checkCardDatums() - if(message) - Fail(message) + var/message = SStrading_card_game.check_cardpacks(SStrading_card_game.card_packs) + message += SStrading_card_game.check_card_datums() + TEST_ASSERT(!message, message) diff --git a/code/modules/unit_tests/chain_pull_through_space.dm b/code/modules/unit_tests/chain_pull_through_space.dm index 10363d5aad..b59de2a467 100644 --- a/code/modules/unit_tests/chain_pull_through_space.dm +++ b/code/modules/unit_tests/chain_pull_through_space.dm @@ -41,22 +41,15 @@ // Walk normally to the left, make sure we're still a chain alice.Move(locate(run_loc_floor_bottom_left.x + 1, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z)) - if (bob.x != run_loc_floor_bottom_left.x + 2) - return Fail("During normal move, Bob was not at the correct x ([bob.x])") - if (charlie.x != run_loc_floor_bottom_left.x + 3) - return Fail("During normal move, Charlie was not at the correct x ([charlie.x])") + TEST_ASSERT_EQUAL(bob.x, run_loc_floor_bottom_left.x + 2, "During normal move, Bob was not at the correct x ([bob.x])") + TEST_ASSERT_EQUAL(charlie.x, run_loc_floor_bottom_left.x + 3, "During normal move, Charlie was not at the correct x ([charlie.x])") // We're going through the space turf now that should teleport us alice.Move(run_loc_floor_bottom_left) - if (alice.z != space_tile.destination_z) - return Fail("Alice did not teleport to the destination z-level. Current location: ([alice.x], [alice.y], [alice.z])") + TEST_ASSERT_EQUAL(alice.z, space_tile.destination_z, "Alice did not teleport to the destination z-level. Current location: ([alice.x], [alice.y], [alice.z])") - if (bob.z != space_tile.destination_z) - return Fail("Bob did not teleport to the destination z-level. Current location: ([bob.x], [bob.y], [bob.z])") - if (!bob.Adjacent(alice)) - return Fail("Bob is not adjacent to Alice. Bob is at [bob.x], Alice is at [alice.x]") + TEST_ASSERT_EQUAL(bob.z, space_tile.destination_z, "Bob did not teleport to the destination z-level. Current location: ([bob.x], [bob.y], [bob.z])") + TEST_ASSERT(bob.Adjacent(alice), "Bob is not adjacent to Alice. Bob is at [bob.x], Alice is at [alice.x]") - if (charlie.z != space_tile.destination_z) - return Fail("Charlie did not teleport to the destination z-level. Current location: ([charlie.x], [charlie.y], [charlie.z])") - if (!charlie.Adjacent(bob)) - return Fail("Charlie is not adjacent to Bob. Charlie is at [charlie.x], Bob is at [bob.x]") + TEST_ASSERT_EQUAL(charlie.z, space_tile.destination_z, "Charlie did not teleport to the destination z-level. Current location: ([charlie.x], [charlie.y], [charlie.z])") + TEST_ASSERT(charlie.Adjacent(bob), "Charlie is not adjacent to Bob. Charlie is at [charlie.x], Bob is at [bob.x]") diff --git a/code/modules/unit_tests/character_saving.dm b/code/modules/unit_tests/character_saving.dm index cca17b81e4..f7ca8b738b 100644 --- a/code/modules/unit_tests/character_saving.dm +++ b/code/modules/unit_tests/character_saving.dm @@ -12,17 +12,17 @@ P.save_character() P.load_character() if(P.features["flavor_text"] != UNIT_TEST_SAVING_FLAVOR_TEXT) - Fail("Flavor text is failing to save.") + TEST_FAIL("Flavor text is failing to save.") if(P.features["silicon_flavor_text"] != UNIT_TEST_SAVING_SILICON_FLAVOR_TEXT) - Fail("Silicon flavor text is failing to save.") + TEST_FAIL("Silicon flavor text is failing to save.") if(P.features["ooc_notes"] != UNIT_TEST_SAVING_OOC_NOTES) - Fail("OOC text is failing to save.") + TEST_FAIL("OOC text is failing to save.") P.save_character() P.load_character() if((P.features["flavor_text"] != UNIT_TEST_SAVING_FLAVOR_TEXT) || (P.features["silicon_flavor_text"] != UNIT_TEST_SAVING_SILICON_FLAVOR_TEXT) || (P.features["ooc_notes"] != UNIT_TEST_SAVING_OOC_NOTES)) - Fail("Repeated saving and loading possibly causing save deletion.") + TEST_FAIL("Repeated saving and loading possibly causing save deletion.") catch(var/exception/e) - Fail("Failed to save and load character due to exception [e.file]:[e.line], [e.name]") + TEST_FAIL("Failed to save and load character due to exception [e.file]:[e.line], [e.name]") #undef UNIT_TEST_SAVING_FLAVOR_TEXT #undef UNIT_TEST_SAVING_SILICON_FLAVOR_TEXT diff --git a/code/modules/unit_tests/component_tests.dm b/code/modules/unit_tests/component_tests.dm index 0099d7508c..f609e73c4b 100644 --- a/code/modules/unit_tests/component_tests.dm +++ b/code/modules/unit_tests/component_tests.dm @@ -8,5 +8,5 @@ var/dupe_type = initial(comp.dupe_type) if(dupe_type && !ispath(dupe_type)) bad_dts += t - if(length(bad_dms) || length(bad_dts)) - Fail("Components with invalid dupe modes: ([bad_dms.Join(",")]) ||| Components with invalid dupe types: ([bad_dts.Join(",")])") + TEST_ASSERT(!length(bad_dms) && !length(bad_dts), + "Components with invalid dupe modes: ([bad_dms.Join(",")]) ||| Components with invalid dupe types: ([bad_dts.Join(",")])") diff --git a/code/modules/unit_tests/crafting_recipes.dm b/code/modules/unit_tests/crafting_recipes.dm index 2d8c273786..9b437c801c 100644 --- a/code/modules/unit_tests/crafting_recipes.dm +++ b/code/modules/unit_tests/crafting_recipes.dm @@ -2,6 +2,6 @@ for(var/i in GLOB.crafting_recipes) var/datum/crafting_recipe/R = i if(!R.subcategory) - Fail("Invalid subcategory on [R] ([R.type]).") + TEST_FAIL("Invalid subcategory on [R] ([R.type]).") if(!R.category && (R.category != CAT_NONE)) - Fail("Invalid category on [R] ([R.type])") + TEST_FAIL("Invalid category on [R] ([R.type])") diff --git a/code/modules/unit_tests/create_and_destroy.dm b/code/modules/unit_tests/create_and_destroy.dm new file mode 100644 index 0000000000..37f1476f5f --- /dev/null +++ b/code/modules/unit_tests/create_and_destroy.dm @@ -0,0 +1,197 @@ +///Delete one of every type, sleep a while, then check to see if anything has gone fucky +/datum/unit_test/create_and_destroy + //You absolutely must run last + priority = TEST_CREATE_AND_DESTROY + +GLOBAL_VAR_INIT(running_create_and_destroy, FALSE) +/datum/unit_test/create_and_destroy/Run() + //We'll spawn everything here + var/turf/spawn_at = run_loc_floor_bottom_left + var/list/ignore = list( + //Never meant to be created, errors out the ass for mobcode reasons + /mob/living/carbon, + //Nother template type, doesn't like being created with no seed + // /obj/item/food/grown, + //And another + /obj/item/slimecross/recurring, + //This should be obvious + /obj/machinery/doomsday_device, + //Yet more templates + // /obj/machinery/restaurant_portal, + //Template type + /obj/effect/mob_spawn, + //Template type + // /obj/structure/holosign/robot_seat, + //Singleton + /mob/dview, + //Template type + /obj/item/bodypart, + //This is meant to fail extremely loud every single time it occurs in any environment in any context, and it falsely alarms when this unit test iterates it. Let's not spawn it in. + // /obj/merge_conflict_marker, + //briefcase launchpads erroring + /obj/machinery/launchpad/briefcase, + ) + //Say it with me now, type template + ignore += typesof(/obj/effect/mapping_helpers) + //This turf existing is an error in and of itself + ignore += typesof(/turf/baseturf_skipover) + ignore += typesof(/turf/baseturf_bottom) + //This demands a borg, so we'll let if off easy + // ignore += typesof(/obj/item/modular_computer/pda/silicon) + //This one demands a computer, ditto + ignore += typesof(/obj/item/modular_computer/processor) + //Very finiky, blacklisting to make things easier + ignore += typesof(/obj/item/poster/wanted) + //This expects a seed, we can't pass it + // ignore += typesof(/obj/item/food/grown) + //Needs clients / mobs to observe it to exist. Also includes hallucinations. + // ignore += typesof(/obj/effect/client_image_holder) + //Same to above. Needs a client / mob / hallucination to observe it to exist. + // ignore += typesof(/obj/projectile/hallucination) + // ignore += typesof(/obj/item/hallucinated) + //Can't pass in a thing to glow + ignore += typesof(/obj/effect/abstract/eye_lighting) + //We don't have a pod + ignore += typesof(/obj/effect/pod_landingzone_effect) + ignore += typesof(/obj/effect/pod_landingzone) + //We have a baseturf limit of 10, adding more than 10 baseturf helpers will kill CI, so here's a future edge case to fix. + ignore += typesof(/obj/effect/baseturf_helper) + //No tauma to pass in + ignore += typesof(/mob/camera/imaginary_friend) + //No pod to gondola + ignore += typesof(/mob/living/simple_animal/pet/gondola/gondolapod) + //No heart to give + // ignore += typesof(/obj/structure/ethereal_crystal) + //No linked console + // ignore += typesof(/mob/camera/ai_eye/remote/base_construction) + //See above + // ignore += typesof(/mob/camera/ai_eye/remote/shuttle_docker) + //Hangs a ref post invoke async, which we don't support. Could put a qdeleted check but it feels hacky + ignore += typesof(/obj/effect/anomaly/grav/high) + //See above + ignore += typesof(/obj/effect/timestop) + //Invoke async in init, skippppp + // ignore += typesof(/mob/living/silicon/robot/model) + //This lad also sleeps + ignore += typesof(/obj/item/hilbertshotel) + //this boi spawns turf changing stuff, and it stacks and causes pain. Let's just not + ignore += typesof(/obj/effect/sliding_puzzle) + //Stacks baseturfs, can't be tested here + ignore += typesof(/obj/effect/temp_visual/lava_warning) + //Stacks baseturfs, can't be tested here + // ignore += typesof(/obj/effect/landmark/ctf) + //Our system doesn't support it without warning spam from unregister calls on things that never registered + ignore += typesof(/obj/docking_port) + //Asks for a shuttle that may not exist, let's leave it alone + ignore += typesof(/obj/item/pinpointer/shuttle) + //This spawns beams as a part of init, which can sleep past an async proc. This hangs a ref, and fucks us. It's only a problem here because the beam sleeps with CHECK_TICK + // ignore += typesof(/obj/structure/alien/resin/flower_bud) + //Needs a linked mecha + ignore += typesof(/obj/effect/skyfall_landingzone) + //Expects a mob to holderize, we have nothing to give + ignore += typesof(/obj/item/clothing/head/mob_holder) + //Needs cards passed into the initilazation args + ignore += typesof(/obj/item/toy/cards/cardhand) + //Needs cards passed into the initilazation args + ignore += typesof(/obj/item/toy/cards/cardhand) + //Needs a holodeck area linked to it which is not guarenteed to exist and technically is supposed to have a 1:1 relationship with computer anyway. + ignore += typesof(/obj/machinery/computer/holodeck) + //runtimes if not paired with a landmark + // ignore += typesof(/obj/structure/industrial_lift) + // Runtimes if the associated machinery does not exist, but not the base type + // ignore += subtypesof(/obj/machinery/airlock_controller) + + var/list/cached_contents = spawn_at.contents.Copy() + var/original_turf_type = spawn_at.type + var/original_baseturfs = islist(spawn_at.baseturfs) ? spawn_at.baseturfs.Copy() : spawn_at.baseturfs + var/original_baseturf_count = length(original_baseturfs) + + GLOB.running_create_and_destroy = TRUE + for(var/type_path in typesof(/atom/movable, /turf) - ignore) //No areas please + if(ispath(type_path, /turf)) + spawn_at.ChangeTurf(type_path) + //We change it back to prevent baseturfs stacking and hitting the limit + spawn_at.ChangeTurf(original_turf_type, original_baseturfs) + if(original_baseturf_count != length(spawn_at.baseturfs)) + TEST_FAIL("[type_path] changed the amount of baseturfs from [original_baseturf_count] to [length(spawn_at.baseturfs)]; [english_list(original_baseturfs)] to [islist(spawn_at.baseturfs) ? english_list(spawn_at.baseturfs) : spawn_at.baseturfs]") + //Warn if it changes again + original_baseturfs = islist(spawn_at.baseturfs) ? spawn_at.baseturfs.Copy() : spawn_at.baseturfs + original_baseturf_count = length(original_baseturfs) + else + var/atom/creation = new type_path(spawn_at) + if(QDELETED(creation)) + continue + //Go all in + qdel(creation, force = TRUE) + //This will hold a ref to the last thing we process unless we set it to null + //Yes byond is fucking sinful + creation = null + + //There's a lot of stuff that either spawns stuff in on create, or removes stuff on destroy. Let's cut it all out so things are easier to deal with + var/list/to_del = spawn_at.contents - cached_contents + if(length(to_del)) + for(var/atom/to_kill in to_del) + qdel(to_kill) + + GLOB.running_create_and_destroy = FALSE + //Hell code, we're bound to have ended the round somehow so let's stop if from ending while we work + SSticker.delay_end = TRUE + //Prevent the garbage subsystem from harddeling anything, if only to save time + SSgarbage.collection_timeout[GC_QUEUE_HARDDELETE] = 10000 HOURS + //Clear it, just in case + cached_contents.Cut() + + //Now that we've qdel'd everything, let's sleep until the gc has processed all the shit we care about + var/time_needed = SSgarbage.collection_timeout[GC_QUEUE_CHECK] + var/start_time = world.time + var/garbage_queue_processed = FALSE + + sleep(time_needed) + while(!garbage_queue_processed) + var/list/queue_to_check = SSgarbage.queues[GC_QUEUE_CHECK] + //How the hell did you manage to empty this? Good job! + if(!length(queue_to_check)) + garbage_queue_processed = TRUE + break + + var/list/oldest_packet = queue_to_check[1] + //Pull out the time we deld at + var/qdeld_at = oldest_packet[1] + //If we've found a packet that got del'd later then we finished, then all our shit has been processed + if(qdeld_at > start_time) + garbage_queue_processed = TRUE + break + + if(world.time > start_time + time_needed + 30 MINUTES) //If this gets us gitbanned I'm going to laugh so hard + TEST_FAIL("Something has gone horribly wrong, the garbage queue has been processing for well over 30 minutes. What the hell did you do") + break + + //Immediately fire the gc right after + SSgarbage.next_fire = 1 + //Unless you've seriously fucked up, queue processing shouldn't take "that" long. Let her run for a bit, see if anything's changed + sleep(20 SECONDS) + + //Alright, time to see if anything messed up + var/list/cache_for_sonic_speed = SSgarbage.items + for(var/path in cache_for_sonic_speed) + var/datum/qdel_item/item = cache_for_sonic_speed[path] + if(item.failures) + TEST_FAIL("[item.name] hard deleted [item.failures] times out of a total del count of [item.qdels]") + if(item.no_respect_force) + TEST_FAIL("[item.name] failed to respect force deletion [item.no_respect_force] times out of a total del count of [item.qdels]") + if(item.no_hint) + TEST_FAIL("[item.name] failed to return a qdel hint [item.no_hint] times out of a total del count of [item.qdels]") + + cache_for_sonic_speed = SSatoms.BadInitializeCalls + for(var/path in cache_for_sonic_speed) + var/fails = cache_for_sonic_speed[path] + if(fails & BAD_INIT_NO_HINT) + TEST_FAIL("[path] didn't return an Initialize hint") + if(fails & BAD_INIT_QDEL_BEFORE) + TEST_FAIL("[path] qdel'd in New()") + if(fails & BAD_INIT_SLEPT) + TEST_FAIL("[path] slept during Initialize()") + + SSticker.delay_end = FALSE + //This shouldn't be needed, but let's be polite + SSgarbage.collection_timeout[GC_QUEUE_HARDDELETE] = 10 SECONDS diff --git a/code/modules/unit_tests/dynamic_ruleset_sanity.dm b/code/modules/unit_tests/dynamic_ruleset_sanity.dm index 837e0b235c..8ec500e1a8 100644 --- a/code/modules/unit_tests/dynamic_ruleset_sanity.dm +++ b/code/modules/unit_tests/dynamic_ruleset_sanity.dm @@ -9,9 +9,9 @@ var/is_lone = initial(ruleset.flags) & (LONE_RULESET | HIGH_IMPACT_RULESET) if (has_scaling_cost && is_lone) - Fail("[ruleset] has a scaling_cost, but is also a lone/highlander ruleset.") + TEST_FAIL("[ruleset] has a scaling_cost, but is also a lone/highlander ruleset.") else if (!has_scaling_cost && !is_lone) - Fail("[ruleset] has no scaling cost, but is also not a lone/highlander ruleset.") + TEST_FAIL("[ruleset] has no scaling cost, but is also not a lone/highlander ruleset.") /// Verifies that dynamic rulesets have unique antag_flag. /datum/unit_test/dynamic_unique_antag_flags @@ -26,11 +26,11 @@ var/antag_flag = initial(ruleset.antag_flag) if (isnull(antag_flag)) - Fail("[ruleset] has a null antag_flag!") + TEST_FAIL("[ruleset] has a null antag_flag!") continue if (antag_flag in known_antag_flags) - Fail("[ruleset] has a non-unique antag_flag [antag_flag] (used by [known_antag_flags[antag_flag]])!") + TEST_FAIL("[ruleset] has a non-unique antag_flag [antag_flag] (used by [known_antag_flags[antag_flag]])!") continue known_antag_flags[antag_flag] = ruleset diff --git a/code/modules/unit_tests/find_reference_sanity.dm b/code/modules/unit_tests/find_reference_sanity.dm index f41714f065..1dcabc7bac 100644 --- a/code/modules/unit_tests/find_reference_sanity.dm +++ b/code/modules/unit_tests/find_reference_sanity.dm @@ -2,12 +2,14 @@ /datum/unit_test/find_reference_sanity /atom/movable/ref_holder + var/static/atom/movable/ref_test/static_test var/atom/movable/ref_test/test var/list/test_list = list() var/list/test_assoc_list = list() /atom/movable/ref_holder/Destroy() test = null + static_test = null test_list.Cut() test_assoc_list.Cut() return ..() @@ -25,6 +27,12 @@ SSgarbage.should_save_refs = TRUE //Sanity check + /* + #if DM_VERSION >= 515 + var/refcount = refcount(victim) + TEST_ASSERT_EQUAL(refcount, 3, "Should be: test references: 0 + baseline references: 3 (victim var,loc,allocated list)") + #endif + */ victim.DoSearchVar(testbed, "Sanity Check", search_time = 1) //We increment search time to get around an optimization TEST_ASSERT(!victim.found_refs.len, "The ref-tracking tool found a ref where none existed") SSgarbage.should_save_refs = FALSE @@ -39,6 +47,12 @@ testbed.test_list += victim testbed.test_assoc_list["baseline"] = victim + /* + #if DM_VERSION >= 515 + var/refcount = refcount(victim) + TEST_ASSERT_EQUAL(refcount, 6, "Should be: test references: 3 + baseline references: 3 (victim var,loc,allocated list)") + #endif + */ victim.DoSearchVar(testbed, "First Run", search_time = 2) TEST_ASSERT(victim.found_refs["test"], "The ref-tracking tool failed to find a regular value") @@ -56,6 +70,12 @@ testbed.vis_contents += victim testbed.test_assoc_list[victim] = TRUE + /* + #if DM_VERSION >= 515 + var/refcount = refcount(victim) + TEST_ASSERT_EQUAL(refcount, 6, "Should be: test references: 3 + baseline references: 3 (victim var,loc,allocated list)") + #endif + */ victim.DoSearchVar(testbed, "Second Run", search_time = 3) //This is another sanity check @@ -76,6 +96,12 @@ var/list/to_find_assoc = list(victim) testbed.test_assoc_list["Nesting"] = to_find_assoc + /* + #if DM_VERSION >= 515 + var/refcount = refcount(victim) + TEST_ASSERT_EQUAL(refcount, 6, "Should be: test references: 3 + baseline references: 3 (victim var,loc,allocated list)") + #endif + */ victim.DoSearchVar(victim, "Third Run Self", search_time = 4) victim.DoSearchVar(testbed, "Third Run Testbed", search_time = 4) TEST_ASSERT(victim.found_refs["self_ref"], "The ref-tracking tool failed to find a self reference") @@ -90,7 +116,12 @@ //Calm before the storm testbed.test_assoc_list = list(null = victim) - + /* + #if DM_VERSION >= 515 + var/refcount = refcount(victim) + TEST_ASSERT_EQUAL(refcount, 4, "Should be: test references: 1 + baseline references: 3 (victim var,loc,allocated list)") + #endif + */ victim.DoSearchVar(testbed, "Fourth Run", search_time = 5) TEST_ASSERT(testbed.test_assoc_list, "The ref-tracking tool failed to find a null key'd assoc list entry") @@ -105,7 +136,39 @@ var/list/to_find_null_assoc_nested = list(victim) testbed.test_assoc_list[null] = to_find_null_assoc_nested + /* + #if DM_VERSION >= 515 + var/refcount = refcount(victim) + TEST_ASSERT_EQUAL(refcount, 5, "Should be: test references: 2 + baseline references: 3 (victim var,loc,allocated list)") + #endif + */ victim.DoSearchVar(testbed, "Fifth Run", search_time = 6) TEST_ASSERT(victim.found_refs[to_find_in_key], "The ref-tracking tool failed to find a nested assoc list key") TEST_ASSERT(victim.found_refs[to_find_null_assoc_nested], "The ref-tracking tool failed to find a null key'd nested assoc list entry") SSgarbage.should_save_refs = FALSE + +/datum/unit_test/find_reference_static_inPvestigation/Run() + var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test) + var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder) + pass(testbed) + SSgarbage.should_save_refs = TRUE + + //Lets check static vars now, since those can be a real headache + testbed.static_test = victim + + //Yes we do actually need to do this. The searcher refuses to read weird lists + //And global.vars is a really weird list + var/global_vars = list() + for(var/key in global.vars) + global_vars[key] = global.vars[key] + + /* + #if DM_VERSION >= 515 + var/refcount = refcount(victim) + TEST_ASSERT_EQUAL(refcount, 5, "Should be: test references: 2 + baseline references: 3 (victim var,loc,allocated list)") + #endif + */ + victim.DoSearchVar(global_vars, "Sixth Run", search_time = 7) + + TEST_ASSERT(victim.found_refs[global_vars], "The ref-tracking tool failed to find a natively global variable") + SSgarbage.should_save_refs = FALSE diff --git a/code/modules/unit_tests/heretic_knowledge.dm b/code/modules/unit_tests/heretic_knowledge.dm index a433bce1ec..484cc90245 100644 --- a/code/modules/unit_tests/heretic_knowledge.dm +++ b/code/modules/unit_tests/heretic_knowledge.dm @@ -18,4 +18,4 @@ var/list/unreachables = all_possible_knowledge - list_to_check for(var/X in unreachables) var/datum/eldritch_knowledge/eldritch_knowledge = X - Fail("[initial(eldritch_knowledge.name)] is unreachable by players! Add it to the blacklist in /code/modules/unit_tests/heretic_knowledge.dm if it is purposeful!") + TEST_FAIL("[initial(eldritch_knowledge.name)] is unreachable by players! Add it to the blacklist in /code/modules/unit_tests/heretic_knowledge.dm if it is purposeful!") diff --git a/code/modules/unit_tests/keybinding_init.dm b/code/modules/unit_tests/keybinding_init.dm index 16141bc553..c9d17f688a 100644 --- a/code/modules/unit_tests/keybinding_init.dm +++ b/code/modules/unit_tests/keybinding_init.dm @@ -3,4 +3,4 @@ var/datum/keybinding/KB = i if(initial(KB.keybind_signal) || !initial(KB.name)) continue - Fail("[initial(KB.name)] does not have a keybind signal defined.") + TEST_FAIL("[KB.name] does not have a keybind signal defined.") diff --git a/code/modules/unit_tests/merge_type.dm b/code/modules/unit_tests/merge_type.dm index 1aed82e6a3..a89df7b492 100644 --- a/code/modules/unit_tests/merge_type.dm +++ b/code/modules/unit_tests/merge_type.dm @@ -12,4 +12,4 @@ for(var/stackpath in paths) var/obj/item/stack/stack = new stackpath if(!stack.merge_type) - Fail("([stack]) lacks set merge_type variable!") + TEST_FAIL("([stack]) lacks set merge_type variable!") diff --git a/code/modules/unit_tests/outfit_sanity.dm b/code/modules/unit_tests/outfit_sanity.dm index e084069cf7..2a4d1c188e 100644 --- a/code/modules/unit_tests/outfit_sanity.dm +++ b/code/modules/unit_tests/outfit_sanity.dm @@ -2,7 +2,7 @@ H.equip_to_slot_or_del(new outfit.##outfit_key(H), ##slot_name, TRUE); \ /* We don't check the result of equip_to_slot_or_del because it returns false for random jumpsuits, as they delete themselves on init */ \ if (!H.get_item_by_slot(##slot_name)) { \ - Fail("[outfit.name]'s [#outfit_key] is invalid!"); \ + TEST_FAIL("[outfit.name]'s [#outfit_key] is invalid!"); \ } \ } @@ -51,6 +51,6 @@ var/number = backpack_contents[path] || 1 for (var/_ in 1 to number) if (!H.equip_to_slot_or_del(new path(H), ITEM_SLOT_BACKPACK, TRUE)) - Fail("[outfit.name]'s backpack_contents are invalid! Couldn't add [path] to backpack.") + TEST_FAIL("[outfit.name]'s backpack_contents are invalid! Couldn't add [path] to backpack.") #undef CHECK_OUTFIT_SLOT diff --git a/code/modules/unit_tests/plantgrowth_tests.dm b/code/modules/unit_tests/plantgrowth_tests.dm index 6b40236860..b1b213f034 100644 --- a/code/modules/unit_tests/plantgrowth_tests.dm +++ b/code/modules/unit_tests/plantgrowth_tests.dm @@ -17,11 +17,11 @@ for(var/i in 1 to seed.growthstages) if("[seed.icon_grow][i]" in states) continue - Fail("[seed.name] ([seed.type]) lacks the [seed.icon_grow][i] icon!") + TEST_FAIL("[seed.name] ([seed.type]) lacks the [seed.icon_grow][i] icon!") if(!(seed.icon_dead in states)) - Fail("[seed.name] ([seed.type]) lacks the [seed.icon_dead] icon!") + TEST_FAIL("[seed.name] ([seed.type]) lacks the [seed.icon_dead] icon!") if(seed.icon_harvest) // mushrooms have no grown sprites, same for items with no product if(!(seed.icon_harvest in states)) - Fail("[seed.name] ([seed.type]) lacks the [seed.icon_harvest] icon!") + TEST_FAIL("[seed.name] ([seed.type]) lacks the [seed.icon_harvest] icon!") diff --git a/code/modules/unit_tests/projectiles.dm b/code/modules/unit_tests/projectiles.dm index f1a2391c07..ddc7979d3d 100644 --- a/code/modules/unit_tests/projectiles.dm +++ b/code/modules/unit_tests/projectiles.dm @@ -2,4 +2,4 @@ for(var/path in typesof(/obj/item/projectile)) var/obj/item/projectile/projectile = path if(initial(projectile.movement_type) & PHASING) - Fail("[path] has default movement type PHASING. Piercing projectiles should be done using the projectile piercing system, not movement_types!") + TEST_FAIL("[path] has default movement type PHASING. Piercing projectiles should be done using the projectile piercing system, not movement_types!") diff --git a/code/modules/unit_tests/reactions.dm b/code/modules/unit_tests/reactions.dm index c2b62f6fdc..596e9eca8d 100644 --- a/code/modules/unit_tests/reactions.dm +++ b/code/modules/unit_tests/reactions.dm @@ -4,4 +4,4 @@ var/test_info = G.test() if(!test_info["success"]) var/message = test_info["message"] - Fail("Gas reaction [G.name] is failing its unit test with the following message: [message]") + TEST_FAIL("Gas reaction [G.name] is failing its unit test with the following message: [message]") diff --git a/code/modules/unit_tests/reagent_id_typos.dm b/code/modules/unit_tests/reagent_id_typos.dm index d6548852fa..f858349999 100644 --- a/code/modules/unit_tests/reagent_id_typos.dm +++ b/code/modules/unit_tests/reagent_id_typos.dm @@ -11,4 +11,4 @@ var/datum/chemical_reaction/R = V for(var/id in (R.required_reagents + R.required_catalysts)) if(!GLOB.chemical_reagents_list[id]) - Fail("Unknown chemical id \"[id]\" in recipe [R.type]") + TEST_FAIL("Unknown chemical id \"[id]\" in recipe [R.type]") diff --git a/code/modules/unit_tests/reagent_recipe_collisions.dm b/code/modules/unit_tests/reagent_recipe_collisions.dm index 20e875422f..b75a17a7e7 100644 --- a/code/modules/unit_tests/reagent_recipe_collisions.dm +++ b/code/modules/unit_tests/reagent_recipe_collisions.dm @@ -12,4 +12,4 @@ var/datum/chemical_reaction/r1 = reactions[i] var/datum/chemical_reaction/r2 = reactions[i2] if(chem_recipes_do_conflict(r1, r2)) - Fail("Chemical recipe conflict between [r1.type] and [r2.type]") + TEST_FAIL("Chemical recipe conflict between [r1.type] and [r2.type]") diff --git a/code/modules/unit_tests/species_whitelists.dm b/code/modules/unit_tests/species_whitelists.dm index 145f3a259f..ec05d0cf9f 100644 --- a/code/modules/unit_tests/species_whitelists.dm +++ b/code/modules/unit_tests/species_whitelists.dm @@ -2,4 +2,4 @@ for(var/typepath in subtypesof(/datum/species)) var/datum/species/S = typepath if(initial(S.changesource_flags) == NONE) - Fail("A species type was detected with no changesource flags: [S]") + TEST_FAIL("A species type was detected with no changesource flags: [S]") diff --git a/code/modules/unit_tests/subsystem_init.dm b/code/modules/unit_tests/subsystem_init.dm index 7d5473bc1b..c377302ba6 100644 --- a/code/modules/unit_tests/subsystem_init.dm +++ b/code/modules/unit_tests/subsystem_init.dm @@ -4,4 +4,4 @@ if(ss.flags & SS_NO_INIT) continue if(!ss.initialized) - Fail("[ss]([ss.type]) is a subsystem meant to initialize but doesn't get set as initialized.") + TEST_FAIL("[ss]([ss.type]) is a subsystem meant to initialize but doesn't get set as initialized.") diff --git a/code/modules/unit_tests/timer_sanity.dm b/code/modules/unit_tests/timer_sanity.dm index d92323a525..dbdf3f6d8e 100644 --- a/code/modules/unit_tests/timer_sanity.dm +++ b/code/modules/unit_tests/timer_sanity.dm @@ -1,3 +1,3 @@ /datum/unit_test/timer_sanity/Run() - if(SStimer.bucket_count < 0) - Fail("SStimer is going into negative bucket count from something") + TEST_ASSERT(SStimer.bucket_count >= 0, + "SStimer is going into negative bucket count from something") diff --git a/code/modules/unit_tests/unit_test.dm b/code/modules/unit_tests/unit_test.dm index aee62b7a52..4fc289a220 100644 --- a/code/modules/unit_tests/unit_test.dm +++ b/code/modules/unit_tests/unit_test.dm @@ -3,7 +3,7 @@ Usage: Override /Run() to run your test code -Call Fail() to fail the test (You should specify a reason) +Call TEST_FAIL() to fail the test (You should specify a reason) You may use /New() and /Destroy() for setup/teardown respectively @@ -15,6 +15,18 @@ GLOBAL_DATUM(current_test, /datum/unit_test) GLOBAL_VAR_INIT(failed_any_test, FALSE) GLOBAL_VAR(test_log) +/// A list of every test that is currently focused. +/// Use the PERFORM_ALL_TESTS macro instead. +GLOBAL_VAR_INIT(focused_tests, focused_tests()) + +/proc/focused_tests() + var/list/focused_tests = list() + for (var/datum/unit_test/unit_test as anything in subtypesof(/datum/unit_test)) + if (initial(unit_test.focus)) + focused_tests += unit_test + + return focused_tests.len > 0 ? focused_tests : null + /datum/unit_test //Bit of metadata for the future maybe var/list/procs_tested @@ -62,15 +74,15 @@ GLOBAL_VAR(test_log) return ..() /datum/unit_test/proc/Run() - Fail("Run() called parent or not implemented") + TEST_FAIL("Run() called parent or not implemented") -/datum/unit_test/proc/Fail(reason = "No reason") +/datum/unit_test/proc/Fail(reason = "No reason", file = "OUTDATED_TEST", line = 1) succeeded = FALSE if(!istext(reason)) reason = "FORMATTED: [reason != null ? reason : "NULL"]" - LAZYADD(fail_reasons, reason) + LAZYADD(fail_reasons, list(list(reason, file, line))) /// Allocates an instance of the provided type, and places it somewhere in an available loc /// Instances allocated through this proc will be destroyed when the test is over @@ -80,16 +92,71 @@ GLOBAL_VAR(test_log) arguments = list(run_loc_floor_bottom_left) else if (arguments[1] == null) arguments[1] = run_loc_floor_bottom_left - var/instance = new type(arglist(arguments)) + var/instance + // Byond will throw an index out of bounds if arguments is empty in that arglist call. Sigh + if(length(arguments)) + instance = new type(arglist(arguments)) + else + instance = new type() allocated += instance return instance +/* +/datum/unit_test/proc/test_screenshot(name, icon/icon) + if (!istype(icon)) + TEST_FAIL("[icon] is not an icon.") + return + + var/path_prefix = replacetext(replacetext("[type]", "/datum/unit_test/", ""), "/", "_") + name = replacetext(name, "/", "_") + + var/filename = "code/modules/unit_tests/screenshots/[path_prefix]_[name].png" + + if (fexists(filename)) + var/data_filename = "data/screenshots/[path_prefix]_[name].png" + fcopy(icon, data_filename) + log_test("\t[path_prefix]_[name] was found, putting in data/screenshots") + else if (fexists("code")) + // We are probably running in a local build + fcopy(icon, filename) + TEST_FAIL("Screenshot for [name] did not exist. One has been created.") + else + // We are probably running in real CI, so just pretend it worked and move on + fcopy(icon, "data/screenshots_new/[path_prefix]_[name].png") + + log_test("\t[path_prefix]_[name] was put in data/screenshots_new") + +/// Helper for screenshot tests to take an image of an atom from all directions and insert it into one icon +/datum/unit_test/proc/get_flat_icon_for_all_directions(atom/thing, no_anim = TRUE) + var/icon/output = icon('icons/effects/effects.dmi', "nothing") + + for (var/direction in GLOB.cardinals) + var/icon/partial = getFlatIcon(thing, defdir = direction, no_anim = no_anim) + output.Insert(partial, dir = direction) + + return output +*/ +/// Logs a test message. Will use GitHub action syntax found at https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions +/datum/unit_test/proc/log_for_test(text, priority, file, line) + var/map_name = SSmapping.config.map_name + + // Need to escape the text to properly support newlines. + var/annotation_text = replacetext(text, "%", "%25") + annotation_text = replacetext(annotation_text, "\n", "%0A") + + log_world("::[priority] file=[file],line=[line],title=[map_name]: [type]::[annotation_text]") + /proc/RunUnitTest(test_path, list/test_results) +/* + if (ispath(test_path, /datum/unit_test/focus_only)) + return +*/ var/datum/unit_test/test = new test_path GLOB.current_test = test var/duration = REALTIMEOFDAY + log_world("::group::[test_path]") test.Run() duration = REALTIMEOFDAY - duration @@ -99,11 +166,28 @@ GLOBAL_VAR(test_log) var/list/log_entry = list("[test.succeeded ? "PASS" : "FAIL"]: [test_path] [duration / 10]s") var/list/fail_reasons = test.fail_reasons - for(var/J in 1 to LAZYLEN(fail_reasons)) - log_entry += "\tREASON #[J]: [fail_reasons[J]]" + for(var/reasonID in 1 to LAZYLEN(fail_reasons)) + var/text = fail_reasons[reasonID][1] + var/file = fail_reasons[reasonID][2] + var/line = fail_reasons[reasonID][3] + + test.log_for_test(text, "error", file, line) + + // Normal log message + log_entry += "\tREASON #[reasonID]: [text] at [file]:[line]" + var/message = log_entry.Join("\n") log_test(message) + var/test_output_desc = "[test_path] [duration / 10]s" + if (test.succeeded) + log_world("[TEST_OUTPUT_GREEN("PASS")] [test_output_desc]") + + log_world("::endgroup::") + + if (!test.succeeded) + log_world("::error::[TEST_OUTPUT_RED("FAIL")] [test_output_desc]") + test_results[test_path] = list("status" = test.succeeded ? UNIT_TEST_PASSED : UNIT_TEST_FAILED, "message" = message, "name" = test_path) qdel(test) @@ -112,11 +196,13 @@ GLOBAL_VAR(test_log) CHECK_TICK var/list/tests_to_run = subtypesof(/datum/unit_test) + var/list/focused_tests = list() for (var/_test_to_run in tests_to_run) var/datum/unit_test/test_to_run = _test_to_run if (initial(test_to_run.focus)) - tests_to_run = list(test_to_run) - break + focused_tests += test_to_run + if(length(focused_tests)) + tests_to_run = focused_tests tests_to_run = sortTim(tests_to_run, /proc/cmp_unit_test_priority) diff --git a/code/modules/unit_tests/vore_tests.dm b/code/modules/unit_tests/vore_tests.dm index 6549aa9ce7..08a525c5d5 100644 --- a/code/modules/unit_tests/vore_tests.dm +++ b/code/modules/unit_tests/vore_tests.dm @@ -11,7 +11,7 @@ break mobloc = default_mobloc if(!mobloc) - Fail("Unable to find a location to create test mob") + TEST_FAIL("Unable to find a location to create test mob") return FALSE var/mob/living/carbon/human/H = new mobtype(mobloc) @@ -44,7 +44,7 @@ endOxyloss = H.getOxyLoss() if(!startOxyloss < endOxyloss) - Fail("Human mob is not taking oxygen damage in space. (Before: [startOxyloss]; after: [endOxyloss])") + TEST_FAIL("Human mob is not taking oxygen damage in space. (Before: [startOxyloss]; after: [endOxyloss])") qdel(H) return 1 @@ -74,7 +74,7 @@ // Now that pred belly exists, we can eat the prey. if(!pred.vore_selected) - Fail("[pred] has no vore_selected.") + TEST_FAIL("[pred] has no vore_selected.") return TRUE // Attempt to eat the prey @@ -82,7 +82,7 @@ pred.vore_selected.nom_mob(prey) if(prey.loc != pred.vore_selected) - Fail("[pred.vore_selected].nom_mob([prey]) did not put prey inside [pred]") + TEST_FAIL("[pred.vore_selected].nom_mob([prey]) did not put prey inside [pred]") return TRUE // Okay, we succeeded in eating them, now lets wait a bit @@ -96,7 +96,7 @@ // Alright lets check it! endOxyloss = prey.getOxyLoss() if(startOxyloss < endOxyloss) - Fail("Prey takes oxygen damage in a pred's belly! (Before: [startOxyloss]; after: [endOxyloss])") + TEST_FAIL("Prey takes oxygen damage in a pred's belly! (Before: [startOxyloss]; after: [endOxyloss])") qdel(prey) qdel(pred) return TRUE @@ -128,7 +128,7 @@ // Now that pred belly exists, we can eat the prey. if(!pred.vore_selected) - Fail("[pred] has no vore_selected.") + TEST_FAIL("[pred] has no vore_selected.") return TRUE // Attempt to eat the prey @@ -136,12 +136,12 @@ pred.vore_selected.nom_mob(prey) if(prey.loc != pred.vore_selected) - Fail("[pred.vore_selected].nom_mob([prey]) did not put prey inside [pred]") + TEST_FAIL("[pred.vore_selected].nom_mob([prey]) did not put prey inside [pred]") return TRUE else var/turf/T = locate(/turf/open/space) if(!T) - Fail("could not find a space turf for testing") + TEST_FAIL("could not find a space turf for testing") return TRUE else pred.forceMove(T) @@ -159,7 +159,7 @@ endOxyloss = prey.getOxyLoss() endBruteloss = prey.getBruteLoss() if(startBruteloss < endBruteloss) - Fail("Prey takes brute damage in space! (Before: [startBruteloss]; after: [endBruteloss])") + TEST_FAIL("Prey takes brute damage in space! (Before: [startBruteloss]; after: [endBruteloss])") qdel(prey) qdel(pred) return TRUE @@ -189,7 +189,7 @@ // Now that pred belly exists, we can eat the prey. if(!pred.vore_selected) - Fail("[pred] has no vore_selected.") + TEST_FAIL("[pred] has no vore_selected.") return TRUE // Attempt to eat the prey @@ -197,7 +197,7 @@ pred.vore_selected.nom_mob(prey) if(prey.loc != pred.vore_selected) - Fail("[pred.vore_selected].nom_mob([prey]) did not put prey inside [pred]") + TEST_FAIL("[pred.vore_selected].nom_mob([prey]) did not put prey inside [pred]") return TRUE // Okay, we succeeded in eating them, now lets wait a bit @@ -212,7 +212,7 @@ // Alright lets check it! endBruteBurn = prey.getBruteLoss() + prey.getFireLoss() if(startBruteBurn >= endBruteBurn) - Fail("Prey doesn't take damage in digesting belly! (Before: [startBruteBurn]; after: [endBruteBurn])") + TEST_FAIL("Prey doesn't take damage in digesting belly! (Before: [startBruteBurn]; after: [endBruteBurn])") qdel(prey) qdel(pred) return TRUE