Currently supports Godot 3.5
Before running tests in this example repo you should first run ./update_addons.sh
For the example project you can run coverage like this to run the gut tests
godot -s addons/gut/gut_cmdln.gd
or to run coverage directly without GUT you can use
godot -s addons/coverage/CoverageTree.gd --scene=res://Spatial.tscn
After the Coverage
instance is created and until Coverage.instance().finalize()
the singleton will monitor the scene tree and ensure that no nodes are added with GDScript which are not excluded or instrumented.
run_tests.sh path/to/godot
will run both examples and merge the coverage results.
- You can pass additional arguments to run_tests to specify the coverage targets:
- e.g.
run_tests.sh path/to/godot 99 90 4
will require 99% total coverage and 90% file coverage, which will cause the test to fail, while setting verbosity to 4.
- e.g.
So far coverage requirements are left to the consumer. You can call Coverage.instance().get_percent()
to get the full coverage percentage
and make a pass/fail decision based on that number. If you are using the Gut
addon you can put this in a post run hook.
See this project's tests/pre_run_hook.gd
, tests/post_run_hook.gd
and .gutconfig.json
for examples.
You may have multiple separate test environments which require separate runs with coverage. In this case you can combine the coverage from multiple test runs to meet an overall coverage target.
You can output a JSON file during tests using Coverage.save_coverage_file(filename)
, then pass multiple coverage files to the addons/merge_coverage.gd
script.
godot -s addons/coverage/merge_coverage.gd [flags] file1.json file2.json [...fileN.json]
flags:
--verbosity 3
: Set the verbosity of the coverage output. See Coverage.gd:Verbosity for levels.--target 100
: set the coverage target, exit with failure if the target isn't met over all files--file-target 100
: set the coverage target for individual files. exit with failure if the target isn't met--output-file output.json
: Save the merged coverage to this file
See run_tests.sh
for an example.
Coverage.new(scene_tree: SceneTree, exclude_paths := [])
- Initialize the coverage singleton. Until
Coverage.instance().finalize()
has been called you may access the singleton withCoverage.instance()
exclude_paths
is a list of resource paths to be skipped when instrumenting files. It follows theString.match
syntax from GDScript.
- Initialize the coverage singleton. Until
static func instance(strict := true) -> Coverage
: Returns the current singleton instance. Must be created first withCoverage.new(...)
. May be called before callingCoverage.new()
if strict isfalse
, in which case it returnsnull
if there is no instance.static func finalize(print_verbose = false)
: Remove the singleton instance and print out the final coverage results. Passtrue
to print a verbose accounting of coverage for all files.func instrument_scripts(path: String)
: Recommended Approach Recursively instrument all*.gd
files within the requested path. Respects theexclude_paths
argument passed to the constructor.- Returns
self
for chaining
- Returns
func instrument_scene_scripts(scene: PackedScene)
: Instrument all scripts and their preloaded dependencies for a specific scene.instrument_scripts
is probably more reliable.- Returns
self
for chaining
- Returns
func instrument_autoloads()
: Just instrument autoload nodes that may have already been loaded. It's recommended to useinstrument_scripts
instead.- Returns
self
for chaining
- Returns
func enforce_node_coverage()
: Enables monitoring on scene tree updates and asserts that all nodes must have script coverage.- Returns
self
for chaining
- Returns
func get_coverage_collector(script_name: String) -> ScriptCoverageCollector
: Obtain the coverage collector for a specific script path, or null if it doesn't exist.func set_coverage_targets(total: float, file: float)
: Set targets for coverage. Should be called beforefinalize()
func coverage_passing() -> bool
: Check coverage targets and return true if they are all passing.func coverage_count() -> int
: Return the total number of lines which have been covered for all scripts.func coverage_line_count() -> int
: Return the total number of lines which have been instrumented for the all script.func coverage_percent() -> int
: Return the aggregate coverage percent (out of 100%) for all scripts.func script_coverage(verbosity := Coverage.Verbosity.None) -> String
: Return a string which describes the script coverage. Check Coverage.Verbosity for possible valuesfunc save_coverage_file(filename: String) -> bool
: Save the coverage to a json file for use withmerge_coverage_file
ormerge_coverage.gd
func merge_from_coverage_file(filename: String, auto_instrument := true) -> bool
: Merge a coverage file into the current coverage. If you don't wish to instrument the files set auto_instrument to false.
An internal class which represents coverage for a specific script, but does not attempt to instrument the script.
func coverage_count() -> int
: Return the number of lines which have been covered for the script.func coverage_line_count() -> int
: Return the total number of lines which have been instrumented for the script.func coverage_percent() -> int
: Return the coverage percent (out of 100%) for the script.func script_coverage(verbosity := Coverage.Verbosity.None, target_coverage: float) -> String
: Return a string which describes coverage for the script.func get_coverage_json() -> Dictionary
: Return the coverage as a dictionary suitable for saving to a json file.func merge_coverage_json(coverage_json: Dictionary)
: Combinecoverage_json
with the currently captured coverage for the script.func add_line_coverage(line_number: int, count := 1)
: Add coverage for a specific line for the script.
- Extends
ScriptCoverage
An internal class which instruments a single GDScript and collects line coverage for that script.
Most likely you will wish to combine this coverage utility with a testing library. For examples refer to tests/pre_run_hook.gd
, tests/post_run_hook.gd
and .gutconfig.json
.
pre_run_hook.gd
extends "res://addons/gut/hook_script.gd"
const Coverage = preload("res://addons/coverage/Coverage.gd")
const exclude_paths = [
"res://addons/*",
# NOTE: Godot may crash if you try to instrument the script that's calling instrument_scripts()
"res://tests/*",
"res://contrib/*"
]
func run():
Coverage.new(gut.get_tree(), exclude_paths)
Coverage.instance().instrument_scripts("res://")
post_run_hook.gd
extends "res://addons/gut/hook_script.gd"
const Coverage = preload("res://addons/coverage/Coverage.gd")
const COVERAGE_TARGET := 75.0
const FILE_TARGET := 33.0
func run():
var coverage = Coverage.instance()
var coverage_file := OS.get_environment("COVERAGE_FILE") if OS.has_environment("COVERAGE_FILE") else ""
if coverage_file:
coverage.save_coverage_file(coverage_file)
coverage.set_coverage_targets(COVERAGE_TARGET, FILE_TARGET)
var verbosity = Coverage.Verbosity.FailingFiles
coverage.finalize(verbosity)
var logger = gut.get_logger()
var coverage_passing = coverage.coverage_passing()
if !coverage_passing:
logger.failed("Coverage target of %.1f%% total (%.1f%% file) was not met" % [COVERAGE_TARGET, FILE_TARGET])
set_exit_code(2)
else:
gut.set_log_level(gut.LOG_LEVEL_ALL_ASSERTS)
logger.passed("Coverage target of %.1f%% total, %.1f%% file coverage" % [COVERAGE_TARGET, FILE_TARGET])
You may use the CoverageTree.gd
as an example of the simplest use of the api.
Here is an abridged sample that adds coverage to all scripts in a scene, then runs the scene and prints coverage when the get_tree().quit() is called.
const Coverage = preload("./Coverage.gd")
const packed_scene = preload("res://Spatial.tscn")
func _initialize():
# pass in the scene tree
Coverage.new(self)
Coverage.instance().instrument_scene_scripts(packed_scene)
add_child(packed_scene.instance())
func _finalize():
var coverage_file := OS.get_environment("COVERAGE_FILE") if OS.has_environment("COVERAGE_FILE") else ""
if coverage_file:
coverage.save_coverage_file(coverage_file)
Coverage.finalize(Coverage.Verbosity.AllFiles)
Make sure you exclude your test scripts, especially pre_run_hook.gd
from being instrumented.
Godot will crash without warning if an currently running script is reloaded!
Some situations can't get coverage. For example any autoload nodes will not get coverage in their _ready() functions.
There is a performance test in the test that compares the instrumented code to the uninstrumented code. It is a microbenchmark so should be taken with a grain of salt, but when running operations like array[i] /= 2
the instrumented code is roughly 1.7x slower than uninstrumented code. array.append(line_number) is the fastest way I've found to record that a line has been covered, deferring more expensive operations.
If you have trouble with instrumenting performance tests you should exclude that code from coverage if you can't figure out a way to make it faster than I have.
You may also use ScripCoverageCollector.set_instrumented(value := true)
to de-instrument a script temporarily:
var coverage_collector = Coverage.instance().get_coverage_collector(HeapQueue.resource_path)
coverage_collector.set_instrumented(false)
# some performance critical test code
coverage_collector.set_instrumented(true)