Unit Test rework & Master/Ticker update (#17912)

* Unit Test rework & Master/Ticker update

* Fixes and working unit testing

* Fixes

* Test fixes and FA update

* Fixed runtimes

* Radio subsystem

* move that glob wherever later

* ident

* CIBUILDING compile option

* Fixed runtimes

* Some changes to the workflow

* CI Split

* More split

* Pathing

* Linters and Annotators

* ci dir fix

* Missing undef fixed

* Enable grep checks

* More test conversions

* More split

* Correct file

* Removes unneeded inputs

* oop

* More dependency changes

* More conversions

* Conversion fixes

* Fixes

* Some assert fixes

* Corrects start gate

* Converted some README.dms to README.mds

* Removes duplicate proc

* Removes unused defines

* Example configs

* fix dll access viol by double calling

* Post-rebase fixes

* Cleans up names global list

* Undef restart counter

* More code/game/ cleanup

* Statpanel update

* Skybox

* add

* Fix ticker

* Roundend fix

* Persistence dependency update

* Reordering

* Reordering

* Reordering

* Initstage fix

* .

* .

* Reorder

* Reorder

* Circle

* Mobs

* Air

* Test fix

* CI Script Fix

* Configs

* More ticker stuff

* This is now in 'reboot world'

* Restart world announcements

* no glob in PreInit

* to define

* Update

* Removed old include

* Make this file normal again

* moved

* test

* shared unit testing objects

* Updates batched_spritesheets and universal_icon

* .

* job data debug

* rm that

* init order

* show us

* .

* i wonder

* .

* .

* urg

* do we not have a job ID?

* .

* rm sleep for now

* updated rust-g linux binaries

* binaries update 2

* binaries update 3

* testing something

* change that

* test something

* .

* .

* .

* locavar

* test

* move that

* .

* debug

* don't run this test

* strack trace it

* cleaner

* .

* .

* cras again

* also comment this out

* return to official rust g

* Update robot_icons.dm

* monitor the generation

* .

---------

Co-authored-by: Kashargul <144968721+Kashargul@users.noreply.github.com>
This commit is contained in:
Selis
2025-08-10 01:37:23 +02:00
committed by GitHub
co-authored by Kashargul
parent 5e54e17ebd
commit f4bf017921
382 changed files with 10193 additions and 4421 deletions
+83
View File
@@ -0,0 +1,83 @@
# Unit Tests
## What is unit testing?
Unit tests are automated code to verify that parts of the game work exactly as they should. For example, [a test to make sure that the amputation surgery actually amputates the limb](https://github.com/tgstation/tgstation/blob/e416283f162b86345a8623125ab866839b1ac40d/code/modules/unit_tests/surgeries.dm#L1-L13). These are ran every time a PR is made, and thus are very helpful for preventing bugs from cropping up in your code that would've otherwise gone unnoticed. For example, would you have thought to check [that beach boys would still work the same after editing pizza](https://github.com/tgstation/tgstation/pull/53641#issuecomment-691384934)? If you value your time, probably not.
On their most basic level, when `UNIT_TESTS` is defined, all subtypes of `/datum/unit_test` will have their `Run` proc executed. From here, if `Fail` is called at any point, then the tests will report as failed.
## How do I write one?
1. Find a relevant file.
All unit test related code is in `code/modules/unit_tests`. If you are adding a new test for a surgery, for example, then you'd open `surgeries.dm`. If a relevant file does not exist, simply create one in this folder, then `#include` it in `_unit_tests.dm`.
2. Create the unit test.
To make a new unit test, you simply need to define a `/datum/unit_test`.
For example, let's suppose that we are creating a test to make sure a proc `square` correctly raises inputs to the power of two. We'd start with first:
```
/datum/unit_test/square/Run()
```
This defines our new unit test, `/datum/unit_test/square`. Inside this function, we're then going to run through whatever we want to check. Tests provide a few assertion functions to make this easy. For now, we're going to use `TEST_ASSERT_EQUAL`.
```
/datum/unit_test/square/Run()
TEST_ASSERT_EQUAL(square(3), 9, "square(3) did not return 9")
TEST_ASSERT_EQUAL(square(4), 16, "square(4) did not return 16")
```
As you can hopefully tell, we're simply checking if the output of `square` matches the output we are expecting. If the test fails, it'll report the error message given as well as whatever the actual output was.
3. Run the unit test
Open `code/_compile_options.dm` and uncomment the following line.
```
//#define UNIT_TESTS //If this is uncommented, we do a single run though of the game setup and tear down process with unit tests in between
```
There are 3 ways to run unit tests
- Run tgstation.dmb in Dream Daemon. Don't bother trying to connect, you won't need to. You'll be able to see the outputs of all the tests. You'll get to see which tests failed and for what reason. If they all pass, you're set!
- Launch game from VS Code. Launch the game as normal & you will see the output of your unit tests in your fancy chat window. This is preferred as you can use the debugger to step through each line of your unit test & can use the games inbuilt debugging tools to further aid in testing
- Use VS Code Tgstation Test Explorer Extension. This allows you to run tests without launching the game & can also run focused tests(either a single or a selected group)
## How to think about tests
Unit tests exist to prevent bugs that would happen in a real game. Thus, they should attempt to emulate the game world wherever possible. For example, the [quick swap sanity test](https://github.com/tgstation/tgstation/blob/e416283f162b86345a8623125ab866839b1ac40d/code/modules/unit_tests/quick_swap_sanity.dm) emulates a _real_ scenario of the bug it fixed occurring by creating a character and giving it real items. The unrecommended alternative would be to create special test-only items. This isn't a hard rule, the [reagent method exposure tests](https://github.com/tgstation/tgstation/blob/e416283f162b86345a8623125ab866839b1ac40d/code/modules/unit_tests/reagent_mod_expose.dm) create a test-only reagent for example, but do keep it in mind.
Unit tests should also be just that--testing _units_ of code. For example, instead of having one massive test for reagents, there are instead several smaller tests for testing exposure, metabolization, etc.
## The unit testing API
You can find more information about all of these from their respective doc comments, but for a brief overview:
`/datum/unit_test` - The base for all tests to be ran. Subtypes must override `Run()`. `New()` and `Destroy()` can be used for setup and teardown. To fail, use `TEST_FAIL(reason)`.
`/datum/unit_test/proc/allocate(type, ...)` - Allocates an instance of the provided type with the given arguments. Is automatically destroyed when the test is over. Commonly seen in the form of `var/mob/living/carbon/human/human = allocate(/mob/living/carbon/human/consistent)`.
`TEST_FAIL(reason)` - Marks a failure at this location, but does not stop the test.
`TEST_ASSERT(assertion, reason)` - Stops the unit test and fails if the assertion is not met. For example: `TEST_ASSERT(powered(), "Machine is not powered")`.
`TEST_ASSERT_NOTNULL(a, message)` - Same as `TEST_ASSERT`, but checks if `!isnull(a)`. For example: `TEST_ASSERT_NOTNULL(myatom, "My atom was never set!")`.
`TEST_ASSERT_NULL(a, message)` - Same as `TEST_ASSERT`, but checks if `isnull(a)`. If not, gives a helpful message showing what `a` was. For example: `TEST_ASSERT_NULL(delme, "Delme was never cleaned up!")`.
`TEST_ASSERT_EQUAL(a, b, message)` - Same as `TEST_ASSERT`, but checks if `a == b`. If not, gives a helpful message showing what both `a` and `b` were. For example: `TEST_ASSERT_EQUAL(2 + 2, 4, "The universe is falling apart before our eyes!")`.
`TEST_ASSERT_NOTEQUAL(a, b, message)` - Same as `TEST_ASSERT_EQUAL`, but reversed.
`TEST_FOCUS(test_path)` - _Only_ run the test provided within the parameters. Useful for reducing noise. For example, if we only want to run our example square test, we can add `TEST_FOCUS(/datum/unit_test/square)`. Should _never_ be pushed in a pull request--you will be laughed at.
## Final Notes
- Writing tests before you attempt to fix the bug can actually speed up development a lot! It means you don't have to go in game and folllow the same exact steps manually every time. This process is known as "TDD" (test driven development). Write the test first, make sure it fails, _then_ start work on the fix/feature, and you'll know you're done when your tests pass. If you do try this, do make sure to confirm in a non-testing environment just to double check.
- Make sure that your tests don't accidentally call RNG functions like `prob`. Since RNG is seeded during tests, you may not realize you have until someone else makes a PR and the tests fail!
- Do your best not to change the behavior of non-testing code during tests. While it may sometimes be necessary in the case of situations such as the above, it is still a slippery slope that can lead to the code you're testing being too different from the production environment to be useful.
+129
View File
@@ -0,0 +1,129 @@
//include unit test files in this module in this ifdef
//Keep this sorted alphabetically
#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"]", __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
#define TEST_ASSERT_EQUAL(a, b, message) do { \
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]" : ""]", __FILE__, __LINE__); \
} \
} while (FALSE)
/// Asserts that the two parameters passed are not equal, fails otherwise
/// Optionally allows an additional message in the case of a failure
#define TEST_ASSERT_NOTEQUAL(a, b, message) do { \
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]" : ""]", __FILE__, __LINE__); \
} \
} while (FALSE)
/// *Only* run the test provided within the parentheses
/// This is useful for debugging when you want to reduce noise, but should never be pushed
/// Intended to be used in the manner of `TEST_FOCUS(/datum/unit_test/math)`
#define TEST_FOCUS(test_path) ##test_path { focus = TRUE; }
/// Logs a noticable message on GitHub, but will not mark as an error.
/// Use this when something shouldn't happen and is of note, but shouldn't block CI.
/// Does not mark the test as failed.
#define TEST_NOTICE(source, message) source.log_for_test((##message), "notice", __FILE__, __LINE__)
/// Constants indicating unit test completion status
#define UNIT_TEST_PASSED 0
#define UNIT_TEST_FAILED 1
#define UNIT_TEST_SKIPPED 2
#define TEST_PRE 0
#define TEST_DEFAULT 1
/// 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 one of last tests 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 9001
/**
* For tests that rely on create and destroy having iterated through every (tangible) atom so they don't have to do something similar.
* Keep in mind tho that create and destroy will absolutely break the test platform, anything that relies on its shape cannot come after it.
*/
#define TEST_AFTER_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
/// Change color to yellow on ANSI terminal output, if enabled with -DANSICOLORS.
#ifdef ANSICOLORS
#define TEST_OUTPUT_YELLOW(text) "\x1B\x5B1;33m[text]\x1B\x5B0m"
#else
#define TEST_OUTPUT_YELLOW(text) (text)
#endif
/// A trait source when adding traits through unit tests
#define TRAIT_SOURCE_UNIT_TESTS "unit_tests"
/// Helper to allocate a new object with the implied type (the type of the variable it's assigned to) in the corner of the test room
#define EASY_ALLOCATE(arguments...) allocate(__IMPLIED_TYPE__, run_loc_floor_bottom_left, ##arguments)
// BEGIN_INCLUDE
#include "asset_smart_cache.dm"
//#include "clothing_tests.dm" // FIXME
#include "component_tests.dm"
#include "cosmetic_tests.dm"
#include "dcs_check_list_arguments.dm"
#include "dcs_get_id_from_elements.dm"
#include "decl_tests.dm"
#include "disease_tests.dm"
#include "focus_only_tests.dm"
#include "font_awesome_icons.dm"
#include "genetics_tests.dm"
#include "language_tests.dm"
#include "loadout_tests.dm"
#include "map_tests.dm"
#include "material_tests.dm"
// #include "nuke_cinematic.dm" // TODO: This is probably fixed later on
#include "poster_tests.dm"
// #include "preferences.dm" // This unit test is missing some other stuff
#include "reagent_tests.dm"
#include "recipe_tests.dm"
#include "recycler_vendor_tests.dm"
#include "robot_tests.dm"
#include "spritesheets.dm"
#include "sqlite_tests.dm"
#include "subsystem_init.dm"
#include "tgui_create_message.dm"
#include "timer_sanity.dm"
#include "trait_tests.dm"
#include "unit_test.dm"
// #include "vore_tests.dm" // FIXME: REWRITE OR FIX THIS
// END_INCLUDE
#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 - 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
@@ -0,0 +1,63 @@
/datum/asset/spritesheet_batched/test
name = "test"
load_immediately = TRUE
force_cache = TRUE
// Don't let the asset subsystem load this. This is how we trick it.
_abstract = /datum/asset/spritesheet_batched/test
var/static/list/items = list(/obj/item/binoculars, /obj/item/camera, /obj/item/clothing/under/color/blue, /obj/item/clothing/under/color/black)
/datum/asset/spritesheet_batched/test/create_spritesheets()
for(var/atom/item as anything in items)
if (!ispath(item, /atom))
return FALSE
var/imgid = replacetext(replacetext("[item]", "/obj/item/", ""), "/", "-")
insert_icon(imgid, get_display_icon_for(item))
// Get some coverage on each operation.
var/datum/universal_icon/I = uni_icon('icons/effects/effects.dmi', "nothing")
I.blend_icon(uni_icon('icons/effects/effects.dmi', "sparks"), ICON_OVERLAY)
I.blend_color("#ff0000", ICON_MULTIPLY)
I.scale(64, 64)
I.crop(1, 1, 128, 64) // we'll test for the scale later.
insert_icon("test", I)
/datum/asset/spritesheet_batched/test/unregister()
SSassets.transport.unregister_asset("spritesheet_[name].css")
if(length(sizes))
for(var/size_id in sizes)
SSassets.transport.unregister_asset("[name]_[size_id].png")
/datum/unit_test/test_asset_smart_cache/Run()
fdel("[ASSET_CROSS_ROUND_SMART_CACHE_DIRECTORY]/spritesheet_cache.test.json")
fdel("data/spritesheets/spritesheet_test.css")
var/datum/asset/spritesheet_batched/test/sheet = new()
TEST_ASSERT(sheet.fully_generated, "Spritesheet not generated!")
// Cache should be invalid initially.
TEST_ASSERT(sheet.cache_result, "Spritesheet smart cache was VALID when it should be INVALID!")
for(var/item in sheet.items)
var/imgid = replacetext(replacetext("[item]", "/obj/item/", ""), "/", "-")
// All items should be in sprites list.
TEST_ASSERT(imgid in sheet.sprites, "Item [item] not present in spritesheet result!")
TEST_ASSERT("test" in sheet.sprites, "Item test not present in spritesheet result!")
TEST_ASSERT("128x64" in sheet.sizes, "Test icon was not output as 128x64!")
// cache wrote properly
TEST_ASSERT(fexists("[ASSET_CROSS_ROUND_SMART_CACHE_DIRECTORY]/spritesheet_cache.test.json"), "Smart cache entry did not write!")
// Clear it out and get ready to do it again, this time loading from cache
sheet.unregister()
sheet.entries = list()
sheet.sprites = list()
sheet.sizes = list()
sheet.job_id = null
sheet.cache_result = null
sheet.cache_data = null
sheet.cache_job_id = null
sheet.fully_generated = FALSE
sheet.register()
TEST_ASSERT(sheet.fully_generated, "Spritesheet did not load from smart cache properly!")
// Check for CACHE_VALID
TEST_ASSERT(!sheet.cache_result, "Spritesheet did not load from smart cache, it was invalid despite having the same input data!")
// Cleanup files.
fdel("[ASSET_CROSS_ROUND_SMART_CACHE_DIRECTORY]/spritesheet_cache.test.json")
fdel("data/spritesheets/spritesheet_test.css")
for(var/size in sheet.sizes)
fdel("data/spritesheets/test_[size].png")
+171
View File
@@ -0,0 +1,171 @@
/// converted unit test, maybe should be fully refactored
/// MIGHT REQUIRE BIGGER REWORK
/// Test that checks if all clothing is valid
/datum/unit_test/all_clothing_shall_be_valid
var/signal_failed = FALSE
/datum/unit_test/all_clothing_shall_be_valid/Run()
var/failed = 0
var/obj/storage = new()
var/list/scan = subtypesof(/obj/item/clothing)
scan -= typesof(/obj/item/clothing/head/hood) // These are part of clothing, need to be tested uniquely
// Remove material armors, as dev_warning cannot be used to set their name
scan -= /obj/item/clothing/suit/armor/material
scan -= /obj/item/clothing/head/helmet/material
scan -= /obj/item/clothing/ears/offear // This is used for equip logic, not ingame
scan -= /obj/item/clothing/mask/ai // Breaks unit test entirely TODO
var/i = 0
var/tenths = 1
var/a_tenth = scan.len / 10
for(var/path as anything in scan)
var/obj/item/clothing/C = new path(storage)
failed += test_clothing(C)
if(i > tenths * a_tenth)
//TEST_NOTICE("Clothing - Progress [tenths * 10]% - [i]/[scan.len]")
//TEST_NOTICE("---------------------------------------------------")
tenths++
if(istype(C,/obj/item/clothing/suit/storage/hooded))
var/obj/item/clothing/suit/storage/hooded/H = C
if(H.hood) // Testing hoods when they init
failed += test_clothing(H.hood,storage)
i++
qdel(C)
qdel(storage)
if(failed)
TEST_FAIL("One or more /obj/item/clothing items had invalid flags or icons")
/datum/unit_test/all_clothing_shall_be_valid/proc/test_clothing(var/obj/item/clothing/C,var/obj/storage)
var/failed = FALSE
// Do not test base-types
if(C.name == DEVELOPER_WARNING_NAME)
return FALSE
// ID
TEST_ASSERT(C.name, "[C.type]: Clothing - Missing name.")
TEST_ASSERT(C.name != "", "[C.type]: Clothing - Empty name.")
// Icons
if(!("[C.icon_state]" in cached_icon_states(C.icon)))
if(C.icon == initial(C.icon) && C.icon_state == initial(C.icon_state))
TEST_NOTICE("[C.type]: Clothing - Icon_state \"[C.icon_state]\" is not present in [C.icon].")
else
TEST_NOTICE("[C.type]: Clothing - Icon_state \"[C.icon_state]\" is not present in [C.icon]. This icon/state was changed by init. Initial icon \"[initial(C.icon)]\". initial icon_state \"[initial(C.icon_state)]\". Check code.")
failed = TRUE
// Disabled, as currently not working in a presentable way, spams the CI hard, do not enable unless fixed
#ifdef UNIT_TEST
// Time for the most brutal part. Dressing up some mobs with set species, and checking they have art
// An entire signal just for unittests had to be made for this!
var/list/body_types = list(SPECIES_HUMAN,SPECIES_VOX,SPECIES_TESHARI) // Otherwise we would be here for centuries
// **************************************************************************************************************************
body_types = list() // DISABLED FOR NOW, No single person can resolve how many sprites are missing.
// **************************************************************************************************************************
if(body_types.len)
if(C.species_restricted && C.species_restricted.len)
if(C.species_restricted[1] == "exclude")
for(var/B in body_types)
if(B in C.species_restricted)
body_types -= B
else
var/list/new_list = list()
for(var/B in body_types)
if(B in C.species_restricted)
new_list += B
body_types = new_list
// Get actual species that can use this, based on the mess of restricted/excluded logic above
var/obj/mob_storage = new()
var/mob/living/carbon/human/H = new(mob_storage)
RegisterSignal(H, COMSIG_UNITTEST_DATA, PROC_REF(get_signal_data))
for(var/B in body_types)
H.set_species(B)
// spawn the mob, signalize it, and then give it the item to see what it gets.
H.put_in_active_hand(C)
H.equip_to_appropriate_slot(C)
H.drop_from_inventory(C, storage)
UnregisterSignal(H, COMSIG_UNITTEST_DATA)
qdel(H)
qdel(mob_storage)
// We failed the mob check
if(signal_failed)
failed = TRUE
#endif
// Temps
TEST_ASSERT(C.min_cold_protection_temperature > 0, "[C.type]: Clothing - Cold protection was lower than 0.")
if(C.max_heat_protection_temperature && C.min_cold_protection_temperature && C.max_heat_protection_temperature < C.min_cold_protection_temperature)
TEST_NOTICE("[C.type]: Clothing - Maximum heat protection was greater than minimum cold protection.")
failed = TRUE
//var/valid_range = HEAD|UPPER_TORSO|LOWER_TORSO|LEGS|FEET|ARMS|HANDS
if(C.cold_protection)
if(islist(C.cold_protection))
TEST_NOTICE("[C.type]: Clothing - cold_protection was defined as a list, when it is a bitflag.")
failed = TRUE
else if(!isnum(C.cold_protection))
TEST_NOTICE("[C.type]: Clothing - cold_protection was defined as something other than a number, when it is a bitflag.")
failed = TRUE
else
if(C.cold_protection && C.cold_protection != FULL_BODY)
// Check flags that should be unused
if(C.cold_protection & FACE)
TEST_NOTICE("[C.type]: Clothing - cold_protection uses FACE bitflag, this provides no protection, use HEAD.")
failed = TRUE
if(C.cold_protection & EYES)
TEST_NOTICE("[C.type]: Clothing - cold_protection uses EYES bitflag, this provides no protection, use HEAD.")
failed = TRUE
if(C.heat_protection)
if(islist(C.heat_protection))
TEST_NOTICE("[C.type]: Clothing - heat_protection was defined as a list, when it is a bitflag.")
failed = TRUE
else if(!isnum(C.heat_protection))
TEST_NOTICE("[C.type]: Clothing - heat_protection was defined as something other than a number, when it is a bitflag.")
failed = TRUE
else
if(C.heat_protection && C.heat_protection != FULL_BODY)
// Check flags that should be unused
if(C.heat_protection & FACE)
TEST_NOTICE("[C.type]: Clothing - heat_protection uses FACE bitflag, this provides no protection, use HEAD.")
failed = TRUE
if(C.heat_protection & EYES)
TEST_NOTICE("[C.type]: Clothing - heat_protection uses EYES bitflag, this provides no protection, use HEAD.")
failed = TRUE
return failed
/datum/unit_test/all_clothing_shall_be_valid/get_signal_data(atom/source, list/data = list())
switch(data[1])
if("set_slot")
var/slot_name = data[2]
var/set_icon = data[3]
var/set_state = data[4]
//var/in_hands = data[5]
var/item_path = data[6]
var/species = data[7]
if(!species)
return
if(!set_icon)
return
if(!set_state)
return
// Ignore storage
if(slot_name == slot_l_hand_str)
return
if(slot_name == slot_r_hand_str)
return
// All that matters
if(!("[set_state]" in cached_icon_states(set_icon)))
TEST_NOTICE("[item_path]: Clothing - Testing \"[species]\" state \"[set_state]\" for slot \"[slot_name]\", but it was not in dmi \"[set_icon]\"")
signal_failed = TRUE
return
@@ -0,0 +1,7 @@
/datum/unit_test/component_duping/Run()
var/list/bad_dms = list()
for(var/t in typesof(/datum/component))
var/datum/component/comp = t
if(!isnum(initial(comp.dupe_mode)))
bad_dms += t
TEST_ASSERT(!length(bad_dms), "Components with invalid dupe modes: ([bad_dms.Join(",")])")
+60
View File
@@ -0,0 +1,60 @@
/// converted unit test, maybe should be fully refactored
/// MIGHT REQUIRE BIGGER REWORK
/// Test that tests that all cosmetics have unique name entries
/datum/unit_test/sprite_accessories_shall_be_unique
/datum/unit_test/sprite_accessories_shall_be_unique/Run()
validate_accessory_list(/datum/sprite_accessory/ears)
validate_accessory_list(/datum/sprite_accessory/facial_hair)
validate_accessory_list(/datum/sprite_accessory/hair)
validate_accessory_list(/datum/sprite_accessory/hair_accessory)
validate_accessory_list(/datum/sprite_accessory/marking)
validate_accessory_list(/datum/sprite_accessory/tail)
validate_accessory_list(/datum/sprite_accessory/wing)
/datum/unit_test/sprite_accessories_shall_be_unique/proc/validate_accessory_list(var/path)
var/list/collection = list()
for(var/SP in subtypesof(path))
var/datum/sprite_accessory/A = new SP()
TEST_ASSERT(A, "[SP]: Cosmetic - Path resolved to null in list.")
if(!A)
continue
TEST_ASSERT(A.name, "[A] - [A.type]: Cosmetic - Missing name.")
if(A.name == DEVELOPER_WARNING_NAME)
continue
TEST_ASSERT(!collection[A.name], "[A] - [A.type]: Cosmetic - Name defined twice. Original def [collection[A.name]]")
if(!collection[A.name])
collection[A.name] = A.type
if(istype(A,text2path("[path]/invisible")))
TEST_ASSERT(!A.icon_state, "[A] - [A.type]: Cosmetic - Invisible subtype has icon_state.")
else if(!A.icon_state)
TEST_ASSERT(A.icon_state, "[A] - [A.type]: Cosmetic - Has no icon_state.")
else
// Check if valid icon
validate_icons(A)
qdel(A)
/datum/unit_test/sprite_accessories_shall_be_unique/proc/validate_icons(datum/sprite_accessory/A)
var/actual_icon_state = A.icon_state
if(istype(A,/datum/sprite_accessory/hair))
actual_icon_state = "[A.icon_state]_s"
TEST_ASSERT(actual_icon_state in cached_icon_states(A.icon), "[A] - [A.type]: Cosmetic - Icon_state \"[actual_icon_state]\" is not present in [A.icon].")
if(istype(A,/datum/sprite_accessory/facial_hair))
actual_icon_state = "[A.icon_state]_s"
TEST_ASSERT(actual_icon_state in cached_icon_states(A.icon), "[A] - [A.type]: Cosmetic - Icon_state \"[actual_icon_state]\" is not present in [A.icon].")
if(istype(A,/datum/sprite_accessory/marking))
var/datum/sprite_accessory/marking/MA = A
for(var/BP in MA.body_parts)
TEST_ASSERT(BP in BP_ALL, "[A] - [A.type]: Cosmetic - Has an illegal bodypart \"[BP]\". ONLY use parts listed in BP_ALL.")
actual_icon_state = "[A.icon_state]-[BP]"
TEST_ASSERT(actual_icon_state in cached_icon_states(A.icon), "[A] - [A.type]: Cosmetic - Icon_state \"[actual_icon_state]\" is not present in [A.icon].")
@@ -0,0 +1,55 @@
/**
* list arguments for bespoke elements are treated as a text ref in the ID, like any other datum.
* Which means that, unless cached, using lists as arguments will lead to multiple instance of the same element
* being created over and over.
*
* Because of how it works, this unit test checks that these list datum args
* do not share similar contents (when rearranged in descending alpha-numerical order), to ensure that
* the least necessary amount of elements is created. So, using static lists may not be enough,
* for example, in the case of two different critters using the death_drops element to drop ectoplasm on death, since,
* despite being static lists, the two are different instances assigned to different mob types.
*
* Most of the time, you won't encounter two different static lists with similar contents used as element args,
* meaning using static lists is accepted. However, should that happen, it's advised to replace the instances
* with either string_list(), string_assoc_list(), string_assoc_nested_list() or string_numbers_list(), depending on the contents of the list.
*
* In the case of an element where the position of the contents of each datum list argument is important,
* ELEMENT_DONT_SORT_LIST_ARGS should be added to its flags, to prevent such issues where the contents are similar
* when sorted, but the element instances are not.
*
* In the off-chance the element is not compatible with this unit test (such as for connect_loc et simila),
* you can also use ELEMENT_NO_LIST_UNIT_TEST so that they won't be processed by this unit test at all.
*/
/datum/unit_test/dcs_check_list_arguments
/**
* This unit test requires every (unless ignored) atom to have been created at least once
* for a more accurate search, which is why it's run after create_and_destroy is done running.
*/
priority = TEST_AFTER_CREATE_AND_DESTROY
/datum/unit_test/dcs_check_list_arguments/Run()
var/we_failed = FALSE
for(var/element_type in SSdcs.arguments_that_are_lists_by_element)
// Keeps track of the lists that shouldn't be compared with again.
var/list/to_ignore = list()
var/list/superlist = SSdcs.arguments_that_are_lists_by_element[element_type]
for(var/list/current as anything in superlist)
to_ignore[current] = TRUE
var/list/bad_lists
for(var/list/compare as anything in superlist)
if(to_ignore[compare])
continue
if(deep_compare_list(current, compare))
if(!bad_lists)
bad_lists = list(list(current))
bad_lists += list(compare)
to_ignore[compare] = TRUE
if(bad_lists)
we_failed = TRUE
//Include the original, unsorted list in the report. It should be easier to find by the contributor.
var/list/unsorted_list = superlist[current]
TEST_FAIL("Found [length(bad_lists)] datum list arguments with similar contents for [element_type]. Contents: [json_encode(unsorted_list)].")
///Let's avoid sending the same instructions over and over, as it's just going to clutter the CI and confuse someone.
if(we_failed)
TEST_FAIL("Ensure that each list is static or cached. string_list() (as well as similar procs) is your friend here.\n\
Check the documentation from dcs_check_list_arguments.dm for more information!")
@@ -0,0 +1,46 @@
/// Tests that DCS' GetIdFromArguments works as expected with standard and odd cases
/datum/unit_test/dcs_get_id_from_arguments
/datum/unit_test/dcs_get_id_from_arguments/Run()
assert_equal(list(1), list(1))
assert_equal(list(1, 2), list(1, 2))
assert_equal(list(src), list(src))
assert_equal(
list(a = "x", b = "y", c = "z"),
list(b = "y", a = "x", c = "z"),
list(c = "z", a = "x", b = "y"),
)
TEST_ASSERT_NOTEQUAL(get_id_from_arguments(list(1, 2)), get_id_from_arguments(list(2, 1)), "Swapped arguments should not return the same id")
TEST_ASSERT_NOTEQUAL(get_id_from_arguments(list(1, a = "x")), get_id_from_arguments(list(1)), "Named arguments were ignored when creating ids")
TEST_ASSERT_NOTEQUAL(get_id_from_arguments(list(1, a = "x")), get_id_from_arguments(list(a = "x")), "Unnamed arguments were ignored when creating ids")
TEST_ASSERT_NOTEQUAL(get_id_from_arguments(list(src)), get_id_from_arguments(list(world)), "References to different datums should not return the same id")
TEST_ASSERT_NOTEQUAL(get_id_from_arguments(list()), SSdcs.GetIdFromArguments(list(/datum/element/dcs_get_id_from_arguments_mock_element2)), "Different elements should not match the same id")
/datum/unit_test/dcs_get_id_from_arguments/proc/assert_equal(reference, ...)
var/result = get_id_from_arguments(reference)
// Start at 1 so the 2nd argument is 2
var/index = 1
for (var/other_case in args)
index += 1
var/other_result = get_id_from_arguments(other_case)
if (other_result == result)
continue
TEST_FAIL("Case #[index] produces a different GetIdFromArguments result from the first. [other_result] != [result]")
/datum/unit_test/dcs_get_id_from_arguments/proc/get_id_from_arguments(list/arguments)
return SSdcs.GetIdFromArguments(list(/datum/element/dcs_get_id_from_arguments_mock_element) + arguments)
// Necessary because GetIdFromArguments uses argument_hash_start_idx from an element type
/datum/element/dcs_get_id_from_arguments_mock_element
argument_hash_start_idx = 2
/datum/element/dcs_get_id_from_arguments_mock_element2
argument_hash_start_idx = 2
+19
View File
@@ -0,0 +1,19 @@
/// converted unit test, maybe should be fully refactored
/datum/unit_test/emotes_shall_have_unique_keys/Run()
var/list/keys = list()
var/list/duplicates = list()
var/list/all_emotes = decls_repository.get_decls_of_subtype(/decl/emote)
for(var/etype in all_emotes)
var/decl/emote/emote = all_emotes[etype]
if(!emote.key)
continue
if(emote.key in keys)
if(!duplicates[emote.key])
duplicates[emote.key] = list()
duplicates[emote.key] += etype
else
keys += emote.key
TEST_ASSERT(!length(duplicates), "[length(duplicates)] emote\s had overlapping keys: [english_list(duplicates)].")
+17
View File
@@ -0,0 +1,17 @@
/// converted unit test, maybe should be fully refactored
/datum/unit_test/disease_tests/Run()
var/list/used_ids = list()
for(var/datum/disease/D as anything in subtypesof(/datum/disease))
if(initial(D.name) == DEVELOPER_WARNING_NAME)
continue
TEST_ASSERT(!(initial(D.medical_name) in used_ids), "[D]: Disease - Had a reused medical name, this is used as an ID and must be unique.")
used_ids.Add(initial(D.medical_name))
TEST_ASSERT_NOTNULL(initial(D.name), "[D]: Disease - Lacks a name.")
TEST_ASSERT_NOTEQUAL(initial(D.name), "", "[D]: Disease - Lacks a name.")
TEST_ASSERT_NOTNULL(initial(D.desc), "[D]: Disease - Lacks a description.")
TEST_ASSERT_NOTEQUAL(initial(D.desc), "", "[D]: Disease - Lacks a description.")
@@ -0,0 +1,152 @@
///Used to test the completeness of the reference finder proc.
/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 ..()
/atom/movable/ref_test
// Gotta make sure we do a full check
references_to_clear = INFINITY
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
var/refcount = refcount(victim)
TEST_ASSERT_EQUAL(refcount, 3, "Should be: test references: 0 + baseline references: 3 (victim var,loc,allocated list)")
victim.DoSearchVar(testbed, "Sanity Check") //We increment search time to get around an optimization
TEST_ASSERT(!LAZYLEN(victim.found_refs), "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
var/refcount = refcount(victim)
TEST_ASSERT_EQUAL(refcount, 6, "Should be: test references: 3 + baseline references: 3 (victim var,loc,allocated list)")
victim.DoSearchVar(testbed, "First Run")
TEST_ASSERT(LAZYACCESS(victim.found_refs, "test"), "The ref-tracking tool failed to find a regular value")
TEST_ASSERT(LAZYACCESS(victim.found_refs, testbed.test_list), "The ref-tracking tool failed to find a list entry")
TEST_ASSERT(LAZYACCESS(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
var/refcount = refcount(victim)
TEST_ASSERT_EQUAL(refcount, 6, "Should be: test references: 3 + baseline references: 3 (victim var,loc,allocated list)")
victim.DoSearchVar(testbed, "Second Run")
//This is another sanity check
TEST_ASSERT(!LAZYACCESS(victim.found_refs, testbed.overlays), "The ref-tracking tool found an overlays entry? That shouldn't be possible")
TEST_ASSERT(LAZYACCESS(victim.found_refs, testbed.vis_contents), "The ref-tracking tool failed to find a vis_contents entry")
TEST_ASSERT(LAZYACCESS(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
var/refcount = refcount(victim)
TEST_ASSERT_EQUAL(refcount, 6, "Should be: test references: 3 + baseline references: 3 (victim var,loc,allocated list)")
victim.DoSearchVar(victim, "Third Run Self")
victim.DoSearchVar(testbed, "Third Run Testbed")
TEST_ASSERT(LAZYACCESS(victim.found_refs, "self_ref"), "The ref-tracking tool failed to find a self reference")
TEST_ASSERT(LAZYACCESS(victim.found_refs, to_find), "The ref-tracking tool failed to find a nested list entry")
TEST_ASSERT(LAZYACCESS(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)
var/refcount = refcount(victim)
TEST_ASSERT_EQUAL(refcount, 4, "Should be: test references: 1 + baseline references: 3 (victim var,loc,allocated list)")
victim.DoSearchVar(testbed, "Fourth Run")
TEST_ASSERT(LAZYACCESS(victim.found_refs, 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
var/refcount = refcount(victim)
TEST_ASSERT_EQUAL(refcount, 5, "Should be: test references: 2 + baseline references: 3 (victim var,loc,allocated list)")
victim.DoSearchVar(testbed, "Fifth Run")
TEST_ASSERT(LAZYACCESS(victim.found_refs, to_find_in_key), "The ref-tracking tool failed to find a nested assoc list key")
TEST_ASSERT(LAZYACCESS(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_investigation/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]
var/refcount = refcount(victim)
TEST_ASSERT_EQUAL(refcount, 5, "Should be: test references: 2 + baseline references: 3 (victim var,loc,allocated list)")
victim.DoSearchVar(global_vars, "Sixth Run")
TEST_ASSERT(LAZYACCESS(victim.found_refs, global_vars), "The ref-tracking tool failed to find a natively global variable")
SSgarbage.should_save_refs = FALSE
@@ -0,0 +1,61 @@
/// These tests perform no behavior of their own, and have their tests offloaded onto other procs.
/// This is useful in cases like in build_appearance_list where we want to know if any fail,
/// but is not useful to right a test for.
/// This file exists so that you can change any of these to TEST_FOCUS and only check for that test.
/// For example, change /datum/unit_test/focus_only/invalid_overlays to TEST_FOCUS(/datum/unit_test/focus_only/invalid_overlays),
/// and you will only test the check for invalid overlays in appearance building.
/datum/unit_test/focus_only
/// Checks that every created emissive has a valid icon_state
/datum/unit_test/focus_only/invalid_emissives
/// Checks that every overlay passed into build_appearance_list exists in the icon
/datum/unit_test/focus_only/invalid_overlays
/// Checks that every icon sent to the research_designs spritesheet is valid
/datum/unit_test/focus_only/invalid_research_designs
/// Checks that every icon sent to vending machines is valid
/datum/unit_test/focus_only/invalid_vending_machine_icon_states
/// Checks that space does not initialize multiple times
/datum/unit_test/focus_only/multiple_space_initialization
/// Checks that smoothing_groups and canSmoothWith are properly sorted in /atom/Initialize
/datum/unit_test/focus_only/sorted_smoothing_groups
/// Checks that floor tiles are properly mapped to broken/burnt
/datum/unit_test/focus_only/valid_turf_states
/// Checks that nightvision eyes have a full set of color lists
/datum/unit_test/focus_only/nightvision_color_cutoffs
/// Checks that no light shares a tile/pixel offsets with another
/datum/unit_test/focus_only/stacked_lights
/// Checks for bad icon / icon state setups in cooking crafting menu
/datum/unit_test/focus_only/bad_cooking_crafting_icons
/// Ensures openspace never spawns on the bottom of a z stack
/datum/unit_test/focus_only/openspace_clear
/// Checks to ensure that variables expected to exist in a job datum (for config reasons) actually exist
/datum/unit_test/focus_only/missing_job_datum_variables
/// Checks that the contents of the fish_counts list are also present in fish_table
/datum/unit_test/focus_only/fish_sources_tables
/// Checks that maploaded mobs with either the `atmos_requirements` or `body_temp_sensitive`
/datum/unit_test/focus_only/atmos_and_temp_requirements
/// Ensures only whitelisted planes can have TOPDOWN_LAYERing, and vis versa
/datum/unit_test/focus_only/topdown_filtering
/// Catches any invalid footstep types set for humans
/datum/unit_test/focus_only/humanstep_validity
/// Checks icon states generated at runtime are valid
/datum/unit_test/focus_only/runtime_icon_states
/// Checks that foodtypes are the same for food whether it's spawned or crafted (with the exact required types)
/datum/unit_test/focus_only/check_foodtypes
@@ -0,0 +1,99 @@
/**
* This unit test verifies that all Font Awesome icons are present in code, and that all quirk icons are valid.
*/
/datum/unit_test/font_awesome_icons
var/font_awesome_css
var/list/allowed_icons
/datum/unit_test/font_awesome_icons/Run()
var/font_awesome_file = file('html/font-awesome/css/all.min.css')
if(isnull(font_awesome_file))
TEST_FAIL("Font Awesome CSS file could not be found!")
return
font_awesome_css = file2text(font_awesome_file)
if(isnull(font_awesome_css))
TEST_NOTICE(src, "Font Awesome CSS file could not be loaded.")
return
load_parse_verify()
//verify_quirk_icons()
generate_helper_dm_file()
/**
* Loads the Font Awesome CSS file, parses it into a list of icon names, and compares it to the list of icons in code.
* If there are any differences, note them.
*/
/datum/unit_test/font_awesome_icons/proc/load_parse_verify()
//log_test("CSS Actual: [length(font_awesome_css)]")
log_unit_test("CSS Actual: [length(font_awesome_css)]")
allowed_icons = parse_fa_css_into_icon_list(font_awesome_css)
/**
* Verifies that all quirk icons are valid.
*/
/* NOT IMPLEMENTED
/datum/unit_test/font_awesome_icons/proc/verify_quirk_icons()
for(var/datum/quirk/quirk as anything in subtypesof(/datum/quirk))
if(quirk == initial(quirk.abstract_parent_type))
continue
var/quirk_icon = initial(quirk.icon)
if(findtext(quirk_icon, "tg-") == 1) // TODO: Validate these as well
continue
if(findtext(quirk_icon, " "))
var/list/split = splittext(quirk_icon, " ")
quirk_icon = split[length(split)] // respect modifier classes
if(!(quirk_icon in allowed_icons))
TEST_FAIL("Quirk [initial(quirk.name)]([quirk]) has invalid icon: [quirk_icon]")
*/
/// Parses the given Font Awesome CSS file into a list of icon names.
/datum/unit_test/font_awesome_icons/proc/parse_fa_css_into_icon_list(css)
css = replacetext(css, "\n", "")
var/list/css_entries = splittext(css, "}")
var/list/icons = list()
for(var/entry in css_entries)
entry = replacetext(entry, "\t", "")
if(!length(entry))
continue
var/entry_contents = splittext(entry, "{")
var/list/entry_names = splittext(entry_contents[1], ",")
for(var/entry_name in entry_names)
entry_names -= entry_name
if(!findtext(entry_name, ":"))
continue
entry_name = splittext(entry_name, ":")[1]
if(!findtext(entry_name, ".fa-"))
continue
entry_name = replacetext(entry_name, ".fa-", "fa-")
entry_names |= entry_name
icons |= entry_names
return sortList(icons)
/datum/unit_test/font_awesome_icons/proc/generate_helper_dm_file()
var/list/output = list()
output += "/* This file is automatically generated by the unit test. Do not edit it manually."
output += " * Generating this file is done by running the unit test locally, see the fail message for more details."
output += " * All valid font awesome icons should be here."
output += " */"
output += ""
for(var/icon in allowed_icons)
var/icon_name = replacetext(icon, "fa-", "")
output += "#define FA_ICON_[uppertext(replacetext(icon_name, "-", "_"))] \"[icon]\"" // #undef FA_ICON_ // we have this here to avoid define_sanity throwing a fit
var/output_file = "[output.Join("\n")]\n"
rustg_file_write(output_file, "data/font_awesome_icons.dm")
var/current = file2text('code/__DEFINES/font_awesome_icons.dm')
if(current == output_file)
return
TEST_FAIL("Font Awesome helper file is out of date. Run locally by enabling unit tests, (see _compile_options.dm) and copy 'data/font_awesome_icons.dm' to 'code/__DEFINES/font_awesome_icons.dm'")
+69
View File
@@ -0,0 +1,69 @@
/// converted unit test, maybe should be fully refactored
/// Test that there are enough free gene slots available
/datum/unit_test/enough_free_gene_slots_must_be_available
/datum/unit_test/enough_free_gene_slots_must_be_available/Run()
// Based off of traitgenes scanned on startup
TEST_ASSERT(!(GLOB.dna_genes.len > (DNA_SE_LENGTH - 10)), "Too few geneslots are empty, minimum 10. Increase DNA_SE_LENGTH.")
/// Test that there is at least one positive gene
/datum/unit_test/enough_positive_genes_must_exist
/datum/unit_test/enough_positive_genes_must_exist/Run()
// Based off of traitgenes scanned on startup
TEST_ASSERT(!(GLOB.dna_genes_good.len < 1), "Must have at least one positive gene.")
/// Test that there is at least one neutral gene
/datum/unit_test/enough_neutral_genes_must_exist
/datum/unit_test/enough_neutral_genes_must_exist/Run()
// Based off of traitgenes scanned on startup
TEST_ASSERT(!(GLOB.dna_genes_neutral.len < 1), "Must have at least one neutral gene.")
/// Test that there is at least one bad gene
/datum/unit_test/enough_bad_genes_must_exist
/datum/unit_test/enough_bad_genes_must_exist/Run()
// Based off of traitgenes scanned on startup
TEST_ASSERT(!(GLOB.dna_genes_bad.len < 1), "Must have at least one bad gene.")
/// Test that all dna injectors are valid
/datum/unit_test/all_dna_injectors_must_be_valid
/datum/unit_test/all_dna_injectors_must_be_valid/Run()
for(var/injector_path in subtypesof(/obj/item/dnainjector/set_trait))
var/obj/item/dnainjector/D = new injector_path()
TEST_ASSERT(D.block, "[injector_path]: Genetics - Injector could not resolve geneblock for trait. Missing traitgene?")
qdel(D)
/// Test that all genes have unique names
/datum/unit_test/all_genes_shall_have_unique_name
/datum/unit_test/all_genes_shall_have_unique_name/Run()
var/collection = list()
for(var/datum/gene/G in GLOB.dna_genes)
TEST_ASSERT(!collection[G.name], "[G.name]: Genetics - Gene name was already in use.")
collection[G.name] = G.name
/// Test that all genes should have valid activation bounds
/datum/unit_test/genetraits_should_have_valid_dna_bounds
/datum/unit_test/genetraits_should_have_valid_dna_bounds/Run()
for(var/datum/gene/trait/G in GLOB.trait_to_dna_genes)
TEST_ASSERT(G.linked_trait, "[G.name]: Genetics - Has missing linked trait.")
TEST_ASSERT(G.linked_trait.activity_bounds, "[G.name]: Genetics - Has no activation bounds.")
TEST_ASSERT(G.linked_trait.activity_bounds.len, "[G.name]: Genetics - Has empty activation bounds.")
// DNA activation bounds. Usually they are in a list as follows:
// [1]DNA_OFF_LOWERBOUND = 1, begining of the threshold where a gene turns off.
// [2]DNA_OFF_UPPERBOUND = a number above 1, end of the treshold where a gene turns off.
// [3]DNA_ON_LOWERBOUND = a number above DNA_OFF_UPPERBOUND(even if just by 1), threshold where a gene turns on.
// [4]DNA_ON_UPPERBOUND = 4095, end of the threshold where a gene turns on.
var/list/bounds = G.linked_trait.activity_bounds
TEST_ASSERT(bounds[1] > 1, "[G.name]: Genetics - DNA_OFF_LOWERBOUND, was smaller than 1.") // lowest value a gene can be to turn off
TEST_ASSERT(bounds[2] > bounds[1], "[G.name]: Genetics - DNA_OFF_UPPERBOUND must be larger than DNA_OFF_LOWERBOUND, and never equal.")
TEST_ASSERT(bounds[2] <= bounds[3], "[G.name]: Genetics - DNA_OFF_UPPERBOUND must be smaller than DNA_ON_LOWERBOUND, and never equal.")
TEST_ASSERT(bounds[3] < bounds[4], "[G.name]: Genetics - DNA_ON_LOWERBOUND must be smaller than DNA_ON_UPPERBOUND, and never equal.")
TEST_ASSERT(bounds[4] < 4095, "[G.name]: Genetics - DNA_ON_UPPERBOUND, was larger than 4095.") // highest value a gene can be to turn on
+25
View File
@@ -0,0 +1,25 @@
/// converted unit test, maybe should be fully refactored
/// Test that language entries have distinct names
/datum/unit_test/language_test_shall_have_distinct_names
/datum/unit_test/language_test_shall_have_distinct_names/Run()
if(length(GLOB.language_name_conflicts) != 0)
var/list/name_conflict_log = list()
for(var/conflicted_name in GLOB.language_name_conflicts)
name_conflict_log += "+[length(GLOB.language_name_conflicts[conflicted_name])] languages with name \"[conflicted_name]\"!"
for(var/datum/language/L in GLOB.language_name_conflicts[conflicted_name])
name_conflict_log += "+-+[L.type]"
TEST_FAIL("Some names are used by more than one language:\n" + name_conflict_log.Join("\n"))
/// Test that language entries have distinct keys
/datum/unit_test/language_test_shall_have_distinct_keys
/datum/unit_test/language_test_shall_have_distinct_keys/Run()
if(length(GLOB.language_key_conflicts) != 0)
var/list/key_conflict_log = list()
for(var/conflicted_key in GLOB.language_key_conflicts)
key_conflict_log += "+[length(GLOB.language_key_conflicts[conflicted_key])] languages with key \"[conflicted_key]\"!"
for(var/datum/language/L in GLOB.language_key_conflicts[conflicted_key])
key_conflict_log += "+-+[L]([L.type])"
TEST_FAIL("Some keys are used by more than one language:\n" + key_conflict_log.Join("\n"))
+7
View File
@@ -0,0 +1,7 @@
/// converted unit test, maybe should be fully refactored
/datum/unit_test/loadout_tests/Run()
for(var/datum/gear/G as anything in subtypesof(/datum/gear))
TEST_ASSERT(initial(G.display_name), "[G]: Loadout - Missing display name.")
TEST_ASSERT_NOTNULL(initial(G.cost), "[G]: Loadout - Missing cost.")
TEST_ASSERT(initial(G.path), "[G]: Loadout - Missing path definition.")
+189
View File
@@ -0,0 +1,189 @@
/// converted unit test, maybe should be fully refactored
/// MIGHT REQUIRE BIGGER REWORK
/// Test that tests the apcs, scrubbers and vents of the defined z-levels
/datum/unit_test/apc_area_test
/datum/unit_test/apc_area_test/Run()
var/list/exempt_areas = typesof(/area/space,
/area/syndicate_station,
/area/skipjack_station,
/area/solar,
/area/shuttle,
/area/holodeck,
/area/supply/station,
/area/mine,
/area/vacant/vacant_shop,
/area/turbolift,
/area/submap
)
var/list/exempt_from_atmos = typesof(/area/maintenance,
/area/storage,
/area/engineering/atmos/storage,
/area/rnd/test_area,
/area/construction,
/area/server,
/area/mine,
/area/vacant/vacant_shop,
/area/rnd/research_storage, // This should probably be fixed,
/area/security/riot_control, // This should probably be fixed,
)
var/list/exempt_from_apc = typesof(/area/construction,
/area/medical/genetics,
/area/mine,
/area/vacant/vacant_shop
)
// Some maps have areas specific to the map, so include those.
exempt_areas += using_map.unit_test_exempt_areas.Copy()
exempt_from_atmos += using_map.unit_test_exempt_from_atmos.Copy()
exempt_from_apc += using_map.unit_test_exempt_from_apc.Copy()
var/list/zs_to_test = using_map.unit_test_z_levels || list(1) //Either you set it, or you just get z1
for(var/area/A in world)
if((A.z in zs_to_test) && !(A.type in exempt_areas))
var/bad_msg = "--------------- [A.name]([A.type])"
// Scan for areas with extra APCs
if(!(A.type in exempt_from_apc))
TEST_ASSERT_NOTNULL(A.apc, "[bad_msg] lacks an APC. (X[A.x]|Y[A.y]) - Z[A.z])")
if(!isnull(A.apc))
var/list/apc_list = list()
for(var/turf/T in get_current_area_turfs(A))
for(var/atom/S in T.contents)
if(istype(S,/obj/machinery/power/apc))
apc_list.Add(S)
if(apc_list.len > 1)
for(var/obj/machinery/power/P in apc_list)
TEST_FAIL("[bad_msg] has too many APCs. (X[P.x]|Y[P.y]) - Z[P.z])")
TEST_ASSERT(!(!A.air_scrub_info.len && !(A.type in exempt_from_atmos)), "[bad_msg] lacks an Air scrubber. (X[A.x]|Y[A.y]) - (Z[A.z])")
TEST_ASSERT(!(!A.air_vent_info.len && !(A.type in exempt_from_atmos)), "[bad_msg] lacks an Air vent. (X[A.x]|Y[A.y]) - (Z[A.z])")
/// Test that tests cables on defined z-levels
/datum/unit_test/wire_test
var/wire_test_count = 0
var/turf/T = null
var/obj/structure/cable/C = null
var/list/cable_turfs = list()
var/list/dirs_checked = list()
var/list/exempt_from_wires = list()
/datum/unit_test/wire_test/Run()
set background = 1
exempt_from_wires += using_map.unit_test_exempt_from_wires.Copy()
var/list/zs_to_test = using_map.unit_test_z_levels || list(1) //Either you set it, or you just get z1
for(var/color in GLOB.possible_cable_coil_colours)
cable_turfs = list()
for(C in world)
T = null
T = get_turf(C)
var/area/A = get_area(T)
if(T && (T.z in zs_to_test) && !(A.type in exempt_from_wires))
if(C.color == GLOB.possible_cable_coil_colours[color])
cable_turfs |= get_turf(C)
for(T in cable_turfs)
var/bad_msg = "--------------- [T.name] \[[T.x] / [T.y] / [T.z]\] [color]"
dirs_checked.Cut()
for(C in T)
wire_test_count++
var/combined_dir = "[C.d1]-[C.d2]"
TEST_ASSERT(!(combined_dir in dirs_checked), "[bad_msg] Contains multiple wires with same direction on top of each other.")
TEST_ASSERT(C.dir == SOUTH, "[bad_msg] Contains wire with dir set, wires MUST face south, use icon_states.")
dirs_checked.Add(combined_dir)
/// Test template no-ops on all maps
/datum/unit_test/template_noops
var/list/log = list()
var/turf_noop_count = 0
/datum/unit_test/template_noops/Run()
for(var/turf/template_noop/T in world)
turf_noop_count++
log += "+-- Template Turf @ [T.x], [T.y], [T.z] ([T.loc])"
var/area_noop_count = 0
for(var/area/template_noop/A in world)
area_noop_count++
log += "+-- Template Area"
if(turf_noop_count || area_noop_count)
TEST_FAIL("Map contained [turf_noop_count] template turfs and [area_noop_count] template areas at round-start.\n" + log.Join("\n"))
/// Test active edges on all maps
/datum/unit_test/active_edges
/datum/unit_test/active_edges/Run()
var/active_edges = SSair.active_edges.len
var/list/edge_log = list()
if(active_edges)
for(var/connection_edge/E in SSair.active_edges)
var/a_temp = E.A.air.temperature
var/a_moles = E.A.air.total_moles
var/a_vol = E.A.air.volume
var/a_gas = ""
for(var/gas in E.A.air.gas)
a_gas += "[gas]=[E.A.air.gas[gas]]"
var/b_temp
var/b_moles
var/b_vol
var/b_gas = ""
// Two zones mixing
if(istype(E, /connection_edge/zone))
var/connection_edge/zone/Z = E
b_temp = Z.B.air.temperature
b_moles = Z.B.air.total_moles
b_vol = Z.B.air.volume
for(var/gas in Z.B.air.gas)
b_gas += "[gas]=[Z.B.air.gas[gas]]"
// Zone and unsimulated turfs mixing
if(istype(E, /connection_edge/unsimulated))
var/connection_edge/unsimulated/U = E
b_temp = U.B.temperature
b_moles = "Unsim"
b_vol = "Unsim"
for(var/gas in U.air.gas)
b_gas += "[gas]=[U.air.gas[gas]]"
edge_log += "Active Edge [E] ([E.type])"
edge_log += "Edge side A: T:[a_temp], Mol:[a_moles], Vol:[a_vol], Gas:[a_gas]"
edge_log += "Edge side B: T:[b_temp], Mol:[b_moles], Vol:[b_vol], Gas:[b_gas]"
for(var/turf/T in E.connecting_turfs)
edge_log += "+--- Connecting Turf [T] ([T.type]) @ [T.x], [T.y], [T.z] ([T.loc])"
if(active_edges)
TEST_FAIL("Maps contained [active_edges] active edges at round-start.\n" + edge_log.Join("\n"))
/// Test the ladders on the maps
/datum/unit_test/ladder_test
var/failed = FALSE
/datum/unit_test/ladder_test/Run()
for(var/obj/structure/ladder/L in world)
var/turf/T = get_turf(L)
TEST_ASSERT(T, "[L.x].[L.y].[L.z]: Map - Ladder on invalid turf")
if(!T)
continue
if(L.allowed_directions & UP)
TEST_ASSERT(L.target_up, "[T.x].[T.y].[T.z]: Map - Ladder allows upward movement, but had no ladder above it")
if(L.allowed_directions & DOWN)
TEST_ASSERT(L.target_down, "[T.x].[T.y].[T.z]: Map - Ladder allows downward movement, but had no ladder beneath it")
TEST_ASSERT(!T.density, "[L.x].[L.y].[L.z]: Map - Ladder is inside a wall")
+17
View File
@@ -0,0 +1,17 @@
/// converted unit test, maybe should be fully refactored
/// Test that a material should have all the name variables set
/datum/unit_test/materials_shall_have_names
/datum/unit_test/materials_shall_have_names/Run()
var/list/failures = list()
populate_material_list()
for(var/name in global.name_to_material)
var/datum/material/mat = global.name_to_material[name]
if(!mat)
continue // how did we get here?
if(!mat.display_name || !mat.use_name || !mat.sheet_singular_name || !mat.sheet_plural_name || !mat.sheet_collective_name)
failures[name] = mat.type
if(length(failures))
TEST_FAIL("[length(failures)] material\s had missing name strings: [english_list(failures)].")
+68
View File
@@ -0,0 +1,68 @@
// Some defines for tracking if the correct cinematic / animation is playing.
#define PLAYING_CORRECT_ANIMATION 2
#define PLAYING_INCORRECT_NUKE_ANIMATION 1
#define NOT_PLAYING_ANIMATION 0
/**
* Unit tests that a nuke going off plays a cinematic,
* and that it actually kills people.
*/
/datum/unit_test/nuke_cinematic
/// Used to track via signal if the correct cinematic / animation is playing.
var/cinematic_playing = NOT_PLAYING_ANIMATION
/// Tracks what typepath of cinematic is being played.
var/cinematic_playing_type
/datum/unit_test/nuke_cinematic/Run()
var/obj/machinery/nuclearbomb/syndicate/nuke = allocate(/obj/machinery/nuclearbomb/syndicate)
var/mob/living/carbon/human/nuked = allocate(/mob/living/carbon/human/consistent)
var/datum/client_interface/mock_client = new
nuked.mock_client = mock_client
mock_client.mob = nuked
var/obj/effect/landmark/observer_start/observer_point = locate(/obj/effect/landmark/observer_start) in landmarks_list
TEST_ASSERT_NOTNULL(observer_point, "Nuke cinematic test couldn't find observer spawn to place the nuke.")
var/turf/turf_on_station = get_turf(observer_point)
TEST_ASSERT(is_station_level(turf_on_station.z), "Nuke cinematic test didn't get a turf which was located on the station.")
nuke.forceMove(turf_on_station)
nuked.forceMove(turf_on_station)
// Pause the check so we don't, y'know, end the round
SSticker.roundend_check_paused = TRUE
RegisterSignal(SSdcs, COMSIG_GLOB_PLAY_CINEMATIC, PROC_REF(check_cinematic))
// actually_explode calls really_actually_explode which sleeps, so this will take a moment.
var/nuke_result = nuke.actually_explode()
TEST_ASSERT_EQUAL(nuke_result, DETONATION_HIT_STATION, "A nuke went off on station, but didn't return DETONATION_HIT_STATION (4). (Got: [nuke_result])")
TEST_ASSERT(GLOB.station_was_nuked, "A nuke went off on station, but didn't set station_was_nuked.")
// Reset the nuke var back so we don't end the round
GLOB.station_was_nuked = FALSE
SSticker.roundend_check_paused = FALSE
switch(cinematic_playing)
if(NOT_PLAYING_ANIMATION)
TEST_FAIL("No nuke cinematic was played when a nuke was detonated.")
if(PLAYING_INCORRECT_NUKE_ANIMATION)
TEST_FAIL("An incorrect cinematic was played on nuke detonation. (Expected: /datum/cinematic/nuke/self_destruct, Got: [cinematic_playing_type])")
TEST_ASSERT(QDELETED(nuked), "The nuke victim next to the nuke wasn't gibbed by the nuke.")
TEST_ASSERT(QDELETED(nuke), "The nuke itself was not deleted after successfully exploding.")
mock_client.mob = null
/// Used to track whenever a cinematic starts playing, so we can check if it's the right one.
/datum/unit_test/nuke_cinematic/proc/check_cinematic(datum/source, datum/cinematic/playing)
SIGNAL_HANDLER
cinematic_playing_type = playing.type
if(istype(playing, /datum/cinematic/nuke/self_destruct))
cinematic_playing = PLAYING_CORRECT_ANIMATION
else if(istype(playing, /datum/cinematic/nuke))
cinematic_playing = PLAYING_INCORRECT_NUKE_ANIMATION
#undef PLAYING_CORRECT_ANIMATION
#undef PLAYING_INCORRECT_NUKE_ANIMATION
#undef NOT_PLAYING_ANIMATION
+13
View File
@@ -0,0 +1,13 @@
/// converted unit test, maybe should be fully refactored
/datum/unit_test/posters_shall_have_legal_states/Run()
var/list/all_posters = decls_repository.get_decls_of_type(/decl/poster)
all_posters -= decls_repository.get_decl(/decl/poster/lewd) // Dumb exclusion for now. This really needs to become a valid poster instead of an illegaly made base type
for(var/path in all_posters)
var/decl/poster/D = all_posters[path]
var/obj/structure/sign/poster/P = /obj/structure/sign/poster // The base poster shows ALL subtypes except /lewd, so all posters should function here regardless!
var/icon/I = initial(P.icon)
if(D.icon_override)
I = D.icon_override
TEST_ASSERT(D.icon_state in cached_icon_states(I), "[D.type]: Poster - missing icon_state \"[D.icon_state]\" in \"[I]\", as [D.icon_override ? "override" : "base"] dmi.")
+77
View File
@@ -0,0 +1,77 @@
/// Requires all preferences to implement required methods.
/datum/unit_test/preferences_implement_everything
/datum/unit_test/preferences_implement_everything/Run()
var/datum/preferences/preferences = new(new /datum/client_interface)
var/mob/living/carbon/human/human = allocate(/mob/living/carbon/human/consistent)
for (var/preference_type in GLOB.preference_entries)
var/datum/preference/preference = GLOB.preference_entries[preference_type]
if (preference.savefile_identifier == PREFERENCE_CHARACTER)
preference.apply_to_human(human, preference.create_informed_default_value(preferences))
if (istype(preference, /datum/preference/choiced))
var/datum/preference/choiced/choiced_preference = preference
choiced_preference.init_possible_values()
// Smoke-test is_valid
preference.is_valid(TRUE)
preference.is_valid("string")
preference.is_valid(100)
preference.is_valid(list(1, 2, 3))
/// Requires all preferences to have a valid, unique savefile_identifier.
/datum/unit_test/preferences_valid_savefile_key
/datum/unit_test/preferences_valid_savefile_key/Run()
var/list/known_savefile_keys = list()
for (var/preference_type in GLOB.preference_entries)
var/datum/preference/preference = GLOB.preference_entries[preference_type]
if (!istext(preference.savefile_key))
TEST_FAIL("[preference_type] has an invalid savefile_key.")
if (preference.savefile_key in known_savefile_keys)
TEST_FAIL("[preference_type] has a non-unique savefile_key `[preference.savefile_key]`!")
known_savefile_keys += preference.savefile_key
/// Requires all main features have a main_feature_name
/datum/unit_test/preferences_valid_main_feature_name
/datum/unit_test/preferences_valid_main_feature_name/Run()
for (var/preference_type in GLOB.preference_entries)
var/datum/preference/choiced/preference = GLOB.preference_entries[preference_type]
if (!istype(preference))
continue
if (preference.category != PREFERENCE_CATEGORY_FEATURES && preference.category != PREFERENCE_CATEGORY_CLOTHING)
continue
TEST_ASSERT(!isnull(preference.main_feature_name), "Preference [preference_type] does not have a main_feature_name set!")
/// Validates that every choiced preference with should_generate_icons implements icon_for,
/// and that every one that doesn't, doesn't.
/datum/unit_test/preferences_should_generate_icons_sanity
/datum/unit_test/preferences_should_generate_icons_sanity/Run()
for (var/preference_type in GLOB.preference_entries)
var/datum/preference/choiced/choiced_preference = GLOB.preference_entries[preference_type]
if (!istype(choiced_preference) || choiced_preference.abstract_type == preference_type)
continue
var/list/values = choiced_preference.get_choices()
if (choiced_preference.should_generate_icons)
for (var/value in values)
var/icon = choiced_preference.icon_for(value)
TEST_ASSERT(istype(icon, /datum/universal_icon) || ispath(icon), "[preference_type] gave [icon] as an icon for [value], which is not a valid value")
else
var/errored = FALSE
try
choiced_preference.icon_for(values[1])
catch
errored = TRUE
TEST_ASSERT(errored, "[preference_type] implemented icon_for, but does not have should_generate_icons = TRUE")
+251
View File
@@ -0,0 +1,251 @@
/// converted unit test, maybe should be fully refactored
/// MIGHT REQUIRE BIGGER REWORK
/// Test that makes sure that reagent ids and names are unique
/datum/unit_test/reagent_shall_have_unique_name_and_id
/datum/unit_test/reagent_shall_have_unique_name_and_id/Run()
var/collection_name = list()
var/collection_id = list()
for(var/Rpath in subtypesof(/datum/reagent))
var/datum/reagent/R = new Rpath()
if(R.name == REAGENT_DEVELOPER_WARNING) // Ignore these types as they are meant to be overridden
continue
TEST_ASSERT(R.name != "", "[Rpath]: Reagents - reagent name blank.")
TEST_ASSERT_NOTEQUAL(R.id, REAGENT_ID_DEVELOPER_WARNING, "[Rpath]: Reagents - reagent ID not set.")
TEST_ASSERT_NOTEQUAL(R.description, REAGENT_DESC_DEVELOPER_WARNING, "[Rpath]: Reagents - reagent description unset.")
TEST_ASSERT(R.id != "", "[Rpath]: Reagents - reagent ID blank.")
TEST_ASSERT_EQUAL(R.id, lowertext(R.id), "[Rpath]: Reagents - Reagent ID must be all lowercase.")
if(!(R.wiki_flag & WIKI_SPOILER)) // If wiki hidden then don't conflict test it against name, used for intentionally copied names like beer2's
TEST_ASSERT(!collection_name[R.name], "[Rpath]: Reagents - reagent name \"[R.name]\" is not unique, used first in [collection_name[R.name]].")
collection_name[R.name] = R.type
TEST_ASSERT(!collection_id[R.id], "[Rpath]: Reagents - reagent ID \"[R.id]\" is not unique, used first in [collection_id[R.id]].")
collection_id[R.id] = R.type
TEST_ASSERT(R.supply_conversion_value, "[Rpath]: Reagents - reagent ID \"[R.id]\" does not have supply_conversion_value set.")
TEST_ASSERT(R.industrial_use && R.industrial_use != "", "[Rpath]: Reagents - reagent ID \"[R.id]\" does not have industrial_use set.")
TEST_ASSERT_NOTEQUAL(R.description, REAGENT_DESC_DEVELOPER_WARNING, "[Rpath]: Reagents - reagent description unset.")
qdel(R)
/// Test that makes sure that chemical reactions use and produce valid reagents
/datum/unit_test/chemical_reactions_shall_use_and_produce_valid_reagents
/datum/unit_test/chemical_reactions_shall_use_and_produce_valid_reagents/Run()
var/list/collection_id = list()
var/list/all_reactions = decls_repository.get_decls_of_subtype(/decl/chemical_reaction)
for(var/rtype in all_reactions)
var/decl/chemical_reaction/CR = all_reactions[rtype]
if(CR.name == REAGENT_DEVELOPER_WARNING) // Ignore these types as they are meant to be overridden
continue
TEST_ASSERT_NOTNULL(CR.name, "[CR.type]: Reagents - chemical reaction had null name.")
TEST_ASSERT(CR.name != "", "[CR.type]: Reagents - chemical reaction had blank name.")
TEST_ASSERT(CR.id, "[CR.type]: Reagents - chemical reaction had invalid ID.")
TEST_ASSERT_EQUAL(CR.id, lowertext(CR.id), "[CR.type]: Reagents - chemical reaction ID must be all lowercase.")
TEST_ASSERT(!(CR.id in collection_id), "[CR.type]: Reagents - chemical reaction ID \"[CR.name]\" is not unique, used first in [collection_id[CR.id]].")
if(!(CR.id in collection_id))
collection_id[CR.id] = CR.type
TEST_ASSERT(CR.result_amount >= 0, "[CR.type]: Reagents - chemical reaction ID \"[CR.name]\" had less than 0 as as result_amount?")
if(CR.required_reagents && CR.required_reagents.len)
for(var/RR in CR.required_reagents)
TEST_ASSERT(SSchemistry.chemical_reagents[RR], "[CR.type]: Reagents - chemical reaction had invalid required reagent ID \"[RR]\".")
TEST_ASSERT(CR.required_reagents[RR] > 0, "[CR.type]: Reagents - chemical reaction had invalid required reagent amount or in invalid format \"[CR.required_reagents[RR]]\".")
if(CR.catalysts && CR.catalysts.len)
for(var/RR in CR.catalysts)
TEST_ASSERT(SSchemistry.chemical_reagents[RR], "[CR.type]: Reagents - chemical reaction had invalid required reagent ID \"[RR]\".")
TEST_ASSERT(CR.catalysts[RR] > 0, "[CR.type]: Reagents - chemical reaction had invalid catalysts amount or in invalid format \"[CR.catalysts[RR]]\".")
if(CR.inhibitors && CR.inhibitors.len)
for(var/RR in CR.inhibitors)
TEST_ASSERT(SSchemistry.chemical_reagents[RR], "[CR.type]: Reagents - chemical reaction had invalid required reagent ID \"[RR]\".")
TEST_ASSERT(CR.inhibitors[RR] > 0, "[CR.type]: Reagents - chemical reaction had invalid inhibitors amount or in invalid format \"[CR.inhibitors[RR]]\".")
if(CR.result)
TEST_ASSERT(SSchemistry.chemical_reagents[CR.result], "[CR.type]: Reagents - chemical reaction had invalid result reagent ID \"[CR.result]\".")
/// Test that makes sure that prefilled reagent containers have valid reagents
/datum/unit_test/prefilled_reagent_containers_shall_have_valid_reagents
/datum/unit_test/prefilled_reagent_containers_shall_have_valid_reagents/Run()
var/obj/container = new /obj
for(var/RC in subtypesof(/obj/item/reagent_containers/glass))
var/obj/item/reagent_containers/glass/R = new RC(container)
if(R.prefill && R.prefill.len)
for(var/ID in R.prefill)
TEST_ASSERT(SSchemistry.chemical_reagents[ID], "[RC]: Reagents - reagent prefill had invalid reagent ID \"[ID]\".")
qdel(R)
for(var/DC in subtypesof(/obj/item/reagent_containers/chem_disp_cartridge))
var/obj/item/reagent_containers/chem_disp_cartridge/D = new DC(container)
if(D.spawn_reagent)
TEST_ASSERT(SSchemistry.chemical_reagents[D.spawn_reagent], "[DC]: Reagents - chemical dispenser cartridge had invalid reagent ID \"[D.spawn_reagent]\".")
qdel(D)
qdel(container)
/// Test that makes sure that chemical reactions do not conflict
/datum/unit_test/chemical_reactions_shall_not_conflict
var/obj/fake_beaker = null
var/list/result_reactions = list()
/datum/unit_test/chemical_reactions_shall_not_conflict/Run()
var/failed = FALSE
#ifdef UNIT_TEST
var/list/all_reactions = decls_repository.get_decls_of_subtype(/decl/chemical_reaction)
for(var/rtype in all_reactions)
var/decl/chemical_reaction/CR = all_reactions[rtype]
if(CR.name == REAGENT_DEVELOPER_WARNING) // Ignore these types as they are meant to be overridden
continue
if(!CR.name || CR.name == "" || !CR.id || CR.id == "")
continue
if(CR.result_amount <= 0) //Makes nothing anyway, or maybe an effect/explosion!
continue
if(!CR.result) // Cannot check for this
continue
if(istype(CR, /decl/chemical_reaction/instant/slime))
// slime time
var/decl/chemical_reaction/instant/slime/SR = CR
if(!SR.required)
continue
var/obj/item/slime_extract/E = new SR.required()
qdel_swap(fake_beaker, E)
fake_beaker.reagents.maximum_volume = 5000
else if(istype(CR, /decl/chemical_reaction/distilling))
// distilling
var/obj/distilling_tester/D = new()
qdel_swap(fake_beaker, D)
fake_beaker.reagents.maximum_volume = 5000
else
// regular beaker
qdel_swap(fake_beaker, new /obj/item/reagent_containers/glass/beaker())
fake_beaker.reagents.maximum_volume = 5000
// Perform test! If it fails once, it will perform a deeper check trying to use the inhibitors of anything in the beaker
RegisterSignal(fake_beaker.reagents, COMSIG_UNITTEST_DATA, PROC_REF(get_signal_data))
// Check if we failed the test with inhibitors in use, if so we absolutely couldn't make it...
// Uncomment the UNIT_TEST section in code\modules\reagents\reactions\_reactions.dm if you require more info
TEST_ASSERT(!perform_reaction(CR), "[CR.type]: Reagents - chemical reaction did not produce \"[CR.result]\". CONTAINS: \"[fake_beaker.reagents.get_reagents()]\"")
UnregisterSignal(fake_beaker.reagents, COMSIG_UNITTEST_DATA)
qdel_null(fake_beaker)
#endif
if(failed)
TEST_FAIL("One or more /decl/chemical_reaction subtypes conflict with another reaction.")
/datum/unit_test/chemical_reactions_shall_not_conflict/proc/perform_reaction(var/decl/chemical_reaction/CR, var/list/inhib = list())
var/scale = 1
if(CR.result_amount < 1)
scale = 1 / CR.result_amount // Create at least 1 unit
// Weird loop here, but this is used to test both instant and distilling reactions
// Instants will meet the while() condition on the first loop and go to the next stuff
// but distilling will repeat over and over until the temperature test is finished!
var/temp_test = 0
do
// clear for inhibitor searches
fake_beaker.reagents.clear_reagents()
result_reactions.Cut()
if(inhib.len) // taken from argument and not reaction! Put in FIRST!
for(var/RR in inhib)
fake_beaker.reagents.add_reagent(RR, inhib[RR] * scale)
if(CR.catalysts) // Required for reaction
for(var/RR in CR.catalysts)
fake_beaker.reagents.add_reagent(RR, CR.catalysts[RR] * scale)
if(CR.required_reagents)
for(var/RR in CR.required_reagents)
fake_beaker.reagents.add_reagent(RR, CR.required_reagents[RR] * scale)
if(!istype(CR, /decl/chemical_reaction/distilling))
break // Skip the next section if we're not distilling
// Check distillation at 10 points along its temperature range!
// This is so multiple reactions with the same requirements, but different temps, can be tested.
temp_test += 0.1
var/obj/distilling_tester/DD = fake_beaker
DD.test_distilling(CR,temp_test)
if(fake_beaker.reagents.has_reagent(CR.result))
return FALSE // Distilling success
while(temp_test > 1)
// Check beaker to see if we reached our goal!
if(fake_beaker.reagents.has_reagent(CR.result))
return FALSE // INSTANT SUCCESS!
if(inhib.len)
// We've checked with inhibitors, so we're already in inhibitor checking phase.
// So we've absolutely failed this time. There is no way to make this...
return TRUE
if(!result_reactions.len)
// Nothing to check for inhibitors...
for(var/decl/chemical_reaction/test_react in result_reactions)
TEST_FAIL("[CR.type]: Reagents - Used [test_react] but failed.")
return TRUE
// Otherwise we check the resulting reagents and use their inhibitor this time!
for(var/decl/chemical_reaction/test_react in result_reactions)
if(!test_react)
continue
if(!test_react.inhibitors.len)
continue
// Test one by one
for(var/each in test_react.inhibitors)
if(!perform_reaction(CR, list("[each]" = test_react.inhibitors["[each]"])))
return FALSE // SUCCESS using an inhibitor!
// Test all at once
if(!perform_reaction(CR, test_react.inhibitors))
return FALSE // SUCCESS using all inhibitors!
// No inhibiting reagent worked...
for(var/decl/chemical_reaction/test_react in result_reactions)
TEST_FAIL("[CR.type]: Reagents - Used [test_react] but failed.")
return TRUE
/datum/unit_test/chemical_reactions_shall_not_conflict/proc/get_signal_data(atom/source, list/data = list())
result_reactions.Add(data[1]) // Append the reactions that happened, then use that to check their inhibitors
/// Test that makes sure that chemical grinding has valid results
/datum/unit_test/chemical_grinding_must_produce_valid_results
/datum/unit_test/chemical_grinding_must_produce_valid_results/Run()
for(var/grind in GLOB.sheet_reagents + GLOB.ore_reagents)
var/list/results = GLOB.sheet_reagents[grind]
if(!results)
results = GLOB.ore_reagents[grind]
// Cursed test
TEST_ASSERT(!(!results || !islist(results)), "[grind]: Reagents - Grinding result had invalid list.")
if(!results || !islist(results))
continue
TEST_ASSERT(results.len, "[grind]: Reagents - Grinding result had empty.")
if(!results.len)
continue
for(var/reg_id in results)
TEST_ASSERT(SSchemistry.chemical_reagents[reg_id], "[grind]: Reagents - Grinding result had invalid reagent id \"[reg_id]\".")
+9
View File
@@ -0,0 +1,9 @@
/// converted unit test, maybe should be fully refactored
/datum/unit_test/recipe_test/Run()
for(var/datum/recipe/R in subtypesof(/datum/recipe))
TEST_ASSERT_NOTNULL(initial(R.result), "[R]: Recipes - Missing result.")
TEST_ASSERT(ispath(initial(R.result), /atom/movable), "[R]: Recipes - Improper result; [initial(R.result)] is not an obj or mob.")
TEST_ASSERT_NOTNULL(initial(R.result_quantity), "[R]: Recipes - result_quantity must be set.")
TEST_ASSERT(initial(R.result_quantity) <= 0, "[R]: Recipes - result_quantity must be greater than zero.")
TEST_ASSERT(ISINTEGER(initial(R.result_quantity)), "[R]: Recipes - result_quantity must be an integer.")
@@ -0,0 +1,7 @@
/// converted unit test, maybe should be fully refactored
/datum/unit_test/recycler_vendor_tests/Run()
for(var/datum/maint_recycler_vendor_entry/R in subtypesof(/datum/maint_recycler_vendor_entry))
TEST_ASSERT(!initial(R.object_type_to_spawn) && !initial(R.is_scam), "[R] : Vendor Entry - Missing Object Type on non-scam entry")
TEST_ASSERT(initial(R.item_cost) > 0, "[R] : Vendor Entry - Negative Cost")
TEST_ASSERT((initial(R.item_cost) == 0) && ((initial(R.per_round_cap) < 0) && (initial(R.per_person_cap) < 0)), "[R] : Vendor Entry - Infinite Item Spawning due to no individual or global item cap")
+169
View File
@@ -0,0 +1,169 @@
/// converted unit test, maybe should be fully refactored
/// Test that all robot sprites are valid
/datum/unit_test/all_robot_sprites_must_be_valid
var/signal_failed = FALSE
var/failed = 0
/datum/unit_test/all_robot_sprites_must_be_valid/Run()
for(var/sprite in subtypesof(/datum/robot_sprite))
var/datum/robot_sprite/RS = new sprite()
if(!RS.name) // Parent type, ignore me
continue
TEST_ASSERT(RS.sprite_icon, "[RS.type]: Robots - Robot sprite \"[RS.name]\", missing sprite_icon.")
if(!RS.sprite_icon)
continue
var/list/checks = list(
"[ROBOT_HAS_SPEED_SPRITE]" = "-roll",
"[ROBOT_HAS_SHIELD_SPRITE]" = "-shield",
"[ROBOT_HAS_SHIELD_SPEED_SPRITE]" = "-speed_shield",
"[ROBOT_HAS_MELEE_SPRITE]" = "-melee",
"[ROBOT_HAS_DAGGER_SPRITE]" = "-dagger",
"[ROBOT_HAS_BLADE_SPRITE]" = "-blade",
"[ROBOT_HAS_GUN_SPRITE]" = "-gun",
"[ROBOT_HAS_LASER_SPRITE]" = "-laser",
"[ROBOT_HAS_TASER_SPRITE]" = "-taser",
"[ROBOT_HAS_DISABLER_SPRITE]" = "-disabler"
)
for(var/C in checks)
if(RS.sprite_flag_check(text2num(C)))
check_state(RS,checks[C])
// eyes, lights, markings
if(RS.has_eye_sprites)
check_state(RS,"-eyes")
if(RS.has_eye_light_sprites)
check_state(RS,"-lights")
if(LAZYLEN(RS.sprite_decals))
for(var/decal in RS.sprite_decals)
check_state(RS,"-[decal]")
if(LAZYLEN(RS.sprite_animations))
for(var/animation in RS.sprite_animations)
check_state(RS,"-[animation]")
// Control panel
if(RS.has_custom_open_sprites)
check_state(RS,"-openpanel_nc")
check_state(RS,"-openpanel_c")
check_state(RS,"-openpanel_w")
// Glow State
if(RS.has_glow_sprites)
check_state(RS,"-glow")
// Bellies
if(RS.has_vore_belly_sprites && !RS.belly_capacity_list)
if(RS.has_sleeper_light_indicator)
// belly r/g light
check_state(RS,"-sleeper-r")
check_state(RS,"-sleeper-g")
if(RS.has_vore_belly_resting_sprites)
for(var/rest_style in RS.rest_sprite_options)
rest_style = lowertext(rest_style)
if(rest_style == "default")
rest_style = "rest"
check_state(RS,"-sleeper-r-[rest_style]")
check_state(RS,"-sleeper-g-[rest_style]")
// struggling
if(RS.has_vore_struggle_sprite)
check_state(RS,"-sleeper-r-struggle")
check_state(RS,"-sleeper-g-struggle")
if(RS.has_vore_belly_resting_sprites)
for(var/rest_style in RS.rest_sprite_options)
rest_style = lowertext(rest_style)
if(rest_style == "default")
rest_style = "rest"
check_state(RS,"-sleeper-r-[rest_style]-struggle")
check_state(RS,"-sleeper-g-[rest_style]-struggle")
else
// belly
check_state(RS,"-sleeper")
if(RS.has_vore_belly_resting_sprites)
for(var/rest_style in RS.rest_sprite_options)
rest_style = lowertext(rest_style)
if(rest_style == "default")
rest_style = "rest"
check_state(RS,"-sleeper-[rest_style]")
// struggling
if(RS.has_vore_struggle_sprite)
check_state(RS,"-sleeper-struggle")
if(RS.has_vore_belly_resting_sprites)
for(var/rest_style in RS.rest_sprite_options)
rest_style = lowertext(rest_style)
if(rest_style == "default")
rest_style = "rest"
check_state(RS,"-sleeper-[rest_style]-struggle")
else if (RS.belly_capacity_list)
for(var/belly in RS.belly_capacity_list)
for(var/num = 1 to RS.belly_capacity_list[belly])
// big belly
check_state(RS,"-[belly]-[num]")
if(RS.has_vore_belly_resting_sprites)
for(var/rest_style in RS.rest_sprite_options)
rest_style = lowertext(rest_style)
if(rest_style == "default")
rest_style = "rest"
check_state(RS,"-[belly]-[num]-[rest_style]")
// struggling
if(RS.has_vore_struggle_sprite)
check_state(RS,"-[belly]-[num]-struggle")
if(RS.has_vore_belly_resting_sprites)
for(var/rest_style in RS.rest_sprite_options)
rest_style = lowertext(rest_style)
if(rest_style == "default")
rest_style = "rest"
check_state(RS,"-[belly]-[num]-[rest_style]-struggle")
if(RS.belly_light_list)
for(var/belly in RS.belly_light_list)
for(var/num = 1 to RS.belly_light_list[belly])
// multi belly r/g light
check_state(RS,"-[belly]-[num]-r")
check_state(RS,"-[belly]-[num]-g")
if(RS.has_vore_belly_resting_sprites)
for(var/rest_style in RS.rest_sprite_options)
rest_style = lowertext(rest_style)
if(rest_style == "default")
rest_style = "rest"
check_state(RS,"-[belly]-[num]-r-[rest_style]")
check_state(RS,"-[belly]-[num]-g-[rest_style]")
// struggling
if(RS.has_vore_struggle_sprite)
check_state(RS,"-[belly]-[num]-r-struggle")
check_state(RS,"-[belly]-[num]-g-struggle")
if(RS.has_vore_belly_resting_sprites)
for(var/rest_style in RS.rest_sprite_options)
rest_style = lowertext(rest_style)
if(rest_style == "default")
rest_style = "rest"
check_state(RS,"-[belly]-[num]-r-[rest_style]-struggle")
check_state(RS,"-[belly]-[num]-g-[rest_style]-struggle")
// reseting
for(var/rest_style in RS.rest_sprite_options)
rest_style = lowertext(rest_style)
if(rest_style == "default")
rest_style = "rest"
check_state(RS,"-[rest_style]")
if(RS.has_glow_sprites)
check_state(RS,"-[rest_style]-glow")
if(RS.has_rest_lights_sprites)
check_state(RS,"-[rest_style]-lights")
if(RS.has_rest_eyes_sprites)
check_state(RS,"-[rest_style]-eyes")
// death
if(RS.has_dead_sprite)
check_state(RS,"-wreck")
if(RS.has_dead_sprite_overlay) // Only one per dmi
TEST_ASSERT("wreck-overlay" in cached_icon_states(RS.sprite_icon), "[RS.type]: Robots - Robot sprite \"[RS.name]\", missing icon_state wreck-overlay, in dmi \"[RS.sprite_icon]\".")
// offset
var/icon/I = new(RS.sprite_icon)
TEST_ASSERT_EQUAL(RS.icon_x, I.Width(), "[RS.type]: Robots - Robot sprite \"[RS.name]\", icon_x \"[RS.icon_x]\" did not match dmi configured width \"[I.Width()]\"")
TEST_ASSERT_EQUAL(RS.icon_y, I.Height(), "[RS.type]: Robots - Robot sprite \"[RS.name]\", icon_y \"[RS.icon_y]\" did not match dmi configured height \"[I.Height()]\"")
TEST_ASSERT_EQUAL(RS.icon_y, RS.vis_height, "[RS.type]: Robots - Robot sprite \"[RS.name]\", vis_height \"[RS.vis_height]\" did not match icon_y \"[RS.icon_y]\"")
var/legal_offset = (I.Width() - world.icon_size) / 2
TEST_ASSERT_EQUAL(RS.pixel_x, -legal_offset, "[RS.type]: Robots - Robot sprite \"[RS.name]\", pixel_x \"[RS.pixel_x]\" did not have correct offset, should be \"[-legal_offset]\"")
qdel(I)
qdel(RS)
/datum/unit_test/all_robot_sprites_must_be_valid/proc/check_state(datum/robot_sprite/RS, append)
var/check_state = "[RS.sprite_icon_state][append]"
TEST_ASSERT(check_state in cached_icon_states(RS.sprite_icon), "[RS.type]: Robots - Robot sprite \"[RS.name]\", enabled but missing icon_state \"[check_state]\", in dmi \"[RS.sprite_icon]\".")
+24
View File
@@ -0,0 +1,24 @@
///Checks if spritesheet assets contain icon states with invalid names
/datum/unit_test/spritesheets
/datum/unit_test/spritesheets/Run()
for(var/datum/asset/spritesheet/sheet as anything in subtypesof(/datum/asset/spritesheet))
if(!initial(sheet.name)) //Ignore abstract types
continue
if (sheet == initial(sheet._abstract))
continue
sheet = get_asset_datum(sheet)
for(var/sprite_name in sheet.sprites)
if(!sprite_name)
TEST_FAIL("Spritesheet [sheet.type] has a nameless icon state.")
// Test IconForge generated sheets as well
for(var/datum/asset/spritesheet_batched/sheet as anything in subtypesof(/datum/asset/spritesheet_batched))
if(!initial(sheet.name)) //Ignore abstract types
continue
if (sheet == initial(sheet._abstract))
continue
sheet = get_asset_datum(sheet)
for(var/sprite_name in sheet.sprites)
if(!sprite_name)
TEST_FAIL("Spritesheet [sheet.type] has a nameless icon state.")
+50
View File
@@ -0,0 +1,50 @@
/// converted unit test, maybe should be fully refactored
/// Test that inserts and retrieves data from an sqlite database
/datum/unit_test/sqlite_tests_insert
/datum/unit_test/sqlite_tests_insert/Run()
// Arrange.
fdel("data/sqlite/testing_sqlite_tests_insert.db") // In case any remain from a previous local test, so we can have a clean new database.
var/database/stub_sqlite_db = new("data/sqlite/testing_sqlite_tests_insert.db") // Unfortunately, byond doesn't like having working sqlite stuff w/o a file existing.
SSsqlite.init_schema(stub_sqlite_db)
var/test_author = "alice"
var/test_topic = "Test"
var/test_content = "Bob is lame."
// Act.
SSsqlite.insert_feedback(author = test_author, topic = test_topic, content = test_content, sqlite_object = stub_sqlite_db)
var/database/query/Q = new("SELECT * FROM [SQLITE_TABLE_FEEDBACK]")
Q.Execute(stub_sqlite_db)
SSsqlite.sqlite_check_for_errors(Q, "Sqlite Insert Unit Test")
Q.NextRow()
// Assert.
var/list/row_data = Q.GetRowData()
if(!(row_data[SQLITE_FEEDBACK_COLUMN_AUTHOR] == test_author && row_data[SQLITE_FEEDBACK_COLUMN_TOPIC] == test_topic && row_data[SQLITE_FEEDBACK_COLUMN_CONTENT] == test_content))
TEST_FAIL("Data insert and loading failed to have matching information.")
/// Test that does a cooldown in a sqlite database
/datum/unit_test/sqlite_tests_cooldown
/datum/unit_test/sqlite_tests_cooldown/Run()
// Arrange.
fdel("data/sqlite/testing_sqlite_tests_cooldown.db") // In case any remain from a previous local test, so we can have a clean new database.
var/database/stub_sqlite_db = new("data/sqlite/testing_sqlite_tests_cooldown.db") // Unfortunately, byond doesn't like having working sqlite stuff w/o a file existing.
SSsqlite.init_schema(stub_sqlite_db)
var/days_to_wait = 1
// Act.
SSsqlite.insert_feedback(author = "Alice", topic = "Testing", content = "This is a test.", sqlite_object = stub_sqlite_db)
var/alice_cooldown_block = SSsqlite.get_feedback_cooldown("Alice", days_to_wait, stub_sqlite_db)
var/bob_cooldown = SSsqlite.get_feedback_cooldown("Bob", days_to_wait, stub_sqlite_db)
days_to_wait = 0
var/alice_cooldown_allow = SSsqlite.get_feedback_cooldown("Alice", days_to_wait, stub_sqlite_db)
// Assert.
TEST_ASSERT(alice_cooldown_block > 0, "User 'Alice' did not receive a cooldown, when they were supposed to.")
TEST_ASSERT(bob_cooldown <= 0, "User 'Bob' did receive a cooldown, when they did not do anything.")
TEST_ASSERT(alice_cooldown_allow <= 0, "User 'Alice' did receive a cooldown, when no cooldown is supposed to be enforced.")
+22
View File
@@ -0,0 +1,22 @@
/// Tests that all subsystems that need to properly initialize.
/datum/unit_test/subsystem_init
/datum/unit_test/subsystem_init/Run()
for(var/datum/controller/subsystem/subsystem as anything in Master.subsystems)
if(subsystem.flags & SS_NO_INIT)
continue
if(subsystem.initialized)
continue
var/should_fail = !(subsystem.flags & SS_OK_TO_FAIL_INIT)
var/list/message_strings = list("[subsystem] ([subsystem.type]) is a subsystem meant to initialize but could not get initialized.")
if(!isnull(subsystem.initialization_failure_message))
message_strings += "The subsystem reported the following: [subsystem.initialization_failure_message]"
if(should_fail)
TEST_FAIL(jointext(message_strings, "\n"))
continue
message_strings += "This subsystem is marked as SS_OK_TO_FAIL_INIT. This is still a bug, but it is non-blocking."
TEST_NOTICE(src, jointext(message_strings, "\n"))
@@ -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")
+3
View File
@@ -0,0 +1,3 @@
/datum/unit_test/timer_sanity/Run()
TEST_ASSERT(SStimer.bucket_count >= 0,
"SStimer is going into negative bucket count from something")
+33
View File
@@ -0,0 +1,33 @@
/// Test that all traits have unique names
/datum/unit_test/all_traits_unique_names
/datum/unit_test/all_traits_unique_names/Run()
var/list/used_named = list()
for(var/traitpath in GLOB.all_traits)
var/datum/trait/T = GLOB.all_traits[traitpath]
TEST_ASSERT(!(T.name in used_named), "[T.type]: Trait - The name \"[T.name]\" is already in use.")
used_named.Add(T.name)
/// Test that autohiss traits shall be excluse
/datum/unit_test/autohiss_shall_be_exclusive
/datum/unit_test/autohiss_shall_be_exclusive/Run()
var/list/hiss_list = list()
for(var/traitpath in GLOB.all_traits)
var/datum/trait/T = GLOB.all_traits[traitpath]
if(!T.var_changes)
continue
if(!islist(T.var_changes["autohiss_basic_map"]))
continue
hiss_list += T
for(var/datum/trait/T in hiss_list)
TEST_ASSERT(!(T.type in T.excludes), "[T.type]: Trait - Autohiss excludes itself.")
TEST_ASSERT(T.excludes, "[T.type]: Trait - Autohiss missing exclusion list.")
if(!T.excludes)
continue
var/list/exempt_list = hiss_list.Copy() - T // MUST exclude all others except itself
for(var/datum/trait/EX in exempt_list)
TEST_ASSERT(EX.type in T.excludes, "[T.type]: Trait - Autohiss missing exclusion for [EX].")
+402
View File
@@ -0,0 +1,402 @@
/*
Usage:
Override /Run() to run your test code
Call TEST_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_floor_bottom_left and run_loc_floor_top_right to get turfs for testing
*/
GLOBAL_DATUM(current_test, /datum/unit_test)
GLOBAL_VAR_INIT(failed_any_test, FALSE)
/// When unit testing, all logs sent to log_mapping are stored here and retrieved in log_mapping unit test.
GLOBAL_LIST_EMPTY(unit_test_mapping_logs)
/// Global assoc list of required mapping items, [item typepath] to [required item datum].
GLOBAL_LIST_EMPTY(required_map_items)
/// 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
/// 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
/// Do not instantiate if type matches this
abstract_type = /datum/unit_test
/// List of atoms that we don't want to ever initialize in an agnostic context, like for Create and Destroy. Stored on the base datum for usability in other relevant tests that need this data.
var/static/list/uncreatables = null
// NOT IMPLEMENTED YET: 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()
// NOT IMPLEMENTED YET: if (isnull(reservation))
// NOT IMPLEMENTED YET: var/datum/map_template/unit_tests/template = new
// NOT IMPLEMENTED YET: reservation = template.load_new_z()
if (isnull(uncreatables))
uncreatables = build_list_of_uncreatables()
allocated = new
run_loc_floor_bottom_left = get_turf(locate(/obj/effect/landmark/unit_test_bottom_left) in GLOB.landmarks_list)
run_loc_floor_top_right = get_turf(locate(/obj/effect/landmark/unit_test_top_right) in GLOB.landmarks_list)
// NOT IMPLENTED YET, SEE THE BEGINNING OF THIS PROC
//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()
QDEL_LIST(allocated)
// clear the test area
// NOT IMPLEMENTED YET, SEE NEW() PROC
//for (var/turf/turf in Z_TURFS(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()
TEST_FAIL("[type]/Run() called parent or not implemented")
/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, 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
/datum/unit_test/proc/allocate(type, ...)
var/list/arguments = args.Copy(2)
if(ispath(type, /atom))
if (!arguments.len)
arguments = list(run_loc_floor_bottom_left)
else if (arguments[1] == null)
arguments[1] = run_loc_floor_bottom_left
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
/// Resets the air of our testing room to its default
/datum/unit_test/proc/restore_atmos()
// NOT IMPLEMENTED YET, SEE NEW() PROC
//var/area/working_area = run_loc_floor_bottom_left.loc
//var/list/turf/to_restore = working_area.get_turfs_from_all_zlevels()
//for(var/turf/simulated/restore in to_restore)
// var/datum/gas_mixture/GM = SSair.parse_gas_string(restore.initial_gas_mix, /datum/gas_mixture/turf)
// restore.copy_air(GM)
// restore.temperature = initial(restore.temperature)
// restore.air_update_turf(update = FALSE, remove = FALSE)
/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")
log_unit_test("\t[path_prefix]_[name] was found, putting in data/screenshots")
else
#ifdef CIBUILDING
// We are runing 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")
log_unit_test("\t[path_prefix]_[name] was put in data/screenshots_new")
#else
// We are probably running in a local build
fcopy(icon, filename)
TEST_FAIL("Screenshot for [name] did not exist. One has been created.")
#endif
/// 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.cardinal)
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.current_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]")
/**
* Helper to perform a click
*
* * clicker: The mob that will be clicking
* * clicked_on: The atom that will be clicked
* * passed_params: A list of parameters to pass to the click
*/
/datum/unit_test/proc/click_wrapper(mob/living/clicker, atom/clicked_on, list/passed_params = list(LEFT_CLICK = 1, BUTTON = LEFT_CLICK))
clicker.next_click = -1
clicker.next_move = -1
clicker.ClickOn(clicked_on, list2params(passed_params))
/proc/RunUnitTest(datum/unit_test/test_path, list/test_results)
if(ispath(test_path, /datum/unit_test/focus_only))
return
if(initial(test_path.abstract_type) == test_path)
return
var/datum/unit_test/test = new test_path
GLOB.current_test = test
var/duration = REALTIMEOFDAY
var/skip_test = (test_path in SSmapping.current_map.skipped_tests)
var/test_output_desc = "[test_path]"
var/message = ""
log_world("::group::[test_path]")
if(skip_test)
log_world("[TEST_OUTPUT_YELLOW("SKIPPED")] Skipped run on map [SSmapping.current_map.name].")
else
test.Run()
test.restore_atmos()
duration = REALTIMEOFDAY - duration
GLOB.current_test = null
GLOB.failed_any_test |= !test.succeeded
var/list/log_entry = list()
var/list/fail_reasons = test.fail_reasons
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 += "\tFAILURE #[reasonID]: [text] at [file]:[line]"
if(length(log_entry))
message = log_entry.Join("\n")
//log_test(message)
log_unit_test(message)
test_output_desc += " [duration / 10]s"
if (test.succeeded)
log_world("[TEST_OUTPUT_GREEN("PASS")] [test_output_desc]")
log_world("::endgroup::")
if (!test.succeeded && !skip_test)
log_world("::error::[TEST_OUTPUT_RED("FAIL")] [test_output_desc]")
var/final_status = skip_test ? UNIT_TEST_SKIPPED : (test.succeeded ? UNIT_TEST_PASSED : UNIT_TEST_FAILED)
test_results[test_path] = list("status" = final_status, "message" = message, "name" = test_path)
qdel(test)
/// Builds (and returns) a list of atoms that we shouldn't initialize in generic testing, like Create and Destroy.
/// It is appreciated to add the reason why the atom shouldn't be initialized if you add it to this list.
/datum/unit_test/proc/build_list_of_uncreatables()
RETURN_TYPE(/list)
var/list/returnable_list = list()
// The following are just generic, singular types.
returnable_list = list(
//Never meant to be created, errors out the ass for mobcode reasons
/mob/living/carbon,
//And another
// NOT IMPLEMENTED: /obj/item/slimecross/recurring,
//This should be obvious
// NOT IMPLEMENTED: /obj/machinery/doomsday_device,
//Yet more templates
// NOT IMPLEMENTED: /obj/machinery/restaurant_portal,
//Template type
/obj/machinery/power/turbine,
//Template type
// NOT IMPLEMENTED: /obj/effect/mob_spawn,
//Template type
// NOT IMPLEMENTED: /obj/structure/holosign/robot_seat,
//Singleton
/mob/dview,
//Template type
// NOT IMPLEMENTED: /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
// NOT IMPLEMENTED: /obj/machinery/launchpad/briefcase,
//Wings abstract path
// NOT IMPLEMENTED: /obj/item/organ/wings,
//Not meant to spawn without the machine wand
// NOT IMPLEMENTED: /obj/effect/bug_moving,
//The abstract grown item expects a seed, but doesn't have one
// NOT IMPLEMENTED: /obj/item/food/grown,
///Single use case holder atom requiring a user
// NOT IMPLEMENTED: /atom/movable/looking_holder,
)
// Everything that follows is a typesof() check.
//Say it with me now, type template
// NOT IMPLEMENTED: returnable_list += typesof(/obj/effect/mapping_helpers)
//This turf existing is an error in and of itself
// NOT IMPLEMENTED: returnable_list += typesof(/turf/baseturf_skipover)
// NOT IMPLEMENTED: returnable_list += typesof(/turf/baseturf_bottom)
//This demands a borg, so we'll let if off easy
// NOT IMPLEMENTED: returnable_list += typesof(/obj/item/modular_computer/pda/silicon)
//This one demands a computer, ditto
// NOT IMPLEMENTED: returnable_list += typesof(/obj/item/modular_computer/processor)
//Very finiky, blacklisting to make things easier
// NOT IMPLEMENTED: returnable_list += typesof(/obj/item/poster/wanted)
//Needs clients / mobs to observe it to exist. Also includes hallucinations.
// NOT IMPLEMENTED: returnable_list += typesof(/obj/effect/client_image_holder)
//Same to above. Needs a client / mob / hallucination to observe it to exist.
// NOT IMPLEMENTED: returnable_list += typesof(/obj/projectile/hallucination)
// NOT IMPLEMENTED: returnable_list += typesof(/obj/item/hallucinated)
//We don't have a pod
// NOT IMPLEMENTED: returnable_list += typesof(/obj/effect/pod_landingzone_effect)
// NOT IMPLEMENTED: returnable_list += 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.
// NOT IMPLEMENTED: returnable_list += typesof(/obj/effect/baseturf_helper)
//No tauma to pass in
// NOT IMPLEMENTED: returnable_list += typesof(/mob/eye/imaginary_friend)
//No heart to give
// NOT IMPLEMENTED: returnable_list += typesof(/obj/structure/ethereal_crystal)
//No linked console
// NOT IMPLEMENTED: returnable_list += typesof(/mob/eye/camera/remote/base_construction)
//See above
// NOT IMPLEMENTED: returnable_list += typesof(/mob/eye/camera/remote/shuttle_docker)
//Hangs a ref post invoke async, which we don't support. Could put a qdeleted check but it feels hacky
// NOT IMPLEMENTED: returnable_list += typesof(/obj/effect/anomaly/grav/high)
//See above
// NOT IMPLEMENTED: returnable_list += typesof(/obj/effect/timestop)
//Sparks can ignite a number of things, causing a fire to burn the floor away. Only you can prevent CI fires
// NOT IMPLEMENTED: returnable_list += typesof(/obj/effect/particle_effect/sparks)
//See above - These are one of those things.
// NOT IMPLEMENTED: returnable_list += typesof(/obj/effect/decal/cleanable/fuel_pool)
//Invoke async in init, skippppp
// NOT IMPLEMENTED: returnable_list += typesof(/mob/living/silicon/robot/model)
//This lad also sleeps
// NOT IMPLEMENTED: returnable_list += typesof(/obj/item/hilbertshotel)
//this boi spawns turf changing stuff, and it stacks and causes pain. Let's just not
// NOT IMPLEMENTED: returnable_list += typesof(/obj/effect/sliding_puzzle)
//these can explode and cause the turf to be destroyed at unexpected moments
returnable_list += typesof(/obj/effect/mine)
// NOT IMPLEMENTED: returnable_list += typesof(/obj/effect/spawner/random/contraband/landmine)
// NOT IMPLEMENTED: returnable_list += typesof(/obj/item/minespawner)
//Stacks baseturfs, can't be tested here
// NOT IMPLEMENTED: returnable_list += typesof(/obj/effect/temp_visual/lava_warning)
//Stacks baseturfs, can't be tested here
// NOT IMPLEMENTED: returnable_list += typesof(/obj/effect/landmark/ctf)
//Our system doesn't support it without warning spam from unregister calls on things that never registered
// NOT IMPLEMENTED: returnable_list += typesof(/obj/docking_port)
//Asks for a shuttle that may not exist, let's leave it alone
returnable_list += 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
// NOT IMPLEMENTED: returnable_list += typesof(/obj/structure/alien/resin/flower_bud)
//Needs a linked mecha
// NOT IMPLEMENTED: returnable_list += typesof(/obj/effect/skyfall_landingzone)
//Expects a mob to holderize, we have nothing to give
// NOT IMPLEMENTED: returnable_list += typesof(/obj/item/clothing/head/mob_holder)
//Needs cards passed into the initilazation args
// NOT IMPLEMENTED: returnable_list += 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.
// NOT IMPLEMENTED: returnable_list += typesof(/obj/machinery/computer/holodeck)
//runtimes if not paired with a landmark
// NOT IMPLEMENTED: returnable_list += typesof(/obj/structure/transport/linear)
// Runtimes if the associated machinery does not exist, but not the base type
// NOT IMPLEMENTED: returnable_list += subtypesof(/obj/machinery/airlock_controller)
// Always ought to have an associated escape menu. Any references it could possibly hold would need one regardless.
// NOT IMPLEMENTED: returnable_list += subtypesof(/atom/movable/screen/escape_menu)
// Can't spawn openspace above nothing, it'll get pissy at me
// NOT IMPLEMENTED: returnable_list += typesof(/turf/open/space/openspace)
// NOT IMPLEMENTED: returnable_list += typesof(/turf/open/openspace)
// NOT IMPLEMENTED: returnable_list += typesof(/obj/item/robot_model) // These should never be spawned outside of a robot.
return returnable_list
/proc/RunUnitTests()
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))
focused_tests += test_to_run
if(length(focused_tests))
tests_to_run = focused_tests
sortTim(tests_to_run, GLOBAL_PROC_REF(cmp_unit_test_priority))
var/list/test_results = list()
//Hell code, we're bound to end the round somehow so let's stop if from ending while we work
SSticker.delay_end = TRUE
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)
SSticker.delay_end = FALSE
var/file_name = "data/unit_tests.json"
fdel(file_name)
file(file_name) << json_encode(test_results)
SSticker.force_ending = ADMIN_FORCE_END_ROUND
//We have to call this manually because del_text can preceed us, and SSticker doesn't fire in the post game
SSticker.declare_completion()
/datum/map_template/unit_tests
name = "Unit Tests Zone"
mappath = "maps/templates/unit_tests.dmm"
+214
View File
@@ -0,0 +1,214 @@
/// converted unit test, maybe should be fully refactored
// FIXME: THIS SHOULD BE REPLACED WITH ALLOCATE IN THE END
// SEE unit_test.dm NEW() WHY THIS ISNT IMPLEMENTED YET
/datum/unit_test
var/static/default_mobloc = null
// FIXME: THIS SHOULD BE REPLACED WITH ALLOCATE IN THE END
// SEE unit_test.dm NEW() WHY THIS ISNT IMPLEMENTED YET
/datum/unit_test/proc/create_test_mob(var/turf/mobloc = null, var/mobtype = /mob/living/carbon/human, var/with_mind = FALSE)
if(isnull(mobloc))
if(!default_mobloc)
for(var/turf/simulated/floor/tiled/T in world)
var/pressure = T.zone.air.return_pressure()
if(90 < pressure && pressure < 120) // Find a turf between 90 and 120
default_mobloc = T
break
mobloc = default_mobloc
if(!mobloc)
fail("Unable to find a location to create test mob")
return 0
var/mob/living/carbon/human/H = new mobtype(mobloc)
if(with_mind)
H.mind_initialize("TestKey[rand(0,10000)]")
return H
/// Test that a human mob does not suffocate in a belly
/datum/unit_test/belly_nonsuffocation
var/startLifeTick
var/startOxyloss
var/endOxyloss
var/mob/living/carbon/human/pred
var/mob/living/carbon/human/prey
/datum/unit_test/belly_nonsuffocation/Run()
pred = create_test_mob()
if(!istype(pred))
return 0
prey = create_test_mob(pred.loc)
if(!istype(prey))
return 0
return 1
/datum/unit_test/belly_nonsuffocation/check_result()
// Unfortuantely we need to wait for the pred's belly to initialize. (Currently after a spawn())
if(!pred.vore_organs || !pred.vore_organs.len)
return 0
// Now that pred belly exists, we can eat the prey.
if(!pred.vore_selected)
fail("[pred] has no vore_selected.")
return 1
// Attempt to eat the prey
if(prey.loc != pred.vore_selected)
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]")
return 1
// Okay, we succeeded in eating them, now lets wait a bit
startLifeTick = pred.life_tick
startOxyloss = prey.getOxyLoss()
return 0
if(pred.life_tick < (startLifeTick + 10))
return 0 // Wait for them to breathe a few times
// Alright lets check it!
endOxyloss = prey.getOxyLoss()
if(startOxyloss < endOxyloss)
fail("Prey takes oxygen damage in a pred's belly! (Before: [startOxyloss]; after: [endOxyloss])")
else
pass("Prey is not taking oxygen damage in pred's belly. (Before: [startOxyloss]; after: [endOxyloss])")
qdel(prey)
qdel(pred)
return 1
////////////////////////////////////////////////////////////////
/datum/unit_test/belly_spacesafe
name = "MOB: human mob protected from space in a belly"
var/startLifeTick
var/startOxyloss
var/endOxyloss
var/mob/living/carbon/human/pred
var/mob/living/carbon/human/prey
async = 1
/datum/unit_test/belly_spacesafe/start_test()
pred = create_test_mob()
if(!istype(pred))
return 0
prey = create_test_mob(pred.loc)
if(!istype(prey))
return 0
return 1
/datum/unit_test/belly_spacesafe/check_result()
// Unfortuantely we need to wait for the pred's belly to initialize. (Currently after a spawn())
if(!pred.vore_organs || !pred.vore_organs.len)
return 0
// Now that pred belly exists, we can eat the prey.
if(!pred.vore_selected)
fail("[pred] has no vore_selected.")
return 1
// Attempt to eat the prey
if(prey.loc != pred.vore_selected)
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]")
return 1
else
// Get an empty space level instead of just picking a random space turf
var/empty_z = using_map.get_empty_zlevel()
if(!empty_z)
fail("Unable to get empty z-level for vore space protection test!")
return 1
// Away from map edges so they don't transit while we're testing
var/mid_w = round(world.maxx*0.5)
var/mid_h = round(world.maxy*0.5)
var/turf/T = locate(mid_w, mid_h, empty_z)
if(!T)
fail("Unable to get turf for vore space protection test!")
return 1
else
pred.forceMove(T)
// Okay, we succeeded in eating them, now lets wait a bit
startLifeTick = pred.life_tick
startOxyloss = prey.getOxyLoss()
return 0
if(pred.life_tick < (startLifeTick + 10))
return 0 // Wait for them to breathe a few times
// Alright lets check it!
endOxyloss = prey.getOxyLoss()
if(startOxyloss < endOxyloss)
fail("Prey takes oxygen damage in space! (Before: [startOxyloss]; after: [endOxyloss])")
else
pass("Prey is not taking oxygen damage in space. (Before: [startOxyloss]; after: [endOxyloss])")
qdel(prey)
qdel(pred)
return 1
////////////////////////////////////////////////////////////////
/datum/unit_test/belly_damage
name = "MOB: human mob takes damage from digestion"
var/startLifeTick
var/startBruteBurn
var/endBruteBurn
var/mob/living/carbon/human/pred
var/mob/living/carbon/human/prey
async = 1
/datum/unit_test/belly_damage/start_test()
pred = create_test_mob()
if(!istype(pred))
return 0
prey = create_test_mob(pred.loc)
if(!istype(prey))
return 0
return 1
/datum/unit_test/belly_damage/check_result()
// Unfortuantely we need to wait for the pred's belly to initialize. (Currently after a spawn())
if(!pred.vore_organs || !pred.vore_organs.len)
return 0
// Now that pred belly exists, we can eat the prey.
if(!pred.vore_selected)
fail("[pred] has no vore_selected.")
return 1
// Attempt to eat the prey
if(prey.loc != pred.vore_selected)
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]")
return 1
// Okay, we succeeded in eating them, now lets wait a bit
pred.vore_selected.digest_mode = DM_DIGEST
startLifeTick = pred.life_tick
startBruteBurn = prey.getBruteLoss() + prey.getFireLoss()
return 0
if(pred.life_tick < (startLifeTick + 10))
return 0 // Wait a few ticks for damage to happen
// 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])")
else
pass("Prey is taking damage in pred's belly. (Before: [startBruteBurn]; after: [endBruteBurn])")
qdel(prey)
qdel(pred)
return 1