Files
Bubberstation/code/modules/unit_tests/breath.dm
Dani Glore f9fe79a307 Organ Unit Tests & Bugfixes (#73026)
## About The Pull Request

This PR adds a new unit test for all organs, a new unit test for lungs,
and includes improvements for the existing breath and organ_set_bonus
tests. Using the tests, I was able to root out bugs in the organs. This
PR includes an advanced refactor of several developer-facing functions.
This PR certainly represents a "quality pass" for organs which will make
them easier to develop from now on.

### Synopsis of changes:
1. Fixed many fundamental bugs in organ code, especially in
`Insert()`/`Remove()` and their overrides.
2. Added two new procs to `/obj/item/organ` named `on_insert` and
`on_remove`, each being called after `Insert()`/`Remove()`.
3. Added `organ_effects` lazylist to `/obj/item/organ`. Converted
`organ_traits` to lazylist. 2x less empty lists per organ.
4. Adding `SHOULD_CALL_PARENT(TRUE)` to `Insert()`/`Remove()` was very
beneficial to stability and overall code health.
5. Created unit test `organ_sanity` for all usable organs in the game.
Tests insertion and removal.
6. Created unit test `lungs_sanity` for
`/obj/item/organ/internal/lungs`.
7. Improved `breath_sanity` unit tests with additional tests and
conditions.
8. Improved `organ_set_bonus_sanity` unit tests with better
documentation and maintainable code.

---

### Granular bug/fix list:

- A lot of organs are overriding `Insert()` to apply unique
side-effects, but aren't checking the return value of the parent proc
which causes the activation of side-effects even if the insertion
technically fails. I noticed the use-case of applying "unique
side-effects" is repeated across a lot of organs in the game, and by
overriding `Insert()` the potential for bugs is very high; I solved this
problem with inversion-of-control by adding two new procs to
`/obj/item/organ` named `on_insert` and `on_remove`, each being called
after `Insert()` and `Remove()` succeed.
- Many organs, such as abductor "glands", cursed heart, demon heart,
alien hive-node, alien plasma-vessel, etc, were not returning their
parent's `Insert()` proc return value at all, and as a result those
organs `Insert()`s were always returning `null`. I have been mopping
those bugs up in my last few PRs, and now the unit test reveals it all.
Functions such as those in surgery expect a truthy value to be returned
from `Insert()` to represent insertion success, and otherwise it
force-moves the organ out of the mob.
- Fixed abductor "glands" which had a hard-del bug due to their
`Remove()` not calling the parent proc.
- Fixed cybernetic arm implants which had a hard-del bug due to
`Remove()` not resetting their `hand` variable to `null`.
- Fixed lungs gas exchange implementation, which was allowing exhaled
gases to feedback into the inhaled gases, which caused Humans to inhale
much more gas than intended and not exhale expected gases.

### Overview of the `organ_sanity` unit test:

- The new `organ_sanity` unit test gathers all "usable" organs in the
game and tests to see if their `Insert()` and `Remove()` functions
behave as we expect them to.
- Some organs, such as the Nightmare Brain, cause the mob's species to
change which subsequently swaps out all of their organs; the unit test
accounts for these organs via the typecache `species_changing_organs`.
- Some organs are not usable in-game and can't be unit tested, so the
unit test accounts for them via the typecache `test_organ_blacklist`.

### Overview of the `lungs_sanity` unit test:

- This unit test focuses on `/obj/item/organ/internal/lungs` including
Plasmaman and Ashwalker lungs. The test focuses on testing the lungs'
`check_breath()` proc.
- The tests are composed of calling `check_breath` with different gas
mixes to test breathing and suffocation.
- Includes gas exchange test for inhaled/exhaled gases, such as O2 to
CO2.

### Improvements to the `breath_sanity` unit tests:

- Added additional tests for suffocation with empty internals, pure
Nitrogen internals, and a gas-less turf.
- Includes slightly more reliable tests for internals tanks.

## Why It's Good For The Game

**Organs and Lungs were mostly untested. Too many refactors have been
submitted without the addition of unit tests to prove the code works at
all.** Time to stop. _Time to get some help_. Due to how bad the code
health is in organs, any time we've tried to work with them some sort of
bug caused them to blow up in our faces. I am trying to fix some of that
by establishing some standard testing for organs. These tests have
revealed and allowed me to fix lot of basic developer errors/oversights,
as well as a few severe bugs.


![image](https://user-images.githubusercontent.com/17753498/220251281-07ef598f-355b-43a9-afd6-1de9690831da.png)

## Changelog

🆑 A.C.M.O.
fix: Fixed lungs gas exchange implementation, so you always inhale and
exhale the correct gases.
fix: Fixed a large quantity of hard-deletes which were being caused by
organs and cybernetic organs.
fix: Fixed many organs which were applying side-effects regardless of
whether or not the insertion failed.
code: Added unit tests for Organs.
code: Added unit tests for Lungs.
code: Improved unit tests for breathing.
code: Improved unit tests for DNA Infuser organs.
/🆑
2023-03-18 17:20:28 -07:00

102 lines
6.0 KiB
Plaintext

/// Tests to ensure humans, plasmamen, and ashwalkers can breath in normal situations.
/// Ensures algorithmic correctness of the "breathe()" and "toggle_internals()" procs.
/// Built to prevent regression on an issue surrounding QUANTIZE() and BREATH_VOLUME.
/// See the comment on BREATH_VOLUME for more details.
/datum/unit_test/breath
abstract_type = /datum/unit_test/breath
/// Equips the given Human with a new instance of the given tank type and a breathing mask.
/// Returns the new equipped tank.
/datum/unit_test/breath/proc/equip_labrat_internals(mob/living/carbon/human/lab_rat, tank_type)
var/obj/item/clothing/mask/breath/mask = allocate(/obj/item/clothing/mask/breath)
var/obj/item/tank/internals/source = allocate(tank_type)
lab_rat.equip_to_slot_if_possible(mask, ITEM_SLOT_MASK)
lab_rat.equip_to_slot_if_possible(source, ITEM_SLOT_HANDS)
return source
/datum/unit_test/breath/breath_sanity/Run()
// Breathing from turf.
var/mob/living/carbon/human/lab_rat = allocate(/mob/living/carbon/human/consistent)
lab_rat.forceMove(run_loc_floor_bottom_left)
var/turf/open/to_fill = run_loc_floor_bottom_left
to_fill.initial_gas_mix = OPENTURF_DEFAULT_ATMOS
to_fill.air = to_fill.create_gas_mixture()
lab_rat.breathe()
TEST_ASSERT(!lab_rat.failed_last_breath && !lab_rat.has_alert(ALERT_NOT_ENOUGH_OXYGEN), "Humans can't get a full breath from the standard initial_gas_mix on a turf")
// Breathing from standard internals tank.
lab_rat = allocate(/mob/living/carbon/human/consistent)
var/obj/item/tank/internals/source = equip_labrat_internals(lab_rat, /obj/item/tank/internals/emergency_oxygen)
lab_rat.breathe()
TEST_ASSERT(!lab_rat.failed_last_breath && !lab_rat.has_alert(ALERT_NOT_ENOUGH_OXYGEN), "Humans can't get a full breath from standard o2 tanks")
if(!isnull(lab_rat.internal))
TEST_ASSERT(source.toggle_internals(lab_rat) && isnull(lab_rat.internal), "toggle_internals() failed to close internals")
// Empty internals suffocation.
lab_rat = allocate(/mob/living/carbon/human/consistent)
source = equip_labrat_internals(lab_rat, /obj/item/tank/internals/emergency_oxygen/empty)
TEST_ASSERT(source.toggle_internals(lab_rat) && !isnull(lab_rat.internal), "Plasmaman toggle_internals() failed to toggle internals")
lab_rat.breathe()
TEST_ASSERT(lab_rat.failed_last_breath && lab_rat.has_alert(ALERT_NOT_ENOUGH_OXYGEN), "Humans should suffocate from empty o2 tanks")
// Nitrogen internals suffocation.
lab_rat = allocate(/mob/living/carbon/human/consistent)
source = equip_labrat_internals(lab_rat, /obj/item/tank/internals/emergency_oxygen/empty)
source.air_contents.assert_gas(/datum/gas/nitrogen)
source.air_contents.gases[/datum/gas/nitrogen][MOLES] = (10 * ONE_ATMOSPHERE) * source.volume / (R_IDEAL_GAS_EQUATION * T20C)
TEST_ASSERT(source.toggle_internals(lab_rat) && !isnull(lab_rat.internal), "Plasmaman toggle_internals() failed to toggle internals")
lab_rat.breathe()
TEST_ASSERT(lab_rat.failed_last_breath && lab_rat.has_alert(ALERT_NOT_ENOUGH_OXYGEN), "Humans should suffocate from pure n2 tanks")
/datum/unit_test/breath/breath_sanity/Destroy()
//Reset initial_gas_mix to avoid future issues on other tests
var/turf/open/to_fill = run_loc_floor_bottom_left
to_fill.initial_gas_mix = OPENTURF_DEFAULT_ATMOS
return ..()
/// Tests to make sure plasmaman can breath from their internal tanks
/datum/unit_test/breath/breath_sanity_plasmamen
/datum/unit_test/breath/breath_sanity_plasmamen/Run()
// Breathing from pure Plasma internals.
var/mob/living/carbon/human/species/plasma/lab_rat = allocate(/mob/living/carbon/human/species/plasma)
var/obj/item/tank/internals/plasmaman/source = equip_labrat_internals(lab_rat, /obj/item/tank/internals/plasmaman)
TEST_ASSERT(source.toggle_internals(lab_rat) && !isnull(lab_rat.internal), "Plasmaman toggle_internals() failed to toggle internals")
lab_rat.breathe()
TEST_ASSERT(!lab_rat.failed_last_breath && !lab_rat.has_alert(ALERT_NOT_ENOUGH_PLASMA), "Plasmamen can't get a full breath from a standard plasma tank")
TEST_ASSERT(source.toggle_internals(lab_rat) && !lab_rat.internal, "Plasmaman toggle_internals() failed to toggle internals")
// Empty internals suffocation.
lab_rat = allocate(/mob/living/carbon/human/species/plasma)
source = equip_labrat_internals(lab_rat, /obj/item/tank/internals/emergency_oxygen/empty)
TEST_ASSERT(source.toggle_internals(lab_rat) && !isnull(lab_rat.internal), "Plasmaman toggle_internals() failed to toggle internals")
lab_rat.breathe()
TEST_ASSERT(lab_rat.failed_last_breath && lab_rat.has_alert(ALERT_NOT_ENOUGH_PLASMA), "Plasmamen should suffocate from empty o2 tanks")
// Nitrogen internals suffocation.
lab_rat = allocate(/mob/living/carbon/human/species/plasma)
source = equip_labrat_internals(lab_rat, /obj/item/tank/internals/emergency_oxygen/empty)
source.air_contents.assert_gas(/datum/gas/nitrogen)
source.air_contents.gases[/datum/gas/nitrogen][MOLES] = (10 * ONE_ATMOSPHERE) * source.volume / (R_IDEAL_GAS_EQUATION * T20C)
TEST_ASSERT(source.toggle_internals(lab_rat) && !isnull(lab_rat.internal), "Plasmaman toggle_internals() failed to toggle internals")
lab_rat.breathe()
TEST_ASSERT(lab_rat.failed_last_breath && lab_rat.has_alert(ALERT_NOT_ENOUGH_PLASMA), "Humans should suffocate from pure n2 tanks")
/// Tests to make sure ashwalkers can breathe from the lavaland air.
/datum/unit_test/breath/breath_sanity_ashwalker
/datum/unit_test/breath/breath_sanity_ashwalker/Run()
var/mob/living/carbon/human/species/lizard/ashwalker/lab_rat = allocate(/mob/living/carbon/human/species/lizard/ashwalker)
lab_rat.forceMove(run_loc_floor_bottom_left)
var/turf/open/to_fill = run_loc_floor_bottom_left
to_fill.initial_gas_mix = LAVALAND_DEFAULT_ATMOS
to_fill.air = to_fill.create_gas_mixture()
lab_rat.breathe()
TEST_ASSERT(!lab_rat.has_alert(ALERT_NOT_ENOUGH_OXYGEN), "Ashwalkers can't get a full breath from the Lavaland's initial_gas_mix on a turf")
/datum/unit_test/breath/breath_sanity_ashwalker/Destroy()
//Reset initial_gas_mix to avoid future issues on other tests
var/turf/open/to_fill = run_loc_floor_bottom_left
to_fill.initial_gas_mix = OPENTURF_DEFAULT_ATMOS
return ..()