code quality

This commit is contained in:
Letter N
2020-12-29 11:12:56 +08:00
parent 51143f89e5
commit f28363ae56
65 changed files with 950 additions and 347 deletions

View File

@@ -0,0 +1,70 @@
# 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
```
Then, 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!
## 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 `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)`.
`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_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.

View File

@@ -1,42 +1,80 @@
//include unit test files in this module in this ifdef
//Keep this sorted alphabetically
#ifdef UNIT_TESTS
#if defined(UNIT_TESTS) || defined(SPACEMAN_DMM)
/// 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"]") }
/// 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) if ((a) != (b)) { return Fail("Expected [isnull(a) ? "null" : a] to be equal to [isnull(b) ? "null" : b].[message ? " [message]" : ""]") }
#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]" : ""]"); \
} \
} 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]" : ""]"); \
} \
} 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; }
#include "anchored_mobs.dm"
#include "bespoke_id.dm"
// #include "binary_insert.dm"
// #include "card_mismatch.dm" shame we don't have this!
#include "binary_insert.dm"
// #include "card_mismatch.dm"
#include "chain_pull_through_space.dm"
#include "character_saving.dm"
// #include "combat.dm"
#include "component_tests.dm"
// #include "confusion.dm"
// #include "keybinding_init.dm"
#include "confusion.dm"
// #include "emoting.dm"
#include "heretic_knowledge.dm"
#include "holidays.dm"
#include "initialize_sanity.dm"
#include "keybinding_init.dm"
#include "machine_disassembly.dm"
#include "medical_wounds.dm"
#include "merge_type.dm"
// #include "metabolizing.dm"
// #include "outfit_sanity.dm"
#include "pills.dm"
// #include "plantgrowth_tests.dm"
// #include "quick_swap_sanity.dm" - we don't have quick swap yet
#include "projectiles.dm"
#include "reagent_id_typos.dm"
#include "reagent_mod_expose.dm"
#include "reagent_mod_procs.dm"
#include "reagent_recipe_collisions.dm"
#include "resist.dm"
// #include "say.dm" //no saymods, someone update saycode please.
// #include "siunit.dm"
// #include "say.dm"
#include "serving_tray.dm"
#include "siunit.dm"
#include "spawn_humans.dm"
// #include "species_whitelists.dm"
#include "species_whitelists.dm"
// #include "stomach.dm"
#include "subsystem_init.dm"
// #include "surgeries.dm" // fails at random due to a race condition, commented out for now
#include "surgeries.dm"
#include "teleporters.dm"
#include "timer_sanity.dm"
#include "unit_test.dm"
/// CIT TESTS
#include "character_saving.dm"
#undef TEST_ASSERT
#undef TEST_ASSERT_EQUAL
#undef TEST_ASSERT_NOTEQUAL
#undef TEST_FOCUS
#endif

View File

@@ -0,0 +1,7 @@
/datum/unit_test/card_mismatch
/datum/unit_test/card_mismatch/Run()
var/message = checkCardpacks(SStrading_card_game.card_packs)
message += checkCardDatums()
if(message)
Fail(message)

View File

@@ -0,0 +1,98 @@
/datum/unit_test/harm_punch/Run()
var/mob/living/carbon/human/puncher = allocate(/mob/living/carbon/human)
var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human)
// Avoid all randomness in tests
ADD_TRAIT(puncher, TRAIT_PERFECT_ATTACKER, INNATE_TRAIT)
puncher.a_intent_change(INTENT_HARM)
victim.attack_hand(puncher)
TEST_ASSERT(victim.getBruteLoss() > 0, "Victim took no brute damage after being punched")
/datum/unit_test/harm_melee/Run()
var/mob/living/carbon/human/tider = allocate(/mob/living/carbon/human)
var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human)
var/obj/item/storage/toolbox/toolbox = allocate(/obj/item/storage/toolbox)
tider.put_in_active_hand(toolbox, forced = TRUE)
tider.a_intent_change(INTENT_HARM)
victim.attackby(toolbox, tider)
TEST_ASSERT(victim.getBruteLoss() > 0, "Victim took no brute damage after being hit by a toolbox")
/datum/unit_test/harm_different_damage/Run()
var/mob/living/carbon/human/attacker = allocate(/mob/living/carbon/human)
var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human)
var/obj/item/weldingtool/welding_tool = allocate(/obj/item/weldingtool)
attacker.put_in_active_hand(welding_tool, forced = TRUE)
attacker.a_intent_change(INTENT_HARM)
welding_tool.attack_self(attacker) // Turn it on
victim.attackby(welding_tool, attacker)
TEST_ASSERT_EQUAL(victim.getBruteLoss(), 0, "Victim took brute damage from a lit welding tool")
TEST_ASSERT(victim.getFireLoss() > 0, "Victim took no burn damage after being hit by a lit welding tool")
/datum/unit_test/attack_chain
var/attack_hit
var/post_attack_hit
var/pre_attack_hit
/datum/unit_test/attack_chain/proc/attack_hit()
attack_hit = TRUE
/datum/unit_test/attack_chain/proc/post_attack_hit()
post_attack_hit = TRUE
/datum/unit_test/attack_chain/proc/pre_attack_hit()
pre_attack_hit = TRUE
/datum/unit_test/attack_chain/Run()
var/mob/living/carbon/human/attacker = allocate(/mob/living/carbon/human)
var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human)
var/obj/item/storage/toolbox/toolbox = allocate(/obj/item/storage/toolbox)
RegisterSignal(toolbox, COMSIG_ITEM_PRE_ATTACK, .proc/pre_attack_hit)
RegisterSignal(toolbox, COMSIG_ITEM_ATTACK, .proc/attack_hit)
RegisterSignal(toolbox, COMSIG_ITEM_AFTERATTACK, .proc/post_attack_hit)
attacker.put_in_active_hand(toolbox, forced = TRUE)
attacker.a_intent_change(INTENT_HARM)
toolbox.melee_attack_chain(attacker, victim)
TEST_ASSERT(pre_attack_hit, "Pre-attack signal was not fired")
TEST_ASSERT(attack_hit, "Attack signal was not fired")
TEST_ASSERT(post_attack_hit, "Post-attack signal was not fired")
/datum/unit_test/disarm/Run()
var/mob/living/carbon/human/attacker = allocate(/mob/living/carbon/human)
var/mob/living/carbon/human/victim = allocate(/mob/living/carbon/human)
var/obj/item/storage/toolbox/toolbox = allocate(/obj/item/storage/toolbox)
victim.put_in_active_hand(toolbox, forced = TRUE)
attacker.a_intent_change(INTENT_DISARM)
var/obj/structure/barricade/dense_object = allocate(/obj/structure/barricade)
// Attacker --> Victim --> Empty space --> Wall
attacker.forceMove(run_loc_bottom_left)
victim.forceMove(locate(run_loc_bottom_left.x + 1, run_loc_bottom_left.y, run_loc_bottom_left.z))
dense_object.forceMove(locate(run_loc_bottom_left.x + 3, run_loc_bottom_left.y, run_loc_bottom_left.z))
// First disarm, world should now look like:
// Attacker --> Empty space --> Victim --> Wall
victim.attack_hand(attacker)
TEST_ASSERT_EQUAL(victim.loc.x, run_loc_bottom_left.x + 2, "Victim wasn't moved back after being pushed")
TEST_ASSERT(!victim.has_status_effect(STATUS_EFFECT_KNOCKDOWN), "Victim was knocked down despite not being against a wall")
TEST_ASSERT_EQUAL(victim.get_active_held_item(), toolbox, "Victim dropped toolbox despite not being against a wall")
attacker.forceMove(get_step(attacker, EAST))
// Second disarm, victim was against wall and should be down
victim.attack_hand(attacker)
TEST_ASSERT_EQUAL(victim.loc.x, run_loc_bottom_left.x + 2, "Victim was moved after being pushed against a wall")
TEST_ASSERT(victim.has_status_effect(STATUS_EFFECT_KNOCKDOWN), "Victim was not knocked down after being pushed against a wall")
TEST_ASSERT_EQUAL(victim.get_active_held_item(), null, "Victim didn't drop toolbox after being pushed against a wall")

View File

@@ -0,0 +1,16 @@
// Checks that the confusion symptom correctly gives, and removes, confusion
/datum/unit_test/confusion_symptom/Run()
var/mob/living/carbon/human/H = allocate(/mob/living/carbon/human)
var/datum/disease/advance/confusion/disease = allocate(/datum/disease/advance/confusion)
var/datum/symptom/confusion/confusion = disease.symptoms[1]
disease.processing = TRUE
disease.update_stage(5)
disease.infect(H, make_copy = FALSE)
confusion.Activate(disease)
TEST_ASSERT(H.get_confusion() > 0, "Human is not confused after getting symptom.")
disease.cure()
TEST_ASSERT_EQUAL(H.get_confusion(), 0, "Human is still confused after curing confusion.")
/datum/disease/advance/confusion/New()
symptoms += new /datum/symptom/confusion
Refresh()

View File

@@ -0,0 +1,25 @@
/datum/unit_test/emoting
var/emotes_used = 0
/datum/unit_test/emoting/Run()
var/mob/living/carbon/human/human = allocate(/mob/living/carbon/human)
RegisterSignal(human, COMSIG_MOB_EMOTE, .proc/on_emote_used)
human.say("*shrug")
TEST_ASSERT_EQUAL(emotes_used, 1, "Human did not shrug")
human.say("*beep")
TEST_ASSERT_EQUAL(emotes_used, 1, "Human beeped, when that should be restricted to silicons")
human.setOxyLoss(140)
TEST_ASSERT(human.stat != CONSCIOUS, "Human is somehow conscious after receiving suffocation damage")
human.say("*shrug")
TEST_ASSERT_EQUAL(emotes_used, 1, "Human shrugged while unconscious")
human.say("*deathgasp")
TEST_ASSERT_EQUAL(emotes_used, 2, "Human could not deathgasp while unconscious")
/datum/unit_test/emoting/proc/on_emote_used()
emotes_used += 1

View File

@@ -0,0 +1,21 @@
/// This test checks all heretic knowledge nodes - excluding the ones which are unreachable on purpose - and ensures players can reach them in game.
/// If it finds a node that is unreachable, it throws an error.
/datum/unit_test/heretic_knowledge/Run()
///List of all knowledge excluding the unreachable base types.
var/list/blacklist = list(/datum/eldritch_knowledge/spell,/datum/eldritch_knowledge/curse,/datum/eldritch_knowledge/final,/datum/eldritch_knowledge/summon)
var/list/all_possible_knowledge = subtypesof(/datum/eldritch_knowledge) - blacklist
var/list/list_to_check = GLOB.heretic_start_knowledge.Copy()
var/i = 0
while(i < length(list_to_check))
var/datum/eldritch_knowledge/eldritch_knowledge = allocate(list_to_check[++i])
for(var/next_knowledge in eldritch_knowledge.next_knowledge)
if(next_knowledge in list_to_check)
continue
list_to_check += next_knowledge
if(length(all_possible_knowledge) != length(all_possible_knowledge & list_to_check))
var/list/unreachables = all_possible_knowledge - list_to_check
for(var/X in unreachables)
var/datum/eldritch_knowledge/eldritch_knowledge = X
Fail("[initial(eldritch_knowledge.name)] is unreachable by players! Add it to the blacklist in /code/modules/unit_tests/heretic_knowledge.dm if it is purposeful!")

View File

@@ -0,0 +1,33 @@
// test Jewish holiday
/datum/unit_test/hanukkah_2123/Run()
var/datum/holiday/hebrew/hanukkah/hanukkah = new
TEST_ASSERT(hanukkah.shouldCelebrate(14, DECEMBER, 2123, 2, TUESDAY), "December 14, 2123 was not Hanukkah.")
// test Islamic holiday
/datum/unit_test/ramadan_2165/Run()
var/datum/holiday/islamic/ramadan/ramadan = new
TEST_ASSERT(ramadan.shouldCelebrate(6, NOVEMBER, 2165, 1, WEDNESDAY), "November 6, 2165 was not Ramadan.")
// nth day of week
/datum/unit_test/thanksgiving_2020/Run()
var/datum/holiday/nth_week/thanksgiving/thanksgiving = new
TEST_ASSERT(thanksgiving.shouldCelebrate(26, NOVEMBER, 2020, 4, THURSDAY), "November 26, 2020 was not Thanksgiving.")
// another nth day of week
/datum/unit_test/indigenous_3683/Run()
var/datum/holiday/nth_week/indigenous/indigenous = new
TEST_ASSERT(indigenous.shouldCelebrate(11, OCTOBER, 3683, 2, MONDAY), "October 11, 3683 was not Indigenous Peoples' Day.")
// plain old simple holiday
/datum/unit_test/hello_2020/Run()
var/datum/holiday/hello/hello = new
TEST_ASSERT(hello.shouldCelebrate(21, NOVEMBER, 2020, 3, SATURDAY), "November 21, 2020 was not Hello day.")
// holiday which goes across months
/datum/unit_test/new_year_1983/Run()
var/datum/holiday/new_year/new_year = new
TEST_ASSERT(new_year.shouldCelebrate(2, JANUARY, 1983, 1, SUNDAY), "January 2, 1983 was not New Year.")
/datum/unit_test/moth_week_2020/Run()
var/datum/holiday/moth/moth = new
TEST_ASSERT(moth.shouldCelebrate(19, JULY, 2020, 3, SATURDAY), "July 19, 2020 was not Moth Week.")

View File

@@ -0,0 +1,11 @@
/datum/unit_test/initialize_sanity/Run()
if(length(SSatoms.BadInitializeCalls))
Fail("Bad Initialize() calls detected. Please read logs.")
var/list/init_failures_to_text = list(
"[BAD_INIT_QDEL_BEFORE]" = "Qdeleted Before Initialized",
"[BAD_INIT_DIDNT_INIT]" = "Did Not Initialize",
"[BAD_INIT_SLEPT]" = "Initialize() Slept",
"[BAD_INIT_NO_HINT]" = "No Initialize() Hint Returned",
)
for(var/failure in SSatoms.BadInitializeCalls)
log_world("[failure]: [init_failures_to_text["[SSatoms.BadInitializeCalls[failure]]"]]") // You like stacked brackets?

View File

@@ -0,0 +1,6 @@
/datum/unit_test/keybinding_init/Run()
for(var/i in subtypesof(/datum/keybinding))
var/datum/keybinding/KB = i
if(initial(KB.keybind_signal) || !initial(KB.name))
continue
Fail("[KB.name] does not have a keybind signal defined.")

View File

@@ -3,11 +3,10 @@
var/obj/machinery/freezer = allocate(/obj/machinery/atmospherics/components/unary/thermomachine/freezer)
var/turf/freezer_location = freezer.loc
freezer_location.ChangeTurf(/turf/open/floor/plasteel)
freezer.deconstruct()
// Check that the components are created
TEST_ASSERT(locate(/obj/item/stock_parts/micro_laser) in freezer_location, "Couldn't find micro-laser when disassembling freezer")
// Check that the circuit board itself is created
TEST_ASSERT(locate(/obj/item/circuitboard/machine/thermomachine/freezer) in freezer_location, "Couldn't find the circuit board when disassembling freezer")
TEST_ASSERT(locate(/obj/item/circuitboard/machine/thermomachine) in freezer_location, "Couldn't find the circuit board when disassembling freezer")

View File

@@ -0,0 +1,15 @@
/datum/unit_test/merge_type/Run()
var/list/blacklist = list(/obj/item/stack/sheet,
/obj/item/stack/sheet/mineral,
/obj/item/stack/ore,
/obj/item/stack/spacecash,
/obj/item/stack/license_plates,
/obj/item/stack/tile/mineral,
/obj/item/stack/tile)
var/list/paths = subtypesof(/obj/item/stack) - blacklist
for(var/stackpath in paths)
var/obj/item/stack/stack = stackpath
if(!initial(stack.merge_type))
Fail("([stack]) lacks set merge_type variable!")

View File

@@ -17,3 +17,22 @@
/datum/unit_test/metabolization/Destroy()
SSmobs.ignite()
return ..()
/datum/unit_test/on_mob_end_metabolize/Run()
var/mob/living/carbon/human/user = allocate(/mob/living/carbon/human)
var/obj/item/reagent_containers/pill/pill = allocate(/obj/item/reagent_containers/pill)
var/datum/reagent/drug/methamphetamine/meth = /datum/reagent/drug/methamphetamine
// Give them enough meth to be consumed in 2 metabolizations
pill.reagents.add_reagent(meth, initial(meth.metabolization_rate) * 1.9)
pill.attack(user, user)
user.Life()
TEST_ASSERT(user.reagents.has_reagent(meth), "User does not have meth in their system after consuming it")
TEST_ASSERT(user.has_movespeed_modifier(/datum/movespeed_modifier/reagent/methamphetamine), "User consumed meth, but did not gain movespeed modifier")
user.Life()
TEST_ASSERT(!user.reagents.has_reagent(meth), "User still has meth in their system when it should've finished metabolizing")
TEST_ASSERT(!user.has_movespeed_modifier(/datum/movespeed_modifier/reagent/methamphetamine), "User still has movespeed modifier despite not containing any more meth")

View File

@@ -30,8 +30,8 @@
CHECK_OUTFIT_SLOT(glasses, ITEM_SLOT_EYES)
CHECK_OUTFIT_SLOT(id, ITEM_SLOT_ID)
CHECK_OUTFIT_SLOT(suit_store, ITEM_SLOT_SUITSTORE)
CHECK_OUTFIT_SLOT(l_pocket, ITEM_SLOT_POCKET)
CHECK_OUTFIT_SLOT(r_pocket, ITEM_SLOT_POCKET)
CHECK_OUTFIT_SLOT(l_pocket, ITEM_SLOT_LPOCKET)
CHECK_OUTFIT_SLOT(r_pocket, ITEM_SLOT_RPOCKET)
if (outfit.backpack_contents || outfit.box)
var/list/backpack_contents = outfit.backpack_contents?.Copy()

View File

@@ -0,0 +1,10 @@
/datum/unit_test/pills/Run()
var/mob/living/carbon/human/human = allocate(/mob/living/carbon/human)
var/obj/item/reagent_containers/pill/iron/pill = allocate(/obj/item/reagent_containers/pill/iron)
TEST_ASSERT_EQUAL(human.has_reagent(/datum/reagent/iron), FALSE, "Human somehow has iron before taking pill")
pill.attack(human, human)
human.Life()
TEST_ASSERT(human.has_reagent(/datum/reagent/iron), "Human doesn't have iron after taking pill")

View File

@@ -0,0 +1,5 @@
/datum/unit_test/projectile_movetypes/Run()
for(var/path in typesof(/obj/projectile))
var/obj/projectile/projectile = path
if(initial(projectile.movement_type) & PHASING)
Fail("[path] has default movement type PHASING. Piercing projectiles should be done using the projectile piercing system, not movement_types!")

View File

@@ -0,0 +1,59 @@
// testing the mob expose procs are working
/datum/reagent/method_patch_test
name = "method patch test"
/datum/reagent/method_patch_test/expose_mob(mob/living/target, methods = PATCH, reac_volume, show_message = TRUE)
. = ..()
if(methods & PATCH)
target.health = 90
if(methods & INJECT)
target.health = 80
/datum/unit_test/reagent_mob_expose/Run()
// Life() is handled just by tests
SSmobs.pause()
var/mob/living/carbon/human/human = allocate(/mob/living/carbon/human)
var/obj/item/reagent_containers/dropper/dropper = allocate(/obj/item/reagent_containers/dropper)
var/obj/item/reagent_containers/food/drinks/drink = allocate(/obj/item/reagent_containers/food/drinks/bottle)
var/obj/item/reagent_containers/pill/patch/patch = allocate(/obj/item/reagent_containers/pill/patch)
var/obj/item/reagent_containers/syringe/syringe = allocate(/obj/item/reagent_containers/syringe)
// INGEST
TEST_ASSERT_EQUAL(human.fire_stacks, 0, "Human has fire stacks before taking phlogiston")
drink.reagents.add_reagent(/datum/reagent/phlogiston, 10)
drink.attack(human, human)
TEST_ASSERT_EQUAL(human.fire_stacks, 1, "Human does not have fire stacks after taking phlogiston")
human.Life()
TEST_ASSERT(human.fire_stacks > 1, "Human fire stacks did not increase after life tick")
// TOUCH
dropper.reagents.add_reagent(/datum/reagent/water, 1)
dropper.afterattack(human, human, TRUE)
TEST_ASSERT_EQUAL(human.fire_stacks, 0, "Human still has fire stacks after touching water")
// VAPOR
TEST_ASSERT_EQUAL(human.drowsyness, 0, "Human is drowsy at the start of testing")
drink.reagents.clear_reagents()
drink.reagents.add_reagent(/datum/reagent/nitrous_oxide, 10)
drink.reagents.trans_to(human, 10, methods = VAPOR)
TEST_ASSERT_NOTEQUAL(human.drowsyness, 0, "Human is not drowsy after exposure to vapors")
// PATCH
human.health = 100
TEST_ASSERT_EQUAL(human.health, 100, "Human health did not set properly")
patch.reagents.add_reagent(/datum/reagent/method_patch_test, 1)
patch.self_delay = 0
patch.attack(human, human)
TEST_ASSERT_EQUAL(human.health, 90, "Human health did not update after patch was applied")
// INJECT
syringe.reagents.add_reagent(/datum/reagent/method_patch_test, 1)
syringe.mode = SYRINGE_INJECT
syringe.afterattack(human, human, TRUE)
TEST_ASSERT_EQUAL(human.health, 80, "Human health did not update after injection from syringe")
/datum/unit_test/reagent_mob_expose/Destroy()
SSmobs.ignite()
return ..()

View File

@@ -0,0 +1,12 @@
/datum/unit_test/reagent_mob_procs/Run()
var/mob/living/carbon/human/human = allocate(/mob/living/carbon/human)
var/obj/item/food/hotdog/debug/fooditem = allocate(/obj/item/food/hotdog/debug)
TEST_ASSERT_EQUAL(human.has_reagent(/datum/reagent/consumable/ketchup), FALSE, "Human somehow has ketchup before eating")
TEST_ASSERT_EQUAL(human.has_reagent(/datum/reagent/medicine/epinephrine), FALSE, "Human somehow has epinephrine before injecting")
fooditem.attack(human, human)
human.reagents.add_reagent(/datum/reagent/medicine/epinephrine, 5)
TEST_ASSERT(human.has_reagent(/datum/reagent/consumable/ketchup), "Human doesn't have ketchup after eating")
TEST_ASSERT(human.has_reagent(/datum/reagent/medicine/epinephrine), "Human doesn't have epinephrine after injecting")

View File

@@ -10,7 +10,6 @@
test(";%Never gonna give you up", "Never gonna give you up", list(MODE_HEADSET = TRUE, MODE_SING = TRUE))
test(".s Gun plz", "Gun plz", list(RADIO_KEY = RADIO_KEY_SECURITY, RADIO_EXTENSION = RADIO_CHANNEL_SECURITY))
test("...What", "...What", list())
//note to lettern: add the ++, ||, __, and the verb*text checks
/datum/unit_test/get_message_mods/proc/test(message, expected_message, list/expected_mods)
var/list/mods = list()

View File

@@ -0,0 +1,47 @@
/**
* Check that standard food items fit on the serving tray
*/
/datum/unit_test/servingtray/Run()
var/mob/living/carbon/human/human = allocate(/mob/living/carbon/human)
var/obj/structure/table/the_table = allocate(/obj/structure/table)
var/obj/item/storage/bag/tray/test_tray = allocate(/obj/item/storage/bag/tray)
var/obj/item/reagent_containers/food/banana = allocate(/obj/item/food/rationpack)
var/obj/item/food/the_bread = allocate(/obj/item/food/breadslice)
var/obj/item/reagent_containers/food/sugarcookie = allocate(/obj/item/food/cookie/sugar)
var/obj/item/clothing/under/jumpsuit = allocate(/obj/item/clothing/under/color/black)
TEST_ASSERT_EQUAL((the_bread in test_tray.contents), FALSE, "The bread is on the serving tray at test start")
// set the tray to single item mode the dirty way
var/datum/component/storage/tray_storage = test_tray.GetComponent(/datum/component/storage)
tray_storage.collection_mode = COLLECT_ONE
test_tray.pre_attack(the_bread, human)
TEST_ASSERT_EQUAL((the_bread in test_tray.contents), TRUE, "The bread did not get picked up by the serving tray")
test_tray.pre_attack(banana, human)
TEST_ASSERT_EQUAL((banana in test_tray.contents), TRUE, "The banana did not get picked up by the serving tray")
the_table.attackby(test_tray, human)
TEST_ASSERT_EQUAL(test_tray.contents.len, 0, "The serving tray did not drop all items on hitting the table")
test_tray.pre_attack(sugarcookie, human)
TEST_ASSERT_EQUAL((sugarcookie in test_tray.contents), TRUE, "The sugarcookie did not get picked up by the serving tray")
human.equip_to_slot(jumpsuit, ITEM_SLOT_ICLOTHING)
TEST_ASSERT(human.get_item_by_slot(ITEM_SLOT_ICLOTHING), "Human does not have jumpsuit on")
human.equip_to_slot(test_tray, ITEM_SLOT_LPOCKET)
TEST_ASSERT(human.get_item_by_slot(ITEM_SLOT_LPOCKET), "Serving tray failed to fit in the Left Pocket")
human.equip_to_slot(test_tray, ITEM_SLOT_RPOCKET)
TEST_ASSERT(human.get_item_by_slot(ITEM_SLOT_RPOCKET), "Serving tray failed to fit in the Right Pocket")
test_tray.attack(human, human)
TEST_ASSERT_EQUAL(test_tray.contents.len, 0, "The serving tray did not drop all items on hitting a human")

View File

@@ -0,0 +1,15 @@
/datum/unit_test/siunit/Run()
TEST_ASSERT_EQUAL(siunit(0.5345, "A", 0), "535 mA", "")
TEST_ASSERT_EQUAL(siunit(0.5344, "A", 0), "534 mA", "")
TEST_ASSERT_EQUAL(siunit(-0.5344, "A", 0), "-534 mA", "")
TEST_ASSERT_EQUAL(siunit_pressure(1.234, 1), "1.2 kPa", "") // test for pascal require *10e-3, as the game thinks in kPa, the proc siunit in Pa
TEST_ASSERT_EQUAL(siunit_pressure(1.234, 2), "1.23 kPa", "")
TEST_ASSERT_EQUAL(siunit_pressure(1.234, 3), "1.234 kPa", "")
TEST_ASSERT_EQUAL(siunit_pressure(1, 4), "1 kPa", "")
TEST_ASSERT_EQUAL(siunit_pressure(0), "0 Pa", "")
TEST_ASSERT_EQUAL(siunit_pressure(1e3), "1 MPa", "")
TEST_ASSERT_EQUAL(siunit_pressure(999e3), "999 MPa", "")
TEST_ASSERT_EQUAL(siunit_pressure(999.9e3), "999.9 MPa" , "")
TEST_ASSERT_EQUAL(siunit_pressure(999.9e3, 0), "1 GPa", "")
TEST_ASSERT_EQUAL(siunit_pressure(1e6), "1 GPa", "")
TEST_ASSERT_EQUAL(siunit_pressure(3e17), "300000 PPa", "")

View File

@@ -1,7 +1,7 @@
/datum/unit_test/spawn_humans/Run()
var/locs = block(run_loc_bottom_left, run_loc_top_right)
var/locs = block(run_loc_bottom_left, run_loc_top_right)
for(var/I in 1 to 5)
new /mob/living/carbon/human(pick(locs))
for(var/I in 1 to 5)
new /mob/living/carbon/human(pick(locs))
sleep(50)
sleep(50)

View File

@@ -0,0 +1,5 @@
/datum/unit_test/species_whitelist_check/Run()
for(var/typepath in subtypesof(/datum/species))
var/datum/species/S = typepath
if(initial(S.changesource_flags) == NONE)
Fail("A species type was detected with no changesource flags: [S]")

View File

@@ -0,0 +1,40 @@
/datum/unit_test/stomach/Run()
// Pause natural mob life so it can be handled entirely by the test
SSmobs.pause()
var/mob/living/carbon/human/human = allocate(/mob/living/carbon/human)
var/obj/item/food/hotdog/debug/fooditem = allocate(/obj/item/food/hotdog/debug)
var/obj/item/organ/stomach/belly = human.getorganslot(ORGAN_SLOT_STOMACH)
var/obj/item/reagent_containers/pill/pill = allocate(/obj/item/reagent_containers/pill)
var/datum/reagent/drug/methamphetamine/meth = /datum/reagent/drug/methamphetamine
TEST_ASSERT_EQUAL(human.has_reagent(/datum/reagent/consumable/ketchup), FALSE, "Human somehow has ketchup before eating")
fooditem.attack(human, human)
TEST_ASSERT(belly.reagents.has_reagent(/datum/reagent/consumable/ketchup), "Stomach doesn't have ketchup after eating")
TEST_ASSERT_EQUAL(human.reagents.has_reagent(/datum/reagent/consumable/ketchup), FALSE, "Human body has ketchup after eating it should only be in the stomach")
//Give them meth and let it kick in
pill.reagents.add_reagent(meth, initial(meth.metabolization_rate) * 1.9)
pill.attack(human, human)
human.Life()
TEST_ASSERT(human.reagents.has_reagent(meth), "Human body does not have meth after life tick")
TEST_ASSERT(human.has_movespeed_modifier(/datum/movespeed_modifier/reagent/methamphetamine), "Human consumed meth, but did not gain movespeed modifier")
belly.Remove(human)
human.reagents.remove_all(human.reagents.total_volume)
TEST_ASSERT_EQUAL(human.has_reagent(/datum/reagent/consumable/ketchup), FALSE, "Human has reagents after clearing")
fooditem.attack(human, human)
TEST_ASSERT_EQUAL(human.has_reagent(/datum/reagent/consumable/ketchup), FALSE, "Human has ketchup without a stomach")
/datum/unit_test/stomach/Destroy()
SSmobs.ignite()
return ..()

View File

@@ -27,6 +27,33 @@
TEST_ASSERT(!patient.has_trauma_type(), "Patient kept their brain trauma after brain surgery")
TEST_ASSERT(patient.getOrganLoss(ORGAN_SLOT_BRAIN) < 20, "Patient did not heal their brain damage after brain surgery")
/datum/unit_test/head_transplant/Run()
var/mob/living/carbon/human/user = allocate(/mob/living/carbon/human)
var/mob/living/carbon/human/alice = allocate(/mob/living/carbon/human)
var/mob/living/carbon/human/bob = allocate(/mob/living/carbon/human)
alice.fully_replace_character_name(null, "Alice")
bob.fully_replace_character_name(null, "Bob")
var/obj/item/bodypart/head/alices_head = alice.get_bodypart(BODY_ZONE_HEAD)
alices_head.drop_limb()
var/obj/item/bodypart/head/bobs_head = bob.get_bodypart(BODY_ZONE_HEAD)
bobs_head.drop_limb()
TEST_ASSERT_EQUAL(alice.get_bodypart(BODY_ZONE_HEAD), null, "Alice still has a head after dismemberment")
TEST_ASSERT_EQUAL(alice.get_visible_name(), "Unknown", "Alice's head was dismembered, but they are not Unknown")
TEST_ASSERT_EQUAL(bobs_head.real_name, "Bob", "Bob's head does not remember that it is from Bob")
// Put Bob's head onto Alice's body
var/datum/surgery_step/add_prosthetic/add_prosthetic = new
user.put_in_active_hand(bobs_head)
add_prosthetic.success(user, alice, BODY_ZONE_HEAD, bobs_head)
TEST_ASSERT(!isnull(alice.get_bodypart(BODY_ZONE_HEAD)), "Alice has no head after prosthetic replacement")
TEST_ASSERT_EQUAL(alice.get_visible_name(), "Bob", "Bob's head was transplanted onto Alice's body, but their name is not Bob")
/datum/unit_test/multiple_surgeries/Run()
var/mob/living/carbon/human/user = allocate(/mob/living/carbon/human)
var/mob/living/carbon/human/patient_zero = allocate(/mob/living/carbon/human)
@@ -41,8 +68,6 @@
TEST_ASSERT(surgery_for_zero.step_in_progress, "Surgery on patient zero was not initiated")
var/datum/surgery/organ_manipulation/surgery_for_one = new
sleep(0.2) // if we don't have this, then the next surgery step can start *before* the previous one does, which is no good
// Without waiting for the incision to complete, try to start a new surgery
TEST_ASSERT(!surgery_step.initiate(user, patient_one, BODY_ZONE_CHEST, scalpel, surgery_for_one), "Was allowed to start a second surgery without the rod of asclepius")

View File

@@ -0,0 +1,10 @@
/datum/unit_test/auto_teleporter_linking/Run()
// Put down the teleporter machinery
var/obj/machinery/teleport/hub/hub = allocate(/obj/machinery/teleport/hub)
var/obj/machinery/teleport/station/station = allocate(/obj/machinery/teleport/station, locate(run_loc_bottom_left.x + 1, run_loc_bottom_left.y, run_loc_bottom_left.z))
var/obj/machinery/computer/teleporter/computer = allocate(/obj/machinery/computer/teleporter, locate(run_loc_bottom_left.x + 2, run_loc_bottom_left.y, run_loc_bottom_left.z))
TEST_ASSERT_EQUAL(hub.power_station, station, "Hub didn't link to the station")
TEST_ASSERT_EQUAL(station.teleporter_console, computer, "Station didn't link to the teleporter console")
TEST_ASSERT_EQUAL(station.teleporter_hub, hub, "Station didn't link to the hub")
TEST_ASSERT_EQUAL(computer.power_station, station, "Teleporter console didn't link to the hub")

View File

@@ -1,9 +1,14 @@
/*
Usage:
Override /Run() to run your test code
Call Fail() to fail the test (You should specify a reason)
You may use /New() and /Destroy() for setup/teardown respectively
You can use the run_loc_bottom_left and run_loc_top_right to get turfs for testing
*/
GLOBAL_DATUM(current_test, /datum/unit_test)
@@ -14,19 +19,33 @@ GLOBAL_VAR(test_log)
//Bit of metadata for the future maybe
var/list/procs_tested
//usable vars
/// The bottom left turf of the testing zone
var/turf/run_loc_bottom_left
/// The top right turf of the testing zone
var/turf/run_loc_top_right
/// The type of turf to allocate for the testing zone
var/test_turf_type = /turf/open/floor/plasteel
//internal shit
var/focus = FALSE
var/succeeded = TRUE
var/list/allocated
var/list/fail_reasons
var/static/datum/turf_reservation/turf_reservation
/datum/unit_test/New()
if (isnull(turf_reservation))
turf_reservation = SSmapping.RequestBlockReservation(5, 5)
for (var/turf/reserved_turf in turf_reservation.reserved_turfs)
reserved_turf.ChangeTurf(test_turf_type)
allocated = new
run_loc_bottom_left = locate(1, 1, 1)
run_loc_top_right = locate(5, 5, 1)
run_loc_bottom_left = locate(turf_reservation.bottom_left_coords[1], turf_reservation.bottom_left_coords[2], turf_reservation.bottom_left_coords[3])
run_loc_top_right = locate(turf_reservation.top_right_coords[1], turf_reservation.top_right_coords[2], turf_reservation.top_right_coords[3])
/datum/unit_test/Destroy()
//clear the test area
@@ -61,7 +80,14 @@ GLOBAL_VAR(test_log)
/proc/RunUnitTests()
CHECK_TICK
for(var/I in subtypesof(/datum/unit_test))
var/tests_to_run = subtypesof(/datum/unit_test)
for (var/_test_to_run in tests_to_run)
var/datum/unit_test/test_to_run = _test_to_run
if (initial(test_to_run.focus))
tests_to_run = list(test_to_run)
break
for(var/I in tests_to_run)
var/datum/unit_test/test = new I
GLOB.current_test = test