Update GUT to version 9.1.1

This commit is contained in:
Leroy Hopson 2024-01-06 23:27:15 +13:00
parent b18726eac9
commit cea41d6567
No known key found for this signature in database
GPG key ID: D2747312A6DB51AA
65 changed files with 4178 additions and 2995 deletions

View file

@ -1,146 +1,12 @@
extends Node2D
# ##############################################################################
# This is a wrapper around the normal and compact gui controls and serves as
# the interface between gut.gd and the gui. The GutRunner creates an instance
# of this and then this takes care of managing the different GUI controls.
# ##############################################################################
@onready var _normal_gui = $Normal
@onready var _compact_gui = $Compact
class GuiHandler:
var _gui = null
var _gut = null
var _ctrls = {
btn_continue = null,
path_dir = null,
path_file = null,
prog_script = null,
prog_test = null,
rtl = null,
rtl_bg = null,
time_label = null
}
func _init(gui):
_gui = gui
# Brute force, but flexible.
_ctrls.btn_continue = _get_first_child_named("Continue", _gui)
_ctrls.path_dir = _get_first_child_named("Path", _gui)
_ctrls.path_file = _get_first_child_named("File", _gui)
_ctrls.prog_script = _get_first_child_named("ProgressScript", _gui)
_ctrls.prog_test = _get_first_child_named("ProgressTest", _gui)
_ctrls.rtl = _get_first_child_named("Output", _gui)
_ctrls.rtl_bg = _get_first_child_named("OutputBG", _gui)
_ctrls.time_label = _get_first_child_named("TimeLabel", _gui)
_ctrls.btn_continue.visible = false
_ctrls.btn_continue.pressed.connect(_on_continue_pressed)
_ctrls.prog_script.value = 0
_ctrls.prog_test.value = 0
_ctrls.path_dir.text = ""
_ctrls.path_file.text = ""
_ctrls.time_label.text = ""
# ------------------
# Events
# ------------------
func _on_continue_pressed():
_ctrls.btn_continue.visible = false
_gut.end_teardown_pause()
func _on_gut_start_run():
if _ctrls.rtl != null:
_ctrls.rtl.clear()
set_num_scripts(_gut.get_test_collector().scripts.size())
func _on_gut_end_run():
_ctrls.time_label.text = ""
func _on_gut_start_script(script_obj):
next_script(script_obj.get_full_name(), script_obj.tests.size())
func _on_gut_end_script():
pass
func _on_gut_start_test(test_name):
next_test(test_name)
func _on_gut_end_test():
pass
func _on_gut_start_pause():
pause_before_teardown()
func _on_gut_end_pause():
pass
# ------------------
# Private
# ------------------
func _get_first_child_named(obj_name, parent_obj):
if parent_obj == null:
return null
var kids = parent_obj.get_children()
var index = 0
var to_return = null
while index < kids.size() and to_return == null:
if str(kids[index]).find(str(obj_name, ":")) != -1:
to_return = kids[index]
else:
to_return = _get_first_child_named(obj_name, kids[index])
if to_return == null:
index += 1
return to_return
# ------------------
# Public
# ------------------
func set_num_scripts(val):
_ctrls.prog_script.value = 0
_ctrls.prog_script.max_value = val
func next_script(path, num_tests):
_ctrls.prog_script.value += 1
_ctrls.prog_test.value = 0
_ctrls.prog_test.max_value = num_tests
_ctrls.path_dir.text = path.get_base_dir()
_ctrls.path_file.text = path.get_file()
func next_test(test_name):
_ctrls.prog_test.value += 1
func pause_before_teardown():
_ctrls.btn_continue.visible = true
func set_gut(g):
_gut = g
g.start_run.connect(_on_gut_start_run)
g.end_run.connect(_on_gut_end_run)
g.start_script.connect(_on_gut_start_script)
g.end_script.connect(_on_gut_end_script)
g.start_test.connect(_on_gut_start_test)
g.end_test.connect(_on_gut_end_test)
g.start_pause_before_teardown.connect(_on_gut_start_pause)
g.end_pause_before_teardown.connect(_on_gut_end_pause)
func get_textbox():
return _ctrls.rtl
func set_elapsed_time(t):
_ctrls.time_label.text = str(t, "s")
func set_bg_color(c):
_ctrls.rtl_bg.color = c
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
var _large_handler = null
var _min_handler = null
var gut = null:
set(val):
gut = val
@ -148,54 +14,92 @@ var gut = null:
func _ready():
_large_handler = GuiHandler.new($Large)
_min_handler = GuiHandler.new($Min)
_normal_gui.switch_modes.connect(use_compact_mode.bind(true))
_compact_gui.switch_modes.connect(use_compact_mode.bind(false))
$Min.visible = false
$Large.visible = !$Min.visible
_normal_gui.set_title("GUT")
_compact_gui.set_title("GUT")
_normal_gui.align_right()
_compact_gui.to_bottom_right()
use_compact_mode(false)
if get_parent() == get_tree().root:
_test_running_setup()
func _process(_delta):
if gut != null and gut.is_running():
_large_handler.set_elapsed_time(gut.get_elapsed_time())
_min_handler.set_elapsed_time(gut.get_elapsed_time())
func _test_running_setup():
set_font_size(100)
_normal_gui.get_textbox().text = "hello world, how are you doing?"
# ------------------------
# Private
# ------------------------
func _set_gut(val):
_large_handler.set_gut(val)
_min_handler.set_gut(val)
if _normal_gui.get_gut() == val:
return
_normal_gui.set_gut(val)
_compact_gui.set_gut(val)
val.start_run.connect(_on_gut_start_run)
val.end_run.connect(_on_gut_end_run)
val.start_pause_before_teardown.connect(_on_gut_pause)
val.end_pause_before_teardown.connect(_on_pause_end)
func _set_both_titles(text):
_normal_gui.set_title(text)
_compact_gui.set_title(text)
# ------------------------
# Events
# ------------------------
func _on_gut_start_run():
_set_both_titles("Running")
func _on_gut_end_run():
_set_both_titles("Finished")
func _on_gut_pause():
_set_both_titles("-- Paused --")
func _on_pause_end():
_set_both_titles("Running")
# ------------------------
# Public
# ------------------------
func get_textbox():
return _large_handler.get_textbox()
return _normal_gui.get_textbox()
func set_font_size(new_size):
var rtl = _large_handler.get_textbox()
if rtl.get("custom_fonts/normal_font") != null:
rtl.get("custom_fonts/bold_italics_font").size = new_size
rtl.get("custom_fonts/bold_font").size = new_size
rtl.get("custom_fonts/italics_font").size = new_size
rtl.get("custom_fonts/normal_font").size = new_size
var rtl = _normal_gui.get_textbox()
rtl.set("theme_override_font_sizes/bold_italics_font_size", new_size)
rtl.set("theme_override_font_sizes/bold_font_size", new_size)
rtl.set("theme_override_font_sizes/italics_font_size", new_size)
rtl.set("theme_override_font_sizes/normal_font_size", new_size)
func set_font(font_name):
pass
#_set_all_fonts_in_rtl(_large_handler.get_textbox(), font_name)
_set_all_fonts_in_rtl(_normal_gui.get_textbox(), font_name)
# Needs rework for 4.0, DynamicFont DNE
func _set_font(rtl, font_name, custom_name):
pass
# if(font_name == null):
# rtl.set('custom_fonts/' + custom_name, null)
# else:
# var dyn_font = DynamicFont.new()
# var font_data = DynamicFontData.new()
# font_data.font_path = 'res://addons/gut/fonts/' + font_name + '.ttf'
# font_data.antialiased = true
# dyn_font.font_data = font_data
# rtl.set('custom_fonts/' + custom_name, dyn_font)
if font_name == null:
rtl.add_theme_font_override(custom_name, null)
else:
var dyn_font = FontFile.new()
dyn_font.load_dynamic_font("res://addons/gut/fonts/" + font_name + ".ttf")
rtl.add_theme_font_override(custom_name, dyn_font)
func _set_all_fonts_in_rtl(rtl, base_name):
@ -212,8 +116,18 @@ func _set_all_fonts_in_rtl(rtl, base_name):
func set_default_font_color(color):
_large_handler.get_textbox().set("custom_colors/default_color", color)
_normal_gui.get_textbox().set("custom_colors/default_color", color)
func set_background_color(color):
_large_handler.set_bg_color(color)
_normal_gui.set_bg_color(color)
func use_compact_mode(should = true):
_compact_gui.visible = should
_normal_gui.visible = !should
func set_opacity(val):
_normal_gui.modulate.a = val
_compact_gui.modulate.a = val

View file

@ -1,375 +1,16 @@
[gd_scene load_steps=4 format=3 uid="uid://m28heqtswbuq"]
[ext_resource type="Script" path="res://addons/gut/GutScene.gd" id="1_b4m8y"]
[ext_resource type="Theme" uid="uid://cstkhwkpajvqu" path="res://addons/gut/gui/GutSceneTheme.tres" id="1_s37wl"]
[ext_resource type="FontFile" uid="uid://bnh0lslf4yh87" path="res://addons/gut/fonts/CourierPrime-Regular.ttf" id="3_qvb8f"]
[ext_resource type="PackedScene" uid="uid://duxblir3vu8x7" path="res://addons/gut/gui/NormalGui.tscn" id="2_j6ywb"]
[ext_resource type="PackedScene" uid="uid://cnqqdfsn80ise" path="res://addons/gut/gui/MinGui.tscn" id="3_3glw1"]
[node name="GutScene" type="Node2D"]
script = ExtResource("1_b4m8y")
[node name="Large" type="Panel" parent="."]
offset_right = 717.0
offset_bottom = 388.0
theme = ExtResource("1_s37wl")
[node name="Normal" parent="." instance=ExtResource("2_j6ywb")]
[node name="MainBox" type="VBoxContainer" parent="Large"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_layout_mode = 1
[node name="TitleBar" type="Panel" parent="Large/MainBox"]
custom_minimum_size = Vector2(0, 25)
offset_right = 717.0
offset_bottom = 25.0
[node name="TitleBox" type="HBoxContainer" parent="Large/MainBox/TitleBar"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = 2.0
offset_bottom = 3.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_layout_mode = 1
[node name="Spacer1" type="CenterContainer" parent="Large/MainBox/TitleBar/TitleBox"]
offset_right = 285.0
offset_bottom = 26.0
size_flags_horizontal = 3
[node name="Title" type="Label" parent="Large/MainBox/TitleBar/TitleBox"]
offset_left = 289.0
offset_top = 3.0
offset_right = 334.0
offset_bottom = 23.0
text = "Title"
[node name="Spacer2" type="CenterContainer" parent="Large/MainBox/TitleBar/TitleBox"]
offset_left = 338.0
offset_right = 623.0
offset_bottom = 26.0
size_flags_horizontal = 3
[node name="TimeLabel" type="Label" parent="Large/MainBox/TitleBar/TitleBox"]
custom_minimum_size = Vector2(90, 0)
offset_left = 627.0
offset_top = 3.0
offset_right = 717.0
offset_bottom = 23.0
text = "999.999s"
[node name="HBoxContainer" type="HBoxContainer" parent="Large/MainBox"]
offset_top = 29.0
offset_right = 717.0
offset_bottom = 379.0
size_flags_vertical = 3
[node name="VBoxContainer" type="VBoxContainer" parent="Large/MainBox/HBoxContainer"]
offset_right = 717.0
offset_bottom = 350.0
size_flags_horizontal = 3
[node name="OutputBG" type="ColorRect" parent="Large/MainBox/HBoxContainer/VBoxContainer"]
offset_right = 717.0
offset_bottom = 300.0
grow_horizontal = 2
grow_vertical = 2
size_flags_vertical = 3
color = Color(0.0745098, 0.0705882, 0.0784314, 1)
metadata/_edit_layout_mode = 1
[node name="HBoxContainer" type="HBoxContainer" parent="Large/MainBox/HBoxContainer/VBoxContainer/OutputBG"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="S2" type="CenterContainer" parent="Large/MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"]
custom_minimum_size = Vector2(5, 0)
offset_right = 5.0
offset_bottom = 300.0
[node name="Output" type="RichTextLabel" parent="Large/MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"]
offset_left = 9.0
offset_right = 708.0
offset_bottom = 300.0
size_flags_horizontal = 3
size_flags_vertical = 3
focus_mode = 2
bbcode_enabled = true
scroll_following = true
selection_enabled = true
[node name="S1" type="CenterContainer" parent="Large/MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"]
custom_minimum_size = Vector2(5, 0)
offset_left = 712.0
offset_right = 717.0
offset_bottom = 300.0
[node name="ControlBox" type="HBoxContainer" parent="Large/MainBox/HBoxContainer/VBoxContainer"]
offset_top = 304.0
offset_right = 717.0
offset_bottom = 350.0
[node name="S1" type="CenterContainer" parent="Large/MainBox/HBoxContainer/VBoxContainer/ControlBox"]
custom_minimum_size = Vector2(5, 0)
offset_right = 5.0
offset_bottom = 46.0
[node name="ProgressBars" type="VBoxContainer" parent="Large/MainBox/HBoxContainer/VBoxContainer/ControlBox"]
offset_left = 9.0
offset_right = 176.0
offset_bottom = 46.0
[node name="TestBox" type="HBoxContainer" parent="Large/MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars"]
offset_right = 167.0
offset_bottom = 21.0
[node name="Label" type="Label" parent="Large/MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/TestBox"]
custom_minimum_size = Vector2(60, 0)
offset_right = 60.0
offset_bottom = 20.0
text = "Tests"
[node name="ProgressTest" type="ProgressBar" parent="Large/MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/TestBox"]
custom_minimum_size = Vector2(100, 0)
offset_left = 64.0
offset_right = 164.0
offset_bottom = 21.0
value = 25.0
[node name="ScriptBox" type="HBoxContainer" parent="Large/MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars"]
offset_top = 25.0
offset_right = 167.0
offset_bottom = 46.0
[node name="Label" type="Label" parent="Large/MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/ScriptBox"]
custom_minimum_size = Vector2(60, 0)
offset_right = 63.0
offset_bottom = 20.0
text = "Scripts"
[node name="ProgressScript" type="ProgressBar" parent="Large/MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/ScriptBox"]
custom_minimum_size = Vector2(100, 0)
offset_left = 67.0
offset_right = 167.0
offset_bottom = 21.0
value = 75.0
[node name="PathDisplay" type="VBoxContainer" parent="Large/MainBox/HBoxContainer/VBoxContainer/ControlBox"]
offset_left = 180.0
offset_right = 385.0
offset_bottom = 46.0
size_flags_vertical = 3
[node name="Path" type="Label" parent="Large/MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay"]
offset_right = 205.0
offset_bottom = 16.0
theme_override_fonts/font = ExtResource("3_qvb8f")
theme_override_font_sizes/font_size = 11
text = "res://test/integration/whatever"
[node name="HBoxContainer" type="HBoxContainer" parent="Large/MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay"]
offset_top = 20.0
offset_right = 205.0
offset_bottom = 36.0
[node name="S3" type="CenterContainer" parent="Large/MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay/HBoxContainer"]
custom_minimum_size = Vector2(5, 0)
offset_right = 5.0
offset_bottom = 16.0
[node name="File" type="Label" parent="Large/MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay/HBoxContainer"]
offset_left = 9.0
offset_right = 128.0
offset_bottom = 16.0
theme_override_fonts/font = ExtResource("3_qvb8f")
theme_override_font_sizes/font_size = 11
text = "test_this_thing.gd"
[node name="Spacer1" type="CenterContainer" parent="Large/MainBox/HBoxContainer/VBoxContainer/ControlBox"]
offset_left = 389.0
offset_right = 624.0
offset_bottom = 46.0
size_flags_horizontal = 3
[node name="Continue" type="Button" parent="Large/MainBox/HBoxContainer/VBoxContainer/ControlBox"]
offset_left = 628.0
offset_top = 10.0
offset_right = 708.0
offset_bottom = 35.0
size_flags_vertical = 4
text = "Continue
"
[node name="S3" type="CenterContainer" parent="Large/MainBox/HBoxContainer/VBoxContainer/ControlBox"]
custom_minimum_size = Vector2(5, 0)
offset_left = 712.0
offset_right = 717.0
offset_bottom = 46.0
[node name="BottomPad" type="CenterContainer" parent="Large/MainBox"]
custom_minimum_size = Vector2(0, 5)
offset_top = 383.0
offset_right = 717.0
offset_bottom = 388.0
[node name="Min" type="Panel" parent="."]
clip_contents = true
offset_left = 456.0
offset_top = 396.0
offset_right = 719.0
offset_bottom = 515.0
theme = ExtResource("1_s37wl")
metadata/_edit_group_ = true
[node name="MainBox" type="VBoxContainer" parent="Min"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_layout_mode = 1
[node name="TitleBar" type="Panel" parent="Min/MainBox"]
custom_minimum_size = Vector2(0, 25)
offset_right = 266.0
offset_bottom = 25.0
[node name="TitleBox" type="HBoxContainer" parent="Min/MainBox/TitleBar"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = 2.0
offset_bottom = 3.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_layout_mode = 1
[node name="Spacer1" type="CenterContainer" parent="Min/MainBox/TitleBar/TitleBox"]
offset_right = 77.0
offset_bottom = 26.0
size_flags_horizontal = 3
[node name="Title" type="Label" parent="Min/MainBox/TitleBar/TitleBox"]
offset_left = 81.0
offset_top = 3.0
offset_right = 126.0
offset_bottom = 23.0
text = "Title"
[node name="Spacer2" type="CenterContainer" parent="Min/MainBox/TitleBar/TitleBox"]
offset_left = 130.0
offset_right = 208.0
offset_bottom = 26.0
size_flags_horizontal = 3
[node name="TimeLabel" type="Label" parent="Min/MainBox/TitleBar/TitleBox"]
offset_left = 212.0
offset_top = 3.0
offset_right = 266.0
offset_bottom = 23.0
text = "0.000s"
[node name="Body" type="HBoxContainer" parent="Min/MainBox"]
offset_top = 29.0
offset_right = 266.0
offset_bottom = 119.0
size_flags_vertical = 3
[node name="LeftMargin" type="CenterContainer" parent="Min/MainBox/Body"]
custom_minimum_size = Vector2(5, 0)
offset_right = 5.0
offset_bottom = 90.0
[node name="BodyRows" type="VBoxContainer" parent="Min/MainBox/Body"]
offset_left = 9.0
offset_right = 257.0
offset_bottom = 90.0
size_flags_horizontal = 3
[node name="ProgressBars" type="HBoxContainer" parent="Min/MainBox/Body/BodyRows"]
offset_right = 248.0
offset_bottom = 21.0
size_flags_horizontal = 3
[node name="HBoxContainer" type="HBoxContainer" parent="Min/MainBox/Body/BodyRows/ProgressBars"]
offset_right = 122.0
offset_bottom = 21.0
[node name="Label" type="Label" parent="Min/MainBox/Body/BodyRows/ProgressBars/HBoxContainer"]
offset_right = 18.0
offset_bottom = 20.0
text = "T:"
[node name="ProgressTest" type="ProgressBar" parent="Min/MainBox/Body/BodyRows/ProgressBars/HBoxContainer"]
custom_minimum_size = Vector2(100, 0)
offset_left = 22.0
offset_right = 122.0
offset_bottom = 21.0
value = 25.0
[node name="HBoxContainer2" type="HBoxContainer" parent="Min/MainBox/Body/BodyRows/ProgressBars"]
offset_left = 126.0
offset_right = 248.0
offset_bottom = 21.0
[node name="Label" type="Label" parent="Min/MainBox/Body/BodyRows/ProgressBars/HBoxContainer2"]
offset_right = 18.0
offset_bottom = 20.0
text = "S:"
[node name="ProgressScript" type="ProgressBar" parent="Min/MainBox/Body/BodyRows/ProgressBars/HBoxContainer2"]
custom_minimum_size = Vector2(100, 0)
offset_left = 22.0
offset_right = 122.0
offset_bottom = 21.0
value = 75.0
[node name="PathDisplay" type="VBoxContainer" parent="Min/MainBox/Body/BodyRows"]
offset_top = 25.0
offset_right = 248.0
offset_bottom = 61.0
size_flags_vertical = 3
[node name="Path" type="Label" parent="Min/MainBox/Body/BodyRows/PathDisplay"]
offset_right = 248.0
offset_bottom = 16.0
theme_override_fonts/font = ExtResource("3_qvb8f")
theme_override_font_sizes/font_size = 11
text = "res://test/integration/whatever"
[node name="HBoxContainer" type="HBoxContainer" parent="Min/MainBox/Body/BodyRows/PathDisplay"]
offset_top = 20.0
offset_right = 248.0
offset_bottom = 36.0
[node name="S3" type="CenterContainer" parent="Min/MainBox/Body/BodyRows/PathDisplay/HBoxContainer"]
custom_minimum_size = Vector2(5, 0)
offset_right = 5.0
offset_bottom = 16.0
[node name="File" type="Label" parent="Min/MainBox/Body/BodyRows/PathDisplay/HBoxContainer"]
offset_left = 9.0
offset_right = 128.0
offset_bottom = 16.0
theme_override_fonts/font = ExtResource("3_qvb8f")
theme_override_font_sizes/font_size = 11
text = "test_this_thing.gd"
[node name="Continue" type="Button" parent="Min/MainBox/Body/BodyRows"]
offset_top = 65.0
offset_right = 248.0
offset_bottom = 90.0
text = "Continue
"
[node name="RightMargin" type="CenterContainer" parent="Min/MainBox/Body"]
custom_minimum_size = Vector2(5, 0)
offset_left = 261.0
offset_right = 266.0
offset_bottom = 90.0
[node name="Compact" parent="." instance=ExtResource("3_3glw1")]
offset_left = 5.0
offset_top = 273.0
offset_right = 265.0
offset_bottom = 403.0

View file

@ -55,7 +55,8 @@ func _on_End_pressed():
func _on_Copy_pressed():
OS.clipboard = rtl.text
return
# OS.clipboard = rtl.text
func _on_file_dialog_visibility_changed():

View file

@ -24,6 +24,9 @@ func _physics_process(delta):
func _end_wait():
if _signal_to_wait_on != null and _signal_to_wait_on.is_connected(_signal_callback):
_signal_to_wait_on.disconnect(_signal_callback)
_wait_time = 0.0
_wait_frames = 0
_signal_to_wait_on = null

View file

@ -0,0 +1,214 @@
# ------------------------------------------------------------------------------
# This holds all the meta information for a test script. It contains the
# name of the inner class and an array of CollectedTests. This does not parse
# anything, it just holds the data about parsed scripts and tests. The
# TestCollector is responsible for populating this object.
#
# This class also facilitates all the exporting and importing of tests.
# ------------------------------------------------------------------------------
var CollectedTest = load("res://addons/gut/collected_test.gd")
var _utils = null
var _lgr = null
# One entry per test found in the script. Added externally by TestCollector
var tests = []
# One entry for before_all and after_all (maybe add before_each and after_each).
# These are added by Gut when running before_all and after_all for the script.
var setup_teardown_tests = []
var inner_class_name: StringName
var path: String
# Set externally by test_collector after it can verify that the script was
# actually loaded. This could probably be changed to just hold the GutTest
# script that was loaded, cutting down on complexity elsewhere.
var is_loaded = false
# Set by Gut when it decides that a script should be skipped.
# Right now this is whenever the script has the variable skip_script declared.
# the value of skip_script is put into skip_reason.
var was_skipped = false
var skip_reason = ""
var was_run = false
var name = "":
get:
return path
set(val):
pass
func _init(utils = null, logger = null):
_utils = utils
_lgr = logger
func get_new():
return load_script().new()
func load_script():
var to_return = load(path)
if inner_class_name != null and inner_class_name != "":
# If we wanted to do inner classes in inner classses
# then this would have to become some kind of loop or recursive
# call to go all the way down the chain or this class would
# have to change to hold onto the loaded class instead of
# just path information.
to_return = to_return.get(inner_class_name)
return to_return
# script.gd.InnerClass
func get_filename_and_inner():
var to_return = get_filename()
if inner_class_name != "":
to_return += "." + String(inner_class_name)
return to_return
# res://foo/bar.gd.FooBar
func get_full_name():
var to_return = path
if inner_class_name != "":
to_return += "." + String(inner_class_name)
return to_return
func get_filename():
return path.get_file()
func has_inner_class():
return inner_class_name != ""
# Note: although this no longer needs to export the inner_class names since
# they are pulled from metadata now, it is easier to leave that in
# so we don't have to cut the export down to unique script names.
func export_to(config_file, section):
config_file.set_value(section, "path", path)
config_file.set_value(section, "inner_class", inner_class_name)
var names = []
for i in range(tests.size()):
names.append(tests[i].name)
config_file.set_value(section, "tests", names)
func _remap_path(source_path):
var to_return = source_path
if !_utils.file_exists(source_path):
_lgr.debug("Checking for remap for: " + source_path)
var remap_path = source_path.get_basename() + ".gd.remap"
if _utils.file_exists(remap_path):
var cf = ConfigFile.new()
cf.load(remap_path)
to_return = cf.get_value("remap", "path")
else:
_lgr.warn("Could not find remap file " + remap_path)
return to_return
func import_from(config_file, section):
path = config_file.get_value(section, "path")
path = _remap_path(path)
# Null is an acceptable value, but you can't pass null as a default to
# get_value since it thinks you didn't send a default...then it spits
# out red text. This works around that.
var inner_name = config_file.get_value(section, "inner_class", "Placeholder")
if inner_name != "Placeholder":
inner_class_name = inner_name
else: # just being explicit
inner_class_name = StringName("")
func get_test_named(name):
return _utils.search_array(tests, "name", name)
func mark_tests_to_skip_with_suffix(suffix):
for single_test in tests:
single_test.should_skip = single_test.name.ends_with(suffix)
func get_ran_test_count():
var count = 0
for t in tests:
if t.was_run:
count += 1
return count
func get_assert_count():
var count = 0
for t in tests:
count += t.pass_texts.size()
count += t.fail_texts.size()
for t in setup_teardown_tests:
count += t.pass_texts.size()
count += t.fail_texts.size()
return count
func get_pass_count():
var count = 0
for t in tests:
count += t.pass_texts.size()
for t in setup_teardown_tests:
count += t.pass_texts.size()
return count
func get_fail_count():
var count = 0
for t in tests:
count += t.fail_texts.size()
for t in setup_teardown_tests:
count += t.fail_texts.size()
return count
func get_pending_count():
var count = 0
for t in tests:
count += t.pending_texts.size()
return count
func get_passing_test_count():
var count = 0
for t in tests:
if t.is_passing():
count += 1
return count
func get_failing_test_count():
var count = 0
for t in tests:
if t.is_failing():
count += 1
return count
func get_risky_count():
var count = 0
if was_skipped:
count = 1
else:
for t in tests:
if t.is_risky():
count += 1
return count
func to_s():
var to_return = path
if inner_class_name != null:
to_return += str(".", inner_class_name)
to_return += "\n"
for i in range(tests.size()):
to_return += str(" ", tests[i].to_s())
return to_return

View file

@ -0,0 +1,117 @@
# ------------------------------------------------------------------------------
# Used to keep track of info about each test ran.
# ------------------------------------------------------------------------------
# the name of the function
var name = ""
# flag to know if the name has been printed yet. Used by the logger.
var has_printed_name = false
# the number of arguments the method has
var arg_count = 0
# The number of asserts in the test. Converted to a property for backwards
# compatibility. This now reflects the text sizes instead of being a value
# that can be altered externally.
var assert_count = 0:
get:
return pass_texts.size() + fail_texts.size()
set(val):
pass
# Converted to propety for backwards compatibility. This now cannot be set
# externally
var pending = false:
get:
return is_pending()
set(val):
pass
# the line number when the test fails
var line_number = -1
# Set internally by Gut using whatever reason Gut wants to use to set this.
# Gut will skip these marked true and the test will be listed as risky.
var should_skip = false
var pass_texts = []
var fail_texts = []
var pending_texts = []
var orphans = 0
var was_run = false
func did_pass():
return is_passing()
func add_fail(fail_text):
fail_texts.append(fail_text)
func add_pending(pending_text):
pending_texts.append(pending_text)
func add_pass(passing_text):
pass_texts.append(passing_text)
# must have passed an assert and not have any other status to be passing
func is_passing():
return pass_texts.size() > 0 and fail_texts.size() == 0 and pending_texts.size() == 0
# failing takes precedence over everything else, so any failures makes the
# test a failure.
func is_failing():
return fail_texts.size() > 0
# test is only pending if pending was called and the test is not failing.
func is_pending():
return pending_texts.size() > 0 and fail_texts.size() == 0
func is_risky():
return should_skip or (was_run and !did_something())
func did_something():
return is_passing() or is_failing() or is_pending()
func get_status_text():
var to_return = GutUtils.TEST_STATUSES.NO_ASSERTS
if should_skip:
to_return = GutUtils.TEST_STATUSES.SKIPPED
elif !was_run:
to_return = GutUtils.TEST_STATUSES.NOT_RUN
elif pending_texts.size() > 0:
to_return = GutUtils.TEST_STATUSES.PENDING
elif fail_texts.size() > 0:
to_return = GutUtils.TEST_STATUSES.FAILED
elif pass_texts.size() > 0:
to_return = GutUtils.TEST_STATUSES.PASSED
return to_return
# Deprecated
func get_status():
return get_status_text()
func to_s():
var pad = " "
var to_return = str(name, "[", get_status_text(), "]\n")
for i in range(fail_texts.size()):
to_return += str(pad, "Fail: ", fail_texts[i])
for i in range(pending_texts.size()):
to_return += str(pad, "Pending: ", pending_texts[i], "\n")
for i in range(pass_texts.size()):
to_return += str(pad, "Pass: ", pass_texts[i], "\n")
return to_return

View file

@ -4,7 +4,6 @@ var _max_length = 100
var _should_compare_int_to_float = true
const MISSING = "|__missing__gut__compare__value__|"
const DICTIONARY_DISCLAIMER = "Dictionaries are compared-by-ref. See assert_eq in wiki."
func _cannot_compare_text(v1, v2):
@ -59,19 +58,12 @@ func simple(v1, v2, missing_string = ""):
result.are_equal = v1 == v2
elif _utils.are_datatypes_same(v1, v2):
result.are_equal = v1 == v2
if typeof(v1) == TYPE_DICTIONARY:
if result.are_equal:
extra = ". Same dictionary ref. "
else:
extra = ". Different dictionary refs. "
extra += DICTIONARY_DISCLAIMER
if typeof(v1) == TYPE_ARRAY:
var array_result = _utils.DiffTool.new(v1, v2, _utils.DIFF.SHALLOW)
result.summary = array_result.get_short_summary()
if !array_result.are_equal:
extra = ".\n" + array_result.get_short_summary()
if typeof(v1) == TYPE_DICTIONARY or typeof(v1) == TYPE_ARRAY:
var sub_result = _utils.DiffTool.new(v1, v2, _utils.DIFF.DEEP)
result.summary = sub_result.get_short_summary()
if !sub_result.are_equal:
extra = ".\n" + sub_result.get_short_summary()
else:
cmp_str = "!="
result.are_equal = false
@ -85,10 +77,9 @@ func simple(v1, v2, missing_string = ""):
func shallow(v1, v2):
var result = null
if _utils.are_datatypes_same(v1, v2):
if typeof(v1) in [TYPE_ARRAY, TYPE_DICTIONARY]:
result = _utils.DiffTool.new(v1, v2, _utils.DIFF.SHALLOW)
result = _utils.DiffTool.new(v1, v2, _utils.DIFF.DEEP)
else:
result = simple(v1, v2)
else:
@ -119,8 +110,6 @@ func compare(v1, v2, diff_type = _utils.DIFF.SIMPLE):
var result = null
if diff_type == _utils.DIFF.SIMPLE:
result = simple(v1, v2)
elif diff_type == _utils.DIFF.SHALLOW:
result = shallow(v1, v2)
elif diff_type == _utils.DIFF.DEEP:
result = deep(v1, v2)

View file

@ -1,6 +1,6 @@
extends "res://addons/gut/compare_result.gd"
const INDENT = " "
enum { DEEP, SHALLOW, SIMPLE }
enum { DEEP, SIMPLE }
var _utils = load("res://addons/gut/utils.gd").get_instance()
var _strutils = _utils.Strutils.new()

View file

@ -1,6 +1,7 @@
{func_decleration}
__gutdbl.spy_on('{method_name}', {param_array})
{vararg_warning}__gutdbl.spy_on('{method_name}', {param_array})
if(__gutdbl.should_call_super('{method_name}', {param_array})):
return {super_call}
else:
return __gutdbl.get_stubbed_return('{method_name}', {param_array})

View file

@ -19,6 +19,7 @@ var __gutdbl_values = {
gut = {gut_id},
from_singleton = '{singleton_name}',
is_partial = {is_partial},
doubled_methods = {doubled_methods},
}
var __gutdbl = load('res://addons/gut/double_tools.gd').new(__gutdbl_values)

View file

@ -10,6 +10,21 @@ var double = null
const NO_DEFAULT_VALUE = "!__gut__no__default__value__!"
func _init(values = null):
if values != null:
double = values.double
thepath = values.thepath
subpath = values.subpath
stubber = from_id(values.stubber)
spy = from_id(values.spy)
gut = from_id(values.gut)
from_singleton = values.from_singleton
is_partial = values.is_partial
if gut != null:
gut.get_autofree().add_free(double)
func from_id(inst_id):
if inst_id == -1:
return null
@ -43,16 +58,13 @@ func default_val(method_name, p_index, default_val = NO_DEFAULT_VALUE):
return null
func _init(values = null):
if values != null:
double = values.double
thepath = values.thepath
subpath = values.subpath
stubber = from_id(values.stubber)
spy = from_id(values.spy)
gut = from_id(values.gut)
from_singleton = values.from_singleton
is_partial = values.is_partial
func vararg_warning():
if gut != null:
gut.get_autofree().add_free(double)
gut.get_logger().warn(
(
"This method contains a vararg argument and the paramter count was not stubbed. "
+ "GUT adds extra parameters to this method which should fill most needs. "
+ "It is recommended that you stub param_count for this object's class to ensure "
+ "that there are not any parameter count mismatch errors."
)
)

View file

@ -121,7 +121,10 @@ func get_strategy():
func set_strategy(strategy):
if GutUtils.DOUBLE_STRATEGY.values().has(strategy):
_strategy = strategy
else:
_lgr.error(str("doubler.gd: invalid double strategy ", strategy))
var _method_maker = _utils.MethodMaker.new()
@ -154,7 +157,7 @@ func _get_indented_line(indents, text):
func _stub_to_call_super(parsed, method_name):
if _utils.non_super_methods.has(method_name):
if !parsed.get_method(method_name).is_eligible_for_doubling():
return
var params = _utils.StubParams.new(parsed.script_path, method_name, parsed.subpath)
@ -162,7 +165,7 @@ func _stub_to_call_super(parsed, method_name):
_stubber.add_stub(params)
func _get_base_script_text(parsed, override_path, partial):
func _get_base_script_text(parsed, override_path, partial, included_methods):
var path = parsed.script_path
if override_path != null:
path = override_path
@ -188,44 +191,74 @@ func _get_base_script_text(parsed, override_path, partial):
"properties": "", #obj_info.get_properties_text(),
# metadata values
"path": path,
"subpath": _utils.nvl(parsed.subpath, ""),
"subpath": GutUtils.nvl(parsed.subpath, ""),
"stubber_id": stubber_id,
"spy_id": spy_id,
"gut_id": gut_id,
"singleton_name": "", #_utils.nvl(obj_info.get_singleton_name(), ''),
"singleton_name": "", #GutUtils.nvl(obj_info.get_singleton_name(), ''),
"is_partial": partial,
"doubled_methods": included_methods,
}
return _base_script_text.format(values)
func _is_method_eligible_for_doubling(parsed_script, parsed_method):
return (
!parsed_method.is_accessor()
and parsed_method.is_eligible_for_doubling()
and !_ignored_methods.has(parsed_script.resource, parsed_method.meta.name)
)
# Disable the native_method_override setting so that doubles do not generate
# errors or warnings when doubling with INCLUDE_NATIVE or when a method has
# been added because of param_count stub.
func _create_script_no_warnings(src):
var prev_native_override_value = null
var native_method_override = "debug/gdscript/warnings/native_method_override"
prev_native_override_value = ProjectSettings.get_setting(native_method_override)
ProjectSettings.set_setting(native_method_override, 0)
var DblClass = _utils.create_script_from_source(src)
ProjectSettings.set_setting(native_method_override, prev_native_override_value)
return DblClass
func _create_double(parsed, strategy, override_path, partial):
var base_script = _get_base_script_text(parsed, override_path, partial)
var super_name = ""
var path = ""
path = parsed.script_path
var dbl_src = ""
dbl_src += base_script
var included_methods = []
for method in parsed.get_local_methods():
if !method.is_black_listed() && !_ignored_methods.has(parsed.resource, method.meta.name):
if _is_method_eligible_for_doubling(parsed, method):
included_methods.append(method.meta.name)
var mthd = parsed.get_local_method(method.meta.name)
dbl_src += _get_func_text(method.meta, path, super_name)
if parsed.is_native:
dbl_src += _get_func_text(method.meta, parsed.resource)
else:
dbl_src += _get_func_text(method.meta, path)
if strategy == _utils.DOUBLE_STRATEGY.INCLUDE_SUPER:
if strategy == _utils.DOUBLE_STRATEGY.INCLUDE_NATIVE:
for method in parsed.get_super_methods():
if (
!method.is_black_listed()
&& !_ignored_methods.has(parsed.resource, method.meta.name)
):
if _is_method_eligible_for_doubling(parsed, method):
included_methods.append(method.meta.name)
_stub_to_call_super(parsed, method.meta.name)
dbl_src += _get_func_text(method.meta, path, super_name)
if parsed.is_native:
dbl_src += _get_func_text(method.meta, parsed.resource)
else:
dbl_src += _get_func_text(method.meta, path)
var base_script = _get_base_script_text(parsed, override_path, partial, included_methods)
dbl_src = base_script + "\n\n" + dbl_src
if print_source:
print(_utils.add_line_numbers(dbl_src))
var DblClass = _utils.create_script_from_source(dbl_src)
var DblClass = _create_script_no_warnings(dbl_src)
if _stubber != null:
_stub_method_default_values(DblClass, parsed, strategy)
@ -234,7 +267,10 @@ func _create_double(parsed, strategy, override_path, partial):
func _stub_method_default_values(which, parsed, strategy):
for method in parsed.get_local_methods():
if !method.is_black_listed() && !_ignored_methods.has(parsed.resource, method.meta.name):
if (
method.is_eligible_for_doubling()
&& !_ignored_methods.has(parsed.resource, method.meta.name)
):
_stubber.stub_defaults_from_meta(parsed.script_path, method.meta)
@ -242,7 +278,7 @@ func _double_scene_and_script(scene, strategy, partial):
var to_return = PackedSceneDouble.new()
to_return.load_scene(scene.get_path())
var script_obj = _utils.get_scene_script_object(scene)
var script_obj = GutUtils.get_scene_script_object(scene)
if script_obj != null:
var script_dbl = null
if partial:
@ -261,14 +297,12 @@ func _get_inst_id_ref_str(inst):
return ref_str
func _get_func_text(method_hash, path, super_ = ""):
func _get_func_text(method_hash, path):
var override_count = null
if _stubber != null:
override_count = _stubber.get_parameter_count(path, method_hash.name)
if override_count != null:
print(method_hash.name, " override: ", override_count)
var text = _method_maker.get_function_text(method_hash, path, override_count, super_) + "\n"
var text = _method_maker.get_function_text(method_hash, override_count) + "\n"
return text
@ -329,11 +363,11 @@ func partial_double_scene(scene, strategy = _strategy):
func double_gdnative(which):
return _double(which, _utils.DOUBLE_STRATEGY.INCLUDE_SUPER)
return _double(which, _utils.DOUBLE_STRATEGY.INCLUDE_NATIVE)
func partial_double_gdnative(which):
return _partial_double(which, _utils.DOUBLE_STRATEGY.INCLUDE_SUPER)
return _partial_double(which, _utils.DOUBLE_STRATEGY.INCLUDE_NATIVE)
func double_inner(parent, inner, strategy = _strategy):

View file

@ -1,232 +1,150 @@
[gd_scene load_steps=3 format=2]
[gd_scene load_steps=3 format=3 uid="uid://bsk32dh41b4gs"]
[ext_resource path="res://addons/gut/gui/ShortcutButton.tscn" type="PackedScene" id=1]
[ext_resource path="res://addons/gut/gui/BottomPanelShortcuts.gd" type="Script" id=2]
[ext_resource type="PackedScene" uid="uid://sfb1fw8j6ufu" path="res://addons/gut/gui/ShortcutButton.tscn" id="1"]
[ext_resource type="Script" path="res://addons/gut/gui/BottomPanelShortcuts.gd" id="2"]
[node name="BottomPanelShortcuts" type="Window"]
[node name="BottomPanelShortcuts" type="Popup"]
title = "Shortcuts"
size = Vector2i(500, 350)
visible = true
anchor_right = 0.234
anchor_bottom = 0.328
offset_right = 195.384
offset_bottom = 62.2
custom_minimum_size = Vector2( 435, 305 )
exclusive = true
window_title = "GUT Shortcuts"
resizable = true
script = ExtResource( 2 )
__meta__ = {
"_edit_use_anchors_": false
}
script = ExtResource("2")
[node name="Layout" type="VBoxContainer" parent="."]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 5.0
offset_right = -5.0
offset_bottom = 2.0
__meta__ = {
"_edit_use_anchors_": false
}
[node name="TopPad" type="CenterContainer" parent="Layout"]
offset_right = 425.0
offset_bottom = 5.0
custom_minimum_size = Vector2( 0, 5 )
custom_minimum_size = Vector2(0, 5)
layout_mode = 2
[node name="Label2" type="Label" parent="Layout"]
offset_top = 9.0
offset_right = 425.0
offset_bottom = 29.0
custom_minimum_size = Vector2( 0, 20 )
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
text = "Always Active"
align = 1
valign = 1
autowrap = true
[node name="ColorRect" type="ColorRect" parent="Layout/Label2"]
show_behind_parent = true
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
color = Color( 0, 0, 0, 0.196078 )
__meta__ = {
"_edit_use_anchors_": false
}
color = Color(0, 0, 0, 0.196078)
[node name="CPanelButton" type="HBoxContainer" parent="Layout"]
offset_top = 33.0
offset_right = 425.0
offset_bottom = 58.0
layout_mode = 2
[node name="Label" type="Label" parent="Layout/CPanelButton"]
offset_right = 138.0
offset_bottom = 25.0
custom_minimum_size = Vector2( 50, 0 )
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Show/Hide GUT Panel"
valign = 1
[node name="ShortcutButton" parent="Layout/CPanelButton" instance=ExtResource( 1 )]
anchor_right = 0.0
anchor_bottom = 0.0
offset_left = 142.0
offset_right = 425.0
offset_bottom = 25.0
[node name="ShortcutButton" parent="Layout/CPanelButton" instance=ExtResource("1")]
layout_mode = 2
size_flags_horizontal = 3
[node name="GutPanelPad" type="CenterContainer" parent="Layout"]
offset_top = 62.0
offset_right = 425.0
offset_bottom = 67.0
custom_minimum_size = Vector2( 0, 5 )
custom_minimum_size = Vector2(0, 5)
layout_mode = 2
[node name="Label" type="Label" parent="Layout"]
offset_top = 71.0
offset_right = 425.0
offset_bottom = 91.0
custom_minimum_size = Vector2( 0, 20 )
custom_minimum_size = Vector2(0, 20)
layout_mode = 2
text = "Only Active When GUT Panel Shown"
align = 1
valign = 1
autowrap = true
[node name="ColorRect2" type="ColorRect" parent="Layout/Label"]
show_behind_parent = true
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
color = Color( 0, 0, 0, 0.196078 )
__meta__ = {
"_edit_use_anchors_": false
}
color = Color(0, 0, 0, 0.196078)
[node name="TopPad2" type="CenterContainer" parent="Layout"]
offset_top = 95.0
offset_right = 425.0
offset_bottom = 100.0
custom_minimum_size = Vector2( 0, 5 )
custom_minimum_size = Vector2(0, 5)
layout_mode = 2
[node name="CRunAll" type="HBoxContainer" parent="Layout"]
offset_top = 104.0
offset_right = 425.0
offset_bottom = 129.0
layout_mode = 2
[node name="Label" type="Label" parent="Layout/CRunAll"]
offset_right = 50.0
offset_bottom = 25.0
custom_minimum_size = Vector2( 50, 0 )
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Run All"
valign = 1
[node name="ShortcutButton" parent="Layout/CRunAll" instance=ExtResource( 1 )]
anchor_right = 0.0
anchor_bottom = 0.0
offset_left = 54.0
offset_right = 425.0
offset_bottom = 25.0
[node name="ShortcutButton" parent="Layout/CRunAll" instance=ExtResource("1")]
layout_mode = 2
size_flags_horizontal = 3
[node name="CRunCurrentScript" type="HBoxContainer" parent="Layout"]
offset_top = 133.0
offset_right = 425.0
offset_bottom = 158.0
layout_mode = 2
[node name="Label" type="Label" parent="Layout/CRunCurrentScript"]
offset_right = 115.0
offset_bottom = 25.0
custom_minimum_size = Vector2( 50, 0 )
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Run Current Script"
valign = 1
[node name="ShortcutButton" parent="Layout/CRunCurrentScript" instance=ExtResource( 1 )]
anchor_right = 0.0
anchor_bottom = 0.0
offset_left = 119.0
offset_right = 425.0
offset_bottom = 25.0
[node name="ShortcutButton" parent="Layout/CRunCurrentScript" instance=ExtResource("1")]
layout_mode = 2
size_flags_horizontal = 3
[node name="CRunCurrentInner" type="HBoxContainer" parent="Layout"]
offset_top = 162.0
offset_right = 425.0
offset_bottom = 187.0
layout_mode = 2
[node name="Label" type="Label" parent="Layout/CRunCurrentInner"]
offset_right = 150.0
offset_bottom = 25.0
custom_minimum_size = Vector2( 50, 0 )
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Run Current Inner Class"
valign = 1
[node name="ShortcutButton" parent="Layout/CRunCurrentInner" instance=ExtResource( 1 )]
anchor_right = 0.0
anchor_bottom = 0.0
offset_left = 154.0
offset_right = 425.0
offset_bottom = 25.0
[node name="ShortcutButton" parent="Layout/CRunCurrentInner" instance=ExtResource("1")]
layout_mode = 2
size_flags_horizontal = 3
[node name="CRunCurrentTest" type="HBoxContainer" parent="Layout"]
offset_top = 191.0
offset_right = 425.0
offset_bottom = 216.0
layout_mode = 2
[node name="Label" type="Label" parent="Layout/CRunCurrentTest"]
offset_right = 106.0
offset_bottom = 25.0
custom_minimum_size = Vector2( 50, 0 )
custom_minimum_size = Vector2(50, 0)
layout_mode = 2
size_flags_vertical = 7
text = "Run Current Test"
valign = 1
[node name="ShortcutButton" parent="Layout/CRunCurrentTest" instance=ExtResource( 1 )]
anchor_right = 0.0
anchor_bottom = 0.0
offset_left = 110.0
offset_right = 425.0
offset_bottom = 25.0
[node name="ShortcutButton" parent="Layout/CRunCurrentTest" instance=ExtResource("1")]
layout_mode = 2
size_flags_horizontal = 3
[node name="CenterContainer2" type="CenterContainer" parent="Layout"]
offset_top = 220.0
offset_right = 425.0
offset_bottom = 241.0
custom_minimum_size = Vector2( 0, 5 )
custom_minimum_size = Vector2(0, 5)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="ShiftDisclaimer" type="Label" parent="Layout"]
offset_top = 245.0
offset_right = 425.0
offset_bottom = 259.0
layout_mode = 2
text = "\"Shift\" cannot be the only modifier for a shortcut."
align = 2
autowrap = true
[node name="HBoxContainer" type="HBoxContainer" parent="Layout"]
offset_top = 263.0
offset_right = 425.0
offset_bottom = 293.0
layout_mode = 2
[node name="CenterContainer" type="CenterContainer" parent="Layout/HBoxContainer"]
offset_right = 361.0
offset_bottom = 30.0
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Hide" type="Button" parent="Layout/HBoxContainer"]
offset_left = 365.0
offset_right = 425.0
offset_bottom = 30.0
custom_minimum_size = Vector2( 60, 30 )
custom_minimum_size = Vector2(60, 30)
layout_mode = 2
text = "Close"
[node name="BottomPad" type="CenterContainer" parent="Layout"]
offset_top = 297.0
offset_right = 425.0
offset_bottom = 307.0
custom_minimum_size = Vector2( 0, 10 )
custom_minimum_size = Vector2(0, 10)
layout_mode = 2
size_flags_horizontal = 3
[connection signal="pressed" from="Layout/HBoxContainer/Hide" to="." method="_on_Hide_pressed"]

View file

@ -1,28 +1,29 @@
[gd_scene load_steps=10 format=3 uid="uid://b3bostcslstem"]
[ext_resource type="Script" path="res://addons/gut/gui/GutBottomPanel.gd" id="1"]
[ext_resource type="PackedScene" path="res://addons/gut/gui/BottomPanelShortcuts.tscn" id="2"]
[ext_resource type="PackedScene" path="res://addons/gut/gui/RunAtCursor.tscn" id="3"]
[ext_resource type="Texture2D" path="res://addons/gut/gui/play.png" id="4"]
[ext_resource type="PackedScene" path="res://addons/gut/gui/RunResults.tscn" id="5"]
[ext_resource type="PackedScene" path="res://addons/gut/gui/OutputText.tscn" id="6"]
[ext_resource type="PackedScene" uid="uid://bsk32dh41b4gs" path="res://addons/gut/gui/BottomPanelShortcuts.tscn" id="2"]
[ext_resource type="PackedScene" uid="uid://0yunjxtaa8iw" path="res://addons/gut/gui/RunAtCursor.tscn" id="3"]
[ext_resource type="Texture2D" uid="uid://cr6tvdv0ve6cv" path="res://addons/gut/gui/play.png" id="4"]
[ext_resource type="PackedScene" uid="uid://4gyyn12um08h" path="res://addons/gut/gui/RunResults.tscn" id="5"]
[ext_resource type="PackedScene" uid="uid://bqmo4dj64c7yl" path="res://addons/gut/gui/OutputText.tscn" id="6"]
[sub_resource type="Shortcut" id="9"]
[sub_resource type="Image" id="Image_r56ab"]
[sub_resource type="Image" id="Image_abbh7"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "LumAlpha8",
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="2"]
image = SubResource("Image_r56ab")
[sub_resource type="ImageTexture" id="ImageTexture_x655i"]
image = SubResource("Image_abbh7")
[node name="GutBottomPanel" type="Control"]
custom_minimum_size = Vector2(250, 250)
layout_mode = 3
anchor_left = -0.0025866
anchor_top = -0.00176575
anchor_right = 0.997413
@ -34,297 +35,206 @@ offset_bottom = 1.05945
script = ExtResource("1")
[node name="layout" type="VBoxContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
[node name="ControlBar" type="HBoxContainer" parent="layout"]
offset_right = 1023.0
offset_bottom = 31.0
layout_mode = 2
[node name="RunAll" type="Button" parent="layout/ControlBar"]
offset_right = 85.0
offset_bottom = 31.0
layout_mode = 2
size_flags_vertical = 11
hint_tooltip = "Run all test scripts in the suite."
shortcut = SubResource("9")
text = "Run All"
icon = ExtResource("4")
[node name="Label" type="Label" parent="layout/ControlBar"]
offset_left = 89.0
offset_top = 2.0
offset_right = 162.0
offset_bottom = 28.0
hint_tooltip = "When a test script is edited, buttons are displayed to
run the opened script or an Inner-Test-Class or a
single test. The buttons change based on the location
of the cursor in the file.
These buttons will remain active when editing other
items so that you can run tests without having to switch
back to the test script.
You can assign keyboard shortcuts for these buttons
using the \"shortcuts\" button in the GUT panel."
layout_mode = 2
mouse_filter = 1
text = "Current: "
[node name="RunAtCursor" parent="layout/ControlBar" instance=ExtResource("3")]
anchor_right = 0.0
anchor_bottom = 0.0
offset_left = 166.0
offset_right = 532.0
offset_bottom = 31.0
layout_mode = 2
[node name="CenterContainer2" type="CenterContainer" parent="layout/ControlBar"]
offset_left = 536.0
offset_right = 903.0
offset_bottom = 31.0
layout_mode = 2
size_flags_horizontal = 3
[node name="Sep1" type="ColorRect" parent="layout/ControlBar"]
offset_left = 907.0
offset_right = 907.0
offset_bottom = 31.0
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="RunResultsBtn" type="Button" parent="layout/ControlBar"]
offset_left = 911.0
offset_right = 935.0
offset_bottom = 31.0
hint_tooltip = "Show/Hide Results Tree Panel."
layout_mode = 2
toggle_mode = true
icon = SubResource("2")
button_pressed = true
icon = SubResource("ImageTexture_x655i")
[node name="OutputBtn" type="Button" parent="layout/ControlBar"]
offset_left = 939.0
offset_right = 963.0
offset_bottom = 31.0
hint_tooltip = "Show/Hide Output Panel."
layout_mode = 2
toggle_mode = true
icon = SubResource("2")
icon = SubResource("ImageTexture_x655i")
[node name="Settings" type="Button" parent="layout/ControlBar"]
offset_left = 967.0
offset_right = 991.0
offset_bottom = 31.0
hint_tooltip = "Show/Hide Settings Panel."
layout_mode = 2
toggle_mode = true
icon = SubResource("2")
button_pressed = true
icon = SubResource("ImageTexture_x655i")
[node name="Sep2" type="ColorRect" parent="layout/ControlBar"]
offset_left = 995.0
offset_right = 995.0
offset_bottom = 31.0
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="Shortcuts" type="Button" parent="layout/ControlBar"]
offset_left = 999.0
offset_right = 1023.0
offset_bottom = 31.0
layout_mode = 2
size_flags_vertical = 11
hint_tooltip = "Set shortcuts for GUT buttons. Shortcuts do not work when the GUT panel is not visible."
icon = SubResource("2")
icon = SubResource("ImageTexture_x655i")
[node name="RSplit" type="HSplitContainer" parent="layout"]
offset_top = 35.0
offset_right = 1023.0
offset_bottom = 599.0
size_flags_horizontal = 3
size_flags_vertical = 3
collapsed = true
[node name="sc" type="ScrollContainer" parent="layout/RSplit"]
visible = false
offset_left = 593.0
offset_right = 1093.0
offset_bottom = 555.0
size_flags_vertical = 3
[node name="Settings" type="VBoxContainer" parent="layout/RSplit/sc"]
offset_right = 500.0
offset_bottom = 555.0
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="CResults" type="VBoxContainer" parent="layout/RSplit"]
offset_right = 1023.0
offset_bottom = 564.0
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="ControlBar" type="HBoxContainer" parent="layout/RSplit/CResults"]
offset_right = 1023.0
layout_mode = 2
[node name="Sep2" type="ColorRect" parent="layout/RSplit/CResults/ControlBar"]
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="Light3D" type="Control" parent="layout/RSplit/CResults/ControlBar"]
visible = false
offset_right = 30.0
offset_bottom = 35.0
custom_minimum_size = Vector2(30, 30)
layout_mode = 2
[node name="Passing" type="HBoxContainer" parent="layout/RSplit/CResults/ControlBar"]
visible = false
offset_left = 34.0
offset_right = 107.0
offset_bottom = 35.0
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/RSplit/CResults/ControlBar/Passing"]
offset_right = 2.0
offset_bottom = 35.0
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/RSplit/CResults/ControlBar/Passing"]
offset_left = 6.0
offset_top = 10.0
offset_right = 54.0
offset_bottom = 24.0
layout_mode = 2
text = "Passing"
[node name="value" type="Label" parent="layout/RSplit/CResults/ControlBar/Passing"]
offset_left = 58.0
offset_top = 10.0
offset_right = 73.0
offset_bottom = 24.0
layout_mode = 2
text = "---"
[node name="Failing" type="HBoxContainer" parent="layout/RSplit/CResults/ControlBar"]
visible = false
offset_left = 34.0
offset_right = 100.0
offset_bottom = 35.0
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/RSplit/CResults/ControlBar/Failing"]
offset_right = 2.0
offset_bottom = 35.0
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/RSplit/CResults/ControlBar/Failing"]
offset_left = 6.0
offset_top = 10.0
offset_right = 47.0
offset_bottom = 24.0
layout_mode = 2
text = "Failing"
[node name="value" type="Label" parent="layout/RSplit/CResults/ControlBar/Failing"]
offset_left = 51.0
offset_top = 10.0
offset_right = 66.0
offset_bottom = 24.0
layout_mode = 2
text = "---"
[node name="Pending" type="HBoxContainer" parent="layout/RSplit/CResults/ControlBar"]
visible = false
offset_left = 34.0
offset_right = 110.0
offset_bottom = 35.0
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/RSplit/CResults/ControlBar/Pending"]
offset_right = 2.0
offset_bottom = 35.0
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/RSplit/CResults/ControlBar/Pending"]
offset_left = 6.0
offset_top = 10.0
offset_right = 57.0
offset_bottom = 24.0
layout_mode = 2
text = "Pending"
[node name="value" type="Label" parent="layout/RSplit/CResults/ControlBar/Pending"]
offset_left = 61.0
offset_top = 10.0
offset_right = 76.0
offset_bottom = 24.0
layout_mode = 2
text = "---"
[node name="Orphans" type="HBoxContainer" parent="layout/RSplit/CResults/ControlBar"]
visible = false
offset_left = 34.0
offset_right = 110.0
offset_bottom = 35.0
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/RSplit/CResults/ControlBar/Orphans"]
offset_right = 2.0
offset_bottom = 35.0
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/RSplit/CResults/ControlBar/Orphans"]
offset_left = 6.0
offset_top = 10.0
offset_right = 57.0
offset_bottom = 24.0
layout_mode = 2
text = "Orphans"
[node name="value" type="Label" parent="layout/RSplit/CResults/ControlBar/Orphans"]
offset_left = 61.0
offset_top = 10.0
offset_right = 76.0
offset_bottom = 24.0
layout_mode = 2
text = "---"
[node name="Errors" type="HBoxContainer" parent="layout/RSplit/CResults/ControlBar"]
visible = false
offset_left = 34.0
offset_right = 96.0
offset_bottom = 35.0
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/RSplit/CResults/ControlBar/Errors"]
offset_right = 2.0
offset_bottom = 35.0
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/RSplit/CResults/ControlBar/Errors"]
offset_left = 6.0
offset_top = 10.0
offset_right = 43.0
offset_bottom = 24.0
hint_tooltip = "The number of GUT errors generated. This does not include engine errors."
layout_mode = 2
text = "Errors"
[node name="value" type="Label" parent="layout/RSplit/CResults/ControlBar/Errors"]
offset_left = 47.0
offset_top = 10.0
offset_right = 62.0
offset_bottom = 24.0
layout_mode = 2
text = "---"
[node name="Warnings" type="HBoxContainer" parent="layout/RSplit/CResults/ControlBar"]
visible = false
offset_left = 34.0
offset_right = 118.0
offset_bottom = 35.0
layout_mode = 2
[node name="Sep" type="ColorRect" parent="layout/RSplit/CResults/ControlBar/Warnings"]
offset_right = 2.0
offset_bottom = 35.0
custom_minimum_size = Vector2(1, 2.08165e-12)
layout_mode = 2
[node name="label" type="Label" parent="layout/RSplit/CResults/ControlBar/Warnings"]
offset_left = 6.0
offset_top = 10.0
offset_right = 65.0
offset_bottom = 24.0
layout_mode = 2
text = "Warnings"
[node name="value" type="Label" parent="layout/RSplit/CResults/ControlBar/Warnings"]
offset_left = 69.0
offset_top = 10.0
offset_right = 84.0
offset_bottom = 24.0
layout_mode = 2
text = "---"
[node name="CenterContainer" type="CenterContainer" parent="layout/RSplit/CResults/ControlBar"]
offset_right = 1023.0
layout_mode = 2
size_flags_horizontal = 3
[node name="TabBar" type="HSplitContainer" parent="layout/RSplit/CResults"]
offset_top = 4.0
offset_right = 1023.0
offset_bottom = 564.0
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="RunResults" parent="layout/RSplit/CResults/TabBar" instance=ExtResource("5")]
offset_right = 505.0
offset_bottom = 560.0
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="OutputText" parent="layout/RSplit/CResults/TabBar" instance=ExtResource("6")]
offset_left = 517.0
offset_right = 1023.0
offset_bottom = 560.0
visible = false
layout_mode = 2
[node name="sc" type="ScrollContainer" parent="layout/RSplit"]
custom_minimum_size = Vector2(500, 2.08165e-12)
layout_mode = 2
size_flags_vertical = 3
[node name="Settings" type="VBoxContainer" parent="layout/RSplit/sc"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="BottomPanelShortcuts" parent="." instance=ExtResource("2")]
visible = false

View file

@ -0,0 +1,322 @@
@tool
extends Control
const RUNNER_JSON_PATH = "res://.gut_editor_config.json"
var GutConfig = load("res://addons/gut/gut_config.gd")
var GutRunnerScene = load("res://addons/gut/gui/GutRunner.tscn")
var GutConfigGui = load("res://addons/gut/gui/gut_config_gui.gd")
var _config = GutConfig.new()
var _config_gui = null
var _gut_runner = GutRunnerScene.instantiate()
var _has_connected = false
var _tree_root: TreeItem = null
var _script_icon = load("res://addons/gut/images/Script.svg")
var _folder_icon = load("res://addons/gut/images/Folder.svg")
var _tree_scripts = {}
var _tree_directories = {}
const TREE_SCRIPT = "Script"
const TREE_DIR = "Directory"
@onready var _ctrls = {
run_tests_button = $VBox/Buttons/RunTests,
run_selected = $VBox/Buttons/RunSelected,
test_tree = $VBox/Tabs/Tests,
settings_vbox = $VBox/Tabs/SettingsScroll/Settings,
tabs = $VBox/Tabs,
bg = $Bg
}
@export var bg_color: Color = Color(.36, .36, .36):
get:
return bg_color
set(val):
bg_color = val
if is_inside_tree():
$Bg.color = bg_color
func _ready():
if Engine.is_editor_hint():
return
$Bg.color = bg_color
_ctrls.tabs.set_tab_title(0, "Tests")
_ctrls.tabs.set_tab_title(1, "Settings")
_config_gui = GutConfigGui.new(_ctrls.settings_vbox)
_ctrls.test_tree.hide_root = true
# Stop tests from kicking off when the runner is "ready" and
# prevents it from writing results file that is used by
# the panel.
_gut_runner.set_cmdln_mode(true)
add_child(_gut_runner)
# Becuase of the janky _utils psuedo-global script, we cannot
# do all this in _ready. If we do this in _ready, it generates
# a bunch of errors. The errors don't matter, but it looks bad.
call_deferred("_post_ready")
func _draw():
if Engine.is_editor_hint():
return
var gut = _gut_runner.get_gut()
if !gut.is_running():
var r = Rect2(Vector2(0, 0), get_rect().size)
draw_rect(r, Color.BLACK, false, 2)
func _post_ready():
var gut = _gut_runner.get_gut()
gut.start_run.connect(_on_gut_run_started)
gut.end_run.connect(_on_gut_run_ended)
_refresh_tree_and_settings()
func _set_meta_for_script_tree_item(item, script, test = null):
var meta = {
type = TREE_SCRIPT, script = script.path, inner_class = script.inner_class_name, test = ""
}
if test != null:
meta.test = test.name
item.set_metadata(0, meta)
func _set_meta_for_directory_tree_item(item, path, temp_item):
var meta = {type = TREE_DIR, path = path, temp_item = temp_item}
item.set_metadata(0, meta)
func _get_script_tree_item(script, parent_item):
if !_tree_scripts.has(script.path):
var item = _ctrls.test_tree.create_item(parent_item)
item.set_text(0, script.path.get_file())
item.set_icon(0, _script_icon)
_tree_scripts[script.path] = item
_set_meta_for_script_tree_item(item, script)
return _tree_scripts[script.path]
func _get_directory_tree_item(path):
var parent = _tree_root
if !_tree_directories.has(path):
var item: TreeItem = null
if parent != _tree_root:
item = parent.create_child(0)
else:
item = parent.create_child()
_tree_directories[path] = item
item.collapsed = false
item.set_text(0, path)
item.set_icon(0, _folder_icon)
item.set_icon_modulate(0, Color.ROYAL_BLUE)
# temp_item is used in calls with move_before since you must use
# move_before or move_after to reparent tree items. This ensures that
# there is an item on all directories. These are deleted later.
var temp_item = item.create_child()
temp_item.set_text(0, "<temp>")
_set_meta_for_directory_tree_item(item, path, temp_item)
return _tree_directories[path]
func _find_dir_item_to_move_before(path):
var max_matching_len = 0
var best_parent = null
# Go through all the directory items finding the one that has the longest
# path that contains our path.
for key in _tree_directories.keys():
if path != key and path.begins_with(key) and key.length() > max_matching_len:
max_matching_len = key.length()
best_parent = _tree_directories[key]
var to_return = null
if best_parent != null:
to_return = best_parent.get_metadata(0).temp_item
return to_return
func _reorder_dir_items():
var the_keys = _tree_directories.keys()
the_keys.sort()
for key in _tree_directories.keys():
var to_move = _tree_directories[key]
to_move.collapsed = false
var move_before = _find_dir_item_to_move_before(key)
if move_before != null:
to_move.move_before(move_before)
var new_text = key.substr(move_before.get_parent().get_metadata(0).path.length())
to_move.set_text(0, new_text)
func _remove_dir_temp_items():
for key in _tree_directories.keys():
var item = _tree_directories[key].get_metadata(0).temp_item
_tree_directories[key].remove_child(item)
func _add_dir_and_script_tree_items():
var tree: Tree = _ctrls.test_tree
tree.clear()
_tree_root = _ctrls.test_tree.create_item()
var scripts = _gut_runner.get_gut().get_test_collector().scripts
for script in scripts:
var dir_item = _get_directory_tree_item(script.path.get_base_dir())
var item = _get_script_tree_item(script, dir_item)
if script.inner_class_name != "":
var inner_item = tree.create_item(item)
inner_item.set_text(0, script.inner_class_name)
_set_meta_for_script_tree_item(inner_item, script)
item = inner_item
for test in script.tests:
var test_item = tree.create_item(item)
test_item.set_text(0, test.name)
_set_meta_for_script_tree_item(test_item, script, test)
func _populate_tree():
_add_dir_and_script_tree_items()
_tree_root.set_collapsed_recursive(true)
_tree_root.set_collapsed(false)
_reorder_dir_items()
_remove_dir_temp_items()
func _refresh_tree_and_settings():
if _config.options.has("panel_options"):
_config_gui.set_options(_config.options)
_config.apply_options(_gut_runner.get_gut())
_gut_runner.set_gut_config(_config)
_populate_tree()
# ---------------------------
# Events
# ---------------------------
func _on_gut_run_started():
_ctrls.run_tests_button.disabled = true
_ctrls.run_selected.visible = false
_ctrls.tabs.visible = false
_ctrls.bg.visible = false
_ctrls.run_tests_button.text = "Running"
queue_redraw()
func _on_gut_run_ended():
_ctrls.run_tests_button.disabled = false
_ctrls.run_selected.visible = true
_ctrls.tabs.visible = true
_ctrls.bg.visible = true
_ctrls.run_tests_button.text = "Run All"
queue_redraw()
func _on_run_tests_pressed():
run_all()
func _on_run_selected_pressed():
run_selected()
func _on_tests_item_activated():
run_selected()
# ---------------------------
# Public
# ---------------------------
func get_gut():
return _gut_runner.get_gut()
func get_config():
return _config
func run_all():
_config.options.selected = ""
_config.options.inner_class_name = ""
_config.options.unit_test_name = ""
run_tests()
func run_tests(options = null):
if options == null:
_config.options = _config_gui.get_options(_config.options)
else:
_config.options = options
_gut_runner.get_gut().get_test_collector().clear()
_gut_runner.set_gut_config(_config)
_gut_runner.run_tests()
func run_selected():
var sel_item = _ctrls.test_tree.get_selected()
if sel_item == null:
return
var options = _config_gui.get_options(_config.options)
var meta = sel_item.get_metadata(0)
if meta.type == TREE_SCRIPT:
options.selected = meta.script.get_file()
options.inner_class_name = meta.inner_class
options.unit_test_name = meta.test
elif meta.type == TREE_DIR:
options.dirs = [meta.path]
options.include_subdirectories = true
options.selected = ""
options.inner_class_name = ""
options.unit_test_name = ""
run_tests(options)
func load_config_file(path):
_config.load_panel_options(path)
_config.options.selected = ""
_config.options.inner_class_name = ""
_config.options.unit_test_name = ""
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2023 Tom "Butch" Wesley
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ##############################################################################

View file

@ -0,0 +1,63 @@
[gd_scene load_steps=2 format=3 uid="uid://4jb53yqktyfg"]
[ext_resource type="Script" path="res://addons/gut/gui/GutControl.gd" id="1_eprql"]
[node name="GutControl" type="Control"]
layout_mode = 3
anchors_preset = 0
offset_right = 295.0
offset_bottom = 419.0
script = ExtResource("1_eprql")
[node name="Bg" type="ColorRect" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.36, 0.36, 0.36, 1)
[node name="VBox" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Tabs" type="TabContainer" parent="VBox"]
layout_mode = 2
size_flags_vertical = 3
[node name="Tests" type="Tree" parent="VBox/Tabs"]
layout_mode = 2
size_flags_vertical = 3
hide_root = true
[node name="SettingsScroll" type="ScrollContainer" parent="VBox/Tabs"]
visible = false
layout_mode = 2
size_flags_vertical = 3
[node name="Settings" type="VBoxContainer" parent="VBox/Tabs/SettingsScroll"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Buttons" type="HBoxContainer" parent="VBox"]
layout_mode = 2
[node name="RunTests" type="Button" parent="VBox/Buttons"]
layout_mode = 2
size_flags_horizontal = 3
text = "Run All"
[node name="RunSelected" type="Button" parent="VBox/Buttons"]
layout_mode = 2
size_flags_horizontal = 3
text = "Run Selected"
[connection signal="item_activated" from="VBox/Tabs/Tests" to="." method="_on_tests_item_activated"]
[connection signal="pressed" from="VBox/Buttons/RunTests" to="." method="_on_run_tests_pressed"]
[connection signal="pressed" from="VBox/Buttons/RunSelected" to="." method="_on_run_selected_pressed"]

View file

@ -1,3 +1,18 @@
# ##############################################################################
# This class joins together GUT, GUT Gui, GutConfig and is the main way to
# run a test suite.
#
# This creates its own instance of gut.gd that it manages.
# Use set_gut_config to set the gut_config.gd that should be used to configure
# gut.
# This will create a GUI and wire it up and apply gut_config.gd options.
#
# Running tests:
# By default, this will run tests once this control has been added to the tree.
# You can override this by setting auto_run_tests to false prior to adding
# this to the tree. To run tests manually, call run_tests.
#
# ##############################################################################
extends Node2D
var Gut = load("res://addons/gut/gut.gd")
@ -19,6 +34,7 @@ var _cmdln_mode = false
@onready var _gut_layer = $GutLayer
@onready var _gui = $GutLayer/GutScene
# When true, tests will be kicked off in _ready.
var auto_run_tests = true
@ -34,29 +50,9 @@ func _ready():
call_deferred("run_tests")
func run_tests(show_gui = true):
func _lazy_make_gut():
if _gut == null:
get_gut()
_setup_gui(show_gui)
_gut.add_children_to = self
if _gut_config.options.gut_on_top:
_gut_layer.add_child(_gut)
else:
add_child(_gut)
if !_cmdln_mode:
_gut.end_run.connect(
_on_tests_finished.bind(
_gut_config.options.should_exit, _gut_config.options.should_exit_on_success
)
)
_gut_config.config_gut(_gut)
var run_rest_of_scripts = _gut_config.options.unit_test_name == ""
_gut.test_scripts(run_rest_of_scripts)
_gut = Gut.new()
func _setup_gui(show_gui):
@ -76,20 +72,18 @@ func _setup_gui(show_gui):
if opts.background_color != null and opts.background_color.is_valid_html_color():
_gui.set_background_color(Color(opts.background_color))
#_tester.set_modulate(Color(1.0, 1.0, 1.0, min(1.0, float(opts.opacity) / 100)))
_gui.set_opacity(min(1.0, float(opts.opacity) / 100))
# if(opts.should_maximize):
# _tester.maximize()
#if(opts.compact_mode):
# _tester.get_gui().compact_mode(true)
_gui.use_compact_mode(opts.compact_mode)
func _write_results():
var content = _gui.get_textbox().text #_gut.logger.get_gui_bbcode()
var content = _gui.get_textbox().get_parsed_text() #_gut.logger.get_gui_bbcode()
var f = FileAccess.open(RESULT_FILE, FileAccess.WRITE)
if f != null:
f.store_string(content)
f.close()
f = null # closes file
else:
push_error("Could not save bbcode, result = ", FileAccess.get_open_error())
@ -112,9 +106,33 @@ func _on_tests_finished(should_exit, should_exit_on_success):
get_tree().quit()
func run_tests(show_gui = true):
_lazy_make_gut()
_setup_gui(show_gui)
_gut.add_children_to = self
if _gut.get_parent() == null:
if _gut_config.options.gut_on_top:
_gut_layer.add_child(_gut)
else:
add_child(_gut)
if !_cmdln_mode:
_gut.end_run.connect(
_on_tests_finished.bind(
_gut_config.options.should_exit, _gut_config.options.should_exit_on_success
)
)
_gut_config.apply_options(_gut)
var run_rest_of_scripts = _gut_config.options.unit_test_name == ""
_gut.test_scripts(run_rest_of_scripts)
func get_gut():
if _gut == null:
_gut = Gut.new()
_lazy_make_gut()
return _gut
@ -124,3 +142,29 @@ func set_gut_config(which):
func set_cmdln_mode(is_it):
_cmdln_mode = is_it
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2023 Tom "Butch" Wesley
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ##############################################################################

File diff suppressed because one or more lines are too long

161
addons/gut/gui/MinGui.tscn Normal file
View file

@ -0,0 +1,161 @@
[gd_scene load_steps=5 format=3 uid="uid://cnqqdfsn80ise"]
[ext_resource type="Theme" uid="uid://cstkhwkpajvqu" path="res://addons/gut/gui/GutSceneTheme.tres" id="1_farmq"]
[ext_resource type="FontFile" uid="uid://bnh0lslf4yh87" path="res://addons/gut/fonts/CourierPrime-Regular.ttf" id="2_a2e2l"]
[ext_resource type="Script" path="res://addons/gut/gui/gut_gui.gd" id="2_eokrf"]
[ext_resource type="PackedScene" uid="uid://bvrqqgjpyouse" path="res://addons/gut/gui/ResizeHandle.tscn" id="4_xrhva"]
[node name="Min" type="Panel"]
clip_contents = true
custom_minimum_size = Vector2(280, 145)
offset_right = 280.0
offset_bottom = 145.0
theme = ExtResource("1_farmq")
script = ExtResource("2_eokrf")
[node name="MainBox" type="VBoxContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_layout_mode = 1
[node name="TitleBar" type="Panel" parent="MainBox"]
custom_minimum_size = Vector2(0, 25)
layout_mode = 2
[node name="TitleBox" type="HBoxContainer" parent="MainBox/TitleBar"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = 2.0
offset_bottom = 3.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_layout_mode = 1
[node name="Spacer1" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Title" type="Label" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
text = "Title"
[node name="Spacer2" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="TimeLabel" type="Label" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
text = "0.000s"
[node name="Body" type="HBoxContainer" parent="MainBox"]
layout_mode = 2
size_flags_vertical = 3
[node name="LeftMargin" type="CenterContainer" parent="MainBox/Body"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="BodyRows" type="VBoxContainer" parent="MainBox/Body"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ProgressBars" type="HBoxContainer" parent="MainBox/Body/BodyRows"]
layout_mode = 2
size_flags_horizontal = 3
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/Body/BodyRows/ProgressBars"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer"]
layout_mode = 2
text = "T:"
[node name="ProgressTest" type="ProgressBar" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
size_flags_horizontal = 3
value = 25.0
[node name="HBoxContainer2" type="HBoxContainer" parent="MainBox/Body/BodyRows/ProgressBars"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer2"]
layout_mode = 2
text = "S:"
[node name="ProgressScript" type="ProgressBar" parent="MainBox/Body/BodyRows/ProgressBars/HBoxContainer2"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
size_flags_horizontal = 3
value = 75.0
[node name="PathDisplay" type="VBoxContainer" parent="MainBox/Body/BodyRows"]
clip_contents = true
layout_mode = 2
size_flags_vertical = 3
[node name="Path" type="Label" parent="MainBox/Body/BodyRows/PathDisplay"]
layout_mode = 2
theme_override_fonts/font = ExtResource("2_a2e2l")
theme_override_font_sizes/font_size = 14
text = "res://test/integration/whatever"
clip_text = true
text_overrun_behavior = 3
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/Body/BodyRows/PathDisplay"]
clip_contents = true
layout_mode = 2
[node name="S3" type="CenterContainer" parent="MainBox/Body/BodyRows/PathDisplay/HBoxContainer"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="File" type="Label" parent="MainBox/Body/BodyRows/PathDisplay/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_fonts/font = ExtResource("2_a2e2l")
theme_override_font_sizes/font_size = 14
text = "test_this_thing.gd"
text_overrun_behavior = 3
[node name="Footer" type="HBoxContainer" parent="MainBox/Body/BodyRows"]
layout_mode = 2
[node name="HandleLeft" parent="MainBox/Body/BodyRows/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_xrhva")]
layout_mode = 2
orientation = 0
resize_control = NodePath("../../../../..")
vertical_resize = false
[node name="SwitchModes" type="Button" parent="MainBox/Body/BodyRows/Footer"]
layout_mode = 2
text = "Expand"
[node name="CenterContainer" type="CenterContainer" parent="MainBox/Body/BodyRows/Footer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Continue" type="Button" parent="MainBox/Body/BodyRows/Footer"]
layout_mode = 2
text = "Continue
"
[node name="HandleRight" parent="MainBox/Body/BodyRows/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_xrhva")]
layout_mode = 2
resize_control = NodePath("../../../../..")
vertical_resize = false
[node name="RightMargin" type="CenterContainer" parent="MainBox/Body"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="CenterContainer" type="CenterContainer" parent="MainBox"]
custom_minimum_size = Vector2(2.08165e-12, 2)
layout_mode = 2

View file

@ -0,0 +1,213 @@
[gd_scene load_steps=5 format=3 uid="uid://duxblir3vu8x7"]
[ext_resource type="Theme" uid="uid://cstkhwkpajvqu" path="res://addons/gut/gui/GutSceneTheme.tres" id="1_5hlsm"]
[ext_resource type="Script" path="res://addons/gut/gui/gut_gui.gd" id="2_fue6q"]
[ext_resource type="FontFile" uid="uid://bnh0lslf4yh87" path="res://addons/gut/fonts/CourierPrime-Regular.ttf" id="2_u5uc1"]
[ext_resource type="PackedScene" uid="uid://bvrqqgjpyouse" path="res://addons/gut/gui/ResizeHandle.tscn" id="4_2r8a8"]
[node name="Large" type="Panel"]
custom_minimum_size = Vector2(500, 150)
offset_right = 632.0
offset_bottom = 260.0
theme = ExtResource("1_5hlsm")
script = ExtResource("2_fue6q")
[node name="MainBox" type="VBoxContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_layout_mode = 1
[node name="TitleBar" type="Panel" parent="MainBox"]
custom_minimum_size = Vector2(0, 25)
layout_mode = 2
[node name="TitleBox" type="HBoxContainer" parent="MainBox/TitleBar"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
offset_top = 2.0
offset_bottom = 3.0
grow_horizontal = 2
grow_vertical = 2
metadata/_edit_layout_mode = 1
[node name="Spacer1" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Title" type="Label" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
text = "Title"
[node name="Spacer2" type="CenterContainer" parent="MainBox/TitleBar/TitleBox"]
layout_mode = 2
size_flags_horizontal = 3
[node name="TimeLabel" type="Label" parent="MainBox/TitleBar/TitleBox"]
custom_minimum_size = Vector2(90, 0)
layout_mode = 2
text = "999.999s"
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox"]
layout_mode = 2
size_flags_vertical = 3
[node name="VBoxContainer" type="VBoxContainer" parent="MainBox/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="OutputBG" type="ColorRect" parent="MainBox/HBoxContainer/VBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
color = Color(0.0745098, 0.0705882, 0.0784314, 1)
metadata/_edit_layout_mode = 1
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG"]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="S2" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="TestOutput" type="RichTextLabel" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
focus_mode = 2
bbcode_enabled = true
scroll_following = true
autowrap_mode = 0
selection_enabled = true
[node name="S1" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/OutputBG/HBoxContainer"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="ControlBox" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer"]
layout_mode = 2
[node name="S1" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="ProgressBars" type="VBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
custom_minimum_size = Vector2(2.08165e-12, 2.08165e-12)
layout_mode = 2
[node name="TestBox" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars"]
layout_mode = 2
[node name="Label" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/TestBox"]
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
size_flags_horizontal = 3
text = "Tests"
[node name="ProgressTest" type="ProgressBar" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/TestBox"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
value = 25.0
[node name="ScriptBox" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars"]
layout_mode = 2
[node name="Label" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/ScriptBox"]
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
size_flags_horizontal = 3
text = "Scripts"
[node name="ProgressScript" type="ProgressBar" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/ProgressBars/ScriptBox"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
value = 75.0
[node name="PathDisplay" type="VBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Path" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay"]
layout_mode = 2
size_flags_vertical = 6
theme_override_fonts/font = ExtResource("2_u5uc1")
theme_override_font_sizes/font_size = 14
text = "res://test/integration/whatever"
text_overrun_behavior = 3
[node name="HBoxContainer" type="HBoxContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay"]
layout_mode = 2
size_flags_vertical = 3
[node name="S3" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay/HBoxContainer"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="File" type="Label" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox/PathDisplay/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_fonts/font = ExtResource("2_u5uc1")
theme_override_font_sizes/font_size = 14
text = "test_this_thing.gd"
text_overrun_behavior = 3
[node name="Spacer1" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
visible = false
layout_mode = 2
size_flags_horizontal = 10
[node name="Continue" type="Button" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
layout_mode = 2
size_flags_vertical = 4
text = "Continue
"
[node name="S3" type="CenterContainer" parent="MainBox/HBoxContainer/VBoxContainer/ControlBox"]
custom_minimum_size = Vector2(5, 0)
layout_mode = 2
[node name="BottomPad" type="CenterContainer" parent="MainBox"]
custom_minimum_size = Vector2(0, 5)
layout_mode = 2
[node name="Footer" type="HBoxContainer" parent="MainBox"]
layout_mode = 2
[node name="SidePad1" type="CenterContainer" parent="MainBox/Footer"]
custom_minimum_size = Vector2(2, 2.08165e-12)
layout_mode = 2
[node name="ResizeHandle3" parent="MainBox/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_2r8a8")]
custom_minimum_size = Vector2(25, 25)
layout_mode = 2
orientation = 0
resize_control = NodePath("../../..")
[node name="SwitchModes" type="Button" parent="MainBox/Footer"]
layout_mode = 2
text = "Compact
"
[node name="CenterContainer" type="CenterContainer" parent="MainBox/Footer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="ResizeHandle2" parent="MainBox/Footer" node_paths=PackedStringArray("resize_control") instance=ExtResource("4_2r8a8")]
custom_minimum_size = Vector2(25, 25)
layout_mode = 2
resize_control = NodePath("../../..")
[node name="SidePad2" type="CenterContainer" parent="MainBox/Footer"]
custom_minimum_size = Vector2(2, 2.08165e-12)
layout_mode = 2
[node name="BottomPad2" type="CenterContainer" parent="MainBox"]
custom_minimum_size = Vector2(2.08165e-12, 2)
layout_mode = 2

View file

@ -1,91 +1,64 @@
extends VBoxContainer
@tool
extends VBoxContainer
class SearchResults:
var L = 0
var C = 0
var positions = []
var te = null
# ##############################################################################
# Keeps search results from the TextEdit
# ##############################################################################
class TextEditSearcher:
var te: TextEdit
var _last_term = ""
var _last_pos = Vector2(-1, -1)
var _ignore_caret_change = false
func _search_te(text, start_position, flags = 0):
var start_pos = start_position
if start_pos[L] < 0 or start_pos[L] > te.get_line_count():
start_pos[L] = 0
if start_pos[C] < 0:
start_pos[L] = 0
func set_text_edit(which):
te = which
te.caret_changed.connect(_on_caret_changed)
var result = te.search(text, flags, start_pos[L], start_pos[C])
if (
result.size() == 2
and result[L] == start_position[L]
and result[C] == start_position[C]
and text == _last_term
):
if flags == TextEdit.SEARCH_BACKWARDS:
result[C] -= 1
func _on_caret_changed():
if _ignore_caret_change:
_ignore_caret_change = false
else:
result[C] += 1
result = _search_te(text, result, flags)
L = result.y
C = result.x
elif result.size() == 2:
te.scroll_vertical = result[L]
te.select(result[L], result[C], result[L], result[C] + text.length())
te.set_caret_column(result[C])
te.set_caret_line(result[L])
te.center_viewport_to_caret()
L = result.y
C = result.x
_last_pos = _get_caret()
_last_term = text
te.center_viewport_to_caret()
return result
func _get_caret():
return Vector2(te.get_caret_column(), te.get_caret_line())
func _cursor_to_pos():
var to_return = [0, 0]
to_return[L] = te.get_caret_line()
to_return[C] = te.get_caret_column()
return to_return
func _set_caret_and_sel(pos, len):
te.set_caret_line(pos.y)
te.set_caret_column(pos.x)
if len > 0:
te.select(pos.y, pos.x, pos.y, pos.x + len)
func _find(term, search_flags):
var pos = _get_caret()
if term == _last_term:
if search_flags == 0:
pos = _last_pos
pos.x += 1
else:
pos = _last_pos
pos.x -= 1
var result = te.search(term, search_flags, pos.y, pos.x)
# print('searching from ', pos, ' for "', term, '" = ', result)
if result.y != -1:
_ignore_caret_change = true
_set_caret_and_sel(result, term.length())
_last_pos = result
_last_term = term
func find_next(term):
return _search_te(term, _cursor_to_pos())
_find(term, 0)
func find_prev(term):
var new_pos = _search_te(term, _cursor_to_pos(), TextEdit.SEARCH_BACKWARDS)
return new_pos
func get_next_pos():
pass
func get_prev_pos():
pass
func clear():
pass
func find_all(text):
var c_pos = [0, 0]
var found = true
var last_pos = [0, 0]
positions.clear()
while found:
c_pos = te.search(text, 0, c_pos[L], c_pos[C])
if (
c_pos.size() > 0
and (c_pos[L] > last_pos[L] or (c_pos[L] == last_pos[L] and c_pos[C] > last_pos[C]))
):
positions.append([c_pos[L], c_pos[C]])
c_pos[C] += 1
last_pos = c_pos
else:
found = false
_find(term, te.SEARCH_BACKWARDS)
# ##############################################################################
# Start OutputText control code
# ##############################################################################
@onready var _ctrls = {
output = $Output,
copy_button = $Toolbar/CopyButton,
@ -93,42 +66,83 @@ class SearchResults:
clear_button = $Toolbar/ClearButton,
word_wrap = $Toolbar/WordWrap,
show_search = $Toolbar/ShowSearch,
caret_position = $Toolbar/LblPosition,
search_bar =
{
bar = $Search,
search_term = $Search/SearchTerm,
}
}
var _sr = SearchResults.new()
var _sr = TextEditSearcher.new()
var _highlighter: CodeHighlighter
var _font_name = null
# Automatically used when running the OutputText scene from the editor. Changes
# to this method only affect test-running the control through the editor.
func _test_running_setup():
_ctrls.use_colors.text = "use colors"
_ctrls.show_search.text = "search"
_ctrls.word_wrap.text = "ww"
set_all_fonts("CourierPrime")
set_font_size(20)
set_font_size(30)
_ctrls.output.queue_redraw()
load_file("user://.gut_editor.bbcode")
await get_tree().process_frame
show_search(true)
_ctrls.output.set_caret_line(0)
_ctrls.output.scroll_vertical = 0
_ctrls.output.caret_changed.connect(_on_caret_changed)
func _ready():
_sr.te = _ctrls.output
_sr.set_text_edit(_ctrls.output)
_ctrls.use_colors.icon = get_theme_icon("RichTextEffect", "EditorIcons")
_ctrls.show_search.icon = get_theme_icon("Search", "EditorIcons")
_ctrls.word_wrap.icon = get_theme_icon("Loop", "EditorIcons")
_setup_colors()
_ctrls.use_colors.button_pressed = true
_use_highlighting(true)
if get_parent() == get_tree().root:
_test_running_setup()
func _on_caret_changed():
var txt = str(
"line:", _ctrls.output.get_caret_line(), " col:", _ctrls.output.get_caret_column()
)
_ctrls.caret_position.text = str(txt)
# ------------------
# Private
# ------------------
func _setup_colors():
_ctrls.output.clear()
# Call this after changes in colors and the like to get them to apply. reloads
# the text of the output control.
func _refresh_output():
var orig_pos = _ctrls.output.scroll_vertical
var text = _ctrls.output.text
_ctrls.output.text = text
_ctrls.output.scroll_vertical = orig_pos
func _create_highlighter(default_color = Color(1, 1, 1, 1)):
var to_return = CodeHighlighter.new()
to_return.function_color = default_color
to_return.number_color = default_color
to_return.symbol_color = default_color
to_return.member_variable_color = default_color
var keywords = [
["Failed", Color.RED],
["Passed", Color.GREEN],
@ -139,34 +153,30 @@ func _setup_colors():
]
for keyword in keywords:
if _ctrls.output.syntax_highlighter == null:
_ctrls.output.syntax_highlighter = CodeHighlighter.new()
_ctrls.output.syntax_highlighter.add_keyword_color(keyword[0], keyword[1])
to_return.add_keyword_color(keyword[0], keyword[1])
return to_return
func _setup_colors():
_ctrls.output.clear()
var f_color = null
if _ctrls.output.theme == null:
f_color = get_theme_color("font_color")
else:
f_color = _ctrls.output.theme.font_color
_ctrls.output.add_theme_color_override("font_color_readonly", f_color)
_ctrls.output.add_theme_color_override("function_color", f_color)
_ctrls.output.add_theme_color_override("member_variable_color", f_color)
_highlighter = _create_highlighter()
_ctrls.output.queue_redraw()
func _set_font(font_name, custom_name):
var rtl = _ctrls.output
if font_name == null:
rtl.set("custom_fonts/" + custom_name, null)
func _use_highlighting(should):
if should:
_ctrls.output.syntax_highlighter = _highlighter
else:
pass
# cuasing issues in 4.0
# var dyn_font = FontFile.new()
# var font_data = FontFile.new()
# font_data.font_path = 'res://addons/gut/fonts/' + font_name + '.ttf'
# font_data.antialiased = true
# dyn_font.font_data = font_data
# rtl.set('custom_fonts/' + custom_name, dyn_font)
_ctrls.output.syntax_highlighter = null
_refresh_output()
# ------------------
@ -177,7 +187,7 @@ func _on_CopyButton_pressed():
func _on_UseColors_pressed():
_ctrls.output.syntax_highlighter = _ctrls.use_colors.button_pressed
_use_highlighting(_ctrls.use_colors.button_pressed)
func _on_ClearButton_pressed():
@ -215,12 +225,16 @@ func _on_SearchTerm_text_entered(new_text):
func _on_SearchTerm_gui_input(event):
if event is InputEventKey and !event.pressed and event.scancode == KEY_ESCAPE:
if event is InputEventKey and !event.pressed and event.keycode == KEY_ESCAPE:
show_search(false)
func _on_WordWrap_pressed():
_ctrls.output.wrap_enabled = _ctrls.word_wrap.pressed
if _ctrls.word_wrap.button_pressed:
_ctrls.output.wrap_mode = TextEdit.LINE_WRAPPING_BOUNDARY
else:
_ctrls.output.wrap_mode = TextEdit.LINE_WRAPPING_NONE
_ctrls.output.queue_redraw()
@ -240,40 +254,57 @@ func search(text, start_pos, highlight = true):
func copy_to_clipboard():
var selected = _ctrls.output.get_selection_text()
var selected = _ctrls.output.get_selected_text()
if selected != "":
OS.clipboard = selected
DisplayServer.clipboard_set(selected)
else:
OS.clipboard = _ctrls.output.text
DisplayServer.clipboard_set(_ctrls.output.text)
func clear():
_ctrls.output.text = ""
func _set_font(font_name, custom_name):
var rtl = _ctrls.output
if font_name == null:
rtl.add_theme_font_override(custom_name, null)
else:
var dyn_font = FontFile.new()
dyn_font.load_dynamic_font("res://addons/gut/fonts/" + font_name + ".ttf")
rtl.add_theme_font_override(custom_name, dyn_font)
func set_all_fonts(base_name):
_font_name = GutUtils.nvl(base_name, "Default")
if base_name == "Default":
_set_font(null, "font")
# _set_font(null, 'normal_font')
# _set_font(null, 'bold_font')
# _set_font(null, 'italics_font')
# _set_font(null, 'bold_italics_font')
_set_font(null, "normal_font")
_set_font(null, "bold_font")
_set_font(null, "italics_font")
_set_font(null, "bold_italics_font")
else:
_set_font(base_name + "-Regular", "font")
# _set_font(base_name + '-Regular', 'normal_font')
# _set_font(base_name + '-Bold', 'bold_font')
# _set_font(base_name + '-Italic', 'italics_font')
# _set_font(base_name + '-BoldItalic', 'bold_italics_font')
_set_font(base_name + "-Regular", "normal_font")
_set_font(base_name + "-Bold", "bold_font")
_set_font(base_name + "-Italic", "italics_font")
_set_font(base_name + "-BoldItalic", "bold_italics_font")
func set_font_size(new_size):
var rtl = _ctrls.output
if rtl.get("custom_fonts/font") != null:
rtl.get("custom_fonts/font").size = new_size
rtl.set("theme_override_font_sizes/font_size", new_size)
# rtl.add_theme_font_size_override("font", new_size)
# rtl.add_theme_font_size_override("normal_font", new_size)
# rtl.add_theme_font_size_override("bold_font", new_size)
# rtl.add_theme_font_size_override("italics_font", new_size)
# rtl.add_theme_font_size_override("bold_italics_font", new_size)
# if(rtl.get('custom_fonts/font') != null):
# rtl.get('custom_fonts/font').size = new_size
# rtl.get('custom_fonts/bold_italics_font').size = new_size
# rtl.get('custom_fonts/bold_font').size = new_size
# rtl.get('custom_fonts/italics_font').size = new_size
@ -298,7 +329,7 @@ func load_file(path):
return
var t = f.get_as_text()
f.close()
f = null # closes file
_ctrls.output.text = t
_ctrls.output.scroll_vertical = _ctrls.output.get_line_count()
_ctrls.output.set_deferred("scroll_vertical", _ctrls.output.get_line_count())

View file

@ -1,113 +1,107 @@
[gd_scene load_steps=4 format=2]
[gd_scene load_steps=5 format=3 uid="uid://bqmo4dj64c7yl"]
[ext_resource path="res://addons/gut/gui/OutputText.gd" type="Script" id=1]
[ext_resource type="Script" path="res://addons/gut/gui/OutputText.gd" id="1"]
[sub_resource type="Image" id=3]
[sub_resource type="Image" id="Image_abbh7"]
data = {
"data": PackedByteArray( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ),
"format": "LumAlpha8",
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id=2]
flags = 4
flags = 4
image = SubResource( 3 )
size = Vector2( 16, 16 )
[sub_resource type="ImageTexture" id="ImageTexture_x655i"]
image = SubResource("Image_abbh7")
[sub_resource type="CodeHighlighter" id="CodeHighlighter_4pcgd"]
number_color = Color(1, 1, 1, 1)
symbol_color = Color(1, 1, 1, 1)
function_color = Color(1, 1, 1, 1)
member_variable_color = Color(1, 1, 1, 1)
keyword_colors = {
"ERROR": Color(1, 0, 0, 1),
"Failed": Color(1, 0, 0, 1),
"Orphans": Color(1, 1, 0, 1),
"Passed": Color(0, 1, 0, 1),
"Pending": Color(1, 1, 0, 1),
"WARNING": Color(1, 1, 0, 1)
}
[node name="OutputText" type="VBoxContainer"]
offset_right = 862.0
offset_bottom = 523.0
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource( 1 )
script = ExtResource("1")
[node name="Toolbar" type="HBoxContainer" parent="."]
offset_right = 862.0
offset_bottom = 24.0
layout_mode = 2
size_flags_horizontal = 3
[node name="ShowSearch" type="Button" parent="Toolbar"]
offset_right = 28.0
offset_bottom = 24.0
layout_mode = 2
tooltip_text = "Search"
toggle_mode = true
icon = SubResource( 2 )
icon = SubResource("ImageTexture_x655i")
[node name="UseColors" type="Button" parent="Toolbar"]
offset_left = 32.0
offset_right = 60.0
offset_bottom = 24.0
hint_tooltip = "Colorize output.
It's not the same as everywhere else (long story),
but it is better than nothing."
layout_mode = 2
tooltip_text = "Colorized Text"
toggle_mode = true
pressed = true
icon = SubResource( 2 )
button_pressed = true
icon = SubResource("ImageTexture_x655i")
[node name="WordWrap" type="Button" parent="Toolbar"]
offset_left = 64.0
offset_right = 92.0
offset_bottom = 24.0
hint_tooltip = "Word wrap"
layout_mode = 2
tooltip_text = "Word Wrap"
toggle_mode = true
icon = SubResource( 2 )
icon = SubResource("ImageTexture_x655i")
[node name="CenterContainer" type="CenterContainer" parent="Toolbar"]
offset_left = 96.0
offset_right = 743.0
offset_bottom = 24.0
layout_mode = 2
size_flags_horizontal = 3
[node name="LblPosition" type="Label" parent="Toolbar"]
layout_mode = 2
[node name="CopyButton" type="Button" parent="Toolbar"]
offset_left = 747.0
offset_right = 798.0
offset_bottom = 24.0
hint_tooltip = "Copy to clipboard"
layout_mode = 2
text = " Copy "
[node name="ClearButton" type="Button" parent="Toolbar"]
offset_left = 802.0
offset_right = 862.0
offset_bottom = 24.0
layout_mode = 2
text = " Clear "
[node name="Output" type="TextEdit" parent="."]
offset_top = 28.0
offset_right = 862.0
offset_bottom = 523.0
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
readonly = true
text = "Hello World
This is a bunch of text
That exists for you to see."
deselect_on_focus_loss_enabled = false
virtual_keyboard_enabled = false
middle_mouse_paste_enabled = false
highlight_all_occurrences = true
highlight_current_line = true
syntax_highlighter = true
show_line_numbers = true
smooth_scrolling = true
syntax_highlighter = SubResource("CodeHighlighter_4pcgd")
scroll_smooth = true
[node name="Search" type="HBoxContainer" parent="."]
visible = false
offset_top = 499.0
offset_right = 862.0
offset_bottom = 523.0
layout_mode = 2
[node name="SearchTerm" type="LineEdit" parent="Search"]
offset_right = 804.0
offset_bottom = 24.0
layout_mode = 2
size_flags_horizontal = 3
[node name="SearchNext" type="Button" parent="Search"]
offset_left = 808.0
offset_right = 862.0
offset_bottom = 24.0
hint_tooltip = "Find next (enter)"
layout_mode = 2
text = "Next"
[node name="SearchPrev" type="Button" parent="Search"]
offset_left = 808.0
offset_right = 820.0
offset_bottom = 20.0
hint_tooltip = "Find previous (shift + enter)"
layout_mode = 2
text = "Prev"
[connection signal="pressed" from="Toolbar/ShowSearch" to="." method="_on_ShowSearch_pressed"]

View file

@ -0,0 +1,108 @@
@tool
extends ColorRect
# #############################################################################
# Resize Handle control. Place onto a control. Set the orientation, then
# set the control that this should resize. Then you can resize the control
# by dragging this thing around. It's pretty neat.
# #############################################################################
enum ORIENTATION { LEFT, RIGHT }
@export var orientation := ORIENTATION.RIGHT:
get:
return orientation
set(val):
orientation = val
queue_redraw()
@export var resize_control: Control = null
@export var vertical_resize := true
var _line_width = .5
var _line_color = Color(.4, .4, .4)
var _active_line_color = Color(.3, .3, .3)
var _invalid_line_color = Color(1, 0, 0)
var _grab_margin = 2
var _line_space = 3
var _num_lines = 8
var _mouse_down = false
# Called when the node enters the scene tree for the first time.
func _draw():
var c = _line_color
if resize_control == null:
c = _invalid_line_color
elif _mouse_down:
c = _active_line_color
if orientation == ORIENTATION.LEFT:
_draw_resize_handle_left(c)
else:
_draw_resize_handle_right(c)
func _gui_input(event):
if resize_control == null:
return
if orientation == ORIENTATION.LEFT:
_handle_left_input(event)
else:
_handle_right_input(event)
# Draw the lines in the corner to show where you can
# drag to resize the dialog
func _draw_resize_handle_right(color):
var br = size
for i in range(_num_lines):
var start = br - Vector2(i * _line_space, 0)
var end = br - Vector2(0, i * _line_space)
draw_line(start, end, color, _line_width, true)
func _draw_resize_handle_left(color):
var bl = Vector2(0, size.y)
for i in range(_num_lines):
var start = bl + Vector2(i * _line_space, 0)
var end = bl - Vector2(0, i * _line_space)
draw_line(start, end, color, _line_width, true)
func _handle_right_input(event: InputEvent):
if event is InputEventMouseMotion:
if (
_mouse_down
and event.global_position.x > 0
and event.global_position.y < DisplayServer.window_get_size().y
):
if vertical_resize:
resize_control.size.y += event.relative.y
resize_control.size.x += event.relative.x
elif event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
_mouse_down = event.pressed
queue_redraw()
func _handle_left_input(event: InputEvent):
if event is InputEventMouseMotion:
if (
_mouse_down
and event.global_position.x > 0
and event.global_position.y < DisplayServer.window_get_size().y
):
var start_size = resize_control.size
resize_control.size.x -= event.relative.x
if resize_control.size.x != start_size.x:
resize_control.global_position.x += event.relative.x
if vertical_resize:
resize_control.size.y += event.relative.y
elif event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
_mouse_down = event.pressed
queue_redraw()

View file

@ -0,0 +1,8 @@
[gd_scene load_steps=2 format=3 uid="uid://bvrqqgjpyouse"]
[ext_resource type="Script" path="res://addons/gut/gui/ResizeHandle.gd" id="1_oi5ed"]
[node name="ResizeHandle" type="ColorRect"]
custom_minimum_size = Vector2(20, 20)
color = Color(1, 1, 1, 0)
script = ExtResource("1_oi5ed")

View file

@ -0,0 +1,360 @@
@tool
extends Control
var _show_orphans = true
var show_orphans = true:
get:
return _show_orphans
set(val):
_show_orphans = val
var _hide_passing = true
var hide_passing = true:
get:
return _hide_passing
set(val):
_hide_passing = val
var _icons = {
red = load("res://addons/gut/images/red.png"),
green = load("res://addons/gut/images/green.png"),
yellow = load("res://addons/gut/images/yellow.png"),
}
const _col_1_bg_color = Color(0, 0, 0, .1)
var _max_icon_width = 10
var _root: TreeItem
@onready var _ctrls = {tree = $Tree, lbl_overlay = $Tree/TextOverlay}
signal item_selected(script_path, inner_class, test_name, line_number)
# -------------------
# Private
# -------------------
func _ready():
_root = _ctrls.tree.create_item()
_root = _ctrls.tree.create_item()
_ctrls.tree.set_hide_root(true)
_ctrls.tree.columns = 2
_ctrls.tree.set_column_expand(0, true)
_ctrls.tree.set_column_expand(1, false)
_ctrls.tree.set_column_clip_content(0, true)
$Tree.item_selected.connect(_on_tree_item_selected)
if get_parent() == get_tree().root:
_test_running_setup()
func _test_running_setup():
load_json_file("user://.gut_editor.json")
func _on_tree_item_selected():
var item = _ctrls.tree.get_selected()
var item_meta = item.get_metadata(0)
var item_type = null
# Only select the left side of the tree item, cause I like that better.
# you can still click the right, but only the left gets highlighted.
if item.is_selected(1):
item.deselect(1)
item.select(0)
if item_meta == null:
return
else:
item_type = item_meta.type
var script_path = ""
var line = -1
var test_name = ""
var inner_class = ""
if item_type == "test":
var s_item = item.get_parent()
script_path = s_item.get_metadata(0)["path"]
inner_class = s_item.get_metadata(0)["inner_class"]
line = -1
test_name = item.get_text(0)
elif item_type == "assert":
var s_item = item.get_parent().get_parent()
script_path = s_item.get_metadata(0)["path"]
inner_class = s_item.get_metadata(0)["inner_class"]
line = _get_line_number_from_assert_msg(item.get_text(0))
test_name = item.get_parent().get_text(0)
elif item_type == "script":
script_path = item.get_metadata(0)["path"]
if item.get_parent() != _root:
inner_class = item.get_text(0)
line = -1
test_name = ""
else:
return
item_selected.emit(script_path, inner_class, test_name, line)
func _get_line_number_from_assert_msg(msg):
var line = -1
if msg.find("at line") > 0:
line = msg.split("at line")[-1].split(" ")[-1].to_int()
return line
func _get_path_and_inner_class_name_from_test_path(path):
var to_return = {path = "", inner_class = ""}
to_return.path = path
if !path.ends_with(".gd"):
var loc = path.find(".gd")
to_return.inner_class = path.split(".")[-1]
to_return.path = path.substr(0, loc + 3)
return to_return
func _find_script_item_with_path(path):
var items = _root.get_children()
var to_return = null
var idx = 0
while idx < items.size() and to_return == null:
var item = items[idx]
if item.get_metadata(0).path == path:
to_return = item
else:
idx += 1
return to_return
func _add_script_tree_item(script_path, script_json):
var path_info = _get_path_and_inner_class_name_from_test_path(script_path)
var item_text = script_path
var parent = _root
if path_info.inner_class != "":
parent = _find_script_item_with_path(path_info.path)
item_text = path_info.inner_class
if parent == null:
parent = _add_script_tree_item(path_info.path, {})
parent.get_metadata(0).inner_tests += script_json["props"]["tests"]
parent.get_metadata(0).inner_passing += script_json["props"]["tests"]
parent.get_metadata(0).inner_passing -= script_json["props"]["failures"]
parent.get_metadata(0).inner_passing -= script_json["props"]["pending"]
var total_text = str("All ", parent.get_metadata(0).inner_tests, " passed")
if parent.get_metadata(0).inner_passing != parent.get_metadata(0).inner_tests:
total_text = str(
parent.get_metadata(0).inner_passing,
"/",
parent.get_metadata(0).inner_tests,
" passed."
)
parent.set_text(1, total_text)
var item = _ctrls.tree.create_item(parent)
item.set_text(0, item_text)
var meta = {
"type": "script",
"path": path_info.path,
"inner_class": path_info.inner_class,
"json": script_json,
"inner_passing": 0,
"inner_tests": 0
}
item.set_metadata(0, meta)
item.set_custom_bg_color(1, _col_1_bg_color)
return item
func _add_assert_item(text, icon, parent_item):
# print(' * adding assert')
var assert_item = _ctrls.tree.create_item(parent_item)
assert_item.set_icon_max_width(0, _max_icon_width)
assert_item.set_text(0, text)
assert_item.set_metadata(0, {"type": "assert"})
assert_item.set_icon(0, icon)
assert_item.set_custom_bg_color(1, _col_1_bg_color)
return assert_item
func _add_test_tree_item(test_name, test_json, script_item):
# print(' * adding test ', test_name)
var no_orphans_to_show = !_show_orphans or (_show_orphans and test_json.orphans == 0)
if _hide_passing and test_json["status"] == "pass" and no_orphans_to_show:
return
var item = _ctrls.tree.create_item(script_item)
var status = test_json["status"]
var meta = {"type": "test", "json": test_json}
item.set_text(0, test_name)
item.set_text(1, status)
item.set_text_alignment(1, HORIZONTAL_ALIGNMENT_RIGHT)
item.set_custom_bg_color(1, _col_1_bg_color)
item.set_metadata(0, meta)
item.set_icon_max_width(0, _max_icon_width)
var orphan_text = "orphans"
if test_json.orphans == 1:
orphan_text = "orphan"
orphan_text = str(test_json.orphans, " ", orphan_text)
if status == "pass" and no_orphans_to_show:
item.set_icon(0, _icons.green)
elif status == "pass" and !no_orphans_to_show:
item.set_icon(0, _icons.yellow)
item.set_text(1, orphan_text)
elif status == "fail":
item.set_icon(0, _icons.red)
else:
item.set_icon(0, _icons.yellow)
if !_hide_passing:
for passing in test_json.passing:
_add_assert_item("pass: " + passing, _icons.green, item)
for failure in test_json.failing:
_add_assert_item("fail: " + failure.replace("\n", ""), _icons.red, item)
for pending in test_json.pending:
_add_assert_item("pending: " + pending.replace("\n", ""), _icons.yellow, item)
if status != "pass" and !no_orphans_to_show:
_add_assert_item(orphan_text, _icons.yellow, item)
return item
func _add_script_to_tree(key, script_json):
var tests = script_json["tests"]
var test_keys = tests.keys()
var s_item = _add_script_tree_item(key, script_json)
var bad_count = 0
for test_key in test_keys:
var t_item = _add_test_tree_item(test_key, tests[test_key], s_item)
if tests[test_key].status != "pass":
bad_count += 1
elif t_item != null:
t_item.collapsed = true
if s_item.get_children().size() == 0:
s_item.free()
else:
var total_text = str("All ", test_keys.size(), " passed")
if bad_count == 0:
s_item.collapsed = true
else:
total_text = str(test_keys.size() - bad_count, "/", test_keys.size(), " passed")
s_item.set_text(1, total_text)
func _free_childless_scripts():
var items = _root.get_children()
for item in items:
var next_item = item.get_next()
if item.get_children().size() == 0:
item.free()
item = next_item
func _show_all_passed():
if _root.get_children().size() == 0:
add_centered_text("Everything passed!")
func _load_result_tree(j):
var scripts = j["test_scripts"]["scripts"]
var script_keys = scripts.keys()
# if we made it here, the json is valid and we did something, otherwise the
# 'nothing to see here' should be visible.
clear_centered_text()
for key in script_keys:
if scripts[key]["props"]["tests"] > 0:
_add_script_to_tree(key, scripts[key])
_free_childless_scripts()
_show_all_passed()
# -------------------
# Public
# -------------------
func load_json_file(path):
var file = FileAccess.open(path, FileAccess.READ)
var text = ""
if file != null:
text = file.get_as_text()
if text != "":
var test_json_conv = JSON.new()
var result = test_json_conv.parse(text)
if result != OK:
add_centered_text(
str(
path,
" has invalid json in it \n",
"Error ",
result,
"@",
test_json_conv.get_error_line(),
"\n",
test_json_conv.get_error_message()
)
)
return
var data = test_json_conv.get_data()
load_json_results(data)
else:
add_centered_text(str(path, " was empty or does not exist."))
func load_json_results(j):
clear()
_load_result_tree(j)
func clear():
_ctrls.tree.clear()
_root = _ctrls.tree.create_item()
func set_summary_min_width(width):
_ctrls.tree.set_column_custom_minimum_width(1, width)
func add_centered_text(t):
_ctrls.lbl_overlay.visible = true
_ctrls.lbl_overlay.text = t
func clear_centered_text():
_ctrls.lbl_overlay.visible = false
_ctrls.lbl_overlay.text = ""
func collapse_all():
set_collapsed_on_all(_root, true)
func expand_all():
set_collapsed_on_all(_root, false)
func set_collapsed_on_all(item, value):
item.set_collapsed_recursive(value)
if item == _root and value:
item.set_collapsed(false)
func get_selected():
return _ctrls.tree.get_selected()

View file

@ -0,0 +1,32 @@
[gd_scene load_steps=2 format=3 uid="uid://dls5r5f6157nq"]
[ext_resource type="Script" path="res://addons/gut/gui/ResultsTree.gd" id="1_b4uub"]
[node name="ResultsTree" type="VBoxContainer"]
custom_minimum_size = Vector2(10, 10)
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_right = -70.0
offset_bottom = -104.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource("1_b4uub")
[node name="Tree" type="Tree" parent="."]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
columns = 2
hide_root = true
[node name="TextOverlay" type="Label" parent="Tree"]
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2

View file

@ -16,7 +16,7 @@ var _editors = null
var _cur_editor = null
var _last_line = -1
var _cur_script_path = null
var _last_info = null
var _last_info = {script = null, inner_class = null, test_method = null}
signal run_tests(what)
@ -26,6 +26,8 @@ func _ready():
_ctrls.btn_script.visible = false
_ctrls.btn_inner.visible = false
_ctrls.btn_method.visible = false
_ctrls.arrow_1.visible = false
_ctrls.arrow_2.visible = false
# ----------------
@ -34,11 +36,13 @@ func _ready():
func _set_editor(which):
_last_line = -1
if _cur_editor != null and _cur_editor.get_ref():
_cur_editor.get_ref().disconnect("cursor_changed", Callable(self, "_on_cursor_changed"))
# _cur_editor.get_ref().disconnect('cursor_changed',Callable(self,'_on_cursor_changed'))
_cur_editor.get_ref().caret_changed.disconnect(_on_cursor_changed)
if which != null:
_cur_editor = weakref(which)
which.connect("cursor_changed", Callable(self, "_on_cursor_changed"), [which])
which.caret_changed.connect(_on_cursor_changed.bind(which))
# which.connect('cursor_changed',Callable(self,'_on_cursor_changed'),[which])
_last_line = which.get_caret_line()
_last_info = _editors.get_line_info()
@ -52,12 +56,12 @@ func _update_buttons(info):
_ctrls.btn_inner.visible = info.inner_class != null
_ctrls.arrow_1.visible = info.inner_class != null
_ctrls.btn_inner.text = str(info.inner_class)
_ctrls.btn_inner.hint_tooltip = str("Run all tests in Inner-Test-Class ", info.inner_class)
_ctrls.btn_inner.tooltip_text = str("Run all tests in Inner-Test-Class ", info.inner_class)
_ctrls.btn_method.visible = info.test_method != null
_ctrls.arrow_2.visible = info.test_method != null
_ctrls.btn_method.text = str(info.test_method)
_ctrls.btn_method.hint_tooltip = str("Run test ", info.test_method)
_ctrls.btn_method.tooltip_text = str("Run test ", info.test_method)
# The button's new size won't take effect until the next frame.
# This appears to be what was causing the button to not be clickable the
@ -66,7 +70,7 @@ func _update_buttons(info):
func _update_size():
custom_minimum_size.x = _ctrls.btn_method.size.x + _ctrls.btn_method.rect_position.x
custom_minimum_size.x = _ctrls.btn_method.size.x + _ctrls.btn_method.position.x
# ----------------
@ -110,9 +114,12 @@ func set_script_text_editors(value):
func activate_for_script(path):
_ctrls.btn_script.visible = true
_ctrls.btn_script.text = path.get_file()
_ctrls.btn_script.hint_tooltip = str("Run all tests in script ", path)
_ctrls.btn_script.tooltip_text = str("Run all tests in script ", path)
_cur_script_path = path
_editors.refresh()
# We have to wait a beat for the visibility to change on
# the editors, otherwise we always get the first one.
await get_tree().process_frame
_set_editor(_editors.get_current_text_edit())
@ -145,7 +152,4 @@ func search_current_editor_for_text(txt):
var result = te.search(txt, 0, 0, 0)
var to_return = -1
if result.size() > 0:
to_return = result[TextEdit.SEARCH_RESULT_LINE]
return to_return

View file

@ -1,79 +1,64 @@
[gd_scene load_steps=4 format=2]
[gd_scene load_steps=4 format=3 uid="uid://0yunjxtaa8iw"]
[ext_resource path="res://addons/gut/gui/RunAtCursor.gd" type="Script" id=1]
[ext_resource path="res://addons/gut/gui/play.png" type="Texture2D" id=2]
[ext_resource path="res://addons/gut/gui/arrow.png" type="Texture2D" id=3]
[ext_resource type="Script" path="res://addons/gut/gui/RunAtCursor.gd" id="1"]
[ext_resource type="Texture2D" uid="uid://cr6tvdv0ve6cv" path="res://addons/gut/gui/play.png" id="2"]
[ext_resource type="Texture2D" uid="uid://6wra5rxmfsrl" path="res://addons/gut/gui/arrow.png" id="3"]
[node name="RunAtCursor" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_right = 1.0
offset_bottom = -527.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
script = ExtResource("1")
[node name="HBox" type="HBoxContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
size_flags_horizontal = 3
size_flags_vertical = 3
__meta__ = {
"_edit_use_anchors_": false
}
[node name="LblNoneSelected" type="Label" parent="HBox"]
offset_top = 29.0
offset_right = 50.0
offset_bottom = 43.0
layout_mode = 2
text = "<None>"
[node name="BtnRunScript" type="Button" parent="HBox"]
visible = false
offset_left = 54.0
offset_right = 140.0
offset_bottom = 73.0
layout_mode = 2
text = "<script>"
icon = ExtResource( 2 )
icon = ExtResource("2")
[node name="Arrow1" type="TextureButton" parent="HBox"]
visible = false
offset_left = 54.0
offset_right = 78.0
offset_bottom = 73.0
custom_minimum_size = Vector2( 24, 0 )
texture_normal = ExtResource( 3 )
expand = true
custom_minimum_size = Vector2(24, 0)
layout_mode = 2
texture_normal = ExtResource("3")
stretch_mode = 3
[node name="BtnRunInnerClass" type="Button" parent="HBox"]
visible = false
offset_left = 134.0
offset_right = 243.0
offset_bottom = 73.0
layout_mode = 2
text = "<inner class>"
icon = ExtResource( 2 )
icon = ExtResource("2")
[node name="Arrow2" type="TextureButton" parent="HBox"]
visible = false
offset_left = 54.0
offset_right = 78.0
offset_bottom = 73.0
custom_minimum_size = Vector2( 24, 0 )
texture_normal = ExtResource( 3 )
expand = true
custom_minimum_size = Vector2(24, 0)
layout_mode = 2
texture_normal = ExtResource("3")
stretch_mode = 3
[node name="BtnRunMethod" type="Button" parent="HBox"]
visible = false
offset_left = 247.0
offset_right = 337.0
offset_bottom = 73.0
layout_mode = 2
text = "<method>"
icon = ExtResource( 2 )
icon = ExtResource("2")
[connection signal="pressed" from="HBox/BtnRunScript" to="." method="_on_BtnRunScript_pressed"]
[connection signal="pressed" from="HBox/BtnRunInnerClass" to="." method="_on_BtnRunInnerClass_pressed"]

View file

@ -1,31 +1,14 @@
extends Control
@tool
extends Control
var _interface = null
var _utils = load("res://addons/gut/utils.gd").new()
var _hide_passing = true
var _font = null
var _font_size = null
var _root = null
var _max_icon_width = 10
var _editors = null # script_text_editor_controls.gd
var _show_orphans = true
var _output_control = null
const _col_1_bg_color = Color(0, 0, 0, .1)
var _icons = {
red = load("res://addons/gut/images/red.png"),
green = load("res://addons/gut/images/green.png"),
yellow = load("res://addons/gut/images/yellow.png"),
}
signal search_for_text(text)
@onready var _ctrls = {
tree = $VBox/Output/Scroll/Tree,
lbl_overlay = $VBox/Output/OverlayMessage,
chk_hide_passing = $VBox/Toolbar/HidePassing,
toolbar =
{
toolbar = $VBox/Toolbar,
@ -40,9 +23,36 @@ signal search_for_text(text)
}
func _ready():
var f = null
if $FontSampler.get_label_settings() == null:
f = get_theme_default_font()
else:
f = $FontSampler.get_label_settings().font
var s_size = f.get_string_size("000 of 000 passed")
_ctrls.tree.set_summary_min_width(s_size.x)
_set_toolbutton_icon(_ctrls.toolbar.collapse, "CollapseTree", "c")
_set_toolbutton_icon(_ctrls.toolbar.collapse_all, "CollapseTree", "c")
_set_toolbutton_icon(_ctrls.toolbar.expand, "ExpandTree", "e")
_set_toolbutton_icon(_ctrls.toolbar.expand_all, "ExpandTree", "e")
_set_toolbutton_icon(_ctrls.toolbar.show_script, "Script", "ss")
_set_toolbutton_icon(_ctrls.toolbar.scroll_output, "Font", "so")
_ctrls.tree.hide_passing = true
_ctrls.toolbar.hide_passing.button_pressed = false
_ctrls.tree.show_orphans = true
_ctrls.tree.item_selected.connect(_on_item_selected)
if get_parent() == get_tree().root:
_test_running_setup()
call_deferred("_update_min_width")
func _test_running_setup():
_hide_passing = true
_show_orphans = true
_ctrls.tree.hide_passing = true
_ctrls.tree.show_orphans = true
var _gut_config = load("res://addons/gut/gut_config.gd").new()
_gut_config.load_panel_options("res://.gut_editor_config.json")
set_font(
@ -50,7 +60,7 @@ func _test_running_setup():
)
_ctrls.toolbar.hide_passing.text = "[hp]"
load_json_file("user://.gut_editor.json")
_ctrls.tree.load_json_file("user://.gut_editor.json")
func _set_toolbutton_icon(btn, icon_name, text):
@ -60,51 +70,17 @@ func _set_toolbutton_icon(btn, icon_name, text):
btn.text = str("[", text, "]")
func _ready():
var f = null
if $FontSampler.get_label_settings() == null:
f = get_theme_default_font()
else:
f = $FontSampler.get_label_settings().font
var s_size = f.get_string_size("000 of 000 passed")
_root = _ctrls.tree.create_item()
_ctrls.tree.set_hide_root(true)
_ctrls.tree.columns = 2
_ctrls.tree.set_column_expand(0, true)
_ctrls.tree.set_column_expand(1, false)
_ctrls.tree.set_column_custom_minimum_width(1, s_size.x)
_set_toolbutton_icon(_ctrls.toolbar.collapse, "CollapseTree", "c")
_set_toolbutton_icon(_ctrls.toolbar.collapse_all, "CollapseTree", "c")
_set_toolbutton_icon(_ctrls.toolbar.expand, "ExpandTree", "e")
_set_toolbutton_icon(_ctrls.toolbar.expand_all, "ExpandTree", "e")
_set_toolbutton_icon(_ctrls.toolbar.show_script, "Script", "ss")
_set_toolbutton_icon(_ctrls.toolbar.scroll_output, "Font", "so")
_ctrls.toolbar.hide_passing.set(
"custom_icons/checked", get_theme_icon("GuiVisibilityHidden", "EditorIcons")
)
_ctrls.toolbar.hide_passing.set(
"custom_icons/unchecked", get_theme_icon("GuiVisibilityVisible", "EditorIcons")
)
if get_parent() == get_tree().root:
_test_running_setup()
call_deferred("_update_min_width")
func _update_min_width():
custom_minimum_size.x = _ctrls.toolbar.toolbar.size.x
func _open_file(path, line_number):
func _open_script_in_editor(path, line_number):
if _interface == null:
print("Too soon, wait a bit and try again.")
return
var r = load(path)
if line_number != -1:
if line_number != null and line_number != -1:
_interface.edit_script(r, line_number)
else:
_interface.edit_script(r)
@ -113,211 +89,6 @@ func _open_file(path, line_number):
_interface.set_main_screen_editor("Script")
func _add_script_tree_item(script_path, script_json):
var path_info = _get_path_and_inner_class_name_from_test_path(script_path)
# print('* adding script ', path_info)
var item_text = script_path
var parent = _root
if path_info.inner_class != "":
parent = _find_script_item_with_path(path_info.path)
item_text = path_info.inner_class
if parent == null:
parent = _add_script_tree_item(path_info.path, {})
var item = _ctrls.tree.create_item(parent)
item.set_text(0, item_text)
var meta = {
"type": "script",
"path": path_info.path,
"inner_class": path_info.inner_class,
"json": script_json
}
item.set_metadata(0, meta)
item.set_custom_bg_color(1, _col_1_bg_color)
return item
func _add_assert_item(text, icon, parent_item):
# print(' * adding assert')
var assert_item = _ctrls.tree.create_item(parent_item)
assert_item.set_icon_max_width(0, _max_icon_width)
assert_item.set_text(0, text)
assert_item.set_metadata(0, {"type": "assert"})
assert_item.set_icon(0, icon)
assert_item.set_custom_bg_color(1, _col_1_bg_color)
return assert_item
func _add_test_tree_item(test_name, test_json, script_item):
# print(' * adding test ', test_name)
var no_orphans_to_show = !_show_orphans or (_show_orphans and test_json.orphans == 0)
if _hide_passing and test_json["status"] == "pass" and no_orphans_to_show:
return
var item = _ctrls.tree.create_item(script_item)
var status = test_json["status"]
var meta = {"type": "test", "json": test_json}
item.set_text(0, test_name)
item.set_text(1, status)
item.set_text_alignment(1, HORIZONTAL_ALIGNMENT_RIGHT)
item.set_custom_bg_color(1, _col_1_bg_color)
item.set_metadata(0, meta)
item.set_icon_max_width(0, _max_icon_width)
var orphan_text = "orphans"
if test_json.orphans == 1:
orphan_text = "orphan"
orphan_text = str(test_json.orphans, " ", orphan_text)
if status == "pass" and no_orphans_to_show:
item.set_icon(0, _icons.green)
elif status == "pass" and !no_orphans_to_show:
item.set_icon(0, _icons.yellow)
item.set_text(1, orphan_text)
elif status == "fail":
item.set_icon(0, _icons.red)
else:
item.set_icon(0, _icons.yellow)
if !_hide_passing:
for passing in test_json.passing:
_add_assert_item("pass: " + passing, _icons.green, item)
for failure in test_json.failing:
_add_assert_item("fail: " + failure.replace("\n", ""), _icons.red, item)
for pending in test_json.pending:
_add_assert_item("pending: " + pending.replace("\n", ""), _icons.yellow, item)
if status != "pass" and !no_orphans_to_show:
_add_assert_item(orphan_text, _icons.yellow, item)
return item
func _load_result_tree(j):
var scripts = j["test_scripts"]["scripts"]
var script_keys = scripts.keys()
# if we made it here, the json is valid and we did something, otherwise the
# 'nothing to see here' should be visible.
clear_centered_text()
var _last_script_item = null
for key in script_keys:
var tests = scripts[key]["tests"]
var test_keys = tests.keys()
var s_item = _add_script_tree_item(key, scripts[key])
var bad_count = 0
for test_key in test_keys:
var t_item = _add_test_tree_item(test_key, tests[test_key], s_item)
if tests[test_key].status != "pass":
bad_count += 1
elif t_item != null:
t_item.collapsed = true
# get_children returns the first child or null. its a dumb name.
if s_item.get_children() == null:
# var m = s_item.get_metadata(0)
# print('!! Deleting ', m.path, ' ', m.inner_class)
s_item.free()
else:
var total_text = str(test_keys.size(), " passed")
# s_item.set_text_alignment(1, s_item.ALIGN_LEFT)
if bad_count == 0:
s_item.collapsed = true
else:
total_text = str(test_keys.size() - bad_count, " of ", test_keys.size(), " passed")
s_item.set_text(1, total_text)
_free_childless_scripts()
_show_all_passed()
func _free_childless_scripts():
var items = _root.get_children()
for item in items:
var next_item = item.get_next()
if item.get_children() == null:
item.free()
item = next_item
func _find_script_item_with_path(path):
var items = _root.get_children()
var to_return = null
var idx = 0
while idx < items.size() and to_return == null:
var item = items[idx]
if item.get_metadata(0).path == path:
to_return = item
else:
idx += 1
return to_return
func _get_line_number_from_assert_msg(msg):
var line = -1
if msg.find("at line") > 0:
line = msg.split("at line")[-1].split(" ")[-1].to_int()
return line
func _get_path_and_inner_class_name_from_test_path(path):
var to_return = {path = "", inner_class = ""}
to_return.path = path
if !path.ends_with(".gd"):
var loc = path.find(".gd")
to_return.inner_class = path.split(".")[-1]
to_return.path = path.substr(0, loc + 3)
return to_return
func _handle_tree_item_select(item, force_scroll):
var item_type = item.get_metadata(0).type
var path = ""
var line = -1
var method_name = ""
var inner_class = ""
if item_type == "test":
var s_item = item.get_parent()
path = s_item.get_metadata(0)["path"]
inner_class = s_item.get_metadata(0)["inner_class"]
line = -1
method_name = item.get_text(0)
elif item_type == "assert":
var s_item = item.get_parent().get_parent()
path = s_item.get_metadata(0)["path"]
inner_class = s_item.get_metadata(0)["inner_class"]
line = _get_line_number_from_assert_msg(item.get_text(0))
method_name = item.get_parent().get_text(0)
elif item_type == "script":
path = item.get_metadata(0)["path"]
if item.get_parent() != _root:
inner_class = item.get_text(0)
line = -1
method_name = ""
else:
return
var path_info = _get_path_and_inner_class_name_from_test_path(path)
if force_scroll or _ctrls.toolbar.show_script.pressed:
_goto_code(path, line, method_name, inner_class)
if force_scroll or _ctrls.toolbar.scroll_output.pressed:
_goto_output(path, method_name, inner_class)
# starts at beginning of text edit and searches for each search term, moving
# through the text as it goes; ensuring that, when done, it found the first
# occurance of the last srting that happend after the first occurance of
@ -325,9 +96,12 @@ func _handle_tree_item_select(item, force_scroll):
# inner class that may have be a duplicate of a method name in a different
# inner class)
func _get_line_number_for_seq_search(search_strings, te):
# var te = _editors.get_current_text_edit()
if te == null:
print("No Text editor to get line number for")
return 0
var result = null
var line = Vector2i(-1, -1)
var line = Vector2i(0, 0)
var s_flags = 0
var i = 0
@ -348,7 +122,7 @@ func _goto_code(path, line, method_name = "", inner_class = ""):
print("going to ", [path, line, method_name, inner_class])
return
_open_file(path, line)
_open_script_in_editor(path, line)
if line == -1:
var search_strings = []
if inner_class != "":
@ -357,8 +131,9 @@ func _goto_code(path, line, method_name = "", inner_class = ""):
if method_name != "":
search_strings.append(method_name)
await get_tree().process_frame
line = _get_line_number_for_seq_search(search_strings, _editors.get_current_text_edit())
if line != -1:
if line != null and line != -1:
_interface.get_script_editor().goto_line(line)
@ -375,44 +150,13 @@ func _goto_output(path, method_name, inner_class):
search_strings.append(method_name)
var line = _get_line_number_for_seq_search(search_strings, _output_control.get_rich_text_edit())
if line != -1:
if line != null and line != -1:
_output_control.scroll_to_line(line)
func _show_all_passed():
if _root.get_children() == null:
add_centered_text("Everything passed!")
func _set_collapsed_on_all(item, value):
if item == _root:
var node = _root.get_children()
while node != null:
node.call_recursive("set_collapsed", value)
node = node.get_next()
else:
item.call_recursive("set_collapsed", value)
# --------------
# Events
# --------------
func _on_Tree_item_selected():
# do not force scroll
var item = _ctrls.tree.get_selected()
_handle_tree_item_select(item, false)
# it just looks better if the left is always selected.
if item.is_selected(1):
item.deselect(1)
item.select(0)
func _on_Tree_item_activated():
# force scroll
print("double clicked")
_handle_tree_item_select(_ctrls.tree.get_selected(), true)
func _on_Collapse_pressed():
collapse_selected()
@ -430,55 +174,30 @@ func _on_ExpandAll_pressed():
func _on_Hide_Passing_pressed():
_hide_passing = _ctrls.toolbar.hide_passing.button_pressed
_ctrls.tree.hide_passing = !_ctrls.toolbar.hide_passing.button_pressed
_ctrls.tree.load_json_file("user://.gut_editor.json")
func _on_item_selected(script_path, inner_class, test_name, line):
if _ctrls.toolbar.show_script.button_pressed:
_goto_code(script_path, line, test_name, inner_class)
if _ctrls.toolbar.scroll_output.button_pressed:
_goto_output(script_path, test_name, inner_class)
# --------------
# Public
# --------------
func load_json_file(path):
var text = _utils.get_file_as_text(path)
if text != "":
var test_json_conv = JSON.new()
test_json_conv.parse(text)
var result = test_json_conv.get_data()
if result.error != OK:
add_centered_text(
str(
path,
" has invalid json in it \n",
"Error ",
result.error,
"@",
result.error_line,
"\n",
result.error_string
)
)
return
load_json_results(result.result)
else:
add_centered_text(str(path, " was empty or does not exist."))
func load_json_results(j):
clear()
add_centered_text("Nothing Here")
_load_result_tree(j)
func add_centered_text(t):
_ctrls.lbl_overlay.text = t
_ctrls.tree.add_centered_text(t)
func clear_centered_text():
_ctrls.lbl_overlay.text = ""
_ctrls.tree.clear_centered_text()
func clear():
_ctrls.tree.clear()
_root = _ctrls.tree.create_item()
clear_centered_text()
@ -491,27 +210,27 @@ func set_script_text_editors(value):
func collapse_all():
_set_collapsed_on_all(_root, true)
_ctrls.tree.collapse_all()
func expand_all():
_set_collapsed_on_all(_root, false)
_ctrls.tree.expand_all()
func collapse_selected():
var item = _ctrls.tree.get_selected()
if item != null:
_set_collapsed_on_all(item, true)
_ctrls.tree.set_collapsed_on_all(item, true)
func expand_selected():
var item = _ctrls.tree.get_selected()
if item != null:
_set_collapsed_on_all(item, false)
_ctrls.tree.set_collapsed_on_all(item, false)
func set_show_orphans(should):
_show_orphans = should
_ctrls.tree.show_orphans = should
func set_font(font_name, size):
@ -531,3 +250,7 @@ func set_font(font_name, size):
func set_output_control(value):
_output_control = value
func load_json_results(j):
_ctrls.tree.load_json_results(j)

View file

@ -1,157 +1,110 @@
[gd_scene load_steps=4 format=2]
[gd_scene load_steps=5 format=3 uid="uid://4gyyn12um08h"]
[ext_resource path="res://addons/gut/gui/RunResults.gd" type="Script" id=1]
[ext_resource type="Script" path="res://addons/gut/gui/RunResults.gd" id="1"]
[ext_resource type="PackedScene" uid="uid://dls5r5f6157nq" path="res://addons/gut/gui/ResultsTree.tscn" id="2_o808v"]
[sub_resource type="Image" id=3]
[sub_resource type="Image" id="Image_abbh7"]
data = {
"data": PackedByteArray( 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ),
"format": "LumAlpha8",
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id=2]
flags = 4
flags = 4
image = SubResource( 3 )
size = Vector2( 16, 16 )
[sub_resource type="ImageTexture" id="ImageTexture_x655i"]
image = SubResource("Image_abbh7")
[node name="RunResults" type="Control"]
offset_right = 595.0
offset_bottom = 459.0
custom_minimum_size = Vector2( 302, 0 )
script = ExtResource( 1 )
custom_minimum_size = Vector2(345, 0)
layout_mode = 3
anchors_preset = 0
offset_right = 709.0
offset_bottom = 321.0
script = ExtResource("1")
[node name="VBox" type="VBoxContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
[node name="Toolbar" type="HBoxContainer" parent="VBox"]
offset_right = 296.0
offset_bottom = 24.0
layout_mode = 2
size_flags_horizontal = 0
[node name="Expand" type="Button" parent="VBox/Toolbar"]
offset_right = 28.0
offset_bottom = 24.0
hint_tooltip = "Expand selected item and all children."
icon = SubResource( 2 )
layout_mode = 2
icon = SubResource("ImageTexture_x655i")
[node name="Collapse" type="Button" parent="VBox/Toolbar"]
offset_left = 32.0
offset_right = 60.0
offset_bottom = 24.0
hint_tooltip = "Collapse selected item and all children."
icon = SubResource( 2 )
layout_mode = 2
icon = SubResource("ImageTexture_x655i")
[node name="Sep" type="ColorRect" parent="VBox/Toolbar"]
offset_left = 64.0
offset_right = 66.0
offset_bottom = 24.0
custom_minimum_size = Vector2( 2, 0 )
custom_minimum_size = Vector2(2, 0)
layout_mode = 2
[node name="LblAll" type="Label" parent="VBox/Toolbar"]
offset_left = 70.0
offset_top = 5.0
offset_right = 91.0
offset_bottom = 19.0
layout_mode = 2
text = "All:"
align = 1
[node name="ExpandAll" type="Button" parent="VBox/Toolbar"]
offset_left = 95.0
offset_right = 123.0
offset_bottom = 24.0
hint_tooltip = "Expand All."
icon = SubResource( 2 )
layout_mode = 2
icon = SubResource("ImageTexture_x655i")
[node name="CollapseAll" type="Button" parent="VBox/Toolbar"]
offset_left = 127.0
offset_right = 155.0
offset_bottom = 24.0
hint_tooltip = "Collapse all."
icon = SubResource( 2 )
layout_mode = 2
icon = SubResource("ImageTexture_x655i")
[node name="Sep2" type="ColorRect" parent="VBox/Toolbar"]
offset_left = 159.0
offset_right = 161.0
offset_bottom = 24.0
custom_minimum_size = Vector2( 2, 0 )
custom_minimum_size = Vector2(2, 0)
layout_mode = 2
[node name="HidePassing" type="CheckBox" parent="VBox/Toolbar"]
offset_left = 165.0
offset_right = 189.0
offset_bottom = 24.0
hint_tooltip = "Show/Hide passing tests. Takes effect on next run."
layout_mode = 2
size_flags_horizontal = 4
custom_icons/checked = SubResource( 2 )
custom_icons/unchecked = SubResource( 2 )
pressed = true
__meta__ = {
"_editor_description_": ""
}
text = "Passing"
[node name="Sep3" type="ColorRect" parent="VBox/Toolbar"]
offset_left = 193.0
offset_right = 195.0
offset_bottom = 24.0
custom_minimum_size = Vector2( 2, 0 )
custom_minimum_size = Vector2(2, 0)
layout_mode = 2
[node name="LblSync" type="Label" parent="VBox/Toolbar"]
offset_left = 199.0
offset_top = 5.0
offset_right = 232.0
offset_bottom = 19.0
layout_mode = 2
text = "Sync:"
align = 1
[node name="ShowScript" type="Button" parent="VBox/Toolbar"]
offset_left = 236.0
offset_right = 264.0
offset_bottom = 24.0
hint_tooltip = "Open script and scroll to line when a tree item is clicked."
layout_mode = 2
toggle_mode = true
pressed = true
icon = SubResource( 2 )
button_pressed = true
icon = SubResource("ImageTexture_x655i")
[node name="ScrollOutput" type="Button" parent="VBox/Toolbar"]
offset_left = 268.0
offset_right = 296.0
offset_bottom = 24.0
hint_tooltip = "Scroll to related line in the output panel when tree item clicked."
layout_mode = 2
toggle_mode = true
pressed = true
icon = SubResource( 2 )
button_pressed = true
icon = SubResource("ImageTexture_x655i")
[node name="Output" type="Panel" parent="VBox"]
self_modulate = Color( 1, 1, 1, 0.541176 )
offset_top = 28.0
offset_right = 595.0
offset_bottom = 459.0
self_modulate = Color(1, 1, 1, 0.541176)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Scroll" type="ScrollContainer" parent="VBox/Output"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="Tree" type="Tree" parent="VBox/Output/Scroll"]
offset_right = 595.0
offset_bottom = 431.0
size_flags_horizontal = 3
size_flags_vertical = 3
columns = 2
hide_root = true
[node name="OverlayMessage" type="Label" parent="VBox/Output"]
anchor_right = 1.0
anchor_bottom = 1.0
align = 1
valign = 1
[node name="Tree" parent="VBox/Output/Scroll" instance=ExtResource("2_o808v")]
layout_mode = 2
[node name="FontSampler" type="Label" parent="."]
visible = false
layout_mode = 0
offset_right = 40.0
offset_bottom = 14.0
text = "000 of 000 passed"
@ -161,5 +114,3 @@ text = "000 of 000 passed"
[connection signal="pressed" from="VBox/Toolbar/ExpandAll" to="." method="_on_ExpandAll_pressed"]
[connection signal="pressed" from="VBox/Toolbar/CollapseAll" to="." method="_on_CollapseAll_pressed"]
[connection signal="pressed" from="VBox/Toolbar/HidePassing" to="." method="_on_Hide_Passing_pressed"]
[connection signal="item_activated" from="VBox/Output/Scroll/Tree" to="." method="_on_Tree_item_activated"]
[connection signal="item_selected" from="VBox/Output/Scroll/Tree" to="." method="_on_Tree_item_selected"]

View file

@ -1,4 +1,4 @@
[gd_scene format=2]
[gd_scene format=3 uid="uid://cvvvtsah38l0e"]
[node name="Settings" type="VBoxContainer"]
offset_right = 388.0

View file

@ -36,24 +36,13 @@ func _display_shortcut():
func _is_shift_only_modifier():
return (
_source_event.shift_pressed
and !(
_source_event.alt_pressed
or _source_event.command_pressed
or _source_event.ctrl_pressed
or _source_event.meta_pressed
)
and !(_source_event.alt_pressed or _source_event.ctrl_pressed or _source_event.meta_pressed)
and !_is_modifier(_source_event.keycode)
)
func _has_modifier(event):
return (
event.alt_pressed
or event.command_pressed
or event.ctrl_pressed
or event.meta_pressed
or event.shift_pressed
)
return event.alt_pressed or event.ctrl_pressed or event.meta_pressed or event.shift_pressed
func _is_modifier(keycode):

View file

@ -1,77 +1,52 @@
[gd_scene load_steps=2 format=2]
[gd_scene load_steps=2 format=3 uid="uid://sfb1fw8j6ufu"]
[ext_resource path="res://addons/gut/gui/ShortcutButton.gd" type="Script" id=1]
[ext_resource type="Script" path="res://addons/gut/gui/ShortcutButton.gd" id="1"]
[node name="ShortcutButton" type="Control"]
custom_minimum_size = Vector2(210, 30)
layout_mode = 3
anchor_right = 0.123
anchor_bottom = 0.04
offset_right = 33.048
offset_bottom = 1.0
custom_minimum_size = Vector2( 125, 25 )
script = ExtResource( 1 )
__meta__ = {
"_edit_use_anchors_": false
}
offset_right = 68.304
offset_bottom = 6.08
script = ExtResource("1")
[node name="Layout" type="HBoxContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
__meta__ = {
"_edit_use_anchors_": false
}
[node name="lblShortcut" type="Label" parent="Layout"]
offset_right = 50.0
offset_bottom = 25.0
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 7
text = "<None>"
align = 2
valign = 1
horizontal_alignment = 2
[node name="CenterContainer" type="CenterContainer" parent="Layout"]
offset_left = 54.0
offset_right = 64.0
offset_bottom = 25.0
custom_minimum_size = Vector2( 10, 0 )
custom_minimum_size = Vector2(10, 0)
layout_mode = 2
[node name="SetButton" type="Button" parent="Layout"]
offset_left = 68.0
offset_right = 128.0
offset_bottom = 25.0
custom_minimum_size = Vector2( 60, 0 )
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
text = "Set"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="SaveButton" type="Button" parent="Layout"]
visible = false
offset_left = 82.0
offset_right = 142.0
offset_bottom = 25.0
custom_minimum_size = Vector2( 60, 0 )
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
text = "Save"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="CancelButton" type="Button" parent="Layout"]
visible = false
offset_left = 82.0
offset_right = 142.0
offset_bottom = 25.0
custom_minimum_size = Vector2( 60, 0 )
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
text = "Cancel"
__meta__ = {
"_edit_use_anchors_": false
}
[node name="ClearButton" type="Button" parent="Layout"]
offset_left = 132.0
offset_right = 192.0
offset_bottom = 25.0
custom_minimum_size = Vector2( 60, 0 )
custom_minimum_size = Vector2(60, 0)
layout_mode = 2
text = "Clear"
[connection signal="pressed" from="Layout/SetButton" to="." method="_on_SetButton_pressed"]

View file

@ -16,9 +16,9 @@ dest_files=["res://.godot/imported/arrow.png-2b5b2d838b5b3467cf300ac2da1630d9.ct
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false

View file

@ -1,4 +1,6 @@
# ------------------------------------------------------------------------------
# Choose an existing directory from res://. Dialog allows for creating a
# directory.
# ------------------------------------------------------------------------------
class DirectoryCtrl:
extends HBoxContainer
@ -9,20 +11,20 @@ class DirectoryCtrl:
set(val):
_txt_path.text = val
var _txt_path = LineEdit.new()
var _btn_dir = Button.new()
var _dialog = FileDialog.new()
var _txt_path := LineEdit.new()
var _btn_dir := Button.new()
var _dialog := FileDialog.new()
func _init():
_btn_dir.text = "..."
_btn_dir.connect("pressed", Callable(self, "_on_dir_button_pressed"))
_btn_dir.pressed.connect(_on_dir_button_pressed)
_txt_path.size_flags_horizontal = _txt_path.SIZE_EXPAND_FILL
_dialog.mode = _dialog.FILE_MODE_OPEN_DIR
_dialog.file_mode = _dialog.FILE_MODE_OPEN_DIR
_dialog.unresizable = false
_dialog.connect("dir_selected", Callable(self, "_on_selected"))
_dialog.connect("file_selected", Callable(self, "_on_selected"))
_dialog.dir_selected.connect(_on_selected)
_dialog.file_selected.connect(_on_selected)
_dialog.size = Vector2(1000, 700)
func _on_selected(path):
@ -42,12 +44,27 @@ class DirectoryCtrl:
# ------------------------------------------------------------------------------
# Choose an existing file in res://
# ------------------------------------------------------------------------------
class FileCtrl:
extends DirectoryCtrl
func _init():
_dialog.mode = _dialog.FILE_MODE_OPEN_FILE
super._init()
_dialog.file_mode = _dialog.FILE_MODE_OPEN_FILE
# ------------------------------------------------------------------------------
# Choose a save location. Can pick anywhere on file system. Will warn if you
# pick a file that already exists.
# ------------------------------------------------------------------------------
class SaveFileAnywhere:
extends DirectoryCtrl
func _init():
super._init()
_dialog.file_mode = _dialog.FILE_MODE_SAVE_FILE
_dialog.access = _dialog.ACCESS_FILESYSTEM
# ------------------------------------------------------------------------------
@ -127,6 +144,11 @@ func _init(cont):
_base_control.add_child(lbl)
func _notification(what):
if what == NOTIFICATION_PREDELETE:
_base_control.free()
# ------------------
# Private
# ------------------
@ -134,7 +156,7 @@ func _new_row(key, disp_text, value_ctrl, hint):
var ctrl = _base_control.duplicate()
var lbl = ctrl.get_child(0)
lbl.hint_tooltip = hint
lbl.tooltip_text = hint
lbl.text = disp_text
_base_container.add_child(ctrl)
@ -167,7 +189,7 @@ func _add_number(key, value, disp_text, v_min, v_max, hint = ""):
value_ctrl.max_value = v_max
_wire_select_on_focus(value_ctrl.get_line_edit())
_new_row(key, disp_text, value_ctrl, hint)
return _new_row(key, disp_text, value_ctrl, hint)
func _add_select(key, value, values, disp_text, hint = ""):
@ -180,7 +202,7 @@ func _add_select(key, value, values, disp_text, hint = ""):
value_ctrl.selected = select_idx
value_ctrl.size_flags_horizontal = value_ctrl.SIZE_EXPAND_FILL
_new_row(key, disp_text, value_ctrl, hint)
return _new_row(key, disp_text, value_ctrl, hint)
func _add_value(key, value, disp_text, hint = ""):
@ -189,14 +211,14 @@ func _add_value(key, value, disp_text, hint = ""):
value_ctrl.text = value
_wire_select_on_focus(value_ctrl)
_new_row(key, disp_text, value_ctrl, hint)
return _new_row(key, disp_text, value_ctrl, hint)
func _add_boolean(key, value, disp_text, hint = ""):
var value_ctrl = CheckBox.new()
value_ctrl.button_pressed = value
_new_row(key, disp_text, value_ctrl, hint)
return _new_row(key, disp_text, value_ctrl, hint)
func _add_directory(key, value, disp_text, hint = ""):
@ -205,7 +227,7 @@ func _add_directory(key, value, disp_text, hint = ""):
value_ctrl.text = value
_wire_select_on_focus(value_ctrl.get_line_edit())
_new_row(key, disp_text, value_ctrl, hint)
return _new_row(key, disp_text, value_ctrl, hint)
func _add_file(key, value, disp_text, hint = ""):
@ -214,7 +236,16 @@ func _add_file(key, value, disp_text, hint = ""):
value_ctrl.text = value
_wire_select_on_focus(value_ctrl.get_line_edit())
_new_row(key, disp_text, value_ctrl, hint)
return _new_row(key, disp_text, value_ctrl, hint)
func _add_save_file_anywhere(key, value, disp_text, hint = ""):
var value_ctrl = SaveFileAnywhere.new()
value_ctrl.size_flags_horizontal = value_ctrl.SIZE_EXPAND_FILL
value_ctrl.text = value
_wire_select_on_focus(value_ctrl.get_line_edit())
return _new_row(key, disp_text, value_ctrl, hint)
func _add_color(key, value, disp_text, hint = ""):
@ -222,7 +253,7 @@ func _add_color(key, value, disp_text, hint = ""):
value_ctrl.size_flags_horizontal = value_ctrl.SIZE_EXPAND_FILL
value_ctrl.color = value
_new_row(key, disp_text, value_ctrl, hint)
return _new_row(key, disp_text, value_ctrl, hint)
func _add_vector2(key, value, disp_text, hint = ""):
@ -232,7 +263,7 @@ func _add_vector2(key, value, disp_text, hint = ""):
_wire_select_on_focus(value_ctrl.x_spin.get_line_edit())
_wire_select_on_focus(value_ctrl.y_spin.get_line_edit())
_new_row(key, disp_text, value_ctrl, hint)
return _new_row(key, disp_text, value_ctrl, hint)
# -----------------------------
@ -242,7 +273,6 @@ func _add_vector2(key, value, disp_text, hint = ""):
# Events
# ------------------
func _wire_select_on_focus(which):
pass
which.connect("focus_entered", _on_ctrl_focus_highlight.bind(which))
which.connect("focus_exited", _on_ctrl_focus_unhighlight.bind(which))
@ -273,8 +303,7 @@ func get_config_issues():
var path = _cfg_ctrls[key].text
if path != null and path != "":
has_directory = true
var dir = DirAccess.open(".")
if !dir.dir_exists(path):
if !DirAccess.dir_exists_absolute(path):
to_return.append(str("Test directory ", path, " does not exist."))
if !has_directory:
@ -286,8 +315,23 @@ func get_config_issues():
return to_return
# --------------
# SUPER dumb but VERY fun hack to hide settings. The various _add methods will
# return what they add. If you want to hide it, just assign the result to this.
# YES, I could have just put .visible at the end, but I didn't think of that
# until just now, and this was fun, non-permanent and the .visible at the end
# isn't as obvious as hide_this =
#
# Also, we can't just skip adding the controls because other things are looking
# for them and things start to blow up if you don't add them.
var hide_this = null:
set(val):
val.visible = false
# --------------
func set_options(options):
_add_title("Settings")
_add_title("Settings") # ----------------------------------
_add_number(
"log_level",
options.log_level,
@ -321,26 +365,48 @@ func set_options(options):
"Exit on Success",
"Exit if there are no failures. Does nothing if 'Exit on Finish' is enabled."
)
var ds = _add_select(
"double_strategy",
"Script Only",
["Include Native", "Script Only"],
"Double Strategy",
(
'"Include Native" will include native methods in Doubles. "Script Only" will not. '
+ "\n"
+ "The native method override warning is disabled when creating Doubles."
+ "\n"
+ "This is the default, you can override this at the script level or when creating doubles."
)
)
_cfg_ctrls["double_strategy"].selected = GutUtils.get_enum_value(
options.double_strategy, GutUtils.DOUBLE_STRATEGY, GutUtils.DOUBLE_STRATEGY.SCRIPT_ONLY
)
_add_boolean(
"errors_cause_failure",
!options.errors_do_not_cause_failure,
"Errors cause failures.",
"When GUT generates an error (not an engine error) it causes tests to fail."
)
_add_title("Panel Output")
_add_title("Panel Output") # ----------------------------------
_add_select(
"output_font_name",
options.panel_options.font_name,
options.panel_options.output_font_name,
_avail_fonts,
"Font",
"The name of the font to use when running tests and in the output panel to the left."
)
_add_number(
"output_font_size",
options.panel_options.font_size,
options.panel_options.output_font_size,
"Font Size",
5,
100,
"The font size to use when running tests and in the output panel to the left."
)
_add_title("Runner Window")
_add_boolean(
_add_title("Runner Window") # ----------------------------------
hide_this = _add_boolean(
"gut_on_top",
options.gut_on_top,
"On Top",
@ -349,7 +415,7 @@ func set_options(options):
_add_number(
"opacity", options.opacity, "Opacity", 0, 100, "The opacity of GUT when tests are running."
)
_add_boolean(
hide_this = _add_boolean(
"should_maximize",
options.should_maximize,
"Maximize",
@ -362,7 +428,7 @@ func set_options(options):
"The runner will be in compact mode. This overrides Maximize."
)
_add_title("Runner Appearance")
_add_title("Runner Appearance") # ----------------------------------
_add_select(
"font_name",
options.font_name,
@ -378,7 +444,7 @@ func set_options(options):
100,
"The font size for text output in the Gut Runner."
)
_add_color(
hide_this = _add_color(
"font_color",
options.font_color,
"Font Color",
@ -397,7 +463,7 @@ func set_options(options):
"Disable formatting and colors used in the Runner. Does not affect panel output."
)
_add_title("Test Directories")
_add_title("Test Directories") # ----------------------------------
_add_boolean(
"include_subdirs",
options.include_subdirs,
@ -411,11 +477,11 @@ func set_options(options):
_add_directory(str("directory_", i), value, str("Directory ", i))
_add_title("XML Output")
_add_value(
_add_title("XML Output") # ----------------------------------
_add_save_file_anywhere(
"junit_xml_file",
options.junit_xml_file,
"Output Path3D",
"Output Path",
(
"Path3D and filename where GUT should create a JUnit compliant XML file. "
+ "This file will contain the results of the last test run. To avoid "
@ -429,7 +495,7 @@ func set_options(options):
"Include a timestamp in the filename so that each run gets its own xml file."
)
_add_title("Hooks")
_add_title("Hooks") # ----------------------------------
_add_file(
"pre_run_script",
options.pre_run_script,
@ -443,7 +509,7 @@ func set_options(options):
"This script will be run by GUT after all tests are run."
)
_add_title("Misc")
_add_title("Misc") # ----------------------------------
_add_value(
"prefix", options.prefix, "Script Prefix", "The filename prefix for all test scripts."
)
@ -466,7 +532,7 @@ func set_options(options):
_cfg_ctrls.paint_after.step = .05
_cfg_ctrls.paint_after.value = options.paint_after
print("paint after = ", options.paint_after)
print("GUT config loaded")
func get_options(base_opts):
@ -478,11 +544,13 @@ func get_options(base_opts):
to_return.hide_orphans = _cfg_ctrls.hide_orphans.button_pressed
to_return.should_exit = _cfg_ctrls.should_exit.button_pressed
to_return.should_exit_on_success = _cfg_ctrls.should_exit_on_success.button_pressed
to_return.double_strategy = _cfg_ctrls.double_strategy.selected
to_return.errors_do_not_cause_failure = !_cfg_ctrls.errors_cause_failure.button_pressed
#Output
to_return.panel_options.font_name = (_cfg_ctrls.output_font_name.get_item_text(
to_return.panel_options.font_name = _cfg_ctrls.output_font_name.get_item_text(
_cfg_ctrls.output_font_name.selected
))
)
to_return.panel_options.font_size = _cfg_ctrls.output_font_size.value
# Runner Appearance

238
addons/gut/gui/gut_gui.gd Normal file
View file

@ -0,0 +1,238 @@
extends Control
# ##############################################################################
# This is the decoupled GUI for gut.gd
#
# This is a "generic" interface between a GUI and gut.gd. It assumes there are
# certain controls with specific names. It will then interact with those
# controls based on signals emitted from gut.gd in order to give the user
# feedback about the progress of the test run and the results.
#
# Optional controls are marked as such in the _ctrls dictionary. The names
# of the controls can be found in _populate_ctrls.
# ##############################################################################
var _gut = null
var _ctrls = {
btn_continue = null,
path_dir = null,
path_file = null,
prog_script = null,
prog_test = null,
rtl = null, # optional
rtl_bg = null, # required if rtl exists
switch_modes = null,
time_label = null,
title = null,
title_bar = null,
}
var _title_mouse = {down = false}
var _resize_mouse = {down = false}
var _resize_left_mouse = {down = false}
signal switch_modes
var _max_position = Vector2(100, 100)
var _utils = null
func _ready():
_populate_ctrls()
_ctrls.btn_continue.visible = false
_ctrls.btn_continue.pressed.connect(_on_continue_pressed)
_ctrls.switch_modes.pressed.connect(_on_switch_modes_pressed)
_ctrls.title_bar.gui_input.connect(_on_title_bar_input)
_ctrls.prog_script.value = 0
_ctrls.prog_test.value = 0
_ctrls.path_dir.text = ""
_ctrls.path_file.text = ""
_ctrls.time_label.text = ""
_max_position = get_display_size() - Vector2(30, _ctrls.title_bar.size.y)
func _process(_delta):
if _gut != null and _gut.is_running():
set_elapsed_time(_gut.get_elapsed_time())
# ------------------
# Private
# ------------------
func get_display_size():
return get_viewport().get_visible_rect().size
func _populate_ctrls():
# Brute force, but flexible. This allows for all the controls to exist
# anywhere, and as long as they all have the right name, they will be
# found.
_ctrls.btn_continue = _get_first_child_named("Continue", self)
_ctrls.path_dir = _get_first_child_named("Path", self)
_ctrls.path_file = _get_first_child_named("File", self)
_ctrls.prog_script = _get_first_child_named("ProgressScript", self)
_ctrls.prog_test = _get_first_child_named("ProgressTest", self)
_ctrls.rtl = _get_first_child_named("TestOutput", self)
_ctrls.rtl_bg = _get_first_child_named("OutputBG", self)
_ctrls.switch_modes = _get_first_child_named("SwitchModes", self)
_ctrls.time_label = _get_first_child_named("TimeLabel", self)
_ctrls.title = _get_first_child_named("Title", self)
_ctrls.title_bar = _get_first_child_named("TitleBar", self)
func _get_first_child_named(obj_name, parent_obj):
if parent_obj == null:
return null
var kids = parent_obj.get_children()
var index = 0
var to_return = null
while index < kids.size() and to_return == null:
if str(kids[index]).find(str(obj_name, ":")) != -1:
to_return = kids[index]
else:
to_return = _get_first_child_named(obj_name, kids[index])
if to_return == null:
index += 1
return to_return
# ------------------
# Events
# ------------------
func _on_title_bar_input(event: InputEvent):
if event is InputEventMouseMotion:
if _title_mouse.down:
position += event.relative
position.x = clamp(position.x, 0, _max_position.x)
position.y = clamp(position.y, 0, _max_position.y)
elif event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
_title_mouse.down = event.pressed
func _on_continue_pressed():
_gut.end_teardown_pause()
func _on_gut_start_run():
if _ctrls.rtl != null:
_ctrls.rtl.clear()
set_num_scripts(_gut.get_test_collector().scripts.size())
func _on_gut_end_run():
_ctrls.prog_test.value = _ctrls.prog_test.max_value
_ctrls.prog_script.value = _ctrls.prog_script.max_value
func _on_gut_start_script(script_obj):
next_script(script_obj.get_full_name(), script_obj.tests.size())
func _on_gut_end_script():
pass
func _on_gut_start_test(test_name):
next_test(test_name)
func _on_gut_end_test():
pass
func _on_gut_start_pause():
pause_before_teardown()
func _on_gut_end_pause():
_ctrls.btn_continue.visible = false
func _on_switch_modes_pressed():
switch_modes.emit()
# ------------------
# Public
# ------------------
func set_num_scripts(val):
_ctrls.prog_script.value = 0
_ctrls.prog_script.max_value = val
func next_script(path, num_tests):
_ctrls.prog_script.value += 1
_ctrls.prog_test.value = 0
_ctrls.prog_test.max_value = num_tests
_ctrls.path_dir.text = path.get_base_dir()
_ctrls.path_file.text = path.get_file()
func next_test(test_name):
_ctrls.prog_test.value += 1
func pause_before_teardown():
_ctrls.btn_continue.visible = true
func set_gut(g):
if _gut == g:
return
_gut = g
g.start_run.connect(_on_gut_start_run)
g.end_run.connect(_on_gut_end_run)
g.start_script.connect(_on_gut_start_script)
g.end_script.connect(_on_gut_end_script)
g.start_test.connect(_on_gut_start_test)
g.end_test.connect(_on_gut_end_test)
g.start_pause_before_teardown.connect(_on_gut_start_pause)
g.end_pause_before_teardown.connect(_on_gut_end_pause)
func get_gut():
return _gut
func get_textbox():
return _ctrls.rtl
func set_elapsed_time(t):
_ctrls.time_label.text = str("%6.1f" % t, "s")
func set_bg_color(c):
_ctrls.rtl_bg.color = c
func set_title(text):
_ctrls.title.text = text
func to_top_left():
self.position = Vector2(5, 5)
func to_bottom_right():
var win_size = get_display_size()
self.position = win_size - Vector2(self.size) - Vector2(5, 5)
func align_right():
var win_size = get_display_size()
self.position.x = win_size.x - self.size.x - 5
self.position.y = 5
self.size.y = win_size.y - 10

View file

@ -16,9 +16,9 @@ dest_files=["res://.godot/imported/play.png-5c90e88e8136487a183a099d67a7de24.cte
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false

View file

@ -1,5 +1,5 @@
# Holds weakrefs to a ScriptTextEditor and related children nodes
# that might be useful. Though the TextEdit is really the only one, but
# that might be useful. Though the CodeEdit is really the only one, but
# since the tree may change, the first TextEdit under a CodeTextEditor is
# the one we use...so we hold a ref to the CodeTextEditor too.
class ScriptEditorControlRef:
@ -10,6 +10,7 @@ class ScriptEditorControlRef:
func _init(script_edit):
_script_editor = weakref(script_edit)
_populate_controls()
# print("_script_editor = ", script_edit, ' vis = ', is_visible())
func _populate_controls():
# who knows if the tree will change so get the first instance of each
@ -27,7 +28,7 @@ class ScriptEditorControlRef:
var to_return = null
while index < kids.size() and to_return == null:
if str(kids[index]).find(str("[", obj_name)) != -1:
if str(kids[index]).find(str("<", obj_name)) != -1:
to_return = kids[index]
else:
to_return = _get_first_child_named(obj_name, kids[index])
@ -79,7 +80,7 @@ func _init(script_edit):
func _is_script_editor(obj):
return str(obj).find("[ScriptTextEditor") != -1
return str(obj).find("<ScriptTextEditor") != -1
# Find the first ScriptTextEditor and then get its parent. Done this way
@ -106,14 +107,19 @@ func _populate_editors():
# easier than trying to find a place where it could be used by both.
func _get_first_child_of_type_name(obj_name, parent_obj):
if parent_obj == null:
# print('aborting search for ', obj_name, ' parent is null')
return null
var kids = parent_obj.get_children()
var index = 0
var to_return = null
var search_for = str("<", obj_name)
# print('searching for ', search_for, ' in ', parent_obj, ' kids ', kids.size())
while index < kids.size() and to_return == null:
if str(kids[index]).find(str("[", obj_name)) != -1:
var this_one = str(kids[index])
# print(search_for, ' :: ', this_one)
if this_one.find(search_for) != -1:
to_return = kids[index]
else:
to_return = _get_first_child_of_type_name(obj_name, kids[index])
@ -140,10 +146,13 @@ func _get_class_name_from_line(text):
func refresh():
if _script_editors_parent == null:
_find_script_editors_parent()
# print("script editors parent = ", _script_editors_parent)
if _script_editors_parent != null:
_populate_editors()
# print("script editor controls = ", _script_editor_controls)
func get_current_text_edit():
var cur_script_editor = null

View file

@ -1,36 +1,13 @@
# ##############################################################################
#(G)odot (U)nit (T)est class
#
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2020 Tom "Butch" Wesley
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ##############################################################################
# View the readme at https://github.com/bitwes/Gut/blob/master/README.md for usage details.
# You should also check out the github wiki at: https://github.com/bitwes/Gut/wiki
# ##############################################################################
extends "res://addons/gut/gut_to_move.gd"
# ##############################################################################
#
# View the readme at https://github.com/bitwes/Gut/blob/master/README.md for usage
# details. You should also check out the github wiki at:
# https://github.com/bitwes/Gut/wiki
#
# ##############################################################################
# ###########################
# Constants
# ###########################
@ -89,7 +66,7 @@ var temp_directory = _temp_directory:
var _log_level = 1
## The log detail level. Valid values are 0 - 2. Larger values do not matter.
var log_level = 1:
var log_level = _log_level:
get:
return _log_level
set(val):
@ -123,14 +100,17 @@ var include_subdirectories = _include_subdirectories:
set(val):
_include_subdirectories = val
var _double_strategy = 1
var _double_strategy = GutUtils.DOUBLE_STRATEGY.SCRIPT_ONLY
## TODO rework what this is and then document it here.
var double_strategy = 1:
var double_strategy = _double_strategy:
get:
return _double_strategy
set(val):
if GutUtils.DOUBLE_STRATEGY.values().has(val):
_double_strategy = val
_doubler.set_strategy(double_strategy)
else:
_lgr.error(str("gut.gd: invalid double_strategy ", val))
var _pre_run_script = ""
## Path to the script that will be run before all tests are run. This script
@ -194,10 +174,6 @@ var unit_test_name = _unit_test_name:
set(val):
_unit_test_name = val
# ###########################
# Public Properties
# ###########################
var _parameter_handler = null
# This is populated by test.gd each time a paramterized test is encountered
# for the first time.
@ -230,6 +206,13 @@ var add_children_to = self:
set(val):
_add_children_to = val
var _treat_error_as_failure = true
var treat_error_as_failure = _treat_error_as_failure:
get:
return _treat_error_as_failure
set(val):
_treat_error_as_failure = val
# ------------
# Read only
# ------------
@ -317,11 +300,11 @@ var _done = false
# msecs ticks when run was started
var _start_time = 0.0
# Collected Test instance for the current test being run.
var _current_test = null
var _pause_before_teardown = false
var _awaiter = _utils.Awaiter.new()
var _new_summary = null
# Used to cancel importing scripts if an error has occurred in the setup. This
# prevents tests from being run if they were exported and ensures that the
@ -331,17 +314,14 @@ var _new_summary = null
# was not broken somewhere and remove if no longer used.
var _cancel_import = false
# Used for proper assert tracking and printing during before_all
var _before_all_test_obj = load("res://addons/gut/test_collector.gd").Test.new()
# Used for proper assert tracking and printing during after_all
var _after_all_test_obj = load("res://addons/gut/test_collector.gd").Test.new()
# this is how long Gut will wait when there are items that must be queued free
# when a test completes (due to calls to add_child_autoqfree)
var _auto_queue_free_delay = .1
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func _init():
_before_all_test_obj.name = "before_all"
_after_all_test_obj.name = "after_all"
# When running tests for GUT itself, _utils has been setup to always return
# a new logger so this does not set the gut instance on the base logger
# when creating test instances of GUT.
@ -390,6 +370,8 @@ func _notification(what):
test_script.free()
_test_script_objects = []
if is_instance_valid(_awaiter):
_awaiter.free()
func _print_versions(send_all = true):
@ -450,8 +432,6 @@ func end_teardown_pause():
# Private
#
#####################
func _log_test_children_warning(test_script):
if !_lgr.is_type_enabled(_lgr.types.orphan):
return
@ -474,48 +454,10 @@ func _log_test_children_warning(test_script):
_lgr.warn(msg)
# ------------------------------------------------------------------------------
# Convert the _summary dictionary into text
# ------------------------------------------------------------------------------
func _print_summary():
if !_should_print_summary:
return
_lgr.log("\n\n\n")
_lgr.log("==============================================", _lgr.fmts.yellow)
_lgr.log("= Run Summary", _lgr.fmts.yellow)
_lgr.log("==============================================", _lgr.fmts.yellow)
_new_summary.log_summary_text(_lgr)
var logger_text = ""
if _lgr.get_errors().size() > 0:
logger_text += str("\n* ", _lgr.get_errors().size(), " Errors.")
if _lgr.get_warnings().size() > 0:
logger_text += str("\n* ", _lgr.get_warnings().size(), " Warnings.")
if _lgr.get_deprecated().size() > 0:
logger_text += str("\n* ", _lgr.get_deprecated().size(), " Deprecated calls.")
if logger_text != "":
logger_text = "\nWarnings/Errors:" + logger_text + "\n\n"
_lgr.log(logger_text)
if _new_summary.get_totals().tests > 0:
var fmt = _lgr.fmts.green
var msg = (
str(_new_summary.get_totals().passing_tests)
+ " passed "
+ str(_new_summary.get_totals().failing_tests)
+ " failed. "
+ str("Tests finished in ", get_elapsed_time(), "s")
)
if _new_summary.get_totals().failing > 0:
fmt = _lgr.fmts.red
elif _new_summary.get_totals().pending > 0:
fmt = _lgr.fmts.yellow
_lgr.log(msg, fmt)
else:
_lgr.log("No tests ran", _lgr.fmts.red)
func _log_end_run():
if _should_print_summary:
var summary = _utils.Summary.new(self)
summary.log_end_run()
func _validate_hook_script(path):
@ -527,7 +469,7 @@ func _validate_hook_script(path):
if FileAccess.file_exists(path):
var inst = load(path).new()
if inst and inst is _utils.HookScript:
if inst and inst is GutHookScript:
result.instance = inst
result.valid = true
else:
@ -558,7 +500,6 @@ func _init_run():
var valid = true
_test_collector.set_test_class_prefix(_inner_class_prefix)
_test_script_objects = []
_new_summary = _utils.Summary.new()
_current_test = null
_is_running = true
@ -576,36 +517,13 @@ func _init_run():
# Print out run information and close out the run.
# ------------------------------------------------------------------------------
func _end_run():
_print_summary()
p("\n")
# Do not count any of the _test_script_objects since these will be released
# when GUT is released.
_orphan_counter._counters.total += _test_script_objects.size()
if _orphan_counter.get_counter("total") > 0 and _lgr.is_type_enabled("orphan"):
_orphan_counter.print_orphans("total", _lgr)
p("Note: This count does not include GUT objects that will be freed upon exit.")
p(" It also does not include any orphans created by global scripts")
p(" loaded before tests were ran.")
p(str("Total orphans = ", _orphan_counter.orphan_count()))
if !_utils.is_null_or_empty(_select_script):
p('Ran Scripts matching "' + _select_script + '"')
if !_utils.is_null_or_empty(_unit_test_name):
p('Ran Tests matching "' + _unit_test_name + '"')
if !_utils.is_null_or_empty(_inner_class_name):
p('Ran Inner Classes matching "' + _inner_class_name + '"')
_log_end_run()
_is_running = false
_run_hook_script(_post_run_script_instance)
_export_results()
end_run.emit()
if _utils.should_display_latest_version:
p("")
p(str("GUT version ", _utils.latest_version, " is now available."))
# ------------------------------------------------------------------------------
# Add additional export types here.
@ -630,34 +548,12 @@ func _export_junit_xml():
p(str("Results saved to ", output_file))
# ------------------------------------------------------------------------------
# Checks the passed in thing to see if it is a "function state" object that gets
# returned when a function yields.
# ------------------------------------------------------------------------------
func _is_function_state(script_result):
return false
# TODO 4.0 Keep this until we know how they are going to handle the
# 4.0 equivalent of GDScriptFunctionState
# return script_result != null and \
# typeof(script_result) == TYPE_OBJECT and \
# script_result is GDScriptFunctionState and \
# script_result.is_valid()
# ------------------------------------------------------------------------------
# Print out the heading for a new script
# ------------------------------------------------------------------------------
func _print_script_heading(script):
if _does_class_name_match(_inner_class_name, script.inner_class_name):
var fmt = _lgr.fmts.underline
var divider = "-----------------------------------------"
var text = ""
if script.inner_class_name == null:
text = script.path
else:
text = str(script.path, ".", script.inner_class_name)
_lgr.log("\n\n" + text, fmt)
func _print_script_heading(coll_script):
if _does_class_name_match(_inner_class_name, coll_script.inner_class_name):
_lgr.log(str("\n\n", coll_script.get_full_name()), _lgr.fmts.underline)
# ------------------------------------------------------------------------------
@ -713,15 +609,9 @@ func _get_indexes_matching_path(path):
# ------------------------------------------------------------------------------
func _run_parameterized_test(test_script, test_name):
await _run_test(test_script, test_name)
# TODO 4.0 GDScriptFunctionState? ----
# var script_result = await _run_test(test_script, test_name)
# if(_is_function_state(script_result)):
# # _run_tests does _wait_for_done so just wait on it to complete
# await script_result.COMPLETED
# ----
if _current_test.assert_count == 0 and !_current_test.pending:
_lgr.warn("Test did not assert")
_lgr.risky("Test did not assert")
if _parameter_handler == null:
_lgr.error(
@ -742,14 +632,8 @@ func _run_parameterized_test(test_script, test_name):
while !_parameter_handler.is_done():
var cur_assert_count = _current_test.assert_count
await _run_test(test_script, test_name)
# TODO 4.0 GDScriptFunctionState? ----
# script_result = await _run_test(test_script, test_name)
# if(_is_function_state(script_result)):
# # _run_tests does _wait_for_done so just wait on it to complete
# await script_result.COMPLETED
# ----
if _current_test.assert_count == cur_assert_count and !_current_test.pending:
_lgr.warn("Test did not assert")
_lgr.risky("Test did not assert")
_parameter_handler = null
@ -764,28 +648,10 @@ func _run_test(script_inst, test_name):
var script_result = null
await script_inst.before_each()
# TODO 4.0 GDScriptFunctionState? ----
# var before_each_result = script_inst.before_each()
# if(_is_function_state(before_each_result)):
# await _wait_for_done(before_each_result)
# ----
start_test.emit(test_name)
await script_inst.call(test_name)
# TODO 4.0 GDScriptFunctionState? ----
# script_result = await script_inst.call(test_name)
# if(_is_function_state(script_result)):
# await _wait_for_done(script_result)
# ----
var test_summary = _new_summary.add_test(test_name)
if test_summary == null:
var msg = "Summary was null. This has been seen to happen when a test \n"
msg += "calls unreference. Adding 'await get_tree().process_frame' somewhere between\n"
msg += "instantiation and calling unreference, in your test, may fix this issue.\n"
msg += "More info at https://github.com/godotengine/godot/issues/69411"
_lgr.error(msg)
test_summary.force_a_runtime_error_to_stop_things_from_progressing_see_error_above = 1
# if the test called pause_before_teardown then await until
# the continue button is pressed.
@ -797,11 +663,6 @@ func _run_test(script_inst, test_name):
# call each post-each-test method until teardown is removed.
await script_inst.after_each()
# TODO 4.0 GDScriptFunctionState? ----
# var after_each_result = await script_inst.after_each()
# if(_is_function_state(after_each_result)):
# await _wait_for_done(after_each_result)
# ----
# Free up everything in the _autofree. Yield for a bit if we
# have anything with a queue_free so that they have time to
@ -809,9 +670,8 @@ func _run_test(script_inst, test_name):
var aqf_count = _autofree.get_queue_free_count()
_autofree.free_all()
if aqf_count > 0:
await get_tree().create_timer(.25).timeout
await get_tree().create_timer(_auto_queue_free_delay).timeout
test_summary.orphans = _orphan_counter.get_counter("test")
if _log_level > 0:
_orphan_counter.print_orphans("test", _lgr)
@ -824,23 +684,22 @@ func _run_test(script_inst, test_name):
#
# Calls both pre-all-tests methods until prerun_setup is removed
# ------------------------------------------------------------------------------
func _call_before_all(test_script):
_current_test = _before_all_test_obj
_current_test.has_printed_name = false
func _call_before_all(test_script, collected_script):
var before_all_test_obj = _utils.CollectedTest.new()
before_all_test_obj.has_printed_name = false
before_all_test_obj.name = "before_all"
collected_script.setup_teardown_tests.append(before_all_test_obj)
_current_test = before_all_test_obj
_lgr.inc_indent()
# Next 3 lines can be removed when prerun_setup removed.
_current_test.name = "prerun_setup"
_current_test.name = "before_all"
await test_script.before_all()
# TODO 4.0 GDScriptFunctionState? ----
# var result = test_script.before_all()
# if(_is_function_state(result)):
# await _wait_for_done(result)
# ----
# before all does not need to assert anything so only mark it as run if
# some assert was done.
before_all_test_obj.was_run = before_all_test_obj.did_something()
_lgr.dec_indent()
_current_test = null
@ -850,23 +709,21 @@ func _call_before_all(test_script):
#
# Calls both post-all-tests methods until postrun_teardown is removed.
# ------------------------------------------------------------------------------
func _call_after_all(test_script):
_current_test = _after_all_test_obj
_current_test.has_printed_name = false
func _call_after_all(test_script, collected_script):
var after_all_test_obj = _utils.CollectedTest.new()
after_all_test_obj.has_printed_name = false
after_all_test_obj.name = "after_all"
collected_script.setup_teardown_tests.append(after_all_test_obj)
_current_test = after_all_test_obj
_lgr.inc_indent()
# Next 3 lines can be removed when postrun_teardown removed.
_current_test.name = "postrun_teardown"
_current_test.name = "after_all"
await test_script.after_all()
# TODO 4.0 GDScriptFunctionState? ----
# var result = test_script.after_all()
# if(_is_function_state(result)):
# await _wait_for_done(result)
# ----
# after all does not need to assert anything so only mark it as run if
# some assert was done.
after_all_test_obj.was_run = after_all_test_obj.did_something()
_lgr.dec_indent()
_current_test = null
@ -901,20 +758,19 @@ func _test_the_scripts(indexes = []):
# loop through scripts
for test_indexes in range(indexes_to_run.size()):
var the_script = _test_collector.scripts[indexes_to_run[test_indexes]]
var coll_script = _test_collector.scripts[indexes_to_run[test_indexes]]
_orphan_counter.add_counter("script")
if the_script.tests.size() > 0:
if coll_script.tests.size() > 0:
_lgr.set_indent_level(0)
_print_script_heading(the_script)
_new_summary.add_script(the_script.get_full_name())
_print_script_heading(coll_script)
if !the_script.is_loaded:
if !coll_script.is_loaded:
break
start_script.emit(the_script)
start_script.emit(coll_script)
var test_script = the_script.get_new()
var test_script = coll_script.get_new()
# ----
# SHORTCIRCUIT
@ -925,8 +781,8 @@ func _test_the_scripts(indexes = []):
_lgr.inc_indent()
_lgr.log(msg, _lgr.fmts.yellow)
_lgr.dec_indent()
_new_summary.get_current_script().was_skipped = true
_new_summary.get_current_script().skip_reason = skip_script
coll_script.skip_reason = skip_script
coll_script.was_skipped = true
continue
# ----
@ -939,32 +795,24 @@ func _test_the_scripts(indexes = []):
# inner class is set and we do not have a match then empty the tests
# for the current test.
# !!!
if !_does_class_name_match(_inner_class_name, the_script.inner_class_name):
the_script.tests = []
if !_does_class_name_match(_inner_class_name, coll_script.inner_class_name):
coll_script.tests = []
else:
await _call_before_all(test_script)
# TODO 4.0 GDScriptFunctionState? ----
# var before_all_result = await _call_before_all(test_script)
# if(_is_function_state(before_all_result)):
# # _call_before_all calls _wait for done, just wait for that to finish
# await before_all_result.COMPLETED
# ----
coll_script.was_run = true
await _call_before_all(test_script, coll_script)
# Each test in the script
var skip_suffix = "_skip__"
the_script.mark_tests_to_skip_with_suffix(skip_suffix)
for i in range(the_script.tests.size()):
coll_script.mark_tests_to_skip_with_suffix(skip_suffix)
for i in range(coll_script.tests.size()):
_stubber.clear()
_spy.clear()
_current_test = the_script.tests[i]
_current_test = coll_script.tests[i]
script_result = null
# ------------------
# SHORTCIRCUI
if _current_test.should_skip:
_new_summary.add_pending(
_current_test.name, "SKIPPED because it ends with " + skip_suffix
)
continue
# ------------------
@ -983,18 +831,14 @@ func _test_the_scripts(indexes = []):
)
)
elif _current_test.arg_count == 1:
_current_test.was_run = true
script_result = await _run_parameterized_test(test_script, _current_test.name)
else:
_current_test.was_run = true
script_result = await _run_test(test_script, _current_test.name)
# TODO 4.0 GDScriptFunctionState? ----
# if(_is_function_state(script_result)):
# # _run_test calls _wait for done, just wait for that to finish
# await script_result.COMPLETED
# ----
if !_current_test.did_assert():
_lgr.warn("Test did not assert")
if !_current_test.did_something():
_lgr.risky(str(_current_test.name, " did not assert"))
_current_test.has_printed_name = false
end_test.emit()
@ -1012,14 +856,8 @@ func _test_the_scripts(indexes = []):
_lgr.dec_indent()
_orphan_counter.print_orphans("script", _lgr)
if _does_class_name_match(_inner_class_name, the_script.inner_class_name):
await _call_after_all(test_script)
# TODO 4.0 GDScriptFunctionState? ----
# var after_all_result = await _call_after_all(test_script)
# if(_is_function_state(after_all_result)):
# # _call_after_all calls _wait for done, just wait for that to finish
# await after_all_result.COMPLETED
# ----
if _does_class_name_match(_inner_class_name, coll_script.inner_class_name):
await _call_after_all(test_script, coll_script)
_log_test_children_warning(test_script)
# This might end up being very resource intensive if the scripts
@ -1031,7 +869,10 @@ func _test_the_scripts(indexes = []):
_lgr.set_indent_level(0)
if test_script.get_assert_count() > 0:
var script_sum = str(
test_script.get_pass_count(), "/", test_script.get_assert_count(), " passed."
coll_script.get_passing_test_count(),
"/",
coll_script.get_ran_test_count(),
" passed."
)
_lgr.log(script_sum, _lgr.fmts.bold)
@ -1046,11 +887,22 @@ func _test_the_scripts(indexes = []):
# ------------------------------------------------------------------------------
func _pass(text = ""):
if _current_test:
_current_test.assert_count += 1
_new_summary.add_pass(_current_test.name, text)
else:
if _new_summary != null: # b/c of tests.
_new_summary.add_pass("script level", text)
_current_test.add_pass(text)
# ------------------------------------------------------------------------------
# Returns an empty string or "(call #x) " if the current test being run has
# parameters. The
# ------------------------------------------------------------------------------
func get_call_count_text():
var to_return = ""
if _parameter_handler != null:
# This uses get_call_count -1 because test.gd's use_parameters method
# should have been called before we get to any calls for this method
# just due to how use_parameters works. There isn't a way to know
# whether we are before or after that call.
to_return = str("params[", _parameter_handler.get_call_count() - 1, "] ")
return to_return
# ------------------------------------------------------------------------------
@ -1062,16 +914,26 @@ func _fail(text = ""):
p(line_text, LOG_LEVEL_FAIL_ONLY)
# format for summary
line_text = "\n " + line_text
var call_count_text = ""
if _parameter_handler != null:
call_count_text = str("(call #", _parameter_handler.get_call_count(), ") ")
_new_summary.add_fail(_current_test.name, call_count_text + text + line_text)
_current_test.passed = false
_current_test.assert_count += 1
var call_count_text = get_call_count_text()
_current_test.line_number = line_number
else:
if _new_summary != null: # b/c of tests.
_new_summary.add_fail("script level", text)
_current_test.add_fail(call_count_text + text + line_text)
# ------------------------------------------------------------------------------
# This is "private" but is only used by the logger, it is not used internally.
# It was either, make this weird method or "do it the right way" with signals
# or some other crazy mechanism.
# ------------------------------------------------------------------------------
func _fail_for_error(err_text):
if _current_test != null and treat_error_as_failure:
_fail(err_text)
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func _pending(text = ""):
if _current_test:
_current_test.add_pending(text)
# ------------------------------------------------------------------------------
@ -1090,14 +952,6 @@ func _extract_line_number(current_test):
return line_number
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func _pending(text = ""):
if _current_test:
_current_test.pending = true
_new_summary.add_pending(_current_test.name, text)
# ------------------------------------------------------------------------------
# Gets all the files in a directory and all subdirectories if include_subdirectories
# is true. The files returned are all sorted by name.
@ -1166,7 +1020,7 @@ func get_elapsed_time():
func p(text, level = 0):
var str_text = str(text)
if level <= _utils.nvl(_log_level, 0):
if level <= GutUtils.nvl(_log_level, 0):
_lgr.log(str_text)
@ -1181,8 +1035,6 @@ func p(text, level = 0):
# Runs all the scripts that were added using add_script
# ------------------------------------------------------------------------------
func test_scripts(run_rest = false):
clear_text()
if _script_name != null and _script_name != "":
var indexes = _get_indexes_matching_script_name(_script_name)
if indexes == []:
@ -1286,8 +1138,8 @@ func import_tests(path = _export_path):
_test_collector.clear()
var result = _test_collector.import_tests(path)
if result:
_lgr.info(_test_collector.to_s())
_lgr.info("Importd from " + path)
_lgr.info("\n" + _test_collector.to_s())
_lgr.info("Imported from " + path)
# ------------------------------------------------------------------------------
@ -1328,36 +1180,35 @@ func clear_text():
# Get the number of tests that were ran
# ------------------------------------------------------------------------------
func get_test_count():
return _new_summary.get_totals().tests
return _test_collector.get_ran_test_count()
# ------------------------------------------------------------------------------
# Get the number of assertions that were made
# ------------------------------------------------------------------------------
func get_assert_count():
var t = _new_summary.get_totals()
return t.passing + t.failing
return _test_collector.get_assert_count()
# ------------------------------------------------------------------------------
# Get the number of assertions that passed
# ------------------------------------------------------------------------------
func get_pass_count():
return _new_summary.get_totals().passing
return _test_collector.get_pass_count()
# ------------------------------------------------------------------------------
# Get the number of assertions that failed
# ------------------------------------------------------------------------------
func get_fail_count():
return _new_summary.get_totals().failing
return _test_collector.get_fail_count()
# ------------------------------------------------------------------------------
# Get the number of tests flagged as pending
# ------------------------------------------------------------------------------
func get_pending_count():
return _new_summary.get_totals().pending
return _test_collector.get_pending_count()
# ------------------------------------------------------------------------------
@ -1417,7 +1268,7 @@ func get_current_test_object():
## Returns a summary.gd object that contains all the information about
## the run results.
func get_summary():
return _new_summary
return _utils.Summary.new(self)
# ------------------------------------------------------------------------------
@ -1436,3 +1287,33 @@ func get_post_run_script_instance():
# ------------------------------------------------------------------------------
func show_orphans(should):
_lgr.set_type_enabled(_lgr.types.orphan, should)
func get_logger():
return _lgr
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2023 Tom "Butch" Wesley
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ##############################################################################

View file

@ -1,31 +1,4 @@
# ##############################################################################
#(G)odot (U)nit (T)est class
#
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2020 Tom "Butch" Wesley
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ##############################################################################
# ------------------------------------------------------------------------------
# Description
# -----------
# Command line interface for the GUT unit testing tool. Allows you to run tests
@ -36,7 +9,7 @@
#
# See the readme for a list of options and examples. You can also use the -gh
# option to get more information about how to use the command line interface.
# ##############################################################################
# ------------------------------------------------------------------------------
extends SceneTree
var Optparse = load("res://addons/gut/optparse.gd")
@ -44,7 +17,6 @@ var Gut = load("res://addons/gut/gut.gd")
var GutRunner = load("res://addons/gut/gui/GutRunner.tscn")
var json = JSON.new()
var exit_code = 0
# ------------------------------------------------------------------------------
@ -128,7 +100,7 @@ class OptionResolver:
# Here starts the actual script that uses the Options class to kick off Gut
# and run your tests.
# ------------------------------------------------------------------------------
var _utils = load("res://addons/gut/utils.gd").get_instance()
var _utils = null
var _gut_config = load("res://addons/gut/gut_config.gd").new()
# instance of gut
var _tester = null
@ -138,19 +110,47 @@ var _final_opts = []
func setup_options(options, font_names):
var opts = Optparse.new()
opts.set_banner(
(
"This is the command line interface for the unit testing tool Gut. With this "
+ "interface you can run one or more test scripts from the command line. In order "
+ "for the Gut options to not clash with any other godot options, each option starts "
+ 'with a "g". Also, any option that requires a value will take the form of '
+ '"-g<name>=<value>". There cannot be any spaces between the option, the "=", or '
+ "inside a specified value or godot will think you are trying to run a scene."
opts
. set_banner(
"""
The GUT CLI
-----------
The default behavior for GUT is to load options from a res://.gutconfig.json if
it exists. Any options specified on the command line will take precedence over
options specified in the gutconfig file. You can specify a different gutconfig
file with the -gconfig option.
To generate a .gutconfig.json file you can use -gprint_gutconfig_sample
To see the effective values of a CLI command and a gutconfig use -gpo
Any option that requires a value will take the form of \"-g<name>=<value>\".
There cannot be any spaces between the option, the \"=\", or ' + 'inside a
specified value or godot will think you are trying to run a scene.
"""
)
)
# Run specific things
opts.add(
"-gselect",
"",
"All scripts that contain the specified string in their filename will be ran"
)
opts.add(
"-ginner_class",
"",
"Only run inner classes that contain the specified string int their name."
)
opts.add(
"-gunit_test_name",
"",
"Any test that contains the specified text will be run, all others will be skipped."
)
opts.add("-gtest", [], "Comma delimited list of full paths to test scripts to run.")
# Run Config
opts.add("-ginclude_subdirs", false, "Include subdirectories of -gdir.")
opts.add("-gdir", options.dirs, "Comma delimited list of directories to add tests from.")
opts.add("-gtest", [], "Comma delimited list of full paths to test scripts to run.")
opts.add(
"-gprefix",
options.prefix,
@ -161,6 +161,39 @@ func setup_options(options, font_names):
options.suffix,
'Test script suffix, including .gd extension. Default "[default]".'
)
opts.add(
"-gconfig",
"res://.gutconfig.json",
"A config file that contains configuration information. Default is res://.gutconfig.json"
)
opts.add("-gpre_run_script", "", "pre-run hook script path")
opts.add("-gpost_run_script", "", "post-run hook script path")
(
opts
. add(
"-gerrors_do_not_cause_failure",
false,
"When an internal GUT error occurs tests will fail. With this option set, that does not happen."
)
)
(
opts
. add(
"-gdouble_strategy",
"SCRIPT_ONLY",
'Default strategy to use when doubling. Valid values are [INCLUDE_NATIVE, SCRIPT_ONLY]. Default "[default]"'
)
)
# Misc
opts.add(
"-gpaint_after",
options.paint_after,
"Delay before GUT will add a 1 frame pause to paint the screen/GUI. default [default]"
)
# Display options
opts.add("-glog", options.log_level, "Log level. Default [default]")
opts.add(
"-ghide_orphans",
false,
@ -170,63 +203,12 @@ func setup_options(options, font_names):
opts.add(
"-gcompact_mode", false, "The runner will be in compact mode. This overrides -gmaximize."
)
opts.add(
"-gexit",
false,
"Exit after running tests. If not specified you have to manually close the window."
)
opts.add("-gexit_on_success", false, "Only exit if all tests pass.")
opts.add("-glog", options.log_level, "Log level. Default [default]")
opts.add("-gignore_pause", false, "Ignores any calls to gut.pause_before_teardown.")
opts.add(
"-gselect",
"",
(
"Select a script to run initially. The first script that "
+ "was loaded using -gtest or -gdir that contains the specified "
+ "string will be executed. You may run others by interacting "
+ "with the GUI."
)
)
opts.add(
"-gunit_test_name",
"",
(
"Name of a test to run. Any test that contains the specified "
+ "text will be run, all others will be skipped."
)
)
opts.add("-gh", false, "Print this help, then quit")
opts.add(
"-gconfig",
"res://.gutconfig.json",
"A config file that contains configuration information. Default is res://.gutconfig.json"
)
opts.add("-ginner_class", "", "Only run inner classes that contain this string")
opts.add(
"-gopacity",
options.opacity,
"Set opacity of test runner window. Use range 0 - 100. 0 = transparent, 100 = opaque."
)
opts.add("-gpo", false, "Print option values from all sources and the value used, then quit.")
opts.add("-ginclude_subdirs", false, "Include subdirectories of -gdir.")
(
opts
. add(
"-gdouble_strategy",
"partial",
'Default strategy to use when doubling. Valid values are [partial, full]. Default "[default]"'
)
)
opts.add("-gdisable_colors", false, "Disable command line colors.")
opts.add("-gpre_run_script", "", "pre-run hook script path")
opts.add("-gpost_run_script", "", "post-run hook script path")
opts.add(
"-gprint_gutconfig_sample",
false,
"Print out json that can be used to make a gutconfig file then quit."
)
opts.add(
"-gfont_name",
options.font_name,
@ -239,12 +221,28 @@ func setup_options(options, font_names):
'Background color as an html color, default "[default]"'
)
opts.add("-gfont_color", options.font_color, 'Font color as an html color, default "[default]"')
# End Behavior
opts.add(
"-gpaint_after",
options.paint_after,
"Delay before GUT will add a 1 frame pause to paint the screen/GUI. default [default]"
"-gexit",
false,
"Exit after running tests. If not specified you have to manually close the window."
)
opts.add("-gexit_on_success", false, "Only exit if all tests pass.")
opts.add("-gignore_pause", false, "Ignores any calls to gut.pause_before_teardown.")
# Helpish options
opts.add(
"-gh", false, "Print this help. You did this to see this, so you probably understand."
)
opts.add("-gpo", false, "Print option values from all sources and the value used.")
opts.add(
"-gprint_gutconfig_sample",
false,
"Print out json that can be used to make a gutconfig file."
)
# Output options
opts.add(
"-gjunit_xml_file",
options.junit_xml_file,
@ -255,6 +253,7 @@ func setup_options(options, font_names):
options.junit_xml_timestamp,
"Include a timestamp in the -gjunit_xml_file, default [default]"
)
return opts
@ -280,6 +279,7 @@ func extract_command_line_options(from, to):
to.compact_mode = from.get_value("-gcompact_mode")
to.hide_orphans = from.get_value("-ghide_orphans")
to.suffix = from.get_value("-gsuffix")
to.errors_do_not_cause_failure = from.get_value("-gerrors_do_not_cause_failure")
to.tests = from.get_value("-gtest")
to.unit_test_name = from.get_value("-gunit_test_name")
@ -294,10 +294,10 @@ func extract_command_line_options(from, to):
func _print_gutconfigs(values):
var header = """Here is a sample of a full super.gutconfig.json file.
var header = """Here is a sample of a full .gutconfig.json file.
You do not need to specify all values in your own file. The values supplied in
this sample are what would be used if you ran gut w/o the -gprint_gutconfig_sample
option (option priority: command-line, super.gutconfig, default)."""
option (option priority: command-line, .gutconfig, default)."""
print("\n", header.replace("\n", " "), "\n\n")
var resolved = values
@ -345,7 +345,7 @@ func _run_gut():
(
"All command line options and where they are specified. "
+ 'The "final" value shows which value will actually be used '
+ "based on order of precedence (default < super.gutconfig < cmd line)."
+ "based on order of precedence (default < .gutconfig < cmd line)."
+ "\n"
)
)
@ -379,21 +379,22 @@ func _run_gut():
func _on_tests_finished(should_exit, should_exit_on_success):
if _final_opts.dirs.size() == 0:
if _tester.get_summary().get_totals().scripts == 0:
var lgr = _tester.get_logger()
var lgr = _tester.logger
(
lgr
. error(
"No directories configured. Add directories with options or a super.gutconfig.json file. Use the -gh option for more information."
"No directories configured. Add directories with options or a .gutconfig.json file. Use the -gh option for more information."
)
)
var exit_code = 0
if _tester.get_fail_count():
set_exit_code(1)
exit_code = 1
# Overwrite the exit code with the post_script
var post_inst = _tester.get_post_run_script_instance()
if post_inst != null and post_inst.get_exit_code() != null:
set_exit_code(post_inst.get_exit_code())
exit_code = post_inst.get_exit_code()
if should_exit or (should_exit_on_success and _tester.get_fail_count() == 0):
quit(exit_code)
@ -401,18 +402,56 @@ func _on_tests_finished(should_exit, should_exit_on_success):
print("Tests finished, exit manually")
func set_exit_code(val):
exit_code = val
# ------------------------------------------------------------------------------
# MAIN
# ------------------------------------------------------------------------------
func _init():
var max_iter = 20
var iter = 0
# Not seen this wait more than 1.
while Engine.get_main_loop() == null and iter < max_iter:
await create_timer(.01).timeout
iter += 1
if Engine.get_main_loop() == null:
push_error("Main loop did not start in time.")
quit(0)
return
_utils = GutUtils.get_instance()
if !_utils.is_version_ok():
print("\n\n", _utils.get_version_text())
push_error(_utils.get_bad_version_text())
set_exit_code(1)
quit()
quit(1)
else:
_run_gut()
# ##############################################################################
#(G)odot (U)nit (T)est class
#
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2023 Tom "Butch" Wesley
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ##############################################################################

View file

@ -1,17 +1,24 @@
# ##############################################################################
#
# This holds all the configuratoin values for GUT. It can load and save values
# to a json file. It is also responsible for applying these settings to GUT.
#
# ##############################################################################
var Gut = load("res://addons/gut/gut.gd")
# Do not want a ref to _utils here due to use by editor plugin.
# _utils needs to be split so that constants and what not do not
# have to rely on the weird singleton thing I made.
enum DOUBLE_STRATEGY { FULL, PARTIAL }
var valid_fonts = ["AnonymousPro", "CourierPro", "LobsterTwo", "Default"]
var default_options = {
background_color = Color(.15, .15, .15, 1).to_html(),
config_file = "res://.gutconfig.json",
dirs = [],
disable_colors = false,
double_strategy = "partial",
# double strategy can be the name of the enum value, the enum value or
# lowercase name with spaces: 0/SCRIPT_ONLY/script only
# The GUI gut config expects the value to be the enum value and not a string
# when saved.
double_strategy = "SCRIPT_ONLY",
# named differently than gut option so we can use it as a flag in the cli
errors_do_not_cause_failure = false,
font_color = Color(.8, .8, .8, 1).to_html(),
font_name = "CourierPrime",
font_size = 16,
@ -41,7 +48,9 @@ var default_options = {
var default_panel_options = {
font_name = "CourierPrime",
font_size = 30,
font_size = 16,
output_font_name = "CourierPrime",
output_font_size = 30,
hide_result_tree = false,
hide_output_text = false,
hide_settings = false,
@ -61,7 +70,6 @@ func _null_copy(h):
func _load_options_from_config_file(file_path, into):
# SHORTCIRCUIT
if !FileAccess.file_exists(file_path):
if file_path != "res://.gutconfig.json":
print('ERROR: Config File "', file_path, '" does not exist.')
@ -105,6 +113,45 @@ func _load_dict_into(source, dest):
dest[key] = source[key]
# Apply all the options specified to tester. This is where the rubber meets
# the road.
func _apply_options(opts, gut):
gut.include_subdirectories = opts.include_subdirs
if opts.inner_class != "":
gut.inner_class_name = opts.inner_class
gut.log_level = opts.log_level
gut.ignore_pause_before_teardown = opts.ignore_pause
gut.select_script(opts.selected)
for i in range(opts.dirs.size()):
gut.add_directory(opts.dirs[i], opts.prefix, opts.suffix)
for i in range(opts.tests.size()):
gut.add_script(opts.tests[i])
# Sometimes it is the index, sometimes it's a string. This sets it regardless
gut.double_strategy = GutUtils.get_enum_value(
opts.double_strategy, GutUtils.DOUBLE_STRATEGY, GutUtils.DOUBLE_STRATEGY.INCLUDE_NATIVE
)
gut.unit_test_name = opts.unit_test_name
gut.pre_run_script = opts.pre_run_script
gut.post_run_script = opts.post_run_script
gut.color_output = !opts.disable_colors
gut.show_orphans(!opts.hide_orphans)
gut.junit_xml_file = opts.junit_xml_file
gut.junit_xml_timestamp = opts.junit_xml_timestamp
gut.paint_after = str(opts.paint_after).to_float()
gut.treat_error_as_failure = !opts.errors_do_not_cause_failure
return gut
# --------------------------
# Public
# --------------------------
func write_options(path):
var content = json.stringify(options, " ")
@ -112,52 +159,12 @@ func write_options(path):
var result = FileAccess.get_open_error()
if f != null:
f.store_string(content)
f.close()
f = null # closes file
else:
print("ERROR: could not open file ", path, " ", result)
return result
# Apply all the options specified to _tester. This is where the rubber meets
# the road.
func _apply_options(opts, _tester):
_tester.include_subdirectories = opts.include_subdirs
if opts.inner_class != "":
_tester.inner_class_name = opts.inner_class
_tester.log_level = opts.log_level
_tester.ignore_pause_before_teardown = opts.ignore_pause
if opts.selected != "":
_tester.select_script(opts.selected)
for i in range(opts.dirs.size()):
_tester.add_directory(opts.dirs[i], opts.prefix, opts.suffix)
for i in range(opts.tests.size()):
_tester.add_script(opts.tests[i])
if opts.double_strategy == "include super":
_tester.double_strategy = DOUBLE_STRATEGY.FULL
elif opts.double_strategy == "script only":
_tester.double_strategy = DOUBLE_STRATEGY.PARTIAL
_tester.unit_test_name = opts.unit_test_name
_tester.pre_run_script = opts.pre_run_script
_tester.post_run_script = opts.post_run_script
_tester.color_output = !opts.disable_colors
_tester.show_orphans(!opts.hide_orphans)
_tester.junit_xml_file = opts.junit_xml_file
_tester.junit_xml_timestamp = opts.junit_xml_timestamp
_tester.paint_after = str(opts.paint_after).to_float()
return _tester
func config_gut(gut):
return _apply_options(options, gut)
func load_options(path):
return _load_options_from_config_file(path, options)
@ -174,3 +181,29 @@ func load_options_no_defaults(path):
func apply_options(gut):
_apply_options(options, gut)
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2023 Tom "Butch" Wesley
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ##############################################################################

View file

@ -66,16 +66,25 @@ func file_touch(path):
# ------------------------------------------------------------------------------
# Call _process or _fixed_process, if they exist, on obj and all it's children
# and their children and so and so forth. Delta will be passed through to all
# the _process or _fixed_process methods.
# Simulate a number of frames by calling '_process' and '_physics_process' (if
# the methods exist) on an object and all of its descendents. The specified frame
# time, 'delta', will be passed to each simulated call.
#
# NOTE: Objects can disable their processing methods using 'set_process(false)' and
# 'set_physics_process(false)'. This is reflected in the 'Object' methods
# 'is_processing()' and 'is_physics_processing()', respectively. To make 'simulate'
# respect this status, for example if you are testing an object which toggles
# processing, pass 'check_is_processing' as 'true'.
# ------------------------------------------------------------------------------
func simulate(obj, times, delta):
func simulate(obj, times, delta, check_is_processing: bool = false):
for _i in range(times):
if obj.has_method("_process"):
if obj.has_method("_process") and (not check_is_processing or obj.is_processing()):
obj._process(delta)
if obj.has_method("_physics_process"):
if (
obj.has_method("_physics_process")
and (not check_is_processing or obj.is_physics_processing())
):
obj._physics_process(delta)
for kid in obj.get_children():
simulate(kid, 1, delta)
simulate(kid, 1, delta, check_is_processing)

View file

@ -16,9 +16,9 @@ dest_files=["res://.godot/imported/icon.png-91b084043b8aaf2f1c906e7b9fa92969.cte
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false

View file

@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m2 2a1 1 0 0 0 -1 1v2 6 2a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-7a1 1 0 0 0 -1-1h-4a1 1 0 0 1 -1-1v-1a1 1 0 0 0 -1-1z" fill="#e0e0e0"/></svg>

After

Width:  |  Height:  |  Size: 227 B

View file

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dhvl14hls3y2j"
path="res://.godot/imported/Folder.svg-caa50e6a0be9d456fd81991dfb537916.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/gut/images/Folder.svg"
dest_files=["res://.godot/imported/Folder.svg-caa50e6a0be9d456fd81991dfb537916.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
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/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><g transform="translate(0 -1036.4)"><path d="m6 1v1a1 1 0 0 0 -1 1v10h-1v-2h-2v2a1 1 0 0 0 .5.86523 1 1 0 0 0 .5.13477v1h7a2 2 0 0 0 2-2v-8h3v-2a2 2 0 0 0 -2-2z" fill="#e0e0e0" transform="translate(0 1036.4)"/><path d="m6 1c-1.1046 0-2 .89543-2 2v7h-2-1v1 2c0 1.1046.89543 2 2 2s2-.89543 2-2v-10c0-.55228.44772-1 1-1s1 .44772 1 1v1 1 1h1 4v-1h-4v-1-1c0-1.1046-.89543-2-2-2zm-4 10h2v2c0 .55228-.44772 1-1 1s-1-.44772-1-1z" fill="#b3b3b3" transform="translate(0 1036.4)"/><circle cx="3" cy="1048.4" fill="#e0e0e0"/></g></svg>

After

Width:  |  Height:  |  Size: 606 B

View file

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cavojn74qp7ij"
path="res://.godot/imported/Script.svg-34c66aae9c985e3e0470426acbbcda04.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/gut/images/Script.svg"
dest_files=["res://.godot/imported/Script.svg-34c66aae9c985e3e0470426acbbcda04.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
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/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -16,9 +16,9 @@ dest_files=["res://.godot/imported/green.png-e3a17091688e10a7013279b38edc7f8a.ct
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false

View file

@ -16,9 +16,9 @@ dest_files=["res://.godot/imported/red.png-47a557c3922e800f76686bc1a4ad0c3c.ctex
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false

View file

@ -16,9 +16,9 @@ dest_files=["res://.godot/imported/yellow.png-b3cf3d463958a169d909273d3d742052.c
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/bptc_ldr=0
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false

View file

@ -51,6 +51,8 @@
# InputEventScreenDrag
# InputEventScreenTouch
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class InputQueueItem:
extends Node
@ -93,6 +95,78 @@ class InputQueueItem:
t.connect("timeout", Callable(self, "_on_time_timeout"))
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
class MouseDraw:
extends Node2D
var down_color = Color(1, 1, 1, .25)
var up_color = Color(0, 0, 0, .25)
var line_color = Color(1, 0, 0)
var disabled = true:
get:
return disabled
set(val):
disabled = val
queue_redraw()
var _draw_at = Vector2(0, 0)
var _b1_down = false
var _b2_down = false
func draw_event(event):
if event is InputEventMouse:
_draw_at = event.position
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT:
_b1_down = event.pressed
elif event.button_index == MOUSE_BUTTON_RIGHT:
_b2_down = event.pressed
queue_redraw()
func _draw_cicled_cursor():
var r = 10
var b1_color = up_color
var b2_color = up_color
if _b1_down:
var pos = _draw_at - (Vector2(r * 1.5, 0))
draw_arc(pos, r / 2, 0, 360, 180, b1_color)
if _b2_down:
var pos = _draw_at + (Vector2(r * 1.5, 0))
draw_arc(pos, r / 2, 0, 360, 180, b2_color)
draw_arc(_draw_at, r, 0, 360, 360, line_color, 1)
draw_line(_draw_at - Vector2(0, r), _draw_at + Vector2(0, r), line_color)
draw_line(_draw_at - Vector2(r, 0), _draw_at + Vector2(r, 0), line_color)
func _draw_square_cursor():
var r = 10
var b1_color = up_color
var b2_color = up_color
if _b1_down:
b1_color = down_color
if _b2_down:
b2_color = down_color
var blen = r * .75
# left button rectangle
draw_rect(Rect2(_draw_at - Vector2(blen, blen), Vector2(blen, blen * 2)), b1_color)
# right button rectrangle
draw_rect(Rect2(_draw_at - Vector2(0, blen), Vector2(blen, blen * 2)), b2_color)
# Crosshair
draw_line(_draw_at - Vector2(0, r), _draw_at + Vector2(0, r), line_color)
draw_line(_draw_at - Vector2(r, 0), _draw_at + Vector2(r, 0), line_color)
func _draw():
if disabled:
return
_draw_square_cursor()
# ##############################################################################
#
# ##############################################################################
@ -105,12 +179,9 @@ var _lgr = _utils.get_logger()
var _receivers = []
var _input_queue = []
var _next_queue_item = null
# used by mouse_relative_motion. These use this instead of _last_event since
# it is logical to have a series of events happen between mouse motions.
var _last_mouse_motion = null
# used by hold_for and echo.
var _last_event = null
# indexed by keycode, each entry contains a boolean value indicating the
# last emitted "pressed" value for that keycode.
var _pressed_keys = {}
@ -118,6 +189,15 @@ var _pressed_actions = {}
var _pressed_mouse_buttons = {}
var _auto_flush_input = false
var _tree_items_parent = null
var _mouse_draw = null
var _default_mouse_position = {position = Vector2(0, 0), global_position = Vector2(0, 0)}
var _last_mouse_position = {}
var mouse_warp = false
var draw_mouse = true
signal idle
@ -126,8 +206,31 @@ func _init(r = null):
if r != null:
add_receiver(r)
_last_mouse_position = _default_mouse_position.duplicate()
_tree_items_parent = Node.new()
Engine.get_main_loop().root.add_child(_tree_items_parent)
func _send_event(event):
_mouse_draw = MouseDraw.new()
_tree_items_parent.add_child(_mouse_draw)
_mouse_draw.disabled = false
func _notification(what):
if what == NOTIFICATION_PREDELETE:
if is_instance_valid(_tree_items_parent):
_tree_items_parent.queue_free()
func _add_queue_item(item):
item.connect("event_ready", _on_queue_item_ready.bind(item))
_next_queue_item = item
_input_queue.append(item)
_tree_items_parent.add_child(item)
if _input_queue.size() == 1:
item.start()
func _handle_pressed_keys(event):
if event is InputEventKey:
if (event.pressed and !event.echo) and is_key_pressed(event.keycode):
_lgr.warn(
@ -162,6 +265,19 @@ func _send_event(event):
)
_pressed_mouse_buttons[event.button_index] = event
func _handle_mouse_position(event):
if event is InputEventMouse:
_mouse_draw.disabled = !draw_mouse
_mouse_draw.draw_event(event)
if mouse_warp:
DisplayServer.warp_mouse(event.position)
func _send_event(event):
_handle_mouse_position(event)
_handle_pressed_keys(event)
for r in _receivers:
if r == Input:
Input.parse_input_event(event)
@ -186,6 +302,32 @@ func _send_or_record_event(event):
_send_event(event)
func _set_last_mouse_positions(event: InputEventMouse):
_last_mouse_position.position = event.position
_last_mouse_position.global_position = event.global_position
func _apply_last_position_and_set_last_position(event, position, global_position):
event.position = GutUtils.nvl(position, _last_mouse_position.position)
event.global_position = GutUtils.nvl(global_position, _last_mouse_position.global_position)
_set_last_mouse_positions(event)
func _new_defaulted_mouse_button_event(position, global_position):
var event = InputEventMouseButton.new()
_apply_last_position_and_set_last_position(event, position, global_position)
return event
func _new_defaulted_mouse_motion_event(position, global_position):
var event = InputEventMouseMotion.new()
_apply_last_position_and_set_last_position(event, position, global_position)
return event
# ------------------------------
# Events
# ------------------------------
func _on_queue_item_ready(item):
for event in item.events:
_send_event(event)
@ -200,15 +342,9 @@ func _on_queue_item_ready(item):
_input_queue[0].start()
func _add_queue_item(item):
item.connect("event_ready", _on_queue_item_ready.bind(item))
_next_queue_item = item
_input_queue.append(item)
Engine.get_main_loop().root.add_child(item)
if _input_queue.size() == 1:
item.start()
# ------------------------------
# Public
# ------------------------------
func add_receiver(obj):
_receivers.append(obj)
@ -217,6 +353,31 @@ func get_receivers():
return _receivers
func is_idle():
return _input_queue.size() == 0
func is_key_pressed(which):
var event = InputFactory.key_up(which)
return _pressed_keys.has(event.keycode) and _pressed_keys[event.keycode]
func is_action_pressed(which):
return _pressed_actions.has(which) and _pressed_actions[which]
func is_mouse_button_pressed(which):
return _pressed_mouse_buttons.has(which) and _pressed_mouse_buttons[which].pressed
func get_auto_flush_input():
return _auto_flush_input
func set_auto_flush_input(val):
_auto_flush_input = val
func wait(t):
if typeof(t) == TYPE_STRING:
var suffix = t.substr(t.length() - 1, 1)
@ -232,16 +393,18 @@ func wait(t):
return self
func wait_frames(num_frames):
var item = InputQueueItem.new(0, num_frames)
_add_queue_item(item)
return self
func clear():
_last_event = null
_next_queue_item = null
for item in _input_queue:
item.free()
_input_queue.clear()
func wait_secs(num_secs):
var item = InputQueueItem.new(num_secs, 0)
_add_queue_item(item)
return self
_pressed_keys.clear()
_pressed_actions.clear()
_pressed_mouse_buttons.clear()
_last_mouse_position = _default_mouse_position.duplicate()
# ------------------------------
@ -279,53 +442,69 @@ func action_down(which, strength = 1.0):
return self
func mouse_left_button_down(position, global_position = null):
var event = InputFactory.mouse_left_button_down(position, global_position)
func mouse_left_button_down(position = null, global_position = null):
var event = _new_defaulted_mouse_button_event(position, global_position)
event.pressed = true
event.button_index = MOUSE_BUTTON_LEFT
_send_or_record_event(event)
return self
func mouse_left_button_up(position, global_position = null):
var event = InputFactory.mouse_left_button_up(position, global_position)
func mouse_left_button_up(position = null, global_position = null):
var event = _new_defaulted_mouse_button_event(position, global_position)
event.pressed = false
event.button_index = MOUSE_BUTTON_LEFT
_send_or_record_event(event)
return self
func mouse_double_click(position, global_position = null):
func mouse_double_click(position = null, global_position = null):
var event = InputFactory.mouse_double_click(position, global_position)
event.double_click = true
_send_or_record_event(event)
return self
func mouse_right_button_down(position, global_position = null):
var event = InputFactory.mouse_right_button_down(position, global_position)
func mouse_right_button_down(position = null, global_position = null):
var event = _new_defaulted_mouse_button_event(position, global_position)
event.pressed = true
event.button_index = MOUSE_BUTTON_RIGHT
_send_or_record_event(event)
return self
func mouse_right_button_up(position, global_position = null):
var event = InputFactory.mouse_right_button_up(position, global_position)
func mouse_right_button_up(position = null, global_position = null):
var event = _new_defaulted_mouse_button_event(position, global_position)
event.pressed = false
event.button_index = MOUSE_BUTTON_RIGHT
_send_or_record_event(event)
return self
func mouse_motion(position, global_position = null):
var event = InputFactory.mouse_motion(position, global_position)
_last_mouse_motion = event
var event = _new_defaulted_mouse_motion_event(position, global_position)
_send_or_record_event(event)
return self
func mouse_relative_motion(offset, speed = Vector2(0, 0)):
var event = InputFactory.mouse_relative_motion(offset, _last_mouse_motion, speed)
_last_mouse_motion = event
var last_event = _new_defaulted_mouse_motion_event(null, null)
var event = InputFactory.mouse_relative_motion(offset, last_event, speed)
_set_last_mouse_positions(event)
_send_or_record_event(event)
return self
func mouse_set_position(position, global_position = null):
_last_mouse_motion = InputFactory.mouse_motion(position, global_position)
var event = _new_defaulted_mouse_motion_event(position, global_position)
return self
func mouse_left_click_at(where, duration = "5f"):
wait_frames(1)
mouse_left_button_down(where)
hold_for(duration)
wait_frames(10)
return self
@ -352,52 +531,26 @@ func release_all():
_send_event(event)
_pressed_mouse_buttons.clear()
return self
func wait_frames(num_frames):
var item = InputQueueItem.new(0, num_frames)
_add_queue_item(item)
return self
func wait_secs(num_secs):
var item = InputQueueItem.new(num_secs, 0)
_add_queue_item(item)
return self
func hold_for(duration):
if _last_event != null and _last_event.pressed:
var next_event = _last_event.duplicate()
next_event.pressed = false
wait(duration)
send_event(next_event)
return self
func clear():
pass
_last_event = null
_last_mouse_motion = null
_next_queue_item = null
for item in _input_queue:
item.free()
_input_queue.clear()
_pressed_keys.clear()
_pressed_actions.clear()
_pressed_mouse_buttons.clear()
func is_idle():
return _input_queue.size() == 0
func is_key_pressed(which):
var event = InputFactory.key_up(which)
return _pressed_keys.has(event.keycode) and _pressed_keys[event.keycode]
func is_action_pressed(which):
return _pressed_actions.has(which) and _pressed_actions[which]
func is_mouse_button_pressed(which):
return _pressed_mouse_buttons.has(which) and _pressed_mouse_buttons[which]
func get_auto_flush_input():
return _auto_flush_input
func set_auto_flush_input(val):
_auto_flush_input = val

View file

@ -39,6 +39,7 @@ var types = {
orphan = "orphan",
passed = "passed",
pending = "pending",
risky = "risky",
warn = "warn",
}
@ -61,6 +62,7 @@ var _type_data = {
types.orphan: {disp = "Orphans", enabled = true, fmt = fmts.yellow},
types.passed: {disp = "Passed", enabled = true, fmt = fmts.green},
types.pending: {disp = "Pending", enabled = true, fmt = fmts.yellow},
types.risky: {disp = "Risky", enabled = true, fmt = fmts.yellow},
types.warn: {disp = "WARNING", enabled = true, fmt = fmts.yellow},
}
@ -77,6 +79,7 @@ var _printers = {terminal = null, gui = null, console = null}
var _gut = null
var _utils = null
var _indent_level = 0
var _min_indent_level = 0
var _indent_string = " "
var _skip_test_name_for_testing = false
var _less_test_names = false
@ -130,7 +133,12 @@ func _print_test_name():
return false
if !cur_test.has_printed_name:
_output("* " + cur_test.name + "\n")
var param_text = ""
if cur_test.arg_count > 0:
# Just an FYI, parameter_handler in gut might not be set yet so can't
# use it here for cooler output.
param_text = "<parameterized>"
_output(str("* ", cur_test.name, param_text, "\n"))
cur_test.has_printed_name = true
@ -223,6 +231,8 @@ func deprecated(text, alt_method = null):
func error(text):
_output_type(types.error, text)
if _gut != null:
_gut._fail_for_error(text)
func failed(text):
@ -245,6 +255,10 @@ func pending(text):
_output_type(types.pending, text)
func risky(text):
_output_type(types.risky, text)
func warn(text):
_output_type(types.warn, text)
@ -292,7 +306,7 @@ func get_indent_level():
func set_indent_level(indent_level):
_indent_level = indent_level
_indent_level = max(_min_indent_level, indent_level)
func get_indent_string():
@ -313,7 +327,7 @@ func inc_indent():
func dec_indent():
_indent_level = max(0, _indent_level - 1)
_indent_level = max(_min_indent_level, _indent_level - 1)
func is_type_enabled(type):

View file

@ -28,6 +28,7 @@ class CallParameters:
var _utils = load("res://addons/gut/utils.gd").get_instance()
var _lgr = _utils.get_logger()
var default_vararg_arg_count = 10
const PARAM_PREFIX = "p_"
# ------------------------------------------------------
@ -134,11 +135,15 @@ func _make_arg_array(method_meta, override_size):
var dflt_text = _make_stub_default(method_meta.name, i)
to_return.append(CallParameters.new(PARAM_PREFIX + pname, dflt_text))
var extra_params = GutUtils.nvl(override_size, 0)
if extra_params == 0:
if method_meta.flags & METHOD_FLAG_VARARG:
extra_params = default_vararg_arg_count
# Add in extra parameters from stub settings.
if override_size != null:
for i in range(method_meta.args.size(), override_size):
if extra_params > 0:
for i in range(method_meta.args.size(), extra_params):
var pname = str(PARAM_PREFIX, "arg", i)
print("-------- ", i, " ", pname)
var dflt_text = _make_stub_default(method_meta.name, i)
to_return.append(CallParameters.new(pname, dflt_text))
@ -163,14 +168,14 @@ func _get_arg_text(arg_array):
# creates a call to the function in meta in the super's class.
func _get_super_call_text(method_name, args, super_name = ""):
func _get_super_call_text(method_name, args):
var params = ""
for i in range(args.size()):
params += args[i].p_name
if i != args.size() - 1:
params += ", "
return str(super_name, "await super(", params, ")")
return str("await super(", params, ")")
func _get_spy_call_parameters_text(args):
@ -203,14 +208,17 @@ func _get_init_text(meta, args, method_params, param_array):
if i != args.size() - 1:
super_params += ", "
text = _init_text.format(
text = (
_init_text
. format(
{
"func_decleration": decleration,
"super_params": super_params,
"param_array": param_array,
"method_name": meta.name
"method_name": meta.name,
}
)
)
return text
@ -219,16 +227,13 @@ func _get_init_text(meta, args, method_params, param_array):
# types whose defaults are supported will have their values. If a datatype
# is not supported and the parameter has a default, a warning message will be
# printed and the declaration will return null.
#
# path is no longer used
func get_function_text(meta, path = null, override_size = null, super_name = ""):
func get_function_text(meta, override_size = null):
var method_params = ""
var text = null
if override_size != null:
print("!!!!!! ", override_size)
var result = _make_arg_array(meta, override_size)
var has_unsupported = result[0]
var args = result[1]
var vararg_warning = ""
var param_array = _get_spy_call_parameters_text(args)
if has_unsupported:
@ -242,20 +247,27 @@ func get_function_text(meta, path = null, override_size = null, super_name = "")
if param_array == "null":
param_array = "[]"
if meta.flags & METHOD_FLAG_VARARG and override_size == null:
vararg_warning = "__gutdbl.vararg_warning()\n\t"
if method_params != null:
if meta.name == "_init":
text = _get_init_text(meta, args, method_params, param_array)
else:
var decleration = str("func ", meta.name, "(", method_params, "):")
# decleration = str('# ', meta, "\n", decleration)
text = _func_text.format(
text = (
_func_text
. format(
{
"func_decleration": decleration,
"method_name": meta.name,
"param_array": param_array,
"super_call": _get_super_call_text(meta.name, args, super_name)
"super_call": _get_super_call_text(meta.name, args),
"vararg_warning": vararg_warning,
}
)
)
return text

View file

@ -3,5 +3,5 @@
name="Gut"
description="Unit Testing tool for Godot."
author="Butch Wesley"
version="7.4.1"
version="9.1.1"
script="gut_plugin.gd"

View file

@ -8,16 +8,17 @@ var _utils = load("res://addons/gut/utils.gd").get_instance()
var json = JSON.new()
func _export_tests(summary_script):
func _export_tests(collected_script):
var to_return = {}
var tests = summary_script.get_tests()
for key in tests.keys():
to_return[key] = {
"status": tests[key].get_status(),
"passing": tests[key].pass_texts,
"failing": tests[key].fail_texts,
"pending": tests[key].pending_texts,
"orphans": tests[key].orphans
var tests = collected_script.tests
for test in tests:
if test.get_status_text() != GutUtils.TEST_STATUSES.NOT_RUN:
to_return[test.name] = {
"status": test.get_status_text(),
"passing": test.pass_texts,
"failing": test.fail_texts,
"pending": test.pending_texts,
"orphans": test.orphans
}
return to_return
@ -25,21 +26,22 @@ func _export_tests(summary_script):
# TODO
# errors
func _export_scripts(summary):
if summary == null:
func _export_scripts(collector):
if collector == null:
return {}
var scripts = {}
for s in summary.get_scripts():
scripts[s.name] = {
for s in collector.scripts:
var test_data = _export_tests(s)
scripts[s.get_full_name()] = {
"props":
{
"tests": s._tests.size(),
"tests": test_data.keys().size(),
"pending": s.get_pending_count(),
"failures": s.get_fail_count(),
},
"tests": _export_tests(s)
"tests": test_data
}
return scripts
@ -69,15 +71,14 @@ func _make_results_dict():
# time
# errors
func get_results_dictionary(gut, include_scripts = true):
var summary = gut.get_summary()
var scripts = []
if include_scripts:
scripts = _export_scripts(summary)
scripts = _export_scripts(gut.get_test_collector())
var result = _make_results_dict()
if summary != null:
var totals = summary.get_totals()
var totals = gut.get_summary().get_totals()
var props = result.test_scripts.props
props.pending = totals.pending

View file

@ -1,29 +1,8 @@
# ------------------------------------------------------------------------------
# List of methods that should not be overloaded when they are not defined
# in the class being doubled. These either break things if they are
# overloaded or do not have a "super" equivalent so we can't just pass
# through.
# These methods didn't have flags that would exclude them from being used
# in a double and they appear to break things if they are included.
const BLACKLIST = [
"_draw",
"_enter_tree",
"_exit_tree",
"_get_minimum_size", # Nonexistent function _get_minimum_size
"_get", # probably
"_input",
"_notification",
"_physics_process",
"_process",
"_set",
"_to_string", # nonexistant function super._to_string
"_unhandled_input",
"_unhandled_key_input",
"draw_mesh", # issue with one parameter, value is `Null((..), (..), (..))``
"emit_signal", # can't handle extra parameters to be sent with signal.
"get_path",
"get_script",
"get",
"has_method",
"print_orphan_nodes"
]
@ -34,17 +13,17 @@ const BLACKLIST = [
# parameter
# ------------------------------------------------------------------------------
class ParsedMethod:
const NO_DEFAULT = "__no__default__"
var _meta = {}
var meta = _meta:
get:
return _meta
set(val):
return
var _parameters = []
var is_local = false
const NO_DEFAULT = "__no__default__"
var _parameters = []
func _init(metadata):
_meta = metadata
@ -52,15 +31,24 @@ class ParsedMethod:
for i in range(_meta.args.size()):
var arg = _meta.args[i]
# Add a "default" property to the metadata so we don't have to do
# weird default position math again.
# weird default paramter position math again.
if i >= start_default:
arg["default"] = _meta.default_args[start_default - i]
else:
arg["default"] = NO_DEFAULT
_parameters.append(arg)
func is_black_listed():
return BLACKLIST.find(_meta.name) != -1
func is_eligible_for_doubling():
var has_bad_flag = (
_meta.flags & (METHOD_FLAG_OBJECT_CORE | METHOD_FLAG_VIRTUAL | METHOD_FLAG_STATIC)
)
return !has_bad_flag and BLACKLIST.find(_meta.name) == -1
func is_accessor():
return (
_meta.name.begins_with("@")
and (_meta.name.ends_with("_getter") or _meta.name.ends_with("_setter"))
)
func to_s():
var s = _meta.name + "("
@ -83,8 +71,6 @@ class ParsedMethod:
# ------------------------------------------------------------------------------
# Doesn't know if a method is local and in super, but not sure if that will
# ever matter.
# ------------------------------------------------------------------------------
class ParsedScript:
# All methods indexed by name.
@ -147,11 +133,6 @@ class ParsedScript:
_parse_methods(to_load)
func _has_flag_to_be_ignored(flags):
return false
# I think this is getting anything that has the 1 flag set...I think
return flags & (1 << 2) == 0 && flags & (1 << 4) == 0 && flags & (1 << 6) == 0
func _print_flags(meta):
print(
str(meta.name, ":").rpad(30),
@ -179,7 +160,6 @@ class ParsedScript:
methods = _get_native_methods(base_type)
for m in methods:
if !_has_flag_to_be_ignored(m.flags):
var parsed = ParsedMethod.new(m)
_methods_by_name[m.name] = parsed
# _init must always be included so that we can initialize
@ -224,10 +204,6 @@ class ParsedScript:
func get_method(name):
return _methods_by_name[name]
func is_method_blacklisted(m_name):
if _methods_by_name.has(m_name):
return _methods_by_name[m_name].is_black_listed()
func get_super_method(name):
var to_return = get_method(name)
if to_return.is_local:

View file

@ -211,3 +211,33 @@ func get_signals_emitted(obj):
emitted.append(signal_name)
return emitted
func get_signal_summary(obj):
var emitted = {}
if is_watching_object(obj):
for signal_name in _watched_signals[obj]:
if _watched_signals[obj][signal_name].size() > 0:
# maybe this could return parameters if any were sent. should
# have an empty list if no parameters were ever sent to the
# signal. Or this all just gets moved into print_signal_summary
# since this wouldn't be that useful without more data in the
# summary.
var entry = {emit_count = get_emit_count(obj, signal_name)}
emitted[signal_name] = entry
return emitted
func print_signal_summary(obj):
if !is_watching_object(obj):
var msg = str("Not watching signals for ", obj)
_utils.get_logger().warn(msg)
return
var summary = get_signal_summary(obj)
print(obj, "::Signals")
var sorted = summary.keys()
sorted.sort()
for key in sorted:
print(" - ", key, " x ", summary[key].emit_count)

View file

@ -8,37 +8,44 @@ var types = {}
func _init_types_dictionary():
types[TYPE_NIL] = "TYPE_NIL"
types[TYPE_BOOL] = "Bool"
types[TYPE_INT] = "Int"
types[TYPE_FLOAT] = "Float/Real"
types[TYPE_STRING] = "String"
types[TYPE_VECTOR2] = "Vector2"
types[TYPE_RECT2] = "Rect2"
types[TYPE_VECTOR3] = "Vector3"
#types[8] = 'Matrix32'
types[TYPE_PLANE] = "Plane"
types[TYPE_QUATERNION] = "QUAT"
types[TYPE_AABB] = "AABB"
#types[12] = 'Matrix3'
types[TYPE_TRANSFORM3D] = "Transform3D"
types[TYPE_COLOR] = "Color"
#types[15] = 'Image'
types[TYPE_NODE_PATH] = "Node Path3D"
types[TYPE_ARRAY] = "ARRAY"
types[TYPE_BASIS] = "BASIS"
types[TYPE_BOOL] = "BOOL"
types[TYPE_CALLABLE] = "CALLABLE"
types[TYPE_COLOR] = "COLOR"
types[TYPE_DICTIONARY] = "DICTIONARY"
types[TYPE_FLOAT] = "FLOAT"
types[TYPE_INT] = "INT"
types[TYPE_MAX] = "MAX"
types[TYPE_NODE_PATH] = "NODE_PATH"
types[TYPE_OBJECT] = "OBJECT"
types[TYPE_PACKED_BYTE_ARRAY] = "PACKED_BYTE_ARRAY"
types[TYPE_PACKED_COLOR_ARRAY] = "PACKED_COLOR_ARRAY"
types[TYPE_PACKED_FLOAT32_ARRAY] = "PACKED_FLOAT32_ARRAY"
types[TYPE_PACKED_FLOAT64_ARRAY] = "PACKED_FLOAT64_ARRAY"
types[TYPE_PACKED_INT32_ARRAY] = "PACKED_INT32_ARRAY"
types[TYPE_PACKED_INT64_ARRAY] = "PACKED_INT64_ARRAY"
types[TYPE_PACKED_STRING_ARRAY] = "PACKED_STRING_ARRAY"
types[TYPE_PACKED_VECTOR2_ARRAY] = "PACKED_VECTOR2_ARRAY"
types[TYPE_PACKED_VECTOR3_ARRAY] = "PACKED_VECTOR3_ARRAY"
types[TYPE_PLANE] = "PLANE"
types[TYPE_PROJECTION] = "PROJECTION"
types[TYPE_QUATERNION] = "QUATERNION"
types[TYPE_RECT2] = "RECT2"
types[TYPE_RECT2I] = "RECT2I"
types[TYPE_RID] = "RID"
types[TYPE_OBJECT] = "TYPE_OBJECT"
#types[19] = 'TYPE_INPUT_EVENT'
types[TYPE_DICTIONARY] = "Dictionary"
types[TYPE_ARRAY] = "Array"
types[TYPE_PACKED_BYTE_ARRAY] = "TYPE_PACKED_BYTE_ARRAY"
types[TYPE_PACKED_INT32_ARRAY] = "TYPE_PACKED_INT32_ARRAY"
types[TYPE_PACKED_FLOAT32_ARRAY] = "TYPE_PACKED_FLOAT32_ARRAY"
types[TYPE_PACKED_STRING_ARRAY] = "TYPE_PACKED_STRING_ARRAY"
types[TYPE_PACKED_VECTOR2_ARRAY] = "TYPE_PACKED_VECTOR2_ARRAY"
types[TYPE_PACKED_VECTOR3_ARRAY] = "TYPE_PACKED_VECTOR3_ARRAY"
types[TYPE_PACKED_COLOR_ARRAY] = "TYPE_PACKED_COLOR_ARRAY"
types[TYPE_MAX] = "TYPE_MAX"
types[TYPE_STRING_NAME] = "TYPE_STRING_NAME"
types[TYPE_SIGNAL] = "SIGNAL"
types[TYPE_STRING_NAME] = "STRING_NAME"
types[TYPE_STRING] = "STRING"
types[TYPE_TRANSFORM2D] = "TRANSFORM2D"
types[TYPE_TRANSFORM3D] = "TRANSFORM3D"
types[TYPE_VECTOR2] = "VECTOR2"
types[TYPE_VECTOR2I] = "VECTOR2I"
types[TYPE_VECTOR3] = "VECTOR3"
types[TYPE_VECTOR3I] = "VECTOR3I"
types[TYPE_VECTOR4] = "VECTOR4"
types[TYPE_VECTOR4I] = "VECTOR4I"
# Types to not be formatted when using _str

View file

@ -41,9 +41,7 @@ func _init(target = null, method = null, subpath = null):
_lgr.warn(str(target, " is not a valid path"))
if stub_target is PackedScene:
stub_target = _utils.get_scene_script_object(stub_target)
elif _utils.is_native_class(stub_target):
print("Got a native class: ", stub_target)
stub_target = GutUtils.get_scene_script_object(stub_target)
# this is used internally to stub default parameters for everything that is
# doubled...or something. Look for stub_defaults_from_meta for usage. This
@ -77,9 +75,6 @@ func to_do_nothing():
func to_call_super():
if stub_method == "_init":
_lgr.error("You cannot stub _init to call super. Super's _init is always called.")
else:
call_super = true
_parameter_override_only = false
return self

View file

@ -152,7 +152,7 @@ func get_return(obj, method, parameters = null):
if stub_info != null:
return stub_info.return_val
else:
_lgr.warn(
_lgr.info(
str(
"Call to [",
method,
@ -165,9 +165,6 @@ func get_return(obj, method, parameters = null):
func should_call_super(obj, method, parameters = null):
if _utils.non_super_methods.has(method):
return false
var stub_info = _find_stub(obj, method, parameters)
var is_partial = false

View file

@ -1,287 +1,204 @@
# ------------------------------------------------------------------------------
# Contains all the results of a single test. Allows for multiple asserts results
# and pending calls.
#
# When determining the status of a test, check for failing then passing then
# pending.
# Prints things, mostly. Knows too much about gut.gd, but it's only supposed to
# work with gut.gd, so I'm fine with that.
# ------------------------------------------------------------------------------
class Test:
var pass_texts = []
var fail_texts = []
var pending_texts = []
var orphans = 0
var line_number = 0
# must have passed an assert and not have any other status to be passing
func is_passing():
return pass_texts.size() > 0 and fail_texts.size() == 0 and pending_texts.size() == 0
# failing takes precedence over everything else, so any failures makes the
# test a failure.
func is_failing():
return fail_texts.size() > 0
# test is only pending if pending was called and the test is not failing.
func is_pending():
return pending_texts.size() > 0 and fail_texts.size() == 0
func did_something():
return is_passing() or is_failing() or is_pending()
# NOTE: The "failed" and "pending" text must match what is outputted by
# the logger in order for text highlighting to occur in summary.
func to_s():
var pad = " "
var to_return = ""
for i in range(fail_texts.size()):
to_return += str(pad, "[Failed]: ", fail_texts[i], "\n")
for i in range(pending_texts.size()):
to_return += str(pad, "[Pending]: ", pending_texts[i], "\n")
return to_return
func get_status():
var to_return = "no asserts"
if pending_texts.size() > 0:
to_return = "pending"
elif fail_texts.size() > 0:
to_return = "fail"
elif pass_texts.size() > 0:
to_return = "pass"
return to_return
# a _test_collector to use when one is not provided.
var _gut = null
# ------------------------------------------------------------------------------
# Contains all the results for a single test-script/inner class. Persists the
# names of the tests and results and the order in which the tests were run.
# ------------------------------------------------------------------------------
class TestScript:
var name = "NOT_SET"
var was_skipped = false
var skip_reason = ""
var _tests = {}
var _test_order = []
func _init(gut = null):
_gut = gut
func _init(script_name):
name = script_name
func get_pass_count():
var count = 0
for key in _tests:
count += _tests[key].pass_texts.size()
return count
# ---------------------
# Private
# ---------------------
func _log_end_run_header(gut):
var lgr = gut.get_logger()
lgr.log("\n\n\n")
lgr.log("==============================================", lgr.fmts.yellow)
lgr.log("= Run Summary", lgr.fmts.yellow)
lgr.log("==============================================", lgr.fmts.yellow)
func get_fail_count():
var count = 0
for key in _tests:
count += _tests[key].fail_texts.size()
return count
func get_pending_count():
var count = 0
for key in _tests:
count += _tests[key].pending_texts.size()
return count
func _log_what_was_run(gut):
if !gut._utils.is_null_or_empty(gut._select_script):
gut.p('Ran Scripts matching "' + gut._select_script + '"')
if !gut._utils.is_null_or_empty(gut._unit_test_name):
gut.p('Ran Tests matching "' + gut._unit_test_name + '"')
if !gut._utils.is_null_or_empty(gut._inner_class_name):
gut.p('Ran Inner Classes matching "' + gut._inner_class_name + '"')
func get_passing_test_count():
var count = 0
for key in _tests:
if _tests[key].is_passing():
count += 1
return count
func get_failing_test_count():
var count = 0
for key in _tests:
if _tests[key].is_failing():
count += 1
return count
func _log_orphans_and_disclaimer(gut):
var orphan_count = gut.get_orphan_counter()
var lgr = gut.get_logger()
# Do not count any of the _test_script_objects since these will be released
# when GUT is released.
orphan_count._counters.total += gut._test_script_objects.size()
if orphan_count.get_counter("total") > 0 and lgr.is_type_enabled("orphan"):
orphan_count.print_orphans("total", lgr)
gut.p("Note: This count does not include GUT objects that will be freed upon exit.")
gut.p(" It also does not include any orphans created by global scripts")
gut.p(" loaded before tests were ran.")
gut.p(str("Total orphans = ", orphan_count.orphan_count()))
gut.p("")
func get_risky_count():
var count = 0
if was_skipped:
count = 1
func _total_fmt(text, value):
var space = 18
if str(value) == "0":
value = "none"
return str(text.rpad(space), value)
func _log_non_zero_total(text, value, lgr):
if str(value) != "0":
lgr.log(_total_fmt(text, value))
return 1
else:
for key in _tests:
if !_tests[key].did_something():
count += 1
return count
func get_test_obj(obj_name):
if !_tests.has(obj_name):
var to_add = Test.new()
_tests[obj_name] = to_add
_test_order.append(obj_name)
var to_return = _tests[obj_name]
return to_return
func add_pass(test_name, reason):
var t = get_test_obj(test_name)
t.pass_texts.append(reason)
func add_fail(test_name, reason):
var t = get_test_obj(test_name)
t.fail_texts.append(reason)
func add_pending(test_name, reason):
var t = get_test_obj(test_name)
t.pending_texts.append(reason)
func get_tests():
return _tests
return 0
# ------------------------------------------------------------------------------
# Summary Class
#
# This class holds the results of all the test scripts and Inner Classes that
# were run.
# ------------------------------------------------------------------------------
var _scripts = []
func _log_totals(gut, totals):
var lgr = gut.get_logger()
lgr.log()
lgr.log("---- Totals ----")
var col1 = 18
var issue_count = 0
issue_count += _log_non_zero_total("Errors", totals.errors, lgr)
issue_count += _log_non_zero_total("Warnings", totals.warnings, lgr)
issue_count += _log_non_zero_total("Deprecated", totals.deprecated, lgr)
if issue_count > 0:
lgr.log("")
func add_script(name):
_scripts.append(TestScript.new(name))
func get_scripts():
return _scripts
func get_current_script():
return _scripts[_scripts.size() - 1]
func add_test(test_name):
# print('-- test_name = ', test_name)
# print('-- current script = ', get_current_script())
# print('-- test_obj = ', get_current_script().get_test_obj(test_name))
return get_current_script().get_test_obj(test_name)
func add_pass(test_name, reason = ""):
get_current_script().add_pass(test_name, reason)
func add_fail(test_name, reason = ""):
get_current_script().add_fail(test_name, reason)
func add_pending(test_name, reason = ""):
get_current_script().add_pending(test_name, reason)
func get_test_text(test_name):
return test_name + "\n" + get_current_script().get_test_obj(test_name).to_s()
# Gets the count of unique script names minus the .<Inner Class Name> at the
# end. Used for displaying the number of scripts without including all the
# Inner Classes.
func get_non_inner_class_script_count():
var counter = load("res://addons/gut/thing_counter.gd").new()
for i in range(_scripts.size()):
var ext_loc = _scripts[i].name.rfind(".gd.")
var to_add = _scripts[i].name
if ext_loc != -1:
to_add = _scripts[i].name.substr(0, ext_loc + 3)
counter.add(to_add)
return counter.get_unique_count()
func get_totals():
var totals = {
passing = 0,
pending = 0,
failing = 0,
risky = 0,
tests = 0,
scripts = 0,
passing_tests = 0,
failing_tests = 0
}
for i in range(_scripts.size()):
# assert totals
totals.passing += _scripts[i].get_pass_count()
totals.pending += _scripts[i].get_pending_count()
totals.failing += _scripts[i].get_fail_count()
# test totals
totals.tests += _scripts[i]._test_order.size()
totals.passing_tests += _scripts[i].get_passing_test_count()
totals.failing_tests += _scripts[i].get_failing_test_count()
totals.risky += _scripts[i].get_risky_count()
totals.scripts = get_non_inner_class_script_count()
lgr.log(_total_fmt("Scripts", totals.scripts))
lgr.log(_total_fmt("Tests", gut.get_test_collector().get_ran_test_count()))
lgr.log(_total_fmt(" Passing", totals.passing_tests))
_log_non_zero_total(" Failing", totals.failing_tests, lgr)
_log_non_zero_total(" Risky/Pending", totals.risky + totals.pending, lgr)
lgr.log(_total_fmt("Asserts", totals.passing + totals.failing))
lgr.log(_total_fmt("Time", str(gut.get_elapsed_time(), "s")))
return totals
func log_summary_text(lgr):
var orig_indent = lgr.get_indent_level()
var found_failing_or_pending = false
# ---------------------
# Public
# ---------------------
func log_all_non_passing_tests(gut = _gut):
var test_collector = gut.get_test_collector()
var lgr = gut.get_logger()
for s in range(_scripts.size()):
var to_return = {passing = 0, non_passing = 0}
for test_script in test_collector.scripts:
lgr.set_indent_level(0)
if (
_scripts[s].was_skipped
or _scripts[s].get_fail_count() > 0
or _scripts[s].get_pending_count() > 0
):
lgr.log(_scripts[s].name, lgr.fmts.underline)
if _scripts[s].was_skipped:
if (
test_script.was_skipped
or test_script.get_fail_count() > 0
or test_script.get_pending_count() > 0
):
lgr.log("\n" + test_script.get_full_name(), lgr.fmts.underline)
if test_script.was_skipped:
lgr.inc_indent()
var skip_msg = str("[Risky] Script was skipped: ", _scripts[s].skip_reason)
var skip_msg = str("[Risky] Script was skipped: ", test_script.skip_reason)
lgr.log(skip_msg, lgr.fmts.yellow)
lgr.dec_indent()
for t in range(_scripts[s]._test_order.size()):
var tname = _scripts[s]._test_order[t]
var test = _scripts[s].get_test_obj(tname)
if !test.is_passing():
found_failing_or_pending = true
lgr.log(str("- ", tname))
for test in test_script.tests:
if test.was_run:
if test.is_passing():
to_return.passing += 1
else:
to_return.non_passing += 1
lgr.log(str("- ", test.name))
lgr.inc_indent()
for i in range(test.fail_texts.size()):
lgr.failed(test.fail_texts[i])
for i in range(test.pending_texts.size()):
lgr.pending(test.pending_texts[i])
if !test.did_something():
lgr.log("[Risky] Did not assert", lgr.fmts.yellow)
if test.is_risky():
lgr.risky("Did not assert")
lgr.dec_indent()
return to_return
func log_the_final_line(totals, gut):
var lgr = gut.get_logger()
var grand_total_text = ""
var grand_total_fmt = lgr.fmts.none
if totals.failing_tests > 0:
grand_total_text = str(totals.failing_tests, " failing tests")
grand_total_fmt = lgr.fmts.red
elif totals.risky > 0 or totals.pending > 0:
grand_total_text = str(totals.risky + totals.pending, " pending/risky tests.")
grand_total_fmt = lgr.fmts.yellow
else:
grand_total_text = "All tests passed!"
grand_total_fmt = lgr.fmts.green
lgr.log(str("---- ", grand_total_text, " ----"), grand_total_fmt)
func log_totals(gut, totals):
var lgr = gut.get_logger()
var orig_indent = lgr.get_indent_level()
lgr.set_indent_level(0)
if !found_failing_or_pending:
lgr.log("All tests passed", lgr.fmts.green)
# just picked a non-printable char, dunno if it is a good or bad choice.
var npws = PackedByteArray([31]).get_string_from_ascii()
lgr.log()
var _totals = get_totals()
lgr.log("Totals", lgr.fmts.yellow)
lgr.log(str("Scripts: ", get_non_inner_class_script_count()))
lgr.log(str("Passing tests ", _totals.passing_tests))
lgr.log(str("Failing tests ", _totals.failing_tests))
lgr.log(str("Risky tests ", _totals.risky))
var pnd = str("Pending: ", _totals.pending)
# add a non printable character so this "pending" isn't highlighted in the
# editor's output panel.
lgr.log(str(npws, pnd))
lgr.log(
str(
"Asserts: ",
_totals.passing,
" of ",
_totals.passing + _totals.failing,
" passed"
)
)
_log_totals(gut, totals)
lgr.set_indent_level(orig_indent)
func get_totals(gut = _gut):
var tc = gut.get_test_collector()
var lgr = gut.get_logger()
var totals = {
failing = 0,
failing_tests = 0,
passing = 0,
passing_tests = 0,
pending = 0,
risky = 0,
scripts = tc.get_ran_script_count(),
tests = 0,
deprecated = lgr.get_deprecated().size(),
errors = lgr.get_errors().size(),
warnings = lgr.get_warnings().size(),
}
for s in tc.scripts:
# assert totals
totals.passing += s.get_pass_count()
totals.pending += s.get_pending_count()
totals.failing += s.get_fail_count()
# test totals
totals.tests += s.get_ran_test_count()
totals.passing_tests += s.get_passing_test_count()
totals.failing_tests += s.get_failing_test_count()
totals.risky += s.get_risky_count()
return totals
func log_end_run(gut = _gut):
_log_end_run_header(gut)
var totals = get_totals(gut)
var tc = gut.get_test_collector()
var lgr = gut.get_logger()
log_all_non_passing_tests(gut)
log_totals(gut, totals)
lgr.log("\n")
_log_orphans_and_disclaimer(gut)
_log_what_was_run(gut)
log_the_final_line(totals, gut)
lgr.log("")

View file

@ -61,7 +61,7 @@ var _summary = {asserts = 0, passed = 0, failed = 0, tests = 0, pending = 0}
var _signal_watcher = load("res://addons/gut/signal_watcher.gd").new()
# Convenience copy of _utils.DOUBLE_STRATEGY
var DOUBLE_STRATEGY = _utils.DOUBLE_STRATEGY
var DOUBLE_STRATEGY = GutUtils.DOUBLE_STRATEGY
var _lgr = _utils.get_logger()
var _strutils = _utils.Strutils.new()
@ -89,7 +89,7 @@ func _fail(text):
_summary.failed += 1
_fail_pass_text.append("failed: " + text)
if gut:
_lgr.failed(text)
_lgr.failed(gut.get_call_count_text() + text)
gut._fail(text)
@ -215,6 +215,58 @@ func _fail_if_parameters_not_array(parameters):
return invalid
# ------------------------------------------------------------------------------
# A bunch of common checkes used when validating a double/method pair. If
# everything is ok then an empty string is returned, otherwise the message
# is returned.
# ------------------------------------------------------------------------------
func _get_bad_double_or_method_message(inst, method_name, what_you_cant_do):
var to_return = ""
if !_utils.is_double(inst):
to_return = str("An instance of a Double was expected, you passed: ", _str(inst))
elif !inst.has_method(method_name):
to_return = str(
"You cannot ",
what_you_cant_do,
" [",
method_name,
"] because the method does not exist. ",
"This can happen if the method is virtual and not overloaded (i.e. _ready) ",
"or you have mistyped the name of the method."
)
elif !inst.__gutdbl_values.doubled_methods.has(method_name):
to_return = str(
"You cannot ",
what_you_cant_do,
" [",
method_name,
"] because ",
_str(inst),
" does not overload it or it was ignored with ",
"ignore_method_when_doubling. See Doubling ",
"Strategy in the wiki for details on including non-overloaded ",
"methods in a double."
)
return to_return
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func _fail_if_not_double_or_does_not_have_method(inst, method_name):
var to_return = OK
var msg = _get_bad_double_or_method_message(inst, method_name, "spy on")
if msg != "":
_fail(msg)
to_return = ERR_INVALID_DATA
return to_return
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func _create_obj_from_type(type):
var obj = null
if type.is_class("PackedScene"):
@ -276,13 +328,16 @@ func assert_eq(got, expected, text = ""):
var disp = "[" + _str(got) + "] expected to equal [" + _str(expected) + "]: " + text
var result = null
if typeof(got) == TYPE_ARRAY:
result = _compare.shallow(got, expected)
else:
result = _compare.simple(got, expected)
if typeof(got) in [TYPE_ARRAY, TYPE_DICTIONARY]:
disp = str(result.summary, " ", text)
(
_lgr
. info(
"Array/Dictionary compared by value. Use assert_same to compare references. Use assert_eq_deep to see diff when failing."
)
)
if result.are_equal:
_pass(disp)
@ -300,13 +355,16 @@ func assert_ne(got, not_expected, text = ""):
)
var result = null
if typeof(got) == TYPE_ARRAY:
result = _compare.shallow(got, not_expected)
else:
result = _compare.simple(got, not_expected)
if typeof(got) in [TYPE_ARRAY, TYPE_DICTIONARY]:
disp = str(result.summary, " ", text)
(
_lgr
. info(
"Array/Dictionary compared by value. Use assert_not_same to compare references. Use assert_ne_deep to see diff."
)
)
if result.are_equal:
_fail(disp)
@ -927,7 +985,7 @@ func assert_is(object, a_class, text = ""):
if !_utils.is_native_class(a_class) and !_utils.is_gdscript(a_class):
_fail(str(bad_param_2, a_str))
else:
if object is a_class:
if is_instance_of(object, a_class):
_pass(disp)
else:
_fail(disp)
@ -1047,11 +1105,7 @@ func assert_called(inst, method_name, parameters = null):
if _fail_if_parameters_not_array(parameters):
return
if !_utils.is_double(inst):
_fail(
"You must pass a doubled instance to assert_called. Check the wiki for info on using double."
)
else:
if _fail_if_not_double_or_does_not_have_method(inst, method_name) == OK:
if gut.get_spy().was_called(inst, method_name, parameters):
_pass(disp)
else:
@ -1071,11 +1125,7 @@ func assert_not_called(inst, method_name, parameters = null):
if _fail_if_parameters_not_array(parameters):
return
if !_utils.is_double(inst):
_fail(
"You must pass a doubled instance to assert_not_called. Check the wiki for info on using double."
)
else:
if _fail_if_not_double_or_does_not_have_method(inst, method_name) == OK:
if gut.get_spy().was_called(inst, method_name, parameters):
if parameters != null:
disp += str(" with parameters ", parameters)
@ -1101,11 +1151,7 @@ func assert_call_count(inst, method_name, expected_count, parameters = null):
var disp = "Expected [%s] on %s to be called [%s] times%s. It was called [%s] times."
disp = disp % [method_name, _str(inst), expected_count, param_text, count]
if !_utils.is_double(inst):
_fail(
"You must pass a doubled instance to assert_call_count. Check the wiki for info on using double."
)
else:
if _fail_if_not_double_or_does_not_have_method(inst, method_name) == OK:
if count == expected_count:
_pass(disp)
else:
@ -1254,7 +1300,7 @@ func assert_property_with_backing_variable(
):
var setter_name = str("@", property_name, "_setter")
var getter_name = str("@", property_name, "_getter")
var backing_name = _utils.nvl(backed_by_name, str("_", property_name))
var backing_name = GutUtils.nvl(backed_by_name, str("_", property_name))
var pre_fail_count = get_fail_count()
var props = obj.get_property_list()
@ -1438,7 +1484,7 @@ func get_summary_text():
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
func _smart_double(thing, double_strat, partial):
var override_strat = _utils.nvl(double_strat, gut.get_doubler().get_strategy())
var override_strat = GutUtils.nvl(double_strat, gut.get_doubler().get_strategy())
var to_return = null
if thing is PackedScene:
@ -1531,7 +1577,7 @@ func double_scene(path, strategy = null):
_lgr.deprecated("test.double_scene has been removed.", "double")
return null
# var override_strat = _utils.nvl(strategy, gut.get_doubler().get_strategy())
# var override_strat = GutUtils.nvl(strategy, gut.get_doubler().get_strategy())
# return gut.get_doubler().double_scene(path, override_strat)
@ -1542,7 +1588,7 @@ func double_script(path, strategy = null):
_lgr.deprecated("test.double_script has been removed.", "double")
return null
# var override_strat = _utils.nvl(strategy, gut.get_doubler().get_strategy())
# var override_strat = GutUtils.nvl(strategy, gut.get_doubler().get_strategy())
# return gut.get_doubler().double(path, override_strat)
@ -1555,7 +1601,7 @@ func double_inner(path, subpath, strategy = null):
)
return null
var override_strat = _utils.nvl(strategy, gut.get_doubler().get_strategy())
var override_strat = GutUtils.nvl(strategy, gut.get_doubler().get_strategy())
return gut.get_doubler().double_inner(path, subpath, override_strat)
@ -1574,11 +1620,8 @@ func ignore_method_when_doubling(thing, method_name):
return
var r = thing
if thing is PackedScene:
var inst = thing.instantiate()
if inst.get_script():
r = inst.get_script()
r = GutUtils.get_scene_script_object(thing)
gut.get_doubler().add_ignored_method(r, method_name)
@ -1595,16 +1638,18 @@ func ignore_method_when_doubling(thing, method_name):
# to leave it but not update the wiki.
# ------------------------------------------------------------------------------
func stub(thing, p2, p3 = null):
if _utils.is_instance(thing) and !_utils.is_double(thing):
_lgr.error(str("You cannot use stub on ", _str(thing), " because it is not a double."))
return _utils.StubParams.new()
var method_name = p2
var subpath = null
if p3 != null:
subpath = p2
method_name = p3
if _utils.is_instance(thing):
var msg = _get_bad_double_or_method_message(thing, method_name, "stub")
if msg != "":
_lgr.error(msg)
return _utils.StubParams.new()
var sp = _utils.StubParams.new(thing, method_name, subpath)
gut.get_stubber().add_stub(sp)
return sp
@ -1613,8 +1658,8 @@ func stub(thing, p2, p3 = null):
# ------------------------------------------------------------------------------
# convenience wrapper.
# ------------------------------------------------------------------------------
func simulate(obj, times, delta):
gut.simulate(obj, times, delta)
func simulate(obj, times, delta, check_is_processing: bool = false):
gut.simulate(obj, times, delta, check_is_processing)
# ------------------------------------------------------------------------------
@ -1670,11 +1715,12 @@ func use_parameters(params):
ph = _utils.ParameterHandler.new(params)
gut.parameter_handler = ph
var output = str(
"(call #", ph.get_call_count() + 1, ") with parameters: ", ph.get_current_parameters()
)
_lgr.log(output)
_lgr.inc_indent()
# DO NOT use gut.gd's get_call_count_text here since it decrements the
# get_call_count value. This method increments the call count in its
# return statement.
var output = str("- params[", ph.get_call_count(), "]", "(", ph.get_current_parameters(), ")")
gut.p(output, gut.LOG_LEVEL_TEST_AND_FAILURES)
return ph.next_parameters()
@ -1729,7 +1775,8 @@ func is_passing():
and !["before_all", "after_all"].has(gut.get_current_test_object().name)
):
return (
gut.get_current_test_object().passed and gut.get_current_test_object().assert_count > 0
gut.get_current_test_object().is_passing()
and gut.get_current_test_object().assert_count > 0
)
else:
_lgr.error("No current test object found. is_passing must be called inside a test.")
@ -1744,7 +1791,7 @@ func is_failing():
gut.get_current_test_object() != null
and !["before_all", "after_all"].has(gut.get_current_test_object().name)
):
return !gut.get_current_test_object().passed
return gut.get_current_test_object().is_failing()
else:
_lgr.error("No current test object found. is_failing must be called inside a test.")
return null
@ -1777,14 +1824,14 @@ func compare_deep(v1, v2, max_differences = null):
# ------------------------------------------------------------------------------
# Peforms a shallow compare on both values, a CompareResult instnace is returned.
# The optional max_differences paramter sets the max_differences to be displayed.
# REMOVED
# ------------------------------------------------------------------------------
func compare_shallow(v1, v2, max_differences = null):
var result = _compare.shallow(v1, v2)
if max_differences != null:
result.max_differences = max_differences
return result
_fail("compare_shallow has been removed. Use compare_deep or just compare using == instead.")
_lgr.error(
"compare_shallow has been removed. Use compare_deep or just compare using == instead."
)
return null
# ------------------------------------------------------------------------------
@ -1810,25 +1857,36 @@ func assert_ne_deep(v1, v2):
# ------------------------------------------------------------------------------
# Performs a shallow compare and asserts the values are equal
# REMOVED
# ------------------------------------------------------------------------------
func assert_eq_shallow(v1, v2):
var result = compare_shallow(v1, v2)
if result.are_equal:
_pass(result.get_short_summary())
else:
_fail(result.summary)
_fail("assert_eq_shallow has been removed. Use assert_eq/assert_same/assert_eq_deep")
# ------------------------------------------------------------------------------
# Performs a shallow compare and asserts the values are not equal
# REMOVED
# ------------------------------------------------------------------------------
func assert_ne_shallow(v1, v2):
var result = compare_shallow(v1, v2)
if !result.are_equal:
_pass(result.get_short_summary())
_fail("assert_eq_shallow has been removed. Use assert_eq/assert_same/assert_eq_deep")
# ------------------------------------------------------------------------------
# Assert wrapper for is_same
# ------------------------------------------------------------------------------
func assert_same(v1, v2, text = ""):
var disp = "[" + _str(v1) + "] expected to be same as [" + _str(v2) + "]: " + text
if is_same(v1, v2):
_pass(disp)
else:
_fail(result.get_short_summary())
_fail(disp)
func assert_not_same(v1, v2, text = ""):
var disp = "[" + _str(v1) + "] expected to not be same as [" + _str(v2) + "]: " + text
if is_same(v1, v2):
_fail(disp)
else:
_pass(disp)
# ------------------------------------------------------------------------------

View file

@ -1,148 +1,21 @@
# ------------------------------------------------------------------------------
# Used to keep track of info about each test ran.
# ------------------------------------------------------------------------------
class Test:
# indicator if it passed or not. defaults to true since it takes only
# one failure to make it not pass. _fail in gut will set this.
var passed = true
# the name of the function
var name = ""
# flag to know if the name has been printed yet.
var has_printed_name = false
# the number of arguments the method has
var arg_count = 0
# The number of asserts in the test
var assert_count = 0
# if the test has been marked pending at anypont during
# execution.
var pending = false
# the line number when the test fails
var line_number = -1
# Set this to true to prevent GUT from running the test.
var should_skip = false
func did_pass():
return passed and !pending and assert_count > 0
func did_assert():
return assert_count > 0 or pending
# ------------------------------------------------------------------------------
# This holds all the meta information for a test script. It contains the
# name of the inner class and an array of Test "structs".
# This class handles calling out to the test parser and maintaining an array of
# collected_script.gd. This is used for both calling the tests and tracking
# the results of each script and test's execution.
#
# This class also facilitates all the exporting and importing of tests.
# This also handles exporting and importing tests.
# ------------------------------------------------------------------------------
class TestScript:
var inner_class_name: StringName
var tests = []
var path: String
var _utils = null
var _lgr = null
var is_loaded = false
var CollectedScript = load("res://addons/gut/collected_script.gd")
var CollectedTest = load("res://addons/gut/collected_test.gd")
func _init(utils = null, logger = null):
_utils = utils
_lgr = logger
func to_s():
var to_return = path
if inner_class_name != null:
to_return += str(".", inner_class_name)
to_return += "\n"
for i in range(tests.size()):
to_return += str(" ", tests[i].name, "\n")
return to_return
func get_new():
return load_script().new()
func load_script():
var to_return = load(path)
if inner_class_name != null and inner_class_name != "":
# If we wanted to do inner classes in inner classses
# then this would have to become some kind of loop or recursive
# call to go all the way down the chain or this class would
# have to change to hold onto the loaded class instead of
# just path information.
to_return = to_return.get(inner_class_name)
return to_return
func get_filename_and_inner():
var to_return = get_filename()
if inner_class_name != "":
to_return += "." + String(inner_class_name)
return to_return
func get_full_name():
var to_return = path
if inner_class_name != "":
to_return += "." + String(inner_class_name)
return to_return
func get_filename():
return path.get_file()
func has_inner_class():
return inner_class_name != ""
# Note: although this no longer needs to export the inner_class names since
# they are pulled from metadata now, it is easier to leave that in
# so we don't have to cut the export down to unique script names.
func export_to(config_file, section):
config_file.set_value(section, "path", path)
config_file.set_value(section, "inner_class", inner_class_name)
var names = []
for i in range(tests.size()):
names.append(tests[i].name)
config_file.set_value(section, "tests", names)
func _remap_path(source_path):
var to_return = source_path
if !_utils.file_exists(source_path):
_lgr.debug("Checking for remap for: " + source_path)
var remap_path = source_path.get_basename() + ".gd.remap"
if _utils.file_exists(remap_path):
var cf = ConfigFile.new()
cf.load(remap_path)
to_return = cf.get_value("remap", "path")
else:
_lgr.warn("Could not find remap file " + remap_path)
return to_return
func import_from(config_file, section):
path = config_file.get_value(section, "path")
path = _remap_path(path)
# Null is an acceptable value, but you can't pass null as a default to
# get_value since it thinks you didn't send a default...then it spits
# out red text. This works around that.
var inner_name = config_file.get_value(section, "inner_class", "Placeholder")
if inner_name != "Placeholder":
inner_class_name = inner_name
else: # just being explicit
inner_class_name = StringName("")
func get_test_named(name):
return _utils.search_array(tests, "name", name)
func mark_tests_to_skip_with_suffix(suffix):
for single_test in tests:
single_test.should_skip = single_test.name.ends_with(suffix)
# ------------------------------------------------------------------------------
# start test_collector, I don't think I like the name.
# ------------------------------------------------------------------------------
var scripts = []
var _test_prefix = "test_"
var _test_class_prefix = "Test"
var _utils = load("res://addons/gut/utils.gd").get_instance()
var _lgr = _utils.get_logger()
# Array of CollectedScripts.
var scripts = []
func _does_inherit_from_test(thing):
var base_script = thing.get_base_script()
@ -156,7 +29,7 @@ func _does_inherit_from_test(thing):
return to_return
func _populate_tests(test_script: TestScript):
func _populate_tests(test_script):
var script = test_script.load_script()
if script == null:
print(" !!! ", test_script.path, " could not be loaded")
@ -167,7 +40,7 @@ func _populate_tests(test_script: TestScript):
for i in range(methods.size()):
var name = methods[i]["name"]
if name.begins_with(_test_prefix):
var t = Test.new()
var t = CollectedTest.new()
t.name = name
t.arg_count = methods[i]["args"].size()
test_script.tests.append(t)
@ -184,11 +57,7 @@ func _get_inner_test_class_names(loaded):
inner_classes.append(key)
else:
_lgr.warn(
str(
"Ignoring Inner Class ",
key,
" because it does not extend res://addons/gut/test.gd"
)
str("Ignoring Inner Class ", key, " because it does not extend GutTest")
)
# This could go deeper and find inner classes within inner classes
@ -208,11 +77,13 @@ func _parse_script(test_script):
_populate_tests(test_script)
scripts_found.append(test_script.path)
inner_classes = _get_inner_test_class_names(loaded)
else:
return []
for i in range(inner_classes.size()):
var loaded_inner = loaded.get(inner_classes[i])
if _does_inherit_from_test(loaded_inner):
var ts = TestScript.new(_utils, _lgr)
var ts = CollectedScript.new(_utils, _lgr)
ts.path = test_script.path
ts.inner_class_name = inner_classes[i]
_populate_tests(ts)
@ -226,7 +97,6 @@ func _parse_script(test_script):
# Public
# -----------------
func add_script(path):
# print('Adding ', path)
# SHORTCIRCUIT
if has_script(path):
return []
@ -236,10 +106,21 @@ func add_script(path):
_lgr.error("Could not find script: " + path)
return
var ts = TestScript.new(_utils, _lgr)
var ts = CollectedScript.new(_utils, _lgr)
ts.path = path
# Append right away because if we don't test_doubler.gd.TestInitParameters
# will HARD crash. I couldn't figure out what was causing the issue but
# appending right away, and then removing if it's not valid seems to fix
# things. It might have to do with the ordering of the test classes in
# the test collecter. I'm not really sure.
scripts.append(ts)
return _parse_script(ts)
var parse_results = _parse_script(ts)
if parse_results.find(path) == -1:
_lgr.warn(str("Ignoring script ", path, " because it does not extend GutTest"))
scripts.remove_at(scripts.find(ts))
return parse_results
func clear():
@ -261,7 +142,7 @@ func export_tests(path):
var success = true
var f = ConfigFile.new()
for i in range(scripts.size()):
scripts[i].export_to(f, str("TestScript-", i))
scripts[i].export_to(f, str("CollectedScript-", i))
var result = f.save(path)
if result != OK:
_lgr.error(str("Could not save exported tests to [", path, "]. Error code: ", result))
@ -278,7 +159,7 @@ func import_tests(path):
else:
var sections = f.get_sections()
for key in sections:
var ts = TestScript.new(_utils, _lgr)
var ts = CollectedScript.new(_utils, _lgr)
ts.import_from(f, key)
_populate_tests(ts)
scripts.append(ts)
@ -330,3 +211,57 @@ func get_test_class_prefix():
func set_test_class_prefix(test_class_prefix):
_test_class_prefix = test_class_prefix
func get_scripts():
return scripts
func get_ran_test_count():
var count = 0
for s in scripts:
count += s.get_ran_test_count()
return count
func get_ran_script_count():
var count = 0
for s in scripts:
if s.was_run:
count += 1
return count
func get_test_count():
var count = 0
for s in scripts:
count += s.tests.size()
return count
func get_assert_count():
var count = 0
for s in scripts:
count += s.get_assert_count()
return count
func get_pass_count():
var count = 0
for s in scripts:
count += s.get_pass_count()
return count
func get_fail_count():
var count = 0
for s in scripts:
count += s.get_fail_count()
return count
func get_pending_count():
var count = 0
for s in scripts:
count += s.get_pending_count()
return count

View file

@ -1,44 +1,41 @@
# ##############################################################################
#(G)odot (U)nit (T)est class
#
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2020 Tom "Butch" Wesley
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ##############################################################################
class_name GutUtils
# ------------------------------------------------------------------------------
# Description
# -----------
# This class is a PSUEDO SINGLETON. You should not make instances of it but use
# the get_instance static method.
# ##############################################################################
extends Node
# ------------------------------------------------------------------------------
# The instance name as a function since you can't have static variables.
# NOTE: I think this can become completely static now that we have static
# variables. A lot would have to change though. But it would be good
# to do.
# ------------------------------------------------------------------------------
static func INSTANCE_NAME():
return "__GutUtilsInstName__"
const GUT_METADATA = "__gutdbl"
# Note, these cannot change since places are checking for TYPE_INT to determine
# how to process parameters.
enum DOUBLE_STRATEGY {
INCLUDE_NATIVE,
SCRIPT_ONLY,
}
enum DIFF { DEEP, SIMPLE }
const TEST_STATUSES = {
NO_ASSERTS = "no asserts",
SKIPPED = "skipped",
NOT_RUN = "not run",
PENDING = "pending",
# These two got the "ed" b/c pass is a reserved word and I could not
# think of better words.
FAILED = "fail",
PASSED = "pass"
}
# This is a holdover from when GUT was making a psuedo autoload. It would add
# an instance of this class to the tree with a name and retrieve it when
# get_instance was called. We now have static variables so this var is now
# used instead of a node.
static var _the_instance = null
# ------------------------------------------------------------------------------
@ -61,17 +58,91 @@ static func get_root_node():
# running.
# ------------------------------------------------------------------------------
static func get_instance():
var the_root = get_root_node()
var inst = null
if the_root.has_node(INSTANCE_NAME()):
inst = the_root.get_node(INSTANCE_NAME())
if _the_instance == null:
_the_instance = GutUtils.new()
return _the_instance
# ------------------------------------------------------------------------------
# Gets the value from an enum. If passed an int it will return it if the enum
# contains it. If passed a string it will convert it to upper case and replace
# spaces with underscores. If the enum contains the key, it will return the
# value for they key. When keys or ints are not found, the default is returned.
# ------------------------------------------------------------------------------
static func get_enum_value(thing, e, default = null):
var to_return = default
if typeof(thing) == TYPE_STRING:
var converted = thing.to_upper().replace(" ", "_")
if e.keys().has(converted):
to_return = e[converted]
else:
inst = load("res://addons/gut/utils.gd").new()
inst.set_name(INSTANCE_NAME())
the_root.add_child(inst)
return inst
if e.values().has(thing):
to_return = thing
return to_return
# ------------------------------------------------------------------------------
# return if_null if value is null otherwise return value
# ------------------------------------------------------------------------------
static func nvl(value, if_null):
if value == null:
return if_null
else:
return value
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
static func pretty_print(dict):
print(JSON.stringify(dict, " "))
# ------------------------------------------------------------------------------
# ------------------------------------------------------------------------------
static func print_properties(props, thing, print_all_meta = false):
for i in range(props.size()):
var prop_name = props[i].name
var prop_value = thing.get(props[i].name)
var print_value = str(prop_value)
if print_value.length() > 100:
print_value = print_value.substr(0, 97) + "..."
elif print_value == "":
print_value = "EMPTY"
print(prop_name, " = ", print_value)
if print_all_meta:
print(" ", props[i])
# ------------------------------------------------------------------------------
# Gets the value of the node_property 'script' from a PackedScene's root node.
# This does not assume the location of the root node in the PackedScene's node
# list. This also does not assume the index of the 'script' node property in
# a nodes's property list.
# ------------------------------------------------------------------------------
static func get_scene_script_object(scene):
var state = scene.get_state()
var to_return = null
var root_node_path = NodePath(".")
var node_idx = 0
while node_idx < state.get_node_count() and to_return == null:
if state.get_node_path(node_idx) == root_node_path:
for i in range(state.get_node_property_count(node_idx)):
if state.get_node_property_name(node_idx, i) == "script":
to_return = state.get_node_property_value(node_idx, i)
node_idx += 1
return to_return
# ##############################################################################
# Start Class
# ##############################################################################
var Logger = load("res://addons/gut/logger.gd") # everything should use get_logger
var _lgr = null
var json = JSON.new()
@ -85,6 +156,7 @@ var CompareResult = load("res://addons/gut/compare_result.gd")
var DiffTool = load("res://addons/gut/diff_tool.gd")
var Doubler = load("res://addons/gut/doubler.gd")
var Gut = load("res://addons/gut/gut.gd")
var GutConfig = load("res://addons/gut/gut_config.gd")
var HookScript = load("res://addons/gut/hook_script.gd")
var InnerClassRegistry = load("res://addons/gut/inner_class_registry.gd")
var InputFactory = load("res://addons/gut/input_factory.gd")
@ -106,69 +178,15 @@ var Summary = load("res://addons/gut/summary.gd")
var Test = load("res://addons/gut/test.gd")
var TestCollector = load("res://addons/gut/test_collector.gd")
var ThingCounter = load("res://addons/gut/thing_counter.gd")
var CollectedTest = load("res://addons/gut/collected_test.gd")
var CollectedScript = load("res://addons/gut/collected_test.gd")
var GutScene = load("res://addons/gut/GutScene.tscn")
# Source of truth for the GUT version
var version = "7.4.1"
var version = "9.1.1"
# The required Godot version as an array.
var req_godot = [3, 2, 0]
# Online fetch of the latest version available on github
var latest_version = null
var should_display_latest_version = false
# These methods all call super implicitly. Stubbing them to call super causes
# super to be called twice.
var non_super_methods = [
"_init",
"_ready",
"_notification",
"_enter_world",
"_exit_world",
"_process",
"_physics_process",
"_exit_tree",
"_gui_input ",
]
func _ready() -> void:
_http_request_latest_version()
func _http_request_latest_version() -> void:
return
var http_request = HTTPRequest.new()
http_request.name = "http_request"
add_child(http_request)
http_request.connect(
"request_completed", Callable(self, "_on_http_request_latest_version_completed")
)
# Perform a GET request. The URL below returns JSON as of writing.
var __error = http_request.request("https://api.github.com/repos/bitwes/Gut/releases/latest")
func _on_http_request_latest_version_completed(result, response_code, headers, body):
if not result == HTTPRequest.RESULT_SUCCESS:
return
var test_json_conv = JSON.new()
test_json_conv.parse(body.get_string_from_utf8())
var response = test_json_conv.get_data()
# Will print the user agent string used by the HTTPRequest node (as recognized by httpbin.org).
if response:
if response.get("html_url"):
latest_version = Array(response.html_url.split("/")).pop_back().right(1)
if latest_version != version:
should_display_latest_version = true
const GUT_METADATA = "__gutdbl"
# Note, these cannot change since places are checking for TYPE_INT to determine
# how to process parameters.
enum DOUBLE_STRATEGY { SCRIPT_ONLY, INCLUDE_SUPER }
enum DIFF { DEEP, SHALLOW, SIMPLE }
var req_godot = [4, 1, 0]
# ------------------------------------------------------------------------------
@ -256,16 +274,6 @@ func get_logger():
return _lgr
# ------------------------------------------------------------------------------
# return if_null if value is null otherwise return value
# ------------------------------------------------------------------------------
func nvl(value, if_null):
if value == null:
return if_null
else:
return value
# ------------------------------------------------------------------------------
# returns true if the object has been freed, false if not
#
@ -443,10 +451,6 @@ func are_datatypes_same(got, expected):
return !(typeof(got) != typeof(expected) and got != null and expected != null)
func pretty_print(dict):
print(json.stringify(dict, " "))
func get_script_text(obj):
return obj.get_script().get_source_code()
@ -514,23 +518,34 @@ func create_script_from_source(source, override_path = null):
return DynamicScript
func get_scene_script_object(scene):
var state = scene.get_state()
var to_return = null
var root_node_path = NodePath(".")
var node_idx = 0
func get_display_size():
return Engine.get_main_loop().get_viewport().get_visible_rect()
while node_idx < state.get_node_count() and to_return == null:
# Assumes that the first node we encounter that has a root node path, one
# property, and that property is named 'script' is the GDScript for the
# scene. This could be flawed.
if (
state.get_node_path(node_idx) == root_node_path
and state.get_node_property_count(node_idx) == 1
):
if state.get_node_property_name(node_idx, 0) == "script":
to_return = state.get_node_property_value(node_idx, 0)
node_idx += 1
return to_return
# ##############################################################################
#(G)odot (U)nit (T)est class
#
# ##############################################################################
# The MIT License (MIT)
# =====================
#
# Copyright (c) 2023 Tom "Butch" Wesley
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ##############################################################################

View file

@ -8,8 +8,8 @@ var env: Dictionary
func before_each():
assert(OS.set_environment(EMPTY_VAR, ""))
assert(OS.set_environment(TEST_VAR, TEST_VAL))
OS.set_environment(EMPTY_VAR, "")
OS.set_environment(TEST_VAR, TEST_VAL)
env = LibuvUtils.get_os_environ()