chore(test): update tests

Adds additional tests for the interface. Creates a new base test class
GodotXtermTest that adds some additional assert methods. Tests
inheriting from this should override the got_described_class() method.
Add instance of the described class named 'subject' will be created and
added to the scene tree before each test.
This commit is contained in:
Leroy Hopson 2024-03-03 15:34:18 +13:00
parent cb86cc95d7
commit 1a5f0f96da
No known key found for this signature in database
GPG key ID: D2747312A6DB51AA
5 changed files with 387 additions and 125 deletions

91
test/godot_xterm_test.gd Normal file
View file

@ -0,0 +1,91 @@
# SPDX-FileCopyrightText: 2024 Leroy Hopson <godot-xterm@leroy.nix.nz>
# SPDX-License-Identifier: MIT
class_name GodotXtermTest
extends GutTest
## Base class for tests in the GodotXterm project.
##
## Contains some helpful methods that extend upon Gut's built-in assertions.
var subject: Object
var described_class_name: String:
get:
return subject.get_class()
var described_class:
get:
return get_described_class()
func before_each():
subject = described_class.new()
watch_signals(subject)
add_child_autofree(subject)
# Override this in your tests to set the class you want to test.
func get_described_class() -> Object:
assert(false, "You need to override get_described_class() in your test.")
return null
func assert_has_property(property_name: String, type: Variant.Type = -1) -> bool:
var has_property = property_name in subject
assert_true(
has_property, "Expected %s to have property '%s'." % [described_class_name, property_name]
)
if has_property and type > -1:
var expected_type = type_string(type)
var actual_type = type_string(typeof(subject.get(property_name)))
assert_eq(
actual_type,
expected_type,
(
"Expected '%s' property of %s to be type '%s', but it was type '%s'."
% [name, described_class_name, expected_type, actual_type]
)
)
return expected_type == actual_type
return false
func assert_has_property_with_default_value(property_name: String, expected_default_value) -> void:
if assert_has_property(property_name, typeof(expected_default_value)):
var actual_default_value = subject.get(property_name)
assert_eq(
actual_default_value,
expected_default_value,
(
"Expected '%s' property of %s to have default value '%s', but it was '%s'."
% [
property_name,
described_class_name,
expected_default_value,
actual_default_value
]
)
)
func assert_has_method_with_return_type(method_name: String, expected_return_type: Variant.Type):
var has_method = subject.has_method(method_name)
if has_method:
var expected_type = type_string(expected_return_type)
var method_list = subject.get_method_list()
for method in method_list:
if method.name == method_name:
var actual_type = type_string(method["return"]["type"])
assert_eq(
actual_type,
expected_type,
(
"Expected method '%s' of %s to return type '%s', but it returns type '%s'."
% [method_name, described_class_name, expected_type, actual_type]
)
)
break
else:
assert_has_method(
subject,
method_name,
"Expected %s to have method '%s'." % [described_class_name, method_name]
)

View file

@ -1,9 +1,12 @@
class_name NixTest extends GutTest
class_name NixTest extends GodotXtermTest
var pty: PTY
var helper: Helper
func get_described_class():
return PTY
func before_all():
if OS.get_name() == "macOS":
helper = MacOSHelper.new()
@ -11,48 +14,42 @@ func before_all():
helper = LinuxHelper.new()
func before_each():
pty = PTY.new()
watch_signals(pty)
add_child_autofree(pty)
func test_fork_succeeds():
var err = pty.fork("sh")
var err = subject.fork("sh")
assert_eq(err, OK)
func test_fork_emits_data_received():
pty.call_deferred("fork", "sh", ["-c", "echo'"])
await wait_for_signal(pty.data_received, 1)
assert_signal_emitted(pty, "data_received")
subject.call_deferred("fork", "sh", ["-c", "echo'"])
await wait_for_signal(subject.data_received, 1)
assert_signal_emitted(subject, "data_received")
func test_open_succeeds():
var err = pty.open()
var err = subject.open()
assert_eq(err, OK)
func test_open_creates_a_new_pty():
var num_pts = helper.get_pts().size()
pty.open()
subject.open()
var new_num_pts = helper.get_pts().size()
assert_eq(new_num_pts, num_pts + 1)
func test_open_pty_has_correct_name():
var original_pts = helper.get_pts()
pty.open()
subject.open()
var new_pts = helper.get_pts()
for pt in original_pts:
new_pts.erase(pt)
assert_eq(pty.get_pts(), new_pts[0])
assert_eq(subject.get_pts(), new_pts[0])
func xtest_open_pty_has_correct_win_size():
var cols = 7684
var rows = 9314
#var result = pty.open(cols, rows)
#var result = subject.open(cols, rows)
#var winsize = helper._get_winsize(result[1].master)
#assert_eq(winsize.cols, cols)
#assert_eq(winsize.rows, rows)
@ -61,7 +58,7 @@ func xtest_open_pty_has_correct_win_size():
func xtest_win_size_supports_max_unsigned_short_value():
var cols = 65535
var rows = 65535
#var result = pty.open(cols, rows)
#var result = subject.open(cols, rows)
#var winsize = helper._get_winsize(result[1].master)
#assert_eq(winsize.cols, cols)
#assert_eq(winsize.cols, rows)
@ -71,45 +68,45 @@ func test_closes_pty_on_free():
if OS.get_name() == "macOS":
return
var num_pts = helper.get_pts().size()
pty.fork("sleep", ["1000"])
pty.free()
subject.fork("sleep", ["1000"])
subject.free()
await wait_frames(1)
var new_num_pts = helper.get_pts().size()
assert_eq(new_num_pts, num_pts)
func test_emits_exited_signal_when_child_process_exits():
pty.call_deferred("fork", "exit")
await wait_for_signal(pty.exited, 1)
assert_signal_emitted(pty, "exited")
subject.call_deferred("fork", "exit")
await wait_for_signal(subject.exited, 1)
assert_signal_emitted(subject, "exited")
func test_emits_exit_code_on_success():
pty.call_deferred("fork", "true")
await wait_for_signal(pty.exited, 1)
assert_signal_emitted_with_parameters(pty, "exited", [0, 0])
subject.call_deferred("fork", "true")
await wait_for_signal(subject.exited, 1)
assert_signal_emitted_with_parameters(subject, "exited", [0, 0])
func test_emits_exit_code_on_failure():
pty.call_deferred("fork", "false")
await wait_for_signal(pty.exited, 1)
assert_signal_emitted_with_parameters(pty, "exited", [1, 0])
subject.call_deferred("fork", "false")
await wait_for_signal(subject.exited, 1)
assert_signal_emitted_with_parameters(subject, "exited", [1, 0])
func test_emits_exited_on_kill():
pty.call("fork", "yes")
subject.call("fork", "yes")
await wait_frames(1)
pty.call_deferred("kill", PTY.SIGNAL_SIGKILL)
await wait_for_signal(pty.exited, 1)
assert_signal_emitted(pty, "exited")
subject.call_deferred("kill", PTY.SIGNAL_SIGKILL)
await wait_for_signal(subject.exited, 1)
assert_signal_emitted(subject, "exited")
func test_emits_exited_with_signal():
pty.call("fork", "yes")
subject.call("fork", "yes")
await wait_frames(1)
pty.call_deferred("kill", PTY.SIGNAL_SIGSEGV)
await wait_for_signal(pty.exited, 1)
assert_signal_emitted_with_parameters(pty, "exited", [0, PTY.SIGNAL_SIGSEGV])
subject.call_deferred("kill", PTY.SIGNAL_SIGSEGV)
await wait_for_signal(subject.exited, 1)
assert_signal_emitted_with_parameters(subject, "exited", [0, PTY.SIGNAL_SIGSEGV])
# Run the same tests, but with use_threads = false.
@ -118,7 +115,7 @@ class TestNoThreads:
func before_each():
super.before_each()
pty.use_threads = false
subject.use_threads = false
class Helper:
@ -156,13 +153,12 @@ class Helper:
class XTestPTYSize:
extends "res://addons/gut/test.gd"
extends NixTest
# Tests to check that psuedoterminal size (as reported by the stty command)
# matches the size of the Terminal node. Uses various scene tree layouts with
# Terminal and PTY nodes in different places.
# See: https://github.com/lihop/godot-xterm/issues/56
var pty: PTY
var terminal: Terminal
var scene: Node
var regex := RegEx.new()
@ -183,14 +179,14 @@ class XTestPTYSize:
"PTYCousinAbove2",
"PTYCousinBelow2"
]:
pty = scene.get_node(s).find_child("PTY")
subject = scene.get_node(s).find_child("PTY")
terminal = scene.get_node(s).find_child("Terminal")
pty.call_deferred("fork", OS.get_environment("SHELL"))
pty.call_deferred("write", "stty -a | head -n1\n")
subject.call_deferred("fork", OS.get_environment("SHELL"))
subject.call_deferred("write", "stty -a | head -n1\n")
var output := ""
while not "rows" in output and not "columns" in output:
output = (await pty.data_received).get_string_from_utf8()
output = (await subject.data_received).get_string_from_utf8()
var regex_match = regex.search(output)
var stty_rows = int(regex_match.get_string("rows"))
var stty_cols = int(regex_match.get_string("columns"))

View file

@ -1,21 +1,86 @@
# SPDX-FileCopyrightText: 2024 Leroy Hopson <godot-xterm@leroy.nix.nz>
# SPDX-License-Identifier: MIT
class_name PTYTest extends "res://addons/gut/test.gd"
var pty: PTY
class_name PTYTest extends GodotXtermTest
func before_each():
pty = PTY.new()
add_child_autofree(pty)
func get_described_class():
return PTY
class TestDefaults:
class TestInterface:
extends PTYTest
func test_default_env() -> void:
assert_eq(pty.env, {"TERM": "xterm-256color", "COLORTERM": "truecolor"})
## API V2.
func test_default_use_os_env() -> void:
assert_eq(pty.use_os_env, true)
# Properties.
# TODO: Implement cols property.
func xtest_has_property_cols() -> void:
assert_has_property_with_default_value("cols", 80)
func test_has_property_env() -> void:
assert_has_property_with_default_value(
"env", {"TERM": "xterm-256color", "COLORTERM": "truecolor"}
)
# TODO: Implement rows property.
func xtest_has_property_rows() -> void:
assert_has_property_with_default_value("rows", 24)
# TODO: Implement terminal_path property.
func xtest_has_property_terminal_path() -> void:
assert_has_property("terminal_path")
func test_has_proprty_use_os_env() -> void:
assert_has_property_with_default_value("use_os_env", true)
# Methods.
func test_has_method_fork():
assert_has_method_with_return_type("fork", TYPE_INT)
func test_has_method_kill():
assert_has_method_with_return_type("kill", TYPE_NIL)
func test_has_method_open():
assert_has_method_with_return_type("open", TYPE_INT)
func test_has_method_resize():
assert_has_method_with_return_type("resize", TYPE_NIL)
func test_has_method_resizev():
assert_has_method_with_return_type("resizev", TYPE_NIL)
func test_has_method_write():
assert_has_method_with_return_type("write", TYPE_NIL)
# Signals.
func test_has_signal_data_received() -> void:
assert_has_signal(subject, "data_received")
func test_has_signal_exited() -> void:
assert_has_signal(subject, "exited")
# Enums.
# Added SIGNAL_ prefix to name.
func test_has_enum_signal():
assert_eq(described_class.SIGNAL_SIGHUP, 1)
assert_eq(described_class.SIGNAL_SIGINT, 2)
assert_eq(described_class.SIGNAL_SIGQUIT, 3)
assert_eq(described_class.SIGNAL_SIGILL, 4)
assert_eq(described_class.SIGNAL_SIGTRAP, 5)
assert_eq(described_class.SIGNAL_SIGABRT, 6)
assert_eq(described_class.SIGNAL_SIGFPE, 8)
assert_eq(described_class.SIGNAL_SIGKILL, 9)
assert_eq(described_class.SIGNAL_SIGSEGV, 11)
assert_eq(described_class.SIGNAL_SIGPIPE, 13)
assert_eq(described_class.SIGNAL_SIGALRM, 14)
assert_eq(described_class.SIGNAL_SIGTERM, 15)
## Other tests.
func test_has_no_visible_children():
assert_eq(subject.get_child_count(), 0)

View file

@ -1,36 +1,38 @@
# SPDX-FileCopyrightText: 2024 Leroy Hopson <godot-xterm@leroy.nix.nz>
# SPDX-License-Identifier: MIT
class_name RenderingTest extends GutTest
class_name RenderingTest extends GodotXtermTest
var terminal: Terminal
func get_described_class():
return Terminal
# Return the color in the center of the given cell.
func pick_cell_color(cell := Vector2i(0, 0)) -> Color:
var cell_size = terminal.get_cell_size()
var cell_size = subject.get_cell_size()
var pixelv = Vector2(cell) * cell_size + (cell_size / 2)
return get_viewport().get_texture().get_image().get_pixelv(cell_size / 2)
func before_each():
terminal = Terminal.new()
terminal.add_theme_font_override("normal_font", preload("res://themes/fonts/regular.tres"))
terminal.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
watch_signals(terminal)
call_deferred("add_child_autofree", terminal)
await wait_for_signal(terminal.ready, 5)
subject = described_class.new()
subject.add_theme_font_override("normal_font", preload("res://themes/fonts/regular.tres"))
subject.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
watch_signals(subject)
call_deferred("add_child_autofree", subject)
await wait_for_signal(subject.ready, 5)
class TestRendering:
extends RenderingTest
func test_update():
terminal.write("\u001b[38;2;255;0;0m")
terminal.write("".repeat(terminal.get_cols() * terminal.get_rows()))
subject.write("\u001b[38;2;255;0;0m")
subject.write("".repeat(subject.get_cols() * subject.get_rows()))
await get_tree().physics_frame
terminal.queue_redraw()
await wait_for_signal(terminal.draw, 3)
subject.queue_redraw()
await wait_for_signal(subject.draw, 3)
await wait_frames(15)
var cell_color = pick_cell_color(Vector2i(0, 0))
assert_eq(cell_color, Color.RED)
@ -44,7 +46,7 @@ class TestKeyPressed:
func before_each():
await super.before_each()
terminal.grab_focus()
subject.grab_focus()
input_event = InputEventKey.new()
input_event.pressed = true
@ -54,38 +56,38 @@ class TestKeyPressed:
input_event.keycode = KEY_A
input_event.unicode = "a".unicode_at(0)
await wait_for_signal(terminal.key_pressed, 1)
assert_signal_emitted(terminal, "key_pressed")
await wait_for_signal(subject.key_pressed, 1)
assert_signal_emitted(subject, "key_pressed")
func test_key_pressed_emitted_only_once_per_key_input():
input_event.keycode = KEY_B
input_event.unicode = "b".unicode_at(0)
await wait_for_signal(terminal.key_pressed, 1)
assert_signal_emit_count(terminal, "key_pressed", 1)
await wait_for_signal(subject.key_pressed, 1)
assert_signal_emit_count(subject, "key_pressed", 1)
func test_key_pressed_emits_interpreted_key_input_as_first_param():
input_event.keycode = KEY_UP
input_event.unicode = 0
await wait_for_signal(terminal.key_pressed, 1)
await wait_for_signal(subject.key_pressed, 1)
var signal_parameters = get_signal_parameters(terminal, "key_pressed", 0)
var signal_parameters = get_signal_parameters(subject, "key_pressed", 0)
assert_eq(signal_parameters[0], "\u001b[A")
func test_key_pressed_emits_original_input_event_as_second_param():
input_event.keycode = KEY_L
input_event.unicode = "l".unicode_at(0)
await wait_for_signal(terminal.key_pressed, 1)
await wait_for_signal(subject.key_pressed, 1)
var signal_parameters = get_signal_parameters(terminal, "key_pressed", 0)
var signal_parameters = get_signal_parameters(subject, "key_pressed", 0)
assert_eq(signal_parameters[1], input_event)
func test_key_pressed_not_emitted_when_writing_to_terminal():
terminal.write("a")
func test_key_pressed_not_emitted_when_writing_to_subject():
subject.write("a")
await wait_frames(1)
assert_signal_emit_count(terminal, "key_pressed", 0)
assert_signal_emit_count(subject, "key_pressed", 0)
func test_key_pressed_not_emitted_by_other_input_type():
var mouse_input = InputEventMouseButton.new()
@ -93,5 +95,5 @@ class TestKeyPressed:
mouse_input.pressed = true
Input.call_deferred("parse_input_event", mouse_input)
await wait_for_signal(terminal.gui_input, 1)
assert_signal_emit_count(terminal, "key_pressed", 0)
await wait_for_signal(subject.gui_input, 1)
assert_signal_emit_count(subject, "key_pressed", 0)

View file

@ -1,93 +1,201 @@
# SPDX-FileCopyrightText: 2021-2024 Leroy Hopson <godot-xterm@leroy.nix.nz>
# SPDX-License-Identifier: MIT
class_name TerminalTest extends "res://addons/gut/test.gd"
class_name TerminalTest extends GodotXtermTest
var terminal: Terminal
func get_described_class():
return Terminal
func before_each():
terminal = Terminal.new()
terminal.size = Vector2(400, 200)
watch_signals(terminal)
add_child_autofree(terminal)
super.before_each()
subject.size = Vector2(400, 200)
class TestInterface:
extends TerminalTest
## API V2.
# Properties.
func test_has_property_bell_muted():
assert_has_property_with_default_value("bell_muted", false)
func test_has_property_bell_cooldown():
assert_has_property_with_default_value("bell_cooldown", 0.1)
func test_has_property_blink_on_time():
assert_has_property_with_default_value("blink_on_time", 0.6)
func test_has_property_blink_off_time():
assert_has_property_with_default_value("blink_off_time", 0.3)
# TODO: Implement copy_on_selection property.
func xtest_has_property_copy_on_selection():
assert_has_property_with_default_value("copy_on_selection", false)
# TODO: Implement update_mode property.
func xtest_has_property_update_mode():
#assert_has_property_with_default_value("update_mode", UPDATE_MODE_AUTO)
pass
# cols and rows removed.
# Methods.
# TODO: Implement clear() method.
func xtest_has_method_clear():
assert_has_method_with_return_type("clear", TYPE_NIL)
# TODO: Implement copy_all() method.
func xtest_has_method_copy_all():
assert_has_method_with_return_type("copy_all", TYPE_STRING)
# TODO: Implement copy_selection() method.
func xtest_has_method_copy_selection():
assert_has_method_with_return_type("copy_selection", TYPE_STRING)
func test_has_method_get_cols():
assert_has_method_with_return_type("get_cols", TYPE_INT)
func test_has_method_get_rows():
assert_has_method_with_return_type("get_rows", TYPE_INT)
func test_has_method_write():
assert_has_method(subject, "write")
# Signals.
func test_has_signal_data_sent():
assert_has_signal(subject, "data_sent")
func test_has_signal_key_pressed():
assert_has_signal(subject, "key_pressed")
func test_has_signal_size_changed():
assert_has_signal(subject, "size_changed")
func test_has_signal_bell():
assert_has_signal(subject, "bell")
# Enums.
# TODO: Implement SelectionMode enum.
func xtest_has_enum_selection_mode():
assert_eq(described_class.SELECTION_MODE_NONE, 0)
assert_eq(described_class.SELECTION_MODE_POINTER, 1)
# TODO: Implement UpdateMode enum.
func xtest_has_enum_update_mode():
assert_eq(described_class.UPDATE_MODE_DISABLED, 0)
assert_eq(described_class.AUTO, 1)
assert_eq(described_class.ALL, 2)
assert_eq(described_class.ALL_NEXT_FRAME, 3)
## API Next.
# Methods.
func test_has_method_get_cursor_pos():
assert_has_method_with_return_type("get_cursor_pos", TYPE_VECTOR2I)
func test_has_method_get_cell_size():
assert_has_method_with_return_type("get_cell_size", TYPE_VECTOR2)
func test_has_method_write_with_response():
assert_has_method_with_return_type("write", TYPE_STRING)
# Enums.
func test_has_enum_inverse_mode():
assert_eq(described_class.INVERSE_MODE_INVERT, 0)
assert_eq(described_class.INVERSE_MODE_SWAP, 1)
## Other tests.
func test_has_no_visible_children():
# We add children like the bell timer for private use that should not
# be visible outside of the node itself.
assert_eq(subject.get_child_count(), 0)
class TestBell:
extends TerminalTest
func test_bell() -> void:
watch_signals(terminal)
terminal.bell_cooldown = 0
terminal.write(char(7))
terminal.write(char(0x07))
terminal.write("\a")
terminal.write("\u0007")
terminal.write("'Ask not for whom the \a tolls; it tolls for thee' - John Donne")
assert_signal_emit_count(terminal, "bell", 5)
watch_signals(subject)
subject.bell_cooldown = 0
subject.write(char(7))
subject.write(char(0x07))
subject.write("\a")
subject.write("\u0007")
subject.write("'Ask not for whom the \a tolls; it tolls for thee' - John Donne")
assert_signal_emit_count(subject, "bell", 5)
func test_bell_mute() -> void:
watch_signals(terminal)
terminal.bell_muted = true
terminal.write("\a")
assert_signal_emit_count(terminal, "bell", 0)
watch_signals(subject)
subject.bell_muted = true
subject.write("\a")
assert_signal_emit_count(subject, "bell", 0)
func test_bell_cooldown() -> void:
watch_signals(terminal)
terminal.bell_cooldown = 10000
terminal.write("\a")
terminal.write("\a")
assert_signal_emit_count(terminal, "bell", 1)
watch_signals(subject)
subject.bell_cooldown = 10000
subject.write("\a")
subject.write("\a")
assert_signal_emit_count(subject, "bell", 1)
func test_change_cooldown_while_active() -> void:
watch_signals(terminal)
terminal.bell_cooldown = 10000
terminal.write("\a")
terminal.bell_cooldown = 0
terminal.write("\a")
assert_signal_emit_count(terminal, "bell", 2)
watch_signals(subject)
subject.bell_cooldown = 10000
subject.write("\a")
subject.bell_cooldown = 0
subject.write("\a")
assert_signal_emit_count(subject, "bell", 2)
class TestCursorPos:
extends TerminalTest
func test_get_cursor_pos_initial():
assert_eq(terminal.get_cursor_pos(), Vector2i.ZERO)
assert_eq(subject.get_cursor_pos(), Vector2i.ZERO)
func test_get_cursor_pos_x():
terminal.write("_")
assert_eq(terminal.get_cursor_pos().x, 1)
subject.write("_")
assert_eq(subject.get_cursor_pos().x, 1)
func test_get_cursor_pos_y():
terminal.write("_".repeat(terminal.cols + 1))
assert_eq(terminal.get_cursor_pos().y, 1)
subject.write("_".repeat(subject.cols + 1))
assert_eq(subject.get_cursor_pos().y, 1)
class TestWrite:
extends TerminalTest
func test_returns_response_when_input_contains_query():
var response = terminal.write("\u001b[6n") # Query cursor position.
var response = subject.write("\u001b[6n") # Query cursor position.
assert_eq(response, "\u001b[1;1R")
func test_returns_response_to_multiple_queries():
var response = terminal.write("\u001b[6n\u001b[5n") # Query cursor position and status.
var response = subject.write("\u001b[6n\u001b[5n") # Query cursor position and status.
assert_eq(response, "\u001b[1;1R\u001b[0n")
func test_returns_response_to_multiple_queries_among_other_data():
var response = terminal.write("hello\r\nworld\u001b[6nother\r\ndata\u001b[5ntest")
var response = subject.write("hello\r\nworld\u001b[6nother\r\ndata\u001b[5ntest")
assert_eq(response, "\u001b[2;6R\u001b[0n")
func test_data_sent_emitted_on_query():
terminal.write("\u001b[6n")
assert_signal_emitted(terminal, "data_sent")
subject.write("\u001b[6n")
assert_signal_emitted(subject, "data_sent")
func test_data_sent_emitted_with_response():
terminal.write("\u001b[6n")
subject.write("\u001b[6n")
assert_signal_emitted_with_parameters(
terminal, "data_sent", ["\u001b[1;1R".to_utf8_buffer()]
subject, "data_sent", ["\u001b[1;1R".to_utf8_buffer()]
)
func test_data_sent_not_emitted_when_empty_string_written():
terminal.write("")
assert_signal_emit_count(terminal, "data_sent", 0)
subject.write("")
assert_signal_emit_count(subject, "data_sent", 0)