diff --git a/code/modules/unit_tests/_unit_tests.dm b/code/modules/unit_tests/_unit_tests.dm index 7cc72ed1c1..7f7c3a1fcc 100644 --- a/code/modules/unit_tests/_unit_tests.dm +++ b/code/modules/unit_tests/_unit_tests.dm @@ -32,47 +32,79 @@ /// Intended to be used in the manner of `TEST_FOCUS(/datum/unit_test/math)` #define TEST_FOCUS(test_path) ##test_path { focus = TRUE; } +/// Constants indicating unit test completion status +#define UNIT_TEST_PASSED 0 +#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" + #include "anchored_mobs.dm" #include "bespoke_id.dm" #include "binary_insert.dm" +// #include "bloody_footprints.dm" +// #include "breath.dm" // #include "card_mismatch.dm" #include "chain_pull_through_space.dm" // #include "combat.dm" #include "component_tests.dm" +// #include "connect_loc.dm" // #include "confusion.dm" +// #include "crayons.dm" +// #include "create_and_destroy.dm" +// #include "designs.dm" +// #include "dynamic_ruleset_sanity.dm" +// #include "egg_glands.dm" // #include "emoting.dm" +// #include "food_edibility_check.dm" +// #include "greyscale_config.dm" // #include "heretic_knowledge.dm" // #include "holidays.dm" -#include "initialize_sanity.dm" +// #include "hydroponics_harvest.dm" // #include "keybinding_init.dm" #include "machine_disassembly.dm" #include "medical_wounds.dm" #include "merge_type.dm" // #include "metabolizing.dm" +// #include "ntnetwork_tests.dm" // #include "outfit_sanity.dm" // #include "pills.dm" // #include "plantgrowth_tests.dm" // #include "projectiles.dm" +// #include "rcd.dm" #include "reagent_id_typos.dm" // #include "reagent_mod_expose.dm" // #include "reagent_mod_procs.dm" #include "reagent_recipe_collisions.dm" #include "resist.dm" -// #include "say.dm" +#include "say.dm" +// #include "security_officer_distribution.dm" // #include "serving_tray.dm" // #include "siunit.dm" #include "spawn_humans.dm" +#include "spawn_mobs.dm" // #include "species_whitelists.dm" // #include "stomach.dm" +// #include "strippable.dm" #include "subsystem_init.dm" #include "surgeries.dm" #include "teleporters.dm" +#include "tgui_create_message.dm" #include "timer_sanity.dm" #include "unit_test.dm" +// #include "wizard.dm" /// CIT TESTS #include "character_saving.dm" +#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 +#include "find_reference_sanity.dm" +#endif + #undef TEST_ASSERT #undef TEST_ASSERT_EQUAL #undef TEST_ASSERT_NOTEQUAL diff --git a/code/modules/unit_tests/chain_pull_through_space.dm b/code/modules/unit_tests/chain_pull_through_space.dm index ffdd1bf7c9..10363d5aad 100644 --- a/code/modules/unit_tests/chain_pull_through_space.dm +++ b/code/modules/unit_tests/chain_pull_through_space.dm @@ -1,6 +1,6 @@ /datum/unit_test/chain_pull_through_space var/turf/open/space/space_tile - var/turf/claimed_tile + var/claimed_tile var/mob/living/carbon/human/alice var/mob/living/carbon/human/bob var/mob/living/carbon/human/charlie @@ -9,25 +9,25 @@ ..() // Create a space tile that goes to another z-level - claimed_tile = run_loc_bottom_left + claimed_tile = run_loc_floor_bottom_left.type - space_tile = new(locate(run_loc_bottom_left.x, run_loc_bottom_left.y, run_loc_bottom_left.z)) + space_tile = new(locate(run_loc_floor_bottom_left.x, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z)) space_tile.destination_x = 100 space_tile.destination_y = 100 space_tile.destination_z = 5 // Create our list of humans, all adjacent to one another - alice = new(locate(run_loc_bottom_left.x + 2, run_loc_bottom_left.y, run_loc_bottom_left.z)) + alice = new(locate(run_loc_floor_bottom_left.x + 2, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z)) alice.name = "Alice" - bob = new(locate(run_loc_bottom_left.x + 3, run_loc_bottom_left.y, run_loc_bottom_left.z)) + bob = new(locate(run_loc_floor_bottom_left.x + 3, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z)) bob.name = "Bob" - charlie = new(locate(run_loc_bottom_left.x + 4, run_loc_bottom_left.y, run_loc_bottom_left.z)) + charlie = new(locate(run_loc_floor_bottom_left.x + 4, run_loc_floor_bottom_left.y, run_loc_floor_bottom_left.z)) charlie.name = "Charlie" /datum/unit_test/chain_pull_through_space/Destroy() - space_tile.copyTurf(claimed_tile) + space_tile.ChangeTurf(claimed_tile) qdel(alice) qdel(bob) qdel(charlie) @@ -40,14 +40,14 @@ bob.start_pulling(charlie) // Walk normally to the left, make sure we're still a chain - alice.Move(locate(run_loc_bottom_left.x + 1, run_loc_bottom_left.y, run_loc_bottom_left.z)) - if (bob.x != run_loc_bottom_left.x + 2) + 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_bottom_left.x + 3) + if (charlie.x != run_loc_floor_bottom_left.x + 3) return Fail("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_bottom_left) + 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])") diff --git a/code/modules/unit_tests/find_reference_sanity.dm b/code/modules/unit_tests/find_reference_sanity.dm new file mode 100644 index 0000000000..f41714f065 --- /dev/null +++ b/code/modules/unit_tests/find_reference_sanity.dm @@ -0,0 +1,111 @@ +///Used to test the completeness of the reference finder proc. +/datum/unit_test/find_reference_sanity + +/atom/movable/ref_holder + var/atom/movable/ref_test/test + 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 + testbed.test_assoc_list["baseline"] = victim + + victim.DoSearchVar(testbed, "First Run", search_time = 2) + + 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 + testbed.vis_contents += victim + testbed.test_assoc_list[victim] = TRUE + + victim.DoSearchVar(testbed, "Second Run", search_time = 3) + + //This is another sanity check + 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 + var/list/to_find = list(victim) + testbed.test_list += list(to_find) + var/list/to_find_assoc = list(victim) + testbed.test_assoc_list["Nesting"] = to_find_assoc + + 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") + 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) + + 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") + +/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") + var/list/to_find_null_assoc_nested = list(victim) + testbed.test_assoc_list[null] = to_find_null_assoc_nested + + 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 diff --git a/code/modules/unit_tests/initialize_sanity.dm b/code/modules/unit_tests/initialize_sanity.dm deleted file mode 100644 index d183f530c8..0000000000 --- a/code/modules/unit_tests/initialize_sanity.dm +++ /dev/null @@ -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? diff --git a/code/modules/unit_tests/spawn_humans.dm b/code/modules/unit_tests/spawn_humans.dm index 0500deae0a..1bc7ca3d8a 100644 --- a/code/modules/unit_tests/spawn_humans.dm +++ b/code/modules/unit_tests/spawn_humans.dm @@ -1,5 +1,5 @@ /datum/unit_test/spawn_humans/Run() - var/locs = block(run_loc_bottom_left, run_loc_top_right) + var/locs = block(run_loc_floor_bottom_left, run_loc_floor_top_right) for(var/I in 1 to 5) new /mob/living/carbon/human(pick(locs)) diff --git a/code/modules/unit_tests/spawn_mobs.dm b/code/modules/unit_tests/spawn_mobs.dm new file mode 100644 index 0000000000..f14d14eb43 --- /dev/null +++ b/code/modules/unit_tests/spawn_mobs.dm @@ -0,0 +1,8 @@ +///Unit test that spawns all mobs that can be spawned by golden slimes +/datum/unit_test/spawn_mobs + +/datum/unit_test/spawn_mobs/Run() + for(var/_animal in typesof(/mob/living/simple_animal)) + var/mob/living/simple_animal/animal = _animal + if (initial(animal.gold_core_spawnable) == HOSTILE_SPAWN || initial(animal.gold_core_spawnable) == FRIENDLY_SPAWN) + allocate(_animal) diff --git a/code/modules/unit_tests/tgui_create_message.dm b/code/modules/unit_tests/tgui_create_message.dm new file mode 100644 index 0000000000..4d5a4bc0a0 --- /dev/null +++ b/code/modules/unit_tests/tgui_create_message.dm @@ -0,0 +1,28 @@ +/// Test that `TGUI_CREATE_MESSAGE` is correctly implemented +/datum/unit_test/tgui_create_message + +/datum/unit_test/tgui_create_message/Run() + var/type = "something/here" + var/list/payload = list( + "name" = "Terry McTider", + "heads_caved" = 100, + "accomplishments" = list( + "nothing", + "literally nothing", + list( + "something" = "just kidding", + ), + ), + ) + + var/message = TGUI_CREATE_MESSAGE(type, payload) + + // Ensure consistent output to compare by performing a round-trip. + var/output = json_encode(json_decode(url_decode(message))) + + var/expected = json_encode(list( + "type" = type, + "payload" = payload, + )) + + TEST_ASSERT_EQUAL(expected, output, "TGUI_CREATE_MESSAGE didn't round trip properly") diff --git a/code/modules/unit_tests/unit_test.dm b/code/modules/unit_tests/unit_test.dm index 15fe6b466c..ef22f7960e 100644 --- a/code/modules/unit_tests/unit_test.dm +++ b/code/modules/unit_tests/unit_test.dm @@ -7,7 +7,7 @@ Call Fail() to fail the test (You should specify a reason) You may use /New() and /Destroy() for setup/teardown respectively -You can use the run_loc_bottom_left and run_loc_top_right to get turfs for testing +You can use the run_loc_floor_bottom_left and run_loc_floor_top_right to get turfs for testing */ @@ -19,39 +19,46 @@ GLOBAL_VAR(test_log) //Bit of metadata for the future maybe var/list/procs_tested - /// The bottom left turf of the testing zone - var/turf/run_loc_bottom_left - - /// The top right turf of the testing zone - var/turf/run_loc_top_right - - /// The type of turf to allocate for the testing zone - var/test_turf_type = /turf/open/floor/plasteel + /// The bottom left floor turf of the testing zone + var/turf/run_loc_floor_bottom_left + /// 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 var/list/allocated var/list/fail_reasons - var/static/datum/turf_reservation/turf_reservation + var/static/datum/turf_reservation/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(turf_reservation)) - turf_reservation = SSmapping.RequestBlockReservation(5, 5) + if (isnull(reservation)) + reservation = SSmapping.RequestBlockReservation(5, 5) - for (var/turf/reserved_turf in turf_reservation.reserved_turfs) - reserved_turf.ChangeTurf(test_turf_type) + for (var/turf/reserved_turf in reservation.reserved_turfs) + reserved_turf.ChangeTurf(/turf/open/floor/plasteel) allocated = new - run_loc_bottom_left = locate(turf_reservation.bottom_left_coords[1], turf_reservation.bottom_left_coords[2], turf_reservation.bottom_left_coords[3]) - run_loc_top_right = locate(turf_reservation.top_right_coords[1], turf_reservation.top_right_coords[2], turf_reservation.top_right_coords[3]) + run_loc_floor_bottom_left = locate(reservation.bottom_left_coords[1], reservation.bottom_left_coords[2], reservation.bottom_left_coords[3]) + run_loc_floor_top_right = locate(reservation.top_right_coords[1], reservation.top_right_coords[2], reservation.top_right_coords[3]) + + TEST_ASSERT(isfloorturf(run_loc_floor_bottom_left), "run_loc_floor_bottom_left was not a floor ([run_loc_floor_bottom_left])") + TEST_ASSERT(isfloorturf(run_loc_floor_top_right), "run_loc_floor_top_right was not a floor ([run_loc_floor_top_right])") /datum/unit_test/Destroy() - //clear the test area - for(var/atom/movable/AM in block(run_loc_bottom_left, run_loc_top_right)) - qdel(AM) QDEL_LIST(allocated) + // clear the test area + for (var/turf/turf in block(locate(1, 1, run_loc_floor_bottom_left.z), locate(world.maxx, world.maxy, run_loc_floor_bottom_left.z))) + for (var/content in turf.contents) + if (istype(content, /obj/effect/landmark)) + continue + qdel(content) return ..() /datum/unit_test/proc/Run() @@ -70,44 +77,63 @@ GLOBAL_VAR(test_log) /datum/unit_test/proc/allocate(type, ...) var/list/arguments = args.Copy(2) if (!arguments.len) - arguments = list(run_loc_bottom_left) + arguments = list(run_loc_floor_bottom_left) else if (arguments[1] == null) - arguments[1] = run_loc_bottom_left + arguments[1] = run_loc_floor_bottom_left var/instance = new type(arglist(arguments)) 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 - for(var/I in tests_to_run) - var/datum/unit_test/test = new I + tests_to_run = sortTim(tests_to_run, /proc/cmp_unit_test_priority) - GLOB.current_test = test - var/duration = REALTIMEOFDAY + var/list/test_results = list() - test.Run() + 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) - duration = REALTIMEOFDAY - duration - GLOB.current_test = null - GLOB.failed_any_test |= !test.succeeded - - var/list/log_entry = list("[test.succeeded ? "PASS" : "FAIL"]: [I] [duration / 10]s") - var/list/fail_reasons = test.fail_reasons - - qdel(test) - - for(var/J in 1 to LAZYLEN(fail_reasons)) - log_entry += "\tREASON #[J]: [fail_reasons[J]]" - log_test(log_entry.Join("\n")) - - CHECK_TICK + 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" +// mappath = "_maps/templates/unit_tests.dmm"