diff --git a/test/godot_xterm_test.gd b/test/godot_xterm_test.gd new file mode 100644 index 0000000..6cfc6e0 --- /dev/null +++ b/test/godot_xterm_test.gd @@ -0,0 +1,91 @@ +# SPDX-FileCopyrightText: 2024 Leroy Hopson +# 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] + ) diff --git a/test/test_nix.gd b/test/test_nix.gd index ba2dbc0..09cdb24 100644 --- a/test/test_nix.gd +++ b/test/test_nix.gd @@ -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")) diff --git a/test/test_pty.gd b/test/test_pty.gd index d5f8a41..7989b99 100644 --- a/test/test_pty.gd +++ b/test/test_pty.gd @@ -1,21 +1,86 @@ # SPDX-FileCopyrightText: 2024 Leroy Hopson # 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) diff --git a/test/test_rendering.gd b/test/test_rendering.gd index 5fa0c23..64fee5a 100644 --- a/test/test_rendering.gd +++ b/test/test_rendering.gd @@ -1,36 +1,38 @@ # SPDX-FileCopyrightText: 2024 Leroy Hopson # 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) diff --git a/test/test_terminal.gd b/test/test_terminal.gd index 822c9a8..21a0f8b 100644 --- a/test/test_terminal.gd +++ b/test/test_terminal.gd @@ -1,93 +1,201 @@ # SPDX-FileCopyrightText: 2021-2024 Leroy Hopson # 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)