diff --git a/.gdcheckignore b/.gdcheckignore new file mode 100644 index 0000000..84bb7a0 --- /dev/null +++ b/.gdcheckignore @@ -0,0 +1,2 @@ +contrib/Gut +addons/gut diff --git a/.gitignore b/.gitignore index f813f91..c02cd2e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ /addons/gut -.import /coverage1.json /coverage2.json /coverage_out.json +/.godot diff --git a/Autoload1.gd b/Autoload1.gd deleted file mode 100644 index 781ea25..0000000 --- a/Autoload1.gd +++ /dev/null @@ -1,22 +0,0 @@ -extends Node - -const Other := preload("res://Other.gd") -const OtherNode := preload("res://OtherNode.gd") - -signal formatting() - -var other_node := OtherNode.new(self) -var other := Other.new() -var _initted := false -var _ready := '0' - -func _init(): - _initted = true -# Called when the node enters the scene tree for the first time. -func _ready(): - _ready = '1' - add_child(other_node) - -func fmt(value: String) -> String: - emit_signal('formatting') - return other_node.fmt(other.fmt('%s:%s:%s' % [_initted, _ready, value])) diff --git a/Autoload2.gd b/Autoload2.gd deleted file mode 100644 index 2eade56..0000000 --- a/Autoload2.gd +++ /dev/null @@ -1,14 +0,0 @@ -extends Node - -var _counter = '0' -# Called when the node enters the scene tree for the first time. -func _ready(): - _counter = '2' - var err := Autoload1.connect('formatting', self, '_on_autoload1_formatting') - assert(err == OK) - -func fmt(value: String): - return Autoload1.fmt('%s:%s' % [_counter, value]) - -func _on_autoload1_formatting(): - _counter = '3' diff --git a/CustomLabel.gd b/CustomLabel.gd deleted file mode 100644 index 95666cf..0000000 --- a/CustomLabel.gd +++ /dev/null @@ -1,6 +0,0 @@ -extends Label - -var custom_text: String setget _set_custom_text - -func _set_custom_text(value: String) -> void: - text = 'custom(%s)' % [value] diff --git a/addons/coverage/CoverageTree.gd b/addons/coverage/CoverageTree.gd deleted file mode 100644 index 951ade9..0000000 --- a/addons/coverage/CoverageTree.gd +++ /dev/null @@ -1,24 +0,0 @@ -extends SceneTree - -const Coverage = preload("./Coverage.gd") - -func _initialize(): - # need to create the instance because gdscript static functions have no access to their own scripts - Coverage.new(self).instrument_autoloads().enforce_node_coverage() - var args = OS.get_cmdline_args() - for a in args: - if a.begins_with('--scene='): - _run_scene(a.replace('--scene=', '')) - -func _run_scene(resource_path: String): - var packed_scene : PackedScene = load(resource_path) - Coverage.instance().instrument_scene_scripts(packed_scene) - var scene = packed_scene.instance() - root.add_child(scene) - -func _finalize(): - 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.finalize(Coverage.Verbosity.AllFiles) diff --git a/addons/coverage/Coverage.gd b/addons/coverage/coverage.gd similarity index 65% rename from addons/coverage/Coverage.gd rename to addons/coverage/coverage.gd index de354eb..cc8e670 100644 --- a/addons/coverage/Coverage.gd +++ b/addons/coverage/coverage.gd @@ -1,23 +1,30 @@ -extends Reference +class_name Coverage +extends RefCounted # verbosity levels: -# None: not verbose -# Filenames: coverage for each file, -# FailingFiles: coverage for only files that failed to meet the file coverage target. -# PartialFiles: coverage for each line (except when file coverage is 0%/100%) -# AllFiles: coverage for each line for every file -enum Verbosity { - None = 0, - Filenames = 1, - FailingFiles = 3, - PartialFiles = 4, - AllFiles = 5 -} +# NONE: not verbose +# FILENAMES: coverage for each file, +# FAILING_FILES: coverage for only files that failed to meet the file coverage target. +# PARTIAL_FILES: coverage for each line (except when file coverage is 0%/100%) +# ALL_FILES: coverage for each line for every file +enum Verbosity { NONE = 0, FILENAMES = 1, FAILING_FILES = 3, PARTIAL_FILES = 4, ALL_FILES = 5 } const MAX_QUEUE_SIZE := 10000 +var coverage_collectors := {} + +var _scene_tree: MainLoop +var _exclude_paths := [] +var _enforce_node_coverage := false +var _autoloads_instrumented := false +var _coverage_target_file := INF +var _coverage_target_total := INF + +static var instance: Coverage + + class ScriptCoverage: - extends Reference + extends RefCounted var coverage_lines := {} # coverage_queue.append() is so far the fastest way to instrument code @@ -26,11 +33,10 @@ class ScriptCoverage: var script_path := "" var source_code := "" - func _init(_script_path: String, load_source_code := true) -> void: + func _init(_script_path: String, _load_source_code = false) -> void: script_path = _script_path - var f := File.new() - var err := f.open(_script_path, File.READ) - assert(err == OK, "Unable to open %s for reading" % [_script_path]) + var f := FileAccess.open(_script_path, FileAccess.READ) + assert(f, "Unable to open %s for reading: %s" % [_script_path, FileAccess.get_open_error()]) source_code = f.get_as_text() f.close() @@ -64,24 +70,34 @@ class ScriptCoverage: for line_number in coverage_json: add_line_coverage(int(line_number), coverage_json[line_number]) - func script_coverage(verbosity := Verbosity.None, target: float = INF): - var result := PoolStringArray() + func script_coverage(verbosity := Verbosity.NONE, target: float = INF): + var result := PackedStringArray() var i = 0 var coverage_percent := coverage_percent() - var partial_show: bool = verbosity == Verbosity.PartialFiles && coverage_percent < 100 && coverage_percent > 0 - var failed_show: bool = verbosity == Verbosity.FailingFiles && coverage_percent < target - var show_source = partial_show || failed_show || verbosity == Verbosity.AllFiles + var partial_show: bool = ( + verbosity == Verbosity.PARTIAL_FILES && coverage_percent < 100 && coverage_percent > 0 + ) + var failed_show: bool = verbosity == Verbosity.FAILING_FILES && coverage_percent < target + var show_source = partial_show || failed_show || verbosity == Verbosity.ALL_FILES var pass_fail := "" + if target != INF: pass_fail = "(fail) " if coverage_percent < target else "(pass) " result.append("%s%.1f%% %s" % [pass_fail, coverage_percent, script_path]) if show_source: for line in source_code.split("\n"): - result.append("%4d %s %s" % [ - i, "%4dx" % [coverage_lines[i]] if i in coverage_lines else " ", line - ]) + result.append( + ( + "%4d %s %s" + % [ + i, + "%4dx" % [coverage_lines[i]] if i in coverage_lines else " ", + line + ] + ) + ) i += 1 - return result.join("\n") + return "\n".join(result) # virtual # Call this function to revert the script object to it's original state. @@ -98,21 +114,21 @@ class ScriptCoverage: add_line_coverage(line) coverage_queue = [] + class ScriptCoverageCollector: extends ScriptCoverage const DEBUG_SCRIPT_COVERAGE := false - const STATIC_VARS := {last_script_id = 0} - const ERR_MAP := { - 43: "PARSE_ERROR" - } + const ERR_MAP := {43: "PARSE_ERROR"} var instrumented_source_code := "" var covered_script: Script + static var last_script_id = 0 + class Indent: - extends Reference - enum State {None, Class, Func, StaticFunc, Match, MatchPattern} + extends RefCounted + enum State { NONE, CLASS, FUNC, STATIC_FUNC, MATCH, MATCH_PATTERN } var depth: int var state: int @@ -123,9 +139,10 @@ class ScriptCoverageCollector: state = _state subclass_name = _subclass_name - func _init(coverage_script_path: String, _script_path: String).(_script_path, false) -> void: - var id = STATIC_VARS.last_script_id + 1 - STATIC_VARS.last_script_id = id + func _init(coverage_script_path: String, _script_path: String) -> void: + super(_script_path, false) + var id = last_script_id + 1 + last_script_id = id covered_script = load(_script_path) source_code = covered_script.source_code if DEBUG_SCRIPT_COVERAGE: @@ -145,11 +162,17 @@ class ScriptCoverageCollector: # sure the coverage variable is set before calling add_line_coverage var err = covered_script.reload(true) - assert(err == OK, "Error reloading %s: error: %s\n-------\n%s" % [ - covered_script.resource_path, - ERR_MAP[err] if err in ERR_MAP else err, - _add_line_numbers(covered_script.source_code) - ]) + assert( + err == OK, + ( + "Error reloading %s: error: %s\n-------\n%s" + % [ + covered_script.resource_path, + ERR_MAP[err] if err in ERR_MAP else err, + _add_line_numbers(covered_script.source_code) + ] + ) + ) func set_instrumented(value := true): _set_script_code(instrumented_source_code if value else source_code) @@ -158,12 +181,12 @@ class ScriptCoverageCollector: set_instrumented(false) func _add_line_numbers(source_code: String) -> String: - var result := PoolStringArray() + var result := PackedStringArray() var i := 0 for line in source_code.split("\n"): result.append("%4d: %s" % [i, line]) i += 1 - return result.join("\n") + return "\n".join(result) func _to_string(): return script_coverage(2) @@ -185,45 +208,42 @@ class ScriptCoverageCollector: return result func _get_leading_whitespace(line: String) -> String: - var leading_whitespace := PoolStringArray() + var leading_whitespace := PackedStringArray() for chr in range(len(line)): if line[chr] in [" ", "\t"]: leading_whitespace.append(line[chr]) else: break - return leading_whitespace.join("") + return "".join(leading_whitespace) - func _get_coverage_collector_expr(coverage_script_path: String, script_resource_path: String) -> String: - return "load(\"%s\").instance().get_coverage_collector(\"%s\")" % [ - coverage_script_path, - script_resource_path - ] + func _get_coverage_collector_expr( + coverage_script_path: String, script_resource_path: String + ) -> String: + return ( + 'load("%s").instance.get_coverage_collector("%s")' + % [coverage_script_path, script_resource_path] + ) - func _interpolate_coverage(coverage_script_path: String, script: GDScript, id: int) -> String: + func _interpolate_coverage(coverage_script_path: String, script: GDScript, _id: int) -> String: var collector_var := "__cl__" - var lines = script.source_code.split("\n") + var lines := script.source_code.split("\n") var indent_stack := [] var ld_stack := [] var write_var := false - var state:int = Indent.State.None - var next_state: int = Indent.State.None + var state: int = Indent.State.NONE + var next_state: int = Indent.State.NONE var subclass_name: String var next_subclass_name: String var depth := 0 - var out_lines := PoolStringArray() + var out_lines := PackedStringArray() # 0 based, start with -1 so that the first increment will give 0 var i := -1 - var block := { - '{}': 0, - '()': 0, - '[]': 0 - } + var block := {"{}": 0, "()": 0, "[]": 0} # the collector var must be placed after 'extends' b var continuation := false - for line_ in lines: + for line in lines: i += 1 - var line := line_ as String var stripped_line := line.strip_edges() if stripped_line == "" || stripped_line.begins_with("#"): out_lines.append(line) @@ -235,7 +255,7 @@ class ScriptCoverageCollector: # if we are in a block or have a continuation from the last line # don't add instrumentation var skip := block_count > 0 || continuation - continuation = stripped_line.ends_with('\\') + continuation = stripped_line.ends_with("\\") if skip: out_lines.append(line) continue @@ -245,123 +265,145 @@ class ScriptCoverageCollector: while line_depth < depth: var indent = indent_stack.pop_back() if DEBUG_SCRIPT_COVERAGE: - print("\t\t\t\tPOP_LINE_DEPTH %s > %s (%s) %s (was %s) %s" % [depth, indent.depth, state, indent.subclass_name, subclass_name, subclass_name && subclass_name != indent.subclass_name]) + print( + ( + "\t\t\t\tPOP_LINE_DEPTH %s > %s (%s) %s (was %s) %s" + % [ + depth, + indent.depth, + state, + indent.subclass_name, + subclass_name, + subclass_name && subclass_name != indent.subclass_name + ] + ) + ) depth = indent.depth state = indent.state next_state = indent.state subclass_name = indent.subclass_name if line_depth > depth: if DEBUG_SCRIPT_COVERAGE: - print("\t\t\t\tPUSH_LINE_DEPTH %s > %s (%s > %s) %s" % [depth, line_depth, state, next_state, subclass_name]) + print( + ( + "\t\t\t\tPUSH_LINE_DEPTH %s > %s (%s > %s) %s" + % [depth, line_depth, state, next_state, subclass_name] + ) + ) indent_stack.append(Indent.new(depth, state, subclass_name)) if next_subclass_name: subclass_name = next_subclass_name - next_subclass_name = '' + next_subclass_name = "" state = next_state depth = line_depth var first_token := _get_token(stripped_line) match first_token: "func": - next_state = Indent.State.Func + next_state = Indent.State.FUNC write_var = true "class": - next_state = Indent.State.Class + next_state = Indent.State.CLASS next_subclass_name = _get_token(stripped_line, 1).trim_suffix(":") "static": write_var = true - next_state = Indent.State.StaticFunc + next_state = Indent.State.STATIC_FUNC "else:", "elif": skip = true "match": - next_state = Indent.State.Match - if state == Indent.State.Match: - next_state = Indent.State.MatchPattern - elif state == Indent.State.MatchPattern: - next_state = Indent.State.Func - if !skip && state in [Indent.State.Func, Indent.State.StaticFunc]: + next_state = Indent.State.MATCH + if state == Indent.State.MATCH: + next_state = Indent.State.MATCH_PATTERN + elif state == Indent.State.MATCH_PATTERN: + next_state = Indent.State.FUNC + if !skip && state in [Indent.State.FUNC, Indent.State.STATIC_FUNC]: if write_var: write_var = false - out_lines.append("%svar %s = %s.coverage_queue" % [ - leading_whitespace, - collector_var, - _get_coverage_collector_expr(coverage_script_path, script.resource_path) - ]) + out_lines.append( + ( + "%svar %s = %s.coverage_queue" + % [ + leading_whitespace, + collector_var, + _get_coverage_collector_expr( + coverage_script_path, script.resource_path + ) + ] + ) + ) coverage_lines[i] = 0 - out_lines.append("%s%s.append(%s)" % [ - leading_whitespace, - collector_var, - i - ]) + out_lines.append("%s%s.append(%s)" % [leading_whitespace, collector_var, i]) out_lines.append(line) - return out_lines.join("\n") + return "\n".join(out_lines) + # this is a placeholder class for when we've finalized and don't want coverage anymore # some scripts will continue to be instrumented so we must have something to accept all these calls class NullCoverage: - extends Reference + extends Coverage + func get_coverage_collector(_script_name: String): return self func add_line_coverage(_line: int): pass -const STATIC_VARS := {instance=null} -var coverage_collectors := {} -var _scene_tree: MainLoop -var _exclude_paths := [] -var _enforce_node_coverage := false -var _autoloads_instrumented := false -var _coverage_target_file := INF -var _coverage_target_total := INF func _init(scene_tree: MainLoop, exclude_paths := []): _exclude_paths += exclude_paths - assert(!STATIC_VARS.instance, "Only one instance of this class is allowed") - STATIC_VARS.instance = self + assert(!instance, "Only one instance of this class is allowed") + instance = self _scene_tree = scene_tree + func enforce_node_coverage(): - var err := _scene_tree.connect("tree_changed", self, "_on_tree_changed") + var err := _scene_tree.connect("tree_changed", Callable(self, "_on_tree_changed")) assert(err == OK) _enforce_node_coverage = true # this may error on autoload if you don"t call `instrument_autoloads()` immediately _on_tree_changed() return self + func _finalize(print_verbosity := 0): for script_path in coverage_collectors: coverage_collectors[script_path].revert() if _enforce_node_coverage: - _scene_tree.disconnect("tree_changed", self, "_on_tree_changed") + _scene_tree.disconnect("tree_changed", Callable(self, "_on_tree_changed")) print(script_coverage(print_verbosity)) + func get_coverage_collector(script_name: String): var result = coverage_collectors[script_name] if script_name in coverage_collectors else null if result: result.maybe_process_queue() return result + func coverage_count() -> int: var result := 0 for script in coverage_collectors: result += coverage_collectors[script].coverage_count() return result + func coverage_line_count() -> int: var result := 0 for script in coverage_collectors: result += coverage_collectors[script].coverage_line_count() return result + func coverage_percent() -> float: var clc = coverage_line_count() return (float(coverage_count()) / float(clc)) * 100.0 if clc > 0 else 100.0 + func set_coverage_targets(total: float, file: float) -> void: _coverage_target_total = total _coverage_target_file = file + func coverage_passing() -> bool: var all_files_passing := true if _coverage_target_file < INF: @@ -370,9 +412,10 @@ func coverage_passing() -> bool: all_files_passing = all_files_passing && script_percent >= _coverage_target_file return coverage_percent() >= _coverage_target_total && all_files_passing + # see ScriptCoverage.Verbosity for verbosity levels func script_coverage(verbosity := 0): - var result = PoolStringArray() + var result = PackedStringArray() var coverage_count := 0 var coverage_lines := 0 var coverage_percent := coverage_percent() @@ -380,65 +423,76 @@ func script_coverage(verbosity := 0): if _coverage_target_total != INF: pass_fail = "(fail) " if coverage_percent < _coverage_target_total else "(pass) " var multiline := false - if verbosity > Verbosity.None: + if verbosity > Verbosity.NONE: for script in coverage_collectors: - var file_coverage = coverage_collectors[script].script_coverage(verbosity, _coverage_target_file) - result.append("%s" % [file_coverage]) + var file_coverage = coverage_collectors[script].script_coverage( + verbosity, _coverage_target_file + ) + result.append(file_coverage) if file_coverage.match("*\n*"): multiline = true - result.append("%s%.1f%% Total Coverage: %s/%s lines" % [ - pass_fail, - coverage_percent, - coverage_count(), - coverage_line_count() - ]) + result.append( + ( + "%s%.1f%% Total Coverage: %s/%s lines" + % [pass_fail, coverage_percent, coverage_count(), coverage_line_count()] + ) + ) + + return ("\n\n" if multiline else "\n").join(result) - return result.join("\n\n" if multiline else "\n") func merge_from_coverage_file(filename: String, auto_instrument := true) -> bool: - var f := File.new() - var err := f.open(filename, File.READ) - if err != OK: - printerr("Error %s opening %s for reading" % [err, filename]) + var f := FileAccess.open(filename, FileAccess.READ) + if !f: + printerr("Error %s opening %s for reading" % [FileAccess.get_open_error(), filename]) return false - var parsed = JSON.parse(f.get_as_text()); + var test_json_conv = JSON.new() + var err := test_json_conv.parse(f.get_as_text()) + var parsed = test_json_conv.data f.close() - if parsed.error != OK: - printerr("Error %s on line %s parsing %s" % [parsed.error, parsed.error_line, filename]) + if err != OK: + printerr( + "Error %s on line %s parsing %s" % [err, test_json_conv.get_error_line(), filename] + ) printerr(parsed.error_string) return false - if !parsed.result is Dictionary: + if !parsed is Dictionary: printerr("Error: content of %s expected to be a dictionary" % [filename]) return false - for script_path in parsed.result: - if !parsed.result[script_path] is Dictionary: - printerr("Error: %s in %s is expected to be a dictionary" % [ - script_path, filename - ]) + for script_path in parsed: + if !parsed[script_path] is Dictionary: + printerr("Error: %s in %s is expected to be a dictionary" % [script_path, filename]) return false if auto_instrument: _instrument_script(load(script_path)) elif !script_path in coverage_collectors: coverage_collectors[script_path] = ScriptCoverage.new(script_path) - coverage_collectors[script_path].merge_coverage_json(parsed.result[script_path]) + coverage_collectors[script_path].merge_coverage_json(parsed[script_path]) return true + func save_coverage_file(filename: String) -> bool: var coverage := {} for script_path in coverage_collectors: coverage[script_path] = coverage_collectors[script_path].get_coverage_json() - var f := File.new() - var err := f.open(filename, File.WRITE) - if err != OK: - printerr("Error %s opening %s for writing" % [err, filename]) + var f := FileAccess.open(filename, FileAccess.WRITE) + if !f: + printerr( + ( + "Error %s opening coverage file %s for writing" + % [FileAccess.get_open_error(), filename] + ) + ) return false - f.store_string(JSON.print(coverage)) + f.store_string(JSON.stringify(coverage)) f.close() return true + func _on_tree_changed(): _ensure_node_script_instrumentation(_scene_tree.root) + func _excluded(resource_path: String) -> bool: var excluded = false for ep in _exclude_paths: @@ -447,30 +501,58 @@ func _excluded(resource_path: String) -> bool: break return excluded + func _ensure_node_script_instrumentation(node: Node): # this is too late, if a node already has the script then reload it fails with ERR_ALREADY_IN_USE var script = node.get_script() if script is GDScript: - assert(_excluded(script.resource_path) || script.resource_path in coverage_collectors, "Node %s has a non-instrumented script %s" % [ - node.get_path() if node.is_inside_tree() else node.name, - script.resource_path - ]) + assert( + _excluded(script.resource_path) || script.resource_path in coverage_collectors, + ( + "Node %s has a non-instrumented script %s" + % [node.get_path() if node.is_inside_tree() else node.name, script.resource_path] + ) + ) for n in node.get_children(): _ensure_node_script_instrumentation(n) + func _instrument_script(script: GDScript) -> void: var script_path = script.resource_path var coverage_script_path = get_script().resource_path - if !script_path: - printerr("script has no path: %s" % [script.source_code]) if !_excluded(script_path) && script_path && !script_path in coverage_collectors: - coverage_collectors[script_path] = ScriptCoverageCollector.new(coverage_script_path ,script_path) + coverage_collectors[script_path] = ScriptCoverageCollector.new( + coverage_script_path, script_path + ) var deps = ResourceLoader.get_dependencies(script_path) + if len(deps) == 0: + # TODO: remove when this issue is resolved: + # https://github.com/godotengine/godot/issues/90643 + deps = _scan_script_for_dependencies(script_path, script.get_source_code()) for dep in deps: if dep.get_extension() == "gd": _instrument_script(load(dep)) + +func _scan_script_for_dependencies(script_path: String, source_code: String): + var script_dir := script_path.get_base_dir() + var load_expr := RegEx.new() + var abs_path_expr := RegEx.new() + load_expr.compile("\\b(?:pre)load\\([\"'](?[^\"']+)[\"']\\)[\\b\\n]") + # ^res://, ^user:// etc or ^/ or ^\ or ^c:\ + abs_path_expr.compile("^(\\w+://|/|\\\\|\\w:\\\\)") + var found := load_expr.search_all(source_code) + var result := PackedStringArray() + for f in found: + var path := f.get_string("path") + var abs_match := abs_path_expr.search(path) + if !abs_match: + path = script_dir.path_join(path.trim_prefix("./")) + result.append(path) + return result + + func instrument_scene_scripts(scene: PackedScene): var s := scene.get_state() for i in range(s.get_node_count()): @@ -480,19 +562,18 @@ func instrument_scene_scripts(scene: PackedScene): instrument_scene_scripts(node_instance) for npi in range(s.get_node_property_count(i)): var p = s.get_node_property_name(i, npi) + print("scene prop %s :%s" % [p, s.get_node_property_value(i, npi)]) if p == "script": _instrument_script(s.get_node_property_value(i, npi)) return self + func _collect_script_objects(obj: Object, objs: Array, obj_set: Dictionary): # prevent cycles obj_set[obj] = true assert(obj && obj.get_script(), "Couldn't collect script from %s" % [obj]) if obj.get_script() && !_excluded(obj.get_script().resource_path): - objs.append({ - obj = obj, - script = obj.get_script() - }) + objs.append({obj = obj, script = obj.get_script()}) # collect all child nodes of an autoload that may have scripts attached if obj is Node: for c in obj.get_children(): @@ -509,19 +590,27 @@ func _collect_script_objects(obj: Object, objs: Array, obj_set: Dictionary): if script && script.resource_path: _collect_script_objects(value, objs, obj_set) + func _collect_autoloads(): assert(!_autoloads_instrumented, "Tried to collect autoloads twice?") _autoloads_instrumented = true var autoloaded := [] var obj_set := {} - assert(_scene_tree is SceneTree, "Cannot collect autoloads from %s because it is not a SceneTree" % [ _scene_tree ]) + assert( + _scene_tree is SceneTree, + "Cannot collect autoloads from %s because it is not a SceneTree" % [_scene_tree] + ) var root := (_scene_tree as SceneTree).root for n in root.get_children(): var setting_name = "autoload/%s" % [n.name] - var autoload_setting = ProjectSettings.get_setting(setting_name) if ProjectSettings.has_setting(setting_name) else "" + var autoload_setting = ( + ProjectSettings.get_setting(setting_name) + if ProjectSettings.has_setting(setting_name) + else "" + ) if autoload_setting: _collect_script_objects(n, autoloaded, obj_set) - autoloaded.invert() + autoloaded.reverse() var deps := [] for item in autoloaded: for d in ResourceLoader.get_dependencies(item.script.resource_path): @@ -530,28 +619,30 @@ func _collect_autoloads(): deps.append({obj = null, script = dep_script}) return deps + autoloaded -func instrument_autoloads(script_list: Array = []): + +func instrument_autoloads(): var autoload_scripts = _collect_autoloads() - autoload_scripts.invert() + autoload_scripts.reverse() for item in autoload_scripts: _instrument_script(item.script) return self + func instrument_scripts(path: String): var list := _list_scripts_recursive(path) for script in list: _instrument_script(load(script)) return self + func _list_scripts_recursive(path: String, list := []) -> Array: - var d := Directory.new() - var err := d.open(path) - assert(err == OK, "Error opening path %s: %s" % [path, err]) - err = d.list_dir_begin(true) + var d := DirAccess.open(path) + assert(d, "Error opening path %s: %s" % [path, DirAccess.get_open_error()]) + var err = d.list_dir_begin() # TODOConverter3To4 fill missing arguments https://github.com/godotengine/godot/pull/40547 assert(err == OK, "Error listing directory %s: %s" % [path, err]) var next := d.get_next() while next: - var next_path = path.plus_file(next) + var next_path = path.path_join(next) if next.get_extension() == "gd": if !_excluded(next_path): list.append(next_path) @@ -561,11 +652,10 @@ func _list_scripts_recursive(path: String, list := []) -> Array: d.list_dir_end() return list -static func instance(strict := true): - # unable to reference the Coverage script from a static so you need to call Coverage.new(...) first - assert(!strict || STATIC_VARS.instance, "No instance has been created, use Coverage.new(get_tree()) first.") - return STATIC_VARS.instance static func finalize(print_verbosity := 0) -> void: - STATIC_VARS.instance._finalize(print_verbosity) - STATIC_VARS.instance = NullCoverage.new() + # gdlint: ignore=private-method-call + instance._finalize(print_verbosity) + var scene_tree = instance._scene_tree + instance = null + instance = NullCoverage.new(scene_tree) diff --git a/addons/coverage/coverage_tree.gd b/addons/coverage/coverage_tree.gd new file mode 100644 index 0000000..78bf50a --- /dev/null +++ b/addons/coverage/coverage_tree.gd @@ -0,0 +1,29 @@ +extends SceneTree + +const Coverage = preload("./coverage.gd") + + +func _initialize(): + # godot3 needed to create the instance because gdscript static functions have no access to their own scripts.. + Coverage.new(self).instrument_autoloads().enforce_node_coverage() + var args = OS.get_cmdline_args() + for a in args: + if a.begins_with("--scene="): + _run_scene(a.replace("--scene=", "")) + + +func _run_scene(resource_path: String): + var packed_scene: PackedScene = load(resource_path) + Coverage.instance.instrument_scene_scripts(packed_scene) + var scene = packed_scene.instantiate() + root.add_child(scene) + + +func _finalize(): + 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.finalize(Coverage.Verbosity.ALL_FILES) diff --git a/addons/coverage/merge_coverage.gd b/addons/coverage/merge_coverage.gd index cf577fe..f275f24 100644 --- a/addons/coverage/merge_coverage.gd +++ b/addons/coverage/merge_coverage.gd @@ -1,27 +1,31 @@ extends MainLoop -const Coverage = preload("./Coverage.gd") +const Coverage = preload("./coverage.gd") var _quit := false var _exit_code := 0 + func _print_usage(): - print("""Usage: godot -s res://addons/coverage/merge_coverage.gd [flags] coverage1.json coverage2.json [...coverageN.json] + print( + """Usage: godot -s res://addons/coverage/merge_coverage.gd [flags] coverage1.json coverage2.json [...coverageN.json] Merges multiple input 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. - """) + """ + ) + func _initialize(): var coverage := Coverage.new(self) var args := Array(OS.get_cmdline_args()) var coverage_target := INF var file_target := INF - var output_filename := '' - var verbosity:int = Coverage.Verbosity.FailingFiles + var output_filename := "" + var verbosity: int = Coverage.Verbosity.FAILING_FILES while len(args) && args[0].begins_with("-"): var flag = args.pop_front() match flag: @@ -62,11 +66,17 @@ func _initialize(): else: quit() + func quit(exit_code := 0): _quit = true _exit_code = exit_code -func _idle(delta): + +func _process(_delta): if _quit: - OS.set_exit_code(_exit_code) + # Would prefer OS.set_exit_code(_exit_code), but it was removed... + # see https://github.com/godotengine/godot/issues/90646 + var st := SceneTree.new() + st.quit(_exit_code) + st.free() return _quit diff --git a/autoload1.gd b/autoload1.gd new file mode 100644 index 0000000..4ef0bc4 --- /dev/null +++ b/autoload1.gd @@ -0,0 +1,19 @@ +extends Node + +signal formatting + +const Other := preload("res://other.gd") +const OtherNode := preload("res://other_node.gd") + +var other_node := OtherNode.new(self) +var other := Other.new() +var _initted := false + + +func _init(): + _initted = true + + +func fmt(value: String) -> String: + emit_signal("formatting") + return other_node.fmt(other.fmt("%s:%s:%s" % [_initted, ready, value])) diff --git a/autoload2.gd b/autoload2.gd new file mode 100644 index 0000000..bfe7c04 --- /dev/null +++ b/autoload2.gd @@ -0,0 +1,18 @@ +extends Node + +var _counter = "0" + + +# Called when the node enters the scene tree for the first time. +func _ready(): + _counter = "2" + var err := Autoload1.connect("formatting", Callable(self, "_on_autoload1_formatting")) + assert(err == OK) + + +func fmt(value: String): + return Autoload1.fmt("%s:%s" % [_counter, value]) + + +func _on_autoload1_formatting(): + _counter = "3" diff --git a/check.sh b/check.sh new file mode 100755 index 0000000..6061c45 --- /dev/null +++ b/check.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +exempt=() +[ -f $dir/.gdcheckignore ] && IFS=$'\n' read -d '' -r -a exempt <<< "$( grep -Ev '^#' $dir/.gdcheckignore )" || true + +lint=1 +format=1 +fix=0 + +set -eu + +usage() { + echo "$0 [flags] + + The default behavior is to run gdlint and gdformat in 'check' mode and fail if either one does not pass. + + --help: Print this help + --fix: Apply formatting with gdformat + --lint: Only do linting (don't run gdformat) + --format: Only do formatting (don't run gdlint) + " +} + +while [[ "${1:-}" =~ ^-- ]]; do + if [ "$1" == "--fix" ]; then + fix=1 + fi + if [ "$1" == "--lint" ]; then + format=0 + fi + if [ "$1" == "--format" ]; then + lint=0 + fi + if [ "$1" == "--help" ]; then + usage + exit 0 + fi + shift +done + +format_flag= +if [ $fix == 0 ]; then + format_flag=--check +fi + +declare -a prune_flags +if [ ${#exempt[@]} -gt 0 ]; then + prune_flags=( '(' ) + for i in "${!exempt[@]}"; do + if [ $i -gt 0 ]; then + prune_flags+=( -o ) + fi + prune_flags+=( -path "./${exempt[$i]}" ) + done + prune_flags+=( ')' -prune -o ) +fi +cd $dir + +set +e + +function activate_venv { + if [[ -d "$dir/.py_venv/bin" ]]; then + source "$dir/.py_venv/bin/activate" + else + source "$dir/.py_venv/Scripts/activate" + fi +} + +if ! [[ -d "$dir/.py_venv" ]]; then + # install gdtoolkit in a venv + PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring python -m pip install 'virtualenv' --user + python -m virtualenv .py_venv + activate_venv + PYTHON_KEYRING_BACKEND=keyring.backends.null.Keyring python -m pip install 'gdtoolkit==4.*' +else + activate_venv +fi + +python --version + +if [ $lint == 1 ]; then +echo "GDLint" +gdlint --version +find . "${prune_flags[@]}" -name '*.gd' -exec gdlint {} + 2>&1 +lint_result=$? +fi + +if [ $format == 1 ]; then +echo "GDFormat $format_flag" +gdformat --version +find . "${prune_flags[@]}" -name '*.gd' -exec gdformat $format_flag {} + 2>&1 +format_result=$? +fi + +result=0 +if [ $lint_result != 0 ]; then + echo "Lint failed with $lint_result" >&2 + result=$lint_result +fi +if [ $format_result != 0 ]; then + echo "Format failed with $format_result" >&2 + result=$format_result +fi + +exit $result diff --git a/contrib/Gut b/contrib/Gut index 1c61a2f..5782983 160000 --- a/contrib/Gut +++ b/contrib/Gut @@ -1 +1 @@ -Subproject commit 1c61a2fd444d4dee4202d1cae5a66e9fb32d8871 +Subproject commit 57829837811946439648b635be231549c2dc6f06 diff --git a/custom_label.gd b/custom_label.gd new file mode 100644 index 0000000..21b7e32 --- /dev/null +++ b/custom_label.gd @@ -0,0 +1,8 @@ +extends Label + +var custom_text: String: + set = _set_custom_text + + +func _set_custom_text(value: String) -> void: + text = "custom(%s)" % [value] diff --git a/default_env.tres b/default_env.tres index 20207a4..03cc06b 100644 --- a/default_env.tres +++ b/default_env.tres @@ -1,7 +1,7 @@ -[gd_resource type="Environment" load_steps=2 format=2] +[gd_resource type="Environment" load_steps=2 format=3 uid="uid://874vsxp2bwvm"] -[sub_resource type="ProceduralSky" id=1] +[sub_resource type="Sky" id="1"] [resource] background_mode = 2 -background_sky = SubResource( 1 ) +sky = SubResource("1") diff --git a/gdlintrc b/gdlintrc new file mode 100644 index 0000000..36180f5 --- /dev/null +++ b/gdlintrc @@ -0,0 +1,46 @@ +class-definitions-order: +- tools +- classnames +# class definitions? (not supported so they count as 'others') +- extends +- signals +- enums +- consts +- exports +- pubvars +- prvvars +- onreadypubvars +- onreadyprvvars +- staticvars +- others +class-load-variable-name: (([A-Z][a-z0-9]*)+|_?[a-z][a-z0-9]*(_[a-z0-9]+)*) +class-name: ([A-Z][a-z0-9]*)+ +class-variable-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)* +comparison-with-itself: null +constant-name: '[A-Z][A-Z0-9]*(_[A-Z0-9]+)*' +disable: [] +duplicated-load: null +enum-element-name: '[A-Z][A-Z0-9]*(_[A-Z0-9]+)*' +enum-name: ([A-Z][a-z0-9]*)+ +excluded_directories: !!set + .git: null +expression-not-assigned: null +function-argument-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)* +function-arguments-number: 10 +function-name: (_on_([A-Z][a-z0-9]*)+(_[a-z0-9]+)*|_?[a-z][a-z0-9]*(_[a-z0-9]+)*) +function-preload-variable-name: ([A-Z][a-z0-9]*)+ +function-variable-name: '[a-z][a-z0-9]*(_[a-z0-9]+)*' +load-constant-name: (([A-Z][a-z0-9]*)+|[A-Z][A-Z0-9]*(_[A-Z0-9]+)*) +loop-variable-name: _?[a-z][a-z0-9]*(_[a-z0-9]+)* +max-file-lines: 1000 +# gdformat enforces max-line-length differently +max-line-length: 9000 +max-public-methods: 20 +mixed-tabs-and-spaces: true +private-method-call: true +signal-name: '[a-z][a-z0-9]*(_[a-z0-9]+)*' +sub-class-name: _?([A-Z][a-z0-9]*)+ +tab-characters: 1 +trailing-whitespace: true +unnecessary-pass: true +unused-argument: null diff --git a/icon.png.import b/icon.png.import index a4c02e6..b2fc978 100644 --- a/icon.png.import +++ b/icon.png.import @@ -1,8 +1,9 @@ [remap] importer="texture" -type="StreamTexture" -path="res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" +type="CompressedTexture2D" +uid="uid://do2fh3dkyu6tr" +path="res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex" metadata={ "vram_texture": false } @@ -10,26 +11,24 @@ metadata={ [deps] source_file="res://icon.png" -dest_files=[ "res://.import/icon.png-487276ed1e3a0c39cad0279d744ee560.stex" ] +dest_files=["res://.godot/imported/icon.png-487276ed1e3a0c39cad0279d744ee560.ctex"] [params] compress/mode=0 +compress/high_quality=false compress/lossy_quality=0.7 -compress/hdr_mode=0 -compress/bptc_ldr=0 +compress/hdr_compression=1 compress/normal_map=0 -flags/repeat=0 -flags/filter=true -flags/mipmaps=false -flags/anisotropic=false -flags/srgb=2 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" process/fix_alpha_border=true process/premult_alpha=false -process/HDR_as_SRGB=false -process/invert_color=false process/normal_map_invert_y=false -stream=false -size_limit=0 -detect_3d=true -svg/scale=1.0 +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/NoCoverage.gd b/no_coverage.gd similarity index 69% rename from NoCoverage.gd rename to no_coverage.gd index 621bee0..870b1bb 100644 --- a/NoCoverage.gd +++ b/no_coverage.gd @@ -1,3 +1,3 @@ -extends Reference +extends RefCounted const NOTE := "This file has no coverage" diff --git a/Other.gd b/other.gd similarity index 71% rename from Other.gd rename to other.gd index b47def3..0aad46a 100644 --- a/Other.gd +++ b/other.gd @@ -1,21 +1,26 @@ -extends Reference +extends RefCounted -var _initted = 0 var performance_result := [] +var _initted = 0 + + func _init(): _initted += 1 + func fmt(value: String): - return '(%s)' % [value] + return "(%s)" % [value] + + +func static_fmt(value: String): + return "static(%s)" % [value] -func static_fmt(value:String): - return 'static(%s)' func performance_test() -> int: var result := 0xFFFFFF for j in range(10): - var start := OS.get_ticks_usec() + var start := Time.get_ticks_usec() var n := [] var repeat = 10000 for i in range(repeat): @@ -26,7 +31,7 @@ func performance_test() -> int: # paranoia, the interpreter doesn't optimize yet # but someday it might optimize the whole loop out if n is discarded performance_result = n - var duration := OS.get_ticks_usec() - start + var duration := Time.get_ticks_usec() - start if duration < result: result = duration return result diff --git a/OtherNode.gd b/other_node.gd similarity index 74% rename from OtherNode.gd rename to other_node.gd index 294c176..1eb0512 100644 --- a/OtherNode.gd +++ b/other_node.gd @@ -1,13 +1,10 @@ extends Node -var ready := false func _init(parent: Node): if parent: parent.add_child(self) -func _ready(): - ready = true func fmt(value: String): return "OtherNode(%s)" % [value] diff --git a/project.godot b/project.godot index f192a0d..3836c65 100644 --- a/project.godot +++ b/project.godot @@ -6,44 +6,23 @@ ; [section] ; section goes between [] ; param=value ; assign values to parameters -config_version=4 - -_global_script_classes=[ { -"base": "Reference", -"class": "GutHookScript", -"language": "GDScript", -"path": "res://addons/gut/hook_script.gd" -}, { -"base": "Node", -"class": "GutTest", -"language": "GDScript", -"path": "res://addons/gut/test.gd" -}, { -"base": "Spatial", -"class": "Spatial1", -"language": "GDScript", -"path": "res://Spatial.gd" -} ] -_global_script_class_icons={ -"GutHookScript": "", -"GutTest": "", -"Spatial1": "" -} +config_version=5 [application] config/name="Godot-code-coverage" -run/main_scene="res://Spatial.tscn" +run/main_scene="res://spatial.tscn" +config/features=PackedStringArray("4.2") config/icon="res://icon.png" [autoload] -Autoload1="*res://Autoload1.gd" -Autoload2="*res://Autoload2.gd" +Autoload1="*res://autoload1.gd" +Autoload2="*res://autoload2.gd" [editor_plugins] -enabled=PoolStringArray( "res://addons/gut/plugin.cfg" ) +enabled=PackedStringArray("res://addons/gut/plugin.cfg") [gui] @@ -55,4 +34,4 @@ common/enable_pause_aware_picking=true [rendering] -environment/default_environment="res://default_env.tres" +environment/defaults/default_environment="res://default_env.tres" diff --git a/run_tests.sh b/run_tests.sh index ccfb961..37922bb 100755 --- a/run_tests.sh +++ b/run_tests.sh @@ -1,3 +1,9 @@ +headless_flag= +if [ "${1:-}" == "--headless" ]; then + headless_flag=--headless + shift +fi + godot="${1:-godot}" target=${2:-78} file_target=${3:-33} @@ -6,13 +12,13 @@ output_file="${5:-coverage_out.json}" set -ex -COVERAGE_FILE=coverage1.json "$godot" -s addons/coverage/CoverageTree.gd --scene=res://Spatial.tscn -COVERAGE_FILE=coverage2.json "$godot" -s addons/gut/gut_cmdln.gd +COVERAGE_FILE=coverage1.json "$godot" $headless_flag -s addons/coverage/coverage_tree.gd --scene=res://spatial.tscn +COVERAGE_FILE=coverage2.json "$godot" $headless_flag -s addons/gut/gut_cmdln.gd -gexit echo "MERGED COVERAGE:" -"$godot" -s addons/coverage/merge_coverage.gd --verbosity $verbosity --target $target --file-target $file_target --output-file "$output_file" coverage1.json coverage2.json +"$godot" $headless_flag -s addons/coverage/merge_coverage.gd --verbosity $verbosity --target $target --file-target $file_target --output-file "$output_file" coverage1.json coverage2.json set +x echo -n "MERGED COVERAGE can fail: " -if test_fail="$("$godot" -s addons/coverage/merge_coverage.gd --verbosity 0 --target 99 --file-target 99 coverage1.json coverage2.json 2>&1)"; then +if test_fail="$("$godot" $headless_flag -s addons/coverage/merge_coverage.gd --verbosity 0 --target 99 --file-target 99 coverage1.json coverage2.json 2>&1)"; then echo "Error, merge_coverage should not have passed:" >&2 echo "$test_fail" >&2 exit 1 diff --git a/Spatial.gd b/spatial.gd similarity index 53% rename from Spatial.gd rename to spatial.gd index 03bf36b..bf8b29b 100644 --- a/Spatial.gd +++ b/spatial.gd @@ -1,55 +1,54 @@ -tool -class_name Spatial1 extends Spatial +@tool +class_name Spatial1 extends Node3D -const Other = preload("./Other.gd") -const CustomLabel = preload("./CustomLabel.gd") +signal done -var auto_quit := true +const Other = preload("./other.gd") +const CustomLabel = preload("./custom_label.gd") -signal done() +var auto_quit := true class Inner: - tool - extends Reference + extends RefCounted class InnerEmpty: - extends Reference + extends RefCounted static func fmt(value: String): - return 'Inner(%s)' % [value] + return "Inner(%s)" % [value] + + +class InnerExtends: + extends RefCounted -class InnerExtends extends Reference: func fmt(value: String): - return 'InterExtends(%s)' % [value] + return "InterExtends(%s)" % [value] + # Called when the node enters the scene tree for the first time. func _ready(): var other = Other.new() $Label.text = InnerExtends.new().fmt(Inner.fmt(other.fmt("hello world"))) + func _on_Timer_timeout(): - print('add child') + print("add child") var custom_label := CustomLabel.new() add_child(custom_label) - custom_label.margin_top = 20 + custom_label.offset_top = 20 var other = Other.new() var text := Autoload1.fmt("timeout") $Label.text = text - custom_label.custom_text = \ - $Label.text + custom_label.custom_text = $Label.text for i in range(2): - yield(get_tree(), "idle_frame") + await get_tree().process_frame if i == 0: $Label.text = other.fmt(str(i)) elif i == 1: - $Label.text = ( - other.fmt(str(i * 1)) - ) + $Label.text = (other.fmt(str(i * 1))) else: - var x = { - a=1 - } + var x = {a = 1} $Label.text = other.fmt(str(i * 2) + str(x)) match i: 0: @@ -57,17 +56,21 @@ func _on_Timer_timeout(): # 1 1: $Label.text = other.fmt(str(i * 1)) - 3: $Label.text = other.fmt(str(i * 1)) + 3: + $Label.text = other.fmt(str(i * 1)) custom_label.custom_text = $Label.text - yield(get_tree(), "idle_frame") - assert(Autoload2._counter == '3', "Autoload2 counter should be 3 because Autoload1 'formatting' signal fired") + await get_tree().process_frame + assert( + Autoload2._counter == "3", + "Autoload2 counter should be 3 because Autoload1 'formatting' signal fired" + ) $Label.text = Autoload2.fmt("done") custom_label.custom_text = $Label.text - yield(get_tree().create_timer(.5), "timeout") + await get_tree().create_timer(.5).timeout print("Performance takes %sus" % [Other.new().performance_test()]) emit_signal("done") if auto_quit: - print('This line is not covered in the unit tests') + print("This line is not covered in the unit tests") get_tree().quit() else: - print('This line is not covered in the CoverageTree tests') + print("This line is not covered in the CoverageTree tests") diff --git a/Spatial.tscn b/spatial.tscn similarity index 57% rename from Spatial.tscn rename to spatial.tscn index a70b1ad..d1a4b8b 100644 --- a/Spatial.tscn +++ b/spatial.tscn @@ -1,13 +1,11 @@ -[gd_scene load_steps=2 format=2] +[gd_scene load_steps=2 format=3 uid="uid://cr85cpalo2gue"] -[ext_resource path="res://Spatial.gd" type="Script" id=1] +[ext_resource type="Script" path="res://spatial.gd" id="1"] -[node name="Spatial" type="Spatial"] -script = ExtResource( 1 ) +[node name="Node3D" type="Node3D"] +script = ExtResource("1") [node name="Label" type="Label" parent="."] -margin_right = 40.0 -margin_bottom = 14.0 text = "InterExtends(Inner((hello world)))" [node name="Timer" type="Timer" parent="."] diff --git a/tests/post_run_hook.gd b/tests/post_run_hook.gd index 83e6516..a5aaf03 100644 --- a/tests/post_run_hook.gd +++ b/tests/post_run_hook.gd @@ -1,26 +1,35 @@ extends "res://addons/gut/hook_script.gd" -const Coverage = preload("res://addons/coverage/Coverage.gd") +const Coverage = preload("res://addons/coverage/coverage.gd") const COVERAGE_TARGET := 74.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 "" + 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 - verbosity = Coverage.Verbosity.AllFiles + var verbosity = Coverage.Verbosity.ALL_FILES var logger = gut.get_logger() coverage.finalize(verbosity) 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]) + logger.failed( + ( + "Coverage target of %.1f%% total (%.1f%% file) was not met" + % [COVERAGE_TARGET, FILE_TARGET] + ) + ) set_exit_code(2) else: - logger.passed("Coverage target of %.1f%% total, %.1f%% file coverage" % [COVERAGE_TARGET, FILE_TARGET]) + logger.passed( + "Coverage target of %.1f%% total, %.1f%% file coverage" % [COVERAGE_TARGET, FILE_TARGET] + ) if coverage.coverage_percent() >= 100: logger.failed("Coverage percent should not be 100: %s" % [coverage.coverage_percent()]) set_exit_code(2) diff --git a/tests/pre_run_hook.gd b/tests/pre_run_hook.gd index 119eb53..bf8a243 100644 --- a/tests/pre_run_hook.gd +++ b/tests/pre_run_hook.gd @@ -1,13 +1,8 @@ extends "res://addons/gut/hook_script.gd" -const Coverage = preload("res://addons/coverage/Coverage.gd") -const exclude_paths = [ - "res://addons/*", - "res://tests/*", - "res://contrib/*" -] +const Coverage = preload("res://addons/coverage/coverage.gd") +const EXCLUDE_PATHS = ["res://addons/*", "res://tests/*", "res://contrib/*"] + func run(): - Coverage.new(gut.get_tree(), exclude_paths) \ - .instrument_scripts("res://") \ - .enforce_node_coverage() + Coverage.new(gut.get_tree(), EXCLUDE_PATHS).instrument_scripts("res://").enforce_node_coverage() diff --git a/tests/test_coverage.gd b/tests/test_coverage.gd index 0dd06d9..d0378dc 100644 --- a/tests/test_coverage.gd +++ b/tests/test_coverage.gd @@ -1,45 +1,75 @@ extends "res://addons/gut/test.gd" # Instrumented code performance should be within this margin of uninstrumented performance -const PERFORMANCE_MARGIN := 1.8 +const PERFORMANCE_MARGIN := 2.1 + +const SPATIAL_SCENE = preload("res://spatial.tscn") +const Coverage = preload("res://addons/coverage/coverage.gd") +const Other = preload("res://other.gd") -const scene = preload("res://Spatial.tscn") -const Coverage = preload("res://addons/coverage/Coverage.gd") -const Other = preload("res://Other.gd") func test_autoload_coverage(): assert_true(Autoload1._initted) - assert_eq(Autoload1._ready, '1') - assert_eq(Autoload2._counter, '2') - var node = add_child_autoqfree(scene.instance()) + assert_true(Autoload1.is_node_ready()) + assert_eq(Autoload2._counter, "2") + var node = add_child_autoqfree(SPATIAL_SCENE.instantiate()) node.auto_quit = false - yield(yield_to(node, "done", 5000), YIELD) + await wait_for_signal(node.done, 5000) assert_signal_emitted(node, "done") assert_true(Autoload1._initted) - assert_eq(Autoload1._ready, '1') - assert_eq(Autoload2._counter, '3') + assert_true(Autoload1.is_node_ready()) + assert_eq(Autoload2._counter, "3") assert_eq(Autoload1.other._initted, 1) + func test_performance(): - var coverage: Coverage = Coverage.instance() - var collector: Coverage.ScriptCoverageCollector = coverage.get_coverage_collector(Other.resource_path) + var coverage: Coverage = Coverage.instance + var collector: Coverage.ScriptCoverageCollector = coverage.get_coverage_collector( + "res://other.gd" + ) var logger = get_logger() var other := Other.new() - assert_true(Other.source_code.match("*Coverage.gd*"), "Other script should be instrumented") - var instrumented_source = Other.source_code + var other_script := other.get_script() as GDScript + assert_true( + other_script.source_code.match("*coverage.gd*"), "Other script should be instrumented" + ) + var instrumented_source = other_script.source_code var instrumented_time = other.performance_test() collector.set_instrumented(false) - assert_false(Other.source_code.match("*Coverage.gd*"), "Other script should no longer be instrumented") + assert_false( + other.get_script().source_code.match("*coverage.gd*"), + "Other script should no longer be instrumented" + ) var uninstrumented_time = other.performance_test() collector.set_instrumented(true) - assert_true(Other.source_code.match("*Coverage.gd*"), "Other script should be instrumented") - var performance_loss := (float(instrumented_time) / float(uninstrumented_time)) + assert_true( + other_script.source_code.match("*coverage.gd*"), "Other script should be instrumented" + ) + var performance_loss := float(instrumented_time) / float(uninstrumented_time) var performance_passing := performance_loss < PERFORMANCE_MARGIN assert_gt(performance_loss, 1.2, "Performance loss should be measureable") - assert_lt(performance_loss, PERFORMANCE_MARGIN, "Performance loss for instrumentation: %sx < %s" % [performance_loss, PERFORMANCE_MARGIN]) + assert_lt( + performance_loss, + PERFORMANCE_MARGIN, + ( + "Performance loss for instrumentation: %.2fx < %.2f" + % [performance_loss, PERFORMANCE_MARGIN] + ) + ) if !performance_passing: - logger.failed( - "%s\nInstrumented performance was not good enough.\n Instrumented: %sus\nUninstrumented: %sus\n%sx > %sx" % [ - instrumented_source.get_slice("performance_test", 1), instrumented_time, uninstrumented_time, performance_loss, PERFORMANCE_MARGIN - ]) + ( + logger + . failed( + ( + "%s\nInstrumented performance was not good enough.\n Instrumented: %sus\nUninstrumented: %sus\n%sx > %sx" + % [ + instrumented_source.get_slice("performance_test", 1), + instrumented_time, + uninstrumented_time, + performance_loss, + PERFORMANCE_MARGIN + ] + ) + ) + ) diff --git a/update_addons.sh b/update_addons.sh index 779fa07..cfa2edb 100755 --- a/update_addons.sh +++ b/update_addons.sh @@ -3,5 +3,7 @@ DIR=$(dirname "${BASH_SOURCE[0]}") DIR=$(realpath "${DIR}") cd $DIR -git submodule update --init +if [ "${1:-}" != "--no-init" ]; then + git submodule update --init --recursive +fi cp -R contrib/Gut/addons/gut addons