Files
Paradise/code/modules/unit_tests/test_runner.dm
warriorstar-orion 4ace2d6c2b Implement map tests for catching common errors. (#19204)
* Implement map tests for catching common errors.

- Adds test runner:
	- to make it easier to track things across test types
	- for example to ensure a fully specified log can be emitted

- Adds map tile test type:
	- when writing a test, coders implement CheckTile, which is
	  handed a single turf
	- when the test runner runs these tests, it iterates over
	  all turfs in the specified z-level, and runs each test's
	  CheckTile on each turf in turn.

- Adds two sample map tile tests:
	- check to see if a pipe exists on the same tile as a scrubber
	  or vent
	- check to see if a tile contains two cables, each with a center
	  node

* Review #1:

- Replace nested loops over map tiles with `block`
- Remove check for valid turf in individual tests, I think it's safe to
  assume `block` will always return legit turfs
- Added proper duration tracking for old tests
- Gave log file an appropriate extension
- Actually use `Fail` for tests

* whoops

* add more tests suggested by @Vi3trice

* Add some more tests courtesy @Bm0n and @Vi3trice

* windows are okay in space as long as it's nearspace

* Add failure threshold to prevent excessive logging.

Once this threshold is reached, a test will stop being processed for
every tile.

Note that this applies to `log_world` and `text2file` equally when
logging large amounts of failures.

* Document each test.

* Remove unnecessary reboot

* Let all map tests run to completion in CI matrix.

* I know what alphabetical means
2022-11-05 15:32:17 +00:00

115 lines
3.2 KiB
Plaintext

// Logging large amounts of test failures can cause performance issues
// significant enough to hit GitHub Actions timeout thresholds. This can happen
// intentionally (if there's a lot of legitimate map errors), or accidentally if
// a test condition is written incorrectly and starts e.g. logging failures for
// every single tile.
#define MAX_MAP_TEST_FAILURE_COUNT 20
/datum/test_runner
var/datum/unit_test/current_test
var/failed_any_test = FALSE
var/list/test_logs = list()
var/list/durations = list()
/datum/test_runner/proc/Start()
//trigger things to run the whole process
Master.sleep_offline_after_initializations = FALSE
// This will have the ticker set the game up
// Running the tests is part of the ticker's start function, because I cant think of any better place to put it
SSticker.force_start = TRUE
/datum/test_runner/proc/RunMap(z_level = 2)
CHECK_TICK
var/list/tests = list()
for(var/I in subtypesof(/datum/map_per_tile_test))
tests += new I
test_logs[I] = list()
durations[I] = 0
for(var/turf/T in block(locate(1, 1, z_level), locate(world.maxx, world.maxy, z_level)))
for(var/datum/map_per_tile_test/test in tests)
if(test.failure_count < MAX_MAP_TEST_FAILURE_COUNT)
var/duration = REALTIMEOFDAY
test.CheckTile(T)
durations[test.type] += REALTIMEOFDAY - duration
if(test.failure_count >= MAX_MAP_TEST_FAILURE_COUNT)
test.Fail(T, "failure threshold reached at this tile")
for(var/datum/map_per_tile_test/test in tests)
if(!test.succeeded)
failed_any_test = TRUE
test_logs[test.type] += test.fail_reasons
QDEL_LIST(tests)
/datum/test_runner/proc/Run()
CHECK_TICK
for(var/I in subtypesof(/datum/unit_test))
var/datum/unit_test/test = new I
test_logs[I] = list()
current_test = test
var/duration = REALTIMEOFDAY
test.Run()
durations[I] = REALTIMEOFDAY - duration
current_test = null
if(!test.succeeded)
failed_any_test = TRUE
test_logs[I] += test.fail_reasons
qdel(test)
CHECK_TICK
SSticker.reboot_helper("Unit Test Reboot", "tests ended", 0)
/datum/test_runner/proc/Finalize(emit_failures = FALSE)
var/time = world.timeofday
set waitfor = FALSE
var/list/fail_reasons
if(GLOB)
if(GLOB.total_runtimes != 0)
fail_reasons = list("Total runtimes: [GLOB.total_runtimes]")
if(!GLOB.log_directory)
LAZYADD(fail_reasons, "Missing GLOB.log_directory!")
if(failed_any_test)
LAZYADD(fail_reasons, "Unit Tests failed!")
else
fail_reasons = list("Missing GLOB!")
if(!fail_reasons)
text2file("Success!", "data/clean_run.lk")
var/list/result = list()
result += "RUN [time2text(time, "YYYY-MM-DD")]T[time2text(time, "hh:mm:ss")]"
for(var/reason in fail_reasons)
result += "FAIL [reason]"
for(var/test in test_logs)
if(length(test_logs[test]) == 0)
result += "PASS [test] [durations[test] / 10]s"
else
result += "FAIL [test] [durations[test] / 10]s"
result += "\t" + test_logs[test].Join("\n\t")
for(var/entry in result)
log_world(entry)
if(emit_failures)
var/filename = "data/test_run-[time2text(time, "YYYY-MM-DD")]T[time2text(time, "hh_mm_ss")].log"
text2file(result.Join("\n"), filename)
sleep(0) //yes, 0, this'll let Reboot finish and prevent byond memes
del(world) //shut it down