mirror of
https://github.com/Bubberstation/Bubberstation.git
synced 2026-07-20 20:45:28 +01:00
Del The World: Unit testing for hard deletes (#59612)
Co-authored-by: SteelSlayer <42044220+SteelSlayer@users.noreply.github.com>
This commit is contained in:
@@ -37,6 +37,9 @@
|
||||
#define UNIT_TEST_FAILED 1
|
||||
#define UNIT_TEST_SKIPPED 2
|
||||
|
||||
#define TEST_DEFAULT 1
|
||||
#define TEST_DEL_WORLD INFINITY
|
||||
|
||||
/// A trait source when adding traits through unit tests
|
||||
#define TRAIT_SOURCE_UNIT_TESTS "unit_tests"
|
||||
|
||||
@@ -52,7 +55,7 @@
|
||||
#include "connect_loc.dm"
|
||||
#include "confusion.dm"
|
||||
#include "crayons.dm"
|
||||
#include "deletions.dm"
|
||||
#include "create_and_destroy.dm"
|
||||
#include "designs.dm"
|
||||
#include "dynamic_ruleset_sanity.dm"
|
||||
#include "egg_glands.dm"
|
||||
@@ -62,7 +65,6 @@
|
||||
#include "heretic_knowledge.dm"
|
||||
#include "holidays.dm"
|
||||
#include "hydroponics_harvest.dm"
|
||||
#include "initialize_sanity.dm"
|
||||
#include "keybinding_init.dm"
|
||||
#include "machine_disassembly.dm"
|
||||
#include "medical_wounds.dm"
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
///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_DEL_WORLD
|
||||
|
||||
/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,
|
||||
//Say it with me now, type template
|
||||
/obj/effect/mapping_helpers/component_injector,
|
||||
//template type
|
||||
/obj/effect/mapping_helpers/trait_injector,
|
||||
//Singleton
|
||||
/mob/dview,
|
||||
//Template,
|
||||
/obj/effect/mapping_helpers/custom_icon,
|
||||
)
|
||||
//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/tablet/integrated)
|
||||
//This one demands a computer, ditto
|
||||
ignore += typesof(/obj/item/modular_computer/processor)
|
||||
//Needs special input, let's be nice
|
||||
ignore += typesof(/obj/effect/abstract/proximity_checker)
|
||||
//Very finiky, blacklisting to make things easier
|
||||
ignore += typesof(/obj/item/poster/wanted)
|
||||
//We can't pass a mind into this
|
||||
ignore += typesof(/obj/item/phylactery)
|
||||
//This expects a seed, we can't pass it
|
||||
ignore += typesof(/obj/item/food/grown)
|
||||
//Nothing to hallucinate if there's nothing to hallicinate
|
||||
ignore += typesof(/obj/effect/hallucination)
|
||||
//These want fried food to take on the shape of, we can't pass that in
|
||||
ignore += typesof(/obj/item/food/deepfryholder)
|
||||
//Can't pass in a thing to glow
|
||||
ignore += typesof(/obj/effect/abstract/eye_lighting)
|
||||
//It wants a lot more context then we have
|
||||
ignore += typesof(/obj/effect/buildmode_line)
|
||||
//We don't have a pod
|
||||
ignore += typesof(/obj/effect/pod_landingzone_effect)
|
||||
ignore += typesof(/obj/effect/pod_landingzone)
|
||||
//We don't have a disease to pass in
|
||||
ignore += typesof(/obj/effect/mapping_helpers/component_injector/infective)
|
||||
//It's a trapdoor to nowhere
|
||||
ignore += typesof(/obj/effect/mapping_helpers/trapdoor_placer)
|
||||
//There's no shapeshift to hold
|
||||
ignore += typesof(/obj/shapeshift_holder)
|
||||
//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)
|
||||
//Leads to errors as a consequence of the logic behind moving back to a tile that's moving you somewhere else
|
||||
ignore += typesof(/obj/effect/mapping_helpers/component_injector/areabound)
|
||||
//Expects a mob to holderize, we have nothing to give
|
||||
ignore += typesof(/obj/item/clothing/head/mob_holder)
|
||||
|
||||
var/list/cached_contents = spawn_at.contents.Copy()
|
||||
var/baseturf_count = length(spawn_at.baseturfs)
|
||||
|
||||
for(var/type_path in typesof(/atom/movable, /turf) - ignore) //No areas please
|
||||
if(ispath(type_path, /turf))
|
||||
spawn_at.ChangeTurf(type_path, /turf/baseturf_skipover)
|
||||
//We change it back to prevent pain, please don't ask
|
||||
spawn_at.ChangeTurf(/turf/open/floor/wood, /turf/baseturf_skipover)
|
||||
if(baseturf_count != length(spawn_at.baseturfs))
|
||||
Fail("[type_path] changed the amount of baseturfs we have [baseturf_count] -> [length(spawn_at.baseturfs)]")
|
||||
baseturf_count = length(spawn_at.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)
|
||||
|
||||
//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 + 8 MINUTES)
|
||||
Fail("Something has gone horribly wrong, the garbage queue has been processing for well over 10 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)
|
||||
Fail("[item.name] hard deleted [item.failures] times out of a total del count of [item.qdels]")
|
||||
if(item.no_respect_force)
|
||||
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)
|
||||
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)
|
||||
Fail("[path] didn't return an Initialize hint")
|
||||
if(fails & BAD_INIT_QDEL_BEFORE)
|
||||
Fail("[path] qdel'd in New()")
|
||||
if(fails & BAD_INIT_SLEPT)
|
||||
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
|
||||
@@ -1,7 +0,0 @@
|
||||
/// This is for regression tests of deletions that used to runtime.
|
||||
/// This would ideally be replaced by Del The World, unit testing every single deletion.
|
||||
/datum/unit_test/deletion_regressions
|
||||
|
||||
/datum/unit_test/deletion_regressions/Run()
|
||||
qdel(new /obj/item/gun/energy/kinetic_accelerator/crossbow)
|
||||
qdel(new /obj/item/gun/syringe/syndicate)
|
||||
@@ -6,20 +6,34 @@
|
||||
var/list/test_list = list()
|
||||
var/list/test_assoc_list = list()
|
||||
|
||||
/atom/movable/ref_holder/Destroy()
|
||||
test = null
|
||||
test_list.Cut()
|
||||
test_assoc_list.Cut()
|
||||
return ..()
|
||||
|
||||
/atom/movable/ref_test
|
||||
var/atom/movable/ref_test/self_ref
|
||||
|
||||
/atom/movable/ref_test/Destroy(force)
|
||||
self_ref = null
|
||||
return ..()
|
||||
|
||||
/datum/unit_test/find_reference_sanity/Run()
|
||||
var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
|
||||
var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
|
||||
SSgarbage.should_save_refs = TRUE
|
||||
|
||||
//Sanity check
|
||||
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
|
||||
|
||||
/datum/unit_test/find_reference_baseline/Run()
|
||||
var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
|
||||
var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
|
||||
SSgarbage.should_save_refs = TRUE
|
||||
|
||||
//Set up for the first round of tests
|
||||
testbed.test = victim
|
||||
testbed.test_list += victim
|
||||
@@ -30,10 +44,12 @@
|
||||
TEST_ASSERT(victim.found_refs["test"], "The ref-tracking tool failed to find a regular value")
|
||||
TEST_ASSERT(victim.found_refs[testbed.test_list], "The ref-tracking tool failed to find a list entry")
|
||||
TEST_ASSERT(victim.found_refs[testbed.test_assoc_list], "The ref-tracking tool failed to find an assoc list value")
|
||||
SSgarbage.should_save_refs = FALSE
|
||||
|
||||
/datum/unit_test/find_reference_exotic/Run()
|
||||
var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
|
||||
var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
|
||||
SSgarbage.should_save_refs = TRUE
|
||||
|
||||
//Second round, bit harder this time
|
||||
testbed.overlays += victim
|
||||
@@ -46,10 +62,12 @@
|
||||
TEST_ASSERT(!victim.found_refs[testbed.overlays], "The ref-tracking tool found an overlays entry? That shouldn't be possible")
|
||||
TEST_ASSERT(victim.found_refs[testbed.vis_contents], "The ref-tracking tool failed to find a vis_contents entry")
|
||||
TEST_ASSERT(victim.found_refs[testbed.test_assoc_list], "The ref-tracking tool failed to find an assoc list key")
|
||||
SSgarbage.should_save_refs = FALSE
|
||||
|
||||
/datum/unit_test/find_reference_esoteric/Run()
|
||||
var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
|
||||
var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
|
||||
SSgarbage.should_save_refs = TRUE
|
||||
|
||||
//Let's get a bit esoteric
|
||||
victim.self_ref = victim
|
||||
@@ -63,10 +81,12 @@
|
||||
TEST_ASSERT(victim.found_refs["self_ref"], "The ref-tracking tool failed to find a self reference")
|
||||
TEST_ASSERT(victim.found_refs[to_find], "The ref-tracking tool failed to find a nested list entry")
|
||||
TEST_ASSERT(victim.found_refs[to_find_assoc], "The ref-tracking tool failed to find a nested assoc list entry")
|
||||
SSgarbage.should_save_refs = FALSE
|
||||
|
||||
/datum/unit_test/find_reference_null_key_entry/Run()
|
||||
var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
|
||||
var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
|
||||
SSgarbage.should_save_refs = TRUE
|
||||
|
||||
//Calm before the storm
|
||||
testbed.test_assoc_list = list(null = victim)
|
||||
@@ -77,6 +97,8 @@
|
||||
/datum/unit_test/find_reference_assoc_investigation/Run()
|
||||
var/atom/movable/ref_test/victim = allocate(/atom/movable/ref_test)
|
||||
var/atom/movable/ref_holder/testbed = allocate(/atom/movable/ref_holder)
|
||||
SSgarbage.should_save_refs = TRUE
|
||||
|
||||
//Let's do some more complex assoc list investigation
|
||||
var/list/to_find_in_key = list(victim)
|
||||
testbed.test_assoc_list[to_find_in_key] = list("memes")
|
||||
@@ -86,3 +108,4 @@
|
||||
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
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
/datum/unit_test/initialize_sanity/Run()
|
||||
if(length(SSatoms.BadInitializeCalls))
|
||||
Fail("Bad Initialize() calls detected. Please read logs.")
|
||||
var/list/init_failures_to_text = list(
|
||||
"[BAD_INIT_QDEL_BEFORE]" = "Qdeleted Before Initialized",
|
||||
"[BAD_INIT_DIDNT_INIT]" = "Did Not Initialize",
|
||||
"[BAD_INIT_SLEPT]" = "Initialize() Slept",
|
||||
"[BAD_INIT_NO_HINT]" = "No Initialize() Hint Returned",
|
||||
)
|
||||
for(var/failure in SSatoms.BadInitializeCalls)
|
||||
log_world("[failure]: [init_failures_to_text["[SSatoms.BadInitializeCalls[failure]]"]]") // You like stacked brackets?
|
||||
@@ -24,7 +24,8 @@ GLOBAL_VAR(test_log)
|
||||
|
||||
/// The top right floor turf of the testing zone
|
||||
var/turf/run_loc_floor_top_right
|
||||
|
||||
///The priority of the test, the larger it is the later it fires
|
||||
var/priority = TEST_DEFAULT
|
||||
//internal shit
|
||||
var/focus = FALSE
|
||||
var/succeeded = TRUE
|
||||
@@ -33,6 +34,9 @@ GLOBAL_VAR(test_log)
|
||||
|
||||
var/static/datum/space_level/reservation
|
||||
|
||||
/proc/cmp_unit_test_priority(datum/unit_test/a, datum/unit_test/b)
|
||||
return initial(a.priority) - initial(b.priority)
|
||||
|
||||
/datum/unit_test/New()
|
||||
if (isnull(reservation))
|
||||
var/datum/map_template/unit_tests/template = new
|
||||
@@ -78,49 +82,55 @@ GLOBAL_VAR(test_log)
|
||||
allocated += instance
|
||||
return instance
|
||||
|
||||
/proc/RunUnitTest(test_path, list/test_results)
|
||||
var/datum/unit_test/test = new test_path
|
||||
|
||||
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"]: [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]]"
|
||||
var/message = log_entry.Join("\n")
|
||||
log_test(message)
|
||||
|
||||
test_results[test_path] = list("status" = test.succeeded ? UNIT_TEST_PASSED : UNIT_TEST_FAILED, "message" = message, "name" = test_path)
|
||||
|
||||
qdel(test)
|
||||
|
||||
/proc/RunUnitTests()
|
||||
CHECK_TICK
|
||||
|
||||
var/tests_to_run = subtypesof(/datum/unit_test)
|
||||
var/list/tests_to_run = subtypesof(/datum/unit_test)
|
||||
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
|
||||
|
||||
tests_to_run = sortTim(tests_to_run, /proc/cmp_unit_test_priority)
|
||||
|
||||
var/list/test_results = list()
|
||||
|
||||
for(var/I in tests_to_run)
|
||||
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
|
||||
|
||||
for(var/J in 1 to LAZYLEN(fail_reasons))
|
||||
log_entry += "\tREASON #[J]: [fail_reasons[J]]"
|
||||
var/message = log_entry.Join("\n")
|
||||
log_test(message)
|
||||
|
||||
test_results[I] = list("status" = test.succeeded ? UNIT_TEST_PASSED : UNIT_TEST_FAILED, "message" = message, "name" = I)
|
||||
|
||||
qdel(test)
|
||||
|
||||
CHECK_TICK
|
||||
for(var/unit_path in tests_to_run)
|
||||
CHECK_TICK //We check tick first because the unit test we run last may be so expensive that checking tick will lock up this loop forever
|
||||
RunUnitTest(unit_path, test_results)
|
||||
|
||||
var/file_name = "data/unit_tests.json"
|
||||
fdel(file_name)
|
||||
file(file_name) << json_encode(test_results)
|
||||
|
||||
SSticker.force_ending = TRUE
|
||||
//We have to call this manually because del_text can preceed us, and SSticker doesn't fire in the post game
|
||||
SSticker.standard_reboot()
|
||||
|
||||
/datum/map_template/unit_tests
|
||||
name = "Unit Tests Zone"
|
||||
|
||||
Reference in New Issue
Block a user