Files
Bubberstation/code/modules/unit_tests/unit_test.dm
GoldenAlpharex fdb3651095 Screenshot tests (#67679) (#14269)
Adds screenshot visual testing workflow and scripts.

Co-authored-by: Mothblocks <35135081+Mothblocks@users.noreply.github.com>
2022-06-17 00:33:25 +01:00

184 lines
6.1 KiB
Plaintext

/*
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)
GLOBAL_VAR(test_log)
/// 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)
/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
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()
if (isnull(reservation))
var/datum/map_template/unit_tests/template = new
reservation = template.load_new_z()
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)
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
for (var/turf/turf in block(locate(1, 1, run_loc_floor_bottom_left.z), locate(world.maxx, world.maxy, 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("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 (!arguments.len)
arguments = list(run_loc_floor_bottom_left)
else if (arguments[1] == null)
arguments[1] = run_loc_floor_bottom_left
var/instance = new type(arglist(arguments))
allocated += instance
return instance
/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("[path_prefix]_[name] was found, putting in data/screenshots")
else if (fexists("code"))
// We are probably running in a local build
fcopy(icon, filename)
TEST_FAIL("Screenshot for [name] did not exist. One has been created.")
else
// We are probably running in real CI, so just pretend it worked and move on
fcopy(icon, "data/screenshots_new/[path_prefix]_[name].png")
log_test("[path_prefix]_[name] was put in data/screenshots_new")
/proc/RunUnitTest(test_path, list/test_results)
var/datum/unit_test/test = new test_path
GLOB.current_test = test
var/duration = REALTIMEOFDAY
test.Run()
duration = REALTIMEOFDAY - duration
GLOB.current_test = null
GLOB.failed_any_test |= !test.succeeded
var/list/log_entry = list(
"[test.succeeded ? TEST_OUTPUT_GREEN("PASS") : TEST_OUTPUT_RED("FAIL")]: [test_path] [duration / 10]s",
)
var/list/fail_reasons = test.fail_reasons
var/map_name = SSmapping.config.map_name
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]
// Github action annotation.
// See https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
// Need to escape the text to properly support newlines.
var/annotation_text = replacetext(text, "%", "%25")
annotation_text = replacetext(annotation_text, "\n", "%0A")
log_world("::error file=[file],line=[line],title=[map_name]: [test_path]::[annotation_text]")
// Normal log message
log_entry += "\tREASON #[reasonID]: [text] at [file]:[line]"
var/message = log_entry.Join("\n")
log_test(message)
test_results[test_path] = list("status" = test.succeeded ? UNIT_TEST_PASSED : UNIT_TEST_FAILED, "message" = message, "name" = test_path)
qdel(test)
/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
tests_to_run = sortTim(tests_to_run, /proc/cmp_unit_test_priority)
var/list/test_results = list()
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)
var/file_name = "data/unit_tests.json"
fdel(file_name)
file(file_name) << json_encode(test_results)
SSticker.force_ending = TRUE
//We have to call this manually because del_text can preceed us, and SSticker doesn't fire in the post game
SSticker.standard_reboot()
/datum/map_template/unit_tests
name = "Unit Tests Zone"
mappath = "_maps/templates/unit_tests.dmm"