unit test

this is the easy part, believe it or not
This commit is contained in:
BongaTheProto
2023-01-22 12:49:09 -05:00
committed by Tsurupeta
parent db6511e64a
commit dfdbd259da
27 changed files with 449 additions and 81 deletions
+33 -7
View File
@@ -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
+1 -3
View File
@@ -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(" ")]")
+1 -1
View File
@@ -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]")
+3 -4
View File
@@ -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)
@@ -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]")
+5 -5
View File
@@ -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
+2 -2
View File
@@ -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(",")])")
+2 -2
View File
@@ -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])")
@@ -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
@@ -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
@@ -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
+1 -1
View File
@@ -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!")
+1 -1
View File
@@ -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.")
+1 -1
View File
@@ -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!")
+2 -2
View File
@@ -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
+3 -3
View File
@@ -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!")
+1 -1
View File
@@ -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!")
+1 -1
View File
@@ -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]")
+1 -1
View File
@@ -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]")
@@ -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]")
@@ -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]")
+1 -1
View File
@@ -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.")
+2 -2
View File
@@ -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")
+95 -9
View File
@@ -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)
+12 -12
View File
@@ -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