mirror of
https://github.com/VOREStation/VOREStation.git
synced 2026-08-21 11:07:46 +01:00
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:
@@ -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.
|
||||
@@ -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")
|
||||
@@ -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(",")])")
|
||||
@@ -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
|
||||
@@ -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)].")
|
||||
@@ -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'")
|
||||
@@ -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
|
||||
@@ -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"))
|
||||
@@ -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.")
|
||||
@@ -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")
|
||||
@@ -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)].")
|
||||
@@ -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
|
||||
@@ -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.")
|
||||
@@ -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")
|
||||
@@ -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]\".")
|
||||
@@ -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")
|
||||
@@ -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]\".")
|
||||
@@ -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.")
|
||||
@@ -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.")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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].")
|
||||
@@ -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"
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user