Refactor PTY

PTY now provides a public interface to an underlying instance of
PTYNative. The PTYNative class can be extended as appropriate for
each platform and the platform-specific implementation will be
selected by PTY at runtime.
This commit is contained in:
Leroy Hopson 2022-06-29 22:58:48 +07:00
parent 29f29bac3c
commit 3881a4d8b9
No known key found for this signature in database
GPG key ID: D2747312A6DB51AA
8 changed files with 539 additions and 333 deletions

View file

@ -61,8 +61,8 @@ func _ready():
func _poll(): func _poll():
if pty and pty._pipe: if pty and pty.has_method("get_master"):
pty._pipe.poll() pty.get_master().poll()
update() update()

View file

@ -1,237 +0,0 @@
# Derived from https://github.com/microsoft/node-pty/blob/main/src/terminal.ts
# Copyright (c) 2012-2015, Christopher Jeffrey (MIT License)
# Copyright (c) 2016, Daniel Imms (MIT License).
# Copyright (c) 2018, Microsoft Corporation (MIT License).
# Copyright (c) 2021, Leroy Hopson (MIT License).
tool
extends Node
const LibuvUtils := preload("./libuv_utils.gd")
const Pipe := preload("./pipe.gdns")
const Terminal := preload("../../terminal.gd")
const DEFAULT_NAME := "xterm-256color"
const DEFAULT_COLS := 80
const DEFAULT_ROWS := 24
const DEFAULT_ENV := {TERM = DEFAULT_NAME, COLORTERM = "truecolor"}
## Default messages to indicate PAUSE/RESUME for automatic flow control.
## To avoid conflicts with rebound XON/XOFF control codes (such as on-my-zsh),
## the sequences can be customized in IPtyForkOptions.
#const FLOW_CONTROL_PAUSE = char(0x13) # defaults to XOFF
#const FLOW_CONTROL_RESUME = char(0x11) # defaults to XON
# Any signal_number can be sent to the pty's process using the kill() function,
# these are just the signals with numbers specified in the POSIX standard.
enum Signal {
SIGHUP = 1, # Hangup
SIGINT = 2, # Terminal interrupt signal
SIGQUIT = 3, # Terminal quit signal
SIGILL = 4, # Illegal instruction
SIGTRAP = 5, # Trace/breakpoint trap
SIGABRT = 6, # Process abort signal
SIGFPE = 8, # Erroneous arithmetic operation
SIGKILL = 9, # Kill (cannot be caught or ignored)
SIGSEGV = 11, # Invalid memory reference
SIGPIPE = 13, # Write on a pipe with no one to read it
SIGALRM = 14, # Alarm clock
SIGTERM = 15, # Termination signal
}
signal data_received(data)
signal exited(exit_code, signum)
export(NodePath) var terminal_path := NodePath() setget set_terminal_path
var _terminal: Terminal = null setget _set_terminal
# The name of the process.
#var process: String
# The process ID.
var _pid: int
# The column size in characters.
export(int) var cols: int = DEFAULT_COLS setget set_cols
# The row size in characters.
export(int) var rows: int = DEFAULT_ROWS setget set_rows
# Environment to be set for the child program.
export(Dictionary) var env := DEFAULT_ENV
# If true the environment variables in the env Dictionary will be merged with
# the environment variables of the operating system (e.g. printenv), with the
# former taking precedence in the case of conflicts.
export(bool) var use_os_env := true
# (EXPERIMENTAL)
# If true, PTY node will create a blocking libuv loop in a new thread.
# signals will be emitted using call_deferred.
#export (bool) var use_threads := false
## String encoding of the underlying pty.
## If set, incoming data will be decoded to strings and outgoing strings to bytes applying this encoding.
## If unset, incoming data will be delivered as raw bytes (PoolByteArray type).
## By default 'utf8' is assumed, to unset it explicitly set it to `null`.
#var encoding: String = "utf8"
## (EXPERIMENTAL)
## Whether to enable flow control handling (false by default). If enabled a message of `flow_control_pause`
## will pause the socket and thus blocking the child program execution due to buffer back pressure.
## A message of `flow_control_resume` will resume the socket into flow mode.
## For performance reasons only a single message as a whole will match (no message part matching).
## If flow control is enabled the `flow_control_pause` and `flow_control_resume` messages are not forwarded to
## the underlying pseudoterminal.
#var handle_flow_control: bool = false
#
## (EXPERIMENTAL)
## The string that should pause the pty when `handle_flow_control` is true. Default is XOFF ("\u0013").
#var flow_control_pause: String = FLOW_CONTROL_PAUSE
#
## (EXPERIMENTAL)
## The string that should resume the pty when `handle_flow_control` is true. Default is XON ("\u0011").
#var flow_control_resume: String = FLOW_CONTROL_RESUME
var _pipe: Pipe
func set_cols(value: int):
resize(value, rows)
func set_rows(value: int):
resize(cols, value)
func set_terminal_path(value := NodePath()):
terminal_path = value
_set_terminal(get_node_or_null(terminal_path))
func _set_terminal(value: Terminal):
if _terminal == value:
return
# Disconect the current terminal, if any.
if _terminal:
disconnect("data_received", _terminal, "write")
_terminal.disconnect("data_sent", self, "write")
_terminal.disconnect("size_changed", self, "resizev")
_terminal = value
if not _terminal:
return
# Connect the new terminal.
# FIXME! resize(terminal.get_cols(), terminal.get_rows())
if not _terminal.is_connected("size_changed", self, "resizev"):
_terminal.connect("size_changed", self, "resizev")
if not _terminal.is_connected("data_sent", self, "write"):
_terminal.connect("data_sent", self, "write")
if not is_connected("data_received", _terminal, "write"):
connect("data_received", _terminal, "write")
# Writes data to the socket.
# data: The data to write.
func write(data) -> void:
assert(data is String or data is PoolByteArray)
if data is PoolByteArray:
data = data.get_string_from_utf8()
# if handle_flow_control:
# # PAUSE/RESUME messages are not forwarded to the pty.
# if data == flow_control_pause:
# pause()
# return
# if data == flow_control_resume:
# resume()
# return
# # Everything else goes to the real pty.
_write(data)
func _write(data: String) -> void:
if _pipe:
_pipe.write(data)
# Resizes the dimensions of the pty.
# cols: The number of columns.
# rows: The number of rows.
# Also accepts a single Vector2 argument where x is the the number of columns
# and y is the number of rows.
func resize(cols, rows = null) -> void:
if cols <= 0 or rows <= 0 or cols == NAN or rows == NAN or cols == INF or rows == INF:
push_error("Resizing must be done using positive cols and rows.")
_resize(cols, rows)
# Same as resize() but takes a Vector2.
func resizev(size: Vector2) -> void:
resize(size.x, size.y)
func _resize(cols: int, rows: int) -> void:
assert(false, "Not implemented.")
# Kill the pty.
# sigint: The signal to send. By default this is SIGHUP.
# This is not supported on Windows.
func kill(signum: int = Signal.SIGHUP) -> void:
if _pipe:
_pipe.close()
if _pid > 0:
LibuvUtils.kill(_pid, signum)
func fork(
p_file: String,
p_args = PoolStringArray(),
p_cwd: String = LibuvUtils.get_cwd(),
p_cols: int = DEFAULT_COLS,
p_rows: int = DEFAULT_ROWS
) -> int:
assert(false, "Not implemented.")
return FAILED
func open(cols: int = DEFAULT_COLS, rows: int = DEFAULT_ROWS) -> Array:
assert(false, "Not implemented.")
return [FAILED]
func _parse_env(env: Dictionary = {}) -> PoolStringArray:
var keys := env.keys()
var pairs := PoolStringArray()
for key in keys:
var value = env[key]
var valid = key is String and value is String
assert(valid, "Env key/value pairs must be of type String/String.")
if not valid:
push_warning("Skipping invalid env key/value pair.")
continue
pairs.append("%s=%s" % [key, value])
return pairs
func _process(_delta):
if _pipe:
_pipe.poll()
func _notification(what: int):
match what:
NOTIFICATION_PARENTED:
var parent = get_parent()
if parent is Terminal:
set_terminal_path(get_path_to(parent))

View file

@ -0,0 +1,28 @@
tool
extends Node
signal data_received(data)
signal exited(exit_code, signum)
func open(cols: int, rows: int):
return _not_implemented()
func resize(cols: int, rows: int):
return _not_implemented()
func get_master():
return _not_implemented()
func _not_implemented() -> int:
var method := ""
var stack = get_stack()
if stack.size() >= 2 and "function" in stack[1]:
method = "%s()" % stack[1].function
push_error("Method %s not implemented on the current platform (%s)." % [method, OS.get_name()])
return ERR_METHOD_NOT_FOUND

View file

@ -0,0 +1,230 @@
# Derived from https://github.com/microsoft/node-pty/blob/main/src/unixTerminal.ts
# Copyright (c) 2012-2015, Christopher Jeffrey (MIT License).
# Copyright (c) 2016, Daniel Imms (MIT License).
# Copyright (c) 2018, Microsoft Corporation (MIT License).
# Copyright (c) 2021-2022, Leroy Hopson (MIT License).
tool
extends "../pty_native.gd"
const LibuvUtils := preload("../libuv_utils.gd")
const Pipe := preload("../pipe.gdns")
const PTYUnix = preload("./pty_unix.gdns")
const DEFAULT_NAME := "xterm-256color"
const DEFAULT_COLS := 80
const DEFAULT_ROWS := 24
const DEFAULT_ENV := {TERM = DEFAULT_NAME, COLORTERM = "truecolor"}
const FALLBACK_FILE = "sh"
## Default messages to indicate PAUSE/RESUME for automatic flow control.
## To avoid conflicts with rebound XON/XOFF control codes (such as on-my-zsh),
## the sequences can be customized in IPtyForkOptions.
#const FLOW_CONTROL_PAUSE = char(0x13) # defaults to XOFF
#const FLOW_CONTROL_RESUME = char(0x11) # defaults to XON
# Any signal_number can be sent to the pty's process using the kill() function,
# these are just the signals with numbers specified in the POSIX standard.
enum Signal {
SIGHUP = 1, # Hangup
SIGINT = 2, # Terminal interrupt signal
SIGQUIT = 3, # Terminal quit signal
SIGILL = 4, # Illegal instruction
SIGTRAP = 5, # Trace/breakpoint trap
SIGABRT = 6, # Process abort signal
SIGFPE = 8, # Erroneous arithmetic operation
SIGKILL = 9, # Kill (cannot be caught or ignored)
SIGSEGV = 11, # Invalid memory reference
SIGPIPE = 13, # Write on a pipe with no one to read it
SIGALRM = 14, # Alarm clock
SIGTERM = 15, # Termination signal
}
# The name of the process.
#var process: String
# The process ID.
var _pid: int
# The column size in characters.
var cols: int = DEFAULT_COLS setget set_cols
# The row size in characters.
var rows: int = DEFAULT_ROWS setget set_rows
# Environment to be set for the child program.
var env := DEFAULT_ENV
# If true the environment variables in the env Dictionary will be merged with
# the environment variables of the operating system (e.g. printenv), with the
# former taking precedence in the case of conflicts.
var use_os_env := true
var _pipe: Pipe
# Security warning: use this option with great caution, as opened file descriptors
# with higher privileges might leak to the child program.
var uid: int
var gid: int
var _fd: int = -1
var _exit_cb: FuncRef
func set_cols(value: int):
resize(value, rows)
func set_rows(value: int):
resize(cols, value)
# Writes data to the socket.
# data: The data to write.
func write(data) -> void:
assert(data is String or data is PoolByteArray)
if data is PoolByteArray:
data = data.get_string_from_utf8()
if _pipe:
_pipe.write(data)
func resize(cols: int, rows: int) -> void:
if cols <= 0 or rows <= 0 or cols == NAN or rows == NAN or cols == INF or rows == INF:
push_error("Resizing must be done using positive cols and rows.")
if _fd < 0:
return
PTYUnix.new().resize(_fd, cols, rows)
func kill(signum: int = Signal.SIGHUP) -> void:
if _pipe:
_pipe.close()
if _pid > 0:
LibuvUtils.kill(_pid, signum)
func _parse_env(env: Dictionary = {}) -> PoolStringArray:
var keys := env.keys()
var pairs := PoolStringArray()
for key in keys:
var value = env[key]
var valid = key is String and value is String
assert(valid, "Env key/value pairs must be of type String/String.")
if not valid:
push_warning("Skipping invalid env key/value pair.")
continue
pairs.append("%s=%s" % [key, value])
return pairs
func _process(_delta):
if _pipe:
_pipe.poll()
func fork(
file: String = OS.get_environment("SHELL"),
args: PoolStringArray = PoolStringArray(),
cwd = LibuvUtils.get_cwd(),
p_cols: int = DEFAULT_COLS,
p_rows: int = DEFAULT_ROWS,
uid: int = -1,
gid: int = -1,
utf8 = true
) -> int:
# File.
if file.empty():
file = FALLBACK_FILE
# Environment variables.
# If we are using OS env vars, sanitize them to remove variables that might confuse our terminal.
var final_env := _sanitize_env(LibuvUtils.get_os_environ()) if use_os_env else {}
for key in env.keys():
final_env[key] = env[key]
var parsed_env: PoolStringArray = _parse_env(final_env)
# Exit callback.
_exit_cb = FuncRef.new()
_exit_cb.set_instance(self)
_exit_cb.set_function("_on_exit")
# Actual fork.
var result = PTYUnix.new().fork( # VERY IMPORTANT: The must be set null or 0, otherwise will get an ENOTSOCK error after connecting our pipe to the fd.
file, null, args, parsed_env, cwd, cols, rows, uid, gid, utf8, _exit_cb
)
if result[0] != OK:
push_error("Fork failed.")
return FAILED
_fd = result[1].fd
if _fd < 0:
push_error("File descriptor must be a non-negative integer value.")
return FAILED
_pid = result[1].pid
_pipe = Pipe.new()
_pipe.open(_fd)
# Must connect to signal AFTER opening, otherwise we will get error ENOTSOCK.
_pipe.connect("data_received", self, "_on_pipe_data_received")
return OK
func open(cols: int = DEFAULT_COLS, rows: int = DEFAULT_ROWS) -> Array:
return PTYUnix.new().open(cols, rows)
func get_master():
if _pipe:
return _pipe
return null
func _exit_tree():
_exit_cb = null
if _pid > 1:
LibuvUtils.kill(_pid, Signal.SIGHUP)
if _pipe:
while _pipe.get_status() != 0:
continue
func _on_pipe_data_received(data):
emit_signal("data_received", data)
func _on_exit(exit_code: int, signum: int) -> void:
if is_instance_valid(self):
_pid = -1
emit_signal("exited", exit_code, signum)
func _sanitize_env(env: Dictionary) -> Dictionary:
# Make sure we didn't start our server from inside tmux.
env.erase("TMUX")
env.erase("TMUX_PANE")
# Make sure we didn't start our server from inside screen.
# http://web.mit.edu/gnu/doc/html/screen_20.html
env.erase("STY")
env.erase("WINDOW")
# Delete some variables that might confuse our terminal.
env.erase("WINDOWID")
env.erase("TERMCAP")
env.erase("COLUMNS")
env.erase("LINES")
return env

View file

@ -27,7 +27,7 @@ func _enter_tree():
var pty_script var pty_script
match OS.get_name(): match OS.get_name():
"X11", "Server", "OSX": "X11", "Server", "OSX":
pty_script = load("%s/nodes/pty/pty.gd" % base_dir) pty_script = load("%s/pty.gd" % base_dir)
add_custom_type("PTY", "Node", pty_script, pty_icon) add_custom_type("PTY", "Node", pty_script, pty_icon)
terminal_panel = preload("./editor_plugins/terminal/terminal_panel.tscn").instance() terminal_panel = preload("./editor_plugins/terminal/terminal_panel.tscn").instance()
terminal_panel.editor_plugin = self terminal_panel.editor_plugin = self

View file

@ -1,131 +1,158 @@
# Derived from https://github.com/microsoft/node-pty/blob/main/src/unixTerminal.ts # Derived from https://github.com/microsoft/node-pty/blob/main/src/terminal.ts
# Copyright (c) 2012-2015, Christopher Jeffrey (MIT License). # Copyright (c) 2012-2015, Christopher Jeffrey (MIT License).
# Copyright (c) 2016, Daniel Imms (MIT License). # Copyright (c) 2016, Daniel Imms (MIT License).
# Copyright (c) 2018, Microsoft Corporation (MIT License). # Copyright (c) 2018, Microsoft Corporation (MIT License).
# Copyright (c) 2021, Leroy Hopson (MIT License). # Copyright (c) 2021-2022, Leroy Hopson (MIT License).
tool tool
extends "./nodes/pty/pty.gd" extends Node
const PTYUnix = preload("./nodes/pty/unix/pty_unix.gdns") const _LibuvUtils := preload("./nodes/pty/libuv_utils.gd")
const _PTYNative := preload("./nodes/pty/pty_native.gd")
const _PTYUnix := preload("./nodes/pty/unix/pty_unix.gd")
const _Terminal := preload("./terminal.gd")
const FALLBACK_FILE = "sh" const DEFAULT_NAME := "xterm-256color"
const DEFAULT_COLS := 80
const DEFAULT_ROWS := 24
const DEFAULT_ENV := {TERM = DEFAULT_NAME, COLORTERM = "truecolor"}
# Security warning: use this option with great caution, as opened file descriptors # Any signal_number can be sent to the pty's process using the kill() function,
# with higher privileges might leak to the child program. # these are just the signals with numbers specified in the POSIX standard.
var uid: int const Signal = _PTYUnix.Signal
var gid: int
var thread: Thread signal data_received(data)
signal exited(exit_code, signum)
var _fd: int = -1 export(NodePath) var terminal_path := NodePath() setget set_terminal_path
var _exit_cb: FuncRef
#static func get_uid() -> int: var _terminal: _Terminal = null setget _set_terminal
# return -1 # Not implemented.
#static func get_gid() -> int: # The column size in characters.
# return -1 # Not implemented. export(int) var cols: int = DEFAULT_COLS setget set_cols
# The row size in characters.
export(int) var rows: int = DEFAULT_ROWS setget set_rows
# Environment to be set for the child program.
export(Dictionary) var env := DEFAULT_ENV
# If true the environment variables in the env Dictionary will be merged with
# the environment variables of the operating system (e.g. printenv), with the
# former taking precedence in the case of conflicts.
export(bool) var use_os_env := true
var _pty_native: _PTYNative
func _resize(cols: int, rows: int) -> void: func _init():
if _fd < 0: var os_name := OS.get_name()
match os_name:
"X11", "Server", "OSX":
_pty_native = _PTYUnix.new()
_:
push_error("PTY is not support on current platform (%s)." % os_name)
_pty_native.connect("data_received", self, "_on_pty_native_data_received")
_pty_native.connect("exited", self, "_on_pty_native_exited")
add_child(_pty_native)
func set_cols(value: int):
resize(value, rows)
func set_rows(value: int):
resize(cols, value)
func set_terminal_path(value := NodePath()):
terminal_path = value
_set_terminal(get_node_or_null(terminal_path))
func _set_terminal(value: _Terminal):
if _terminal == value:
return return
PTYUnix.new().resize(_fd, cols, rows) # Disconect the current terminal, if any.
if _terminal:
disconnect("data_received", _terminal, "write")
_terminal.disconnect("data_sent", self, "write")
_terminal.disconnect("size_changed", self, "resizev")
_terminal = value
if not _terminal:
return
# Connect the new terminal.
# FIXME! resize(terminal.get_cols(), terminal.get_rows())
if not _terminal.is_connected("size_changed", self, "resizev"):
_terminal.connect("size_changed", self, "resizev")
if not _terminal.is_connected("data_sent", self, "write"):
_terminal.connect("data_sent", self, "write")
if not is_connected("data_received", _terminal, "write"):
connect("data_received", _terminal, "write")
func _fork_thread(args): # Writes data to the socket.
var result = preload("./nodes/pty/unix/pty_unix.gdns").new().callv("fork", args) # data: The data to write.
return result func write(data) -> void:
_pty_native.write(data)
# Resizes the dimensions of the pty.
# cols: The number of columns.
# rows: The number of rows.
func resize(cols, rows = null) -> void:
_pty_native.resize(cols, rows)
# Same as resize() but takes a Vector2.
func resizev(size: Vector2) -> void:
resize(size.x, size.y)
# Kill the pty.
# sigint: The signal to send. By default this is SIGHUP.
# This is not supported on Windows.
func kill(signum: int = Signal.SIGHUP) -> void:
_pty_native.kill(signum)
func _notification(what: int):
match what:
NOTIFICATION_PARENTED:
var parent = get_parent()
if parent is _Terminal:
set_terminal_path(get_path_to(parent))
func fork( func fork(
file: String = OS.get_environment("SHELL"), file: String = OS.get_environment("SHELL"),
args: PoolStringArray = PoolStringArray(), args: PoolStringArray = PoolStringArray(),
cwd = LibuvUtils.get_cwd(), cwd = _LibuvUtils.get_cwd(),
p_cols: int = DEFAULT_COLS, p_cols: int = DEFAULT_COLS,
p_rows: int = DEFAULT_ROWS, p_rows: int = DEFAULT_ROWS,
uid: int = -1, uid: int = -1,
gid: int = -1, gid: int = -1,
utf8 = true utf8 = true
) -> int: ) -> int:
# File. return _pty_native.fork(file, args, cwd, p_cols, p_rows, uid, gid, utf8)
if file.empty():
file = FALLBACK_FILE
# Environment variables.
# If we are using OS env vars, sanitize them to remove variables that might confuse our terminal.
var final_env := _sanitize_env(LibuvUtils.get_os_environ()) if use_os_env else {}
for key in env.keys():
final_env[key] = env[key]
var parsed_env: PoolStringArray = _parse_env(final_env)
# Exit callback.
_exit_cb = FuncRef.new()
_exit_cb.set_instance(self)
_exit_cb.set_function("_on_exit")
# Actual fork.
var result = PTYUnix.new().fork( # VERY IMPORTANT: The must be set null or 0, otherwise will get an ENOTSOCK error after connecting our pipe to the fd.
file, null, args, parsed_env, cwd, cols, rows, uid, gid, utf8, _exit_cb
)
if result[0] != OK:
push_error("Fork failed.")
return FAILED
_fd = result[1].fd
if _fd < 0:
push_error("File descriptor must be a non-negative integer value.")
return FAILED
_pid = result[1].pid
_pipe = Pipe.new()
_pipe.open(_fd)
# Must connect to signal AFTER opening, otherwise we will get error ENOTSOCK.
_pipe.connect("data_received", self, "_on_pipe_data_received")
return OK
func open(cols: int = DEFAULT_COLS, rows: int = DEFAULT_ROWS) -> Array: func open(cols: int = DEFAULT_COLS, rows: int = DEFAULT_ROWS) -> Array:
return PTYUnix.new().open(cols, rows) return _pty_native.open(cols, rows)
func _exit_tree(): func get_master():
if _pid > 1: return _pty_native.get_master()
LibuvUtils.kill(_pid, Signal.SIGHUP)
while _pipe.get_status() != 0:
continue
func _on_pipe_data_received(data): func _on_pty_native_data_received(data):
emit_signal("data_received", data) emit_signal("data_received", data)
func _on_exit(exit_code: int, signum: int) -> void: func _on_pty_native_exited(exit_code: int, signum: int) -> void:
if is_instance_valid(self):
_pid = -1
emit_signal("exited", exit_code, signum) emit_signal("exited", exit_code, signum)
func _sanitize_env(env: Dictionary) -> Dictionary:
# Make sure we didn't start our server from inside tmux.
env.erase("TMUX")
env.erase("TMUX_PANE")
# Make sure we didn't start our server from inside screen.
# http://web.mit.edu/gnu/doc/html/screen_20.html
env.erase("STY")
env.erase("WINDOW")
# Delete some variables that might confuse our terminal.
env.erase("WINDOWID")
env.erase("TERMCAP")
env.erase("COLUMNS")
env.erase("LINES")
return env

View file

@ -24,6 +24,62 @@ func test_fork_succeeds():
assert_eq(err, OK) assert_eq(err, OK)
func test_fork_has_output():
pty.call_deferred("fork", "exit")
yield(yield_to(pty, "data_received", 1), YIELD)
var expected := PoolByteArray(
[
101,
120,
101,
99,
118,
112,
40,
51,
41,
32,
102,
97,
105,
108,
101,
100,
46,
58,
32,
78,
111,
32,
115,
117,
99,
104,
32,
102,
105,
108,
101,
32,
111,
114,
32,
100,
105,
114,
101,
99,
116,
111,
114,
121,
13,
10
]
)
assert_signal_emitted_with_parameters(pty, "data_received", [expected])
func test_open_succeeds(): func test_open_succeeds():
var result = pty.open() var result = pty.open()
assert_eq(result[0], OK) assert_eq(result[0], OK)

102
test/unit/pty.test.gd Normal file
View file

@ -0,0 +1,102 @@
extends "res://addons/gut/test.gd"
class MockPTY:
extends "res://addons/godot_xterm/nodes/pty/pty_native.gd"
func write(data):
emit_signal("data_received", data)
class BaseTest:
extends "res://addons/gut/test.gd"
const PTY := preload("res://addons/godot_xterm/pty.gd")
var pty: PTY
var mock_pty_native: MockPTY
func before_each():
pty = add_child_autofree(PTY.new())
mock_pty_native = autofree(MockPTY.new())
pty._pty_native = mock_pty_native
watch_signals(mock_pty_native)
class TestPTYInterfaceGodotXterm2_0_0:
extends BaseTest
# Test that PTY class conforms to the GodotXterm 2.0.0 specification published at:
# https://github.com/lihop/godot-xterm/wiki/PTY
func test_has_property_terminal_path():
assert_true("terminal_path" in pty, "Expected pty to have property terminal_path")
assert_typeof(pty.terminal_path, typeof(NodePath()))
func test_has_property_cols():
assert_true("cols" in pty, "Expected pty to have property cols.")
assert_typeof(pty.cols, typeof(0))
func test_has_property_rows():
assert_true("rows" in pty, "Expected pty to have property rows.")
assert_typeof(pty.rows, typeof(0))
func test_has_property_env():
assert_true("env" in pty, "Expected pty to have property env.")
assert_typeof(pty.env, typeof(Dictionary()))
func test_has_property_use_os_env():
assert_true("use_os_env" in pty, "Expected pty to have property use_os_env.")
assert_typeof(pty.use_os_env, typeof(false))
func test_has_method_fork():
assert_has_method(pty, "fork")
func test_has_method_kill():
assert_has_method(pty, "kill")
func test_has_method_open():
assert_has_method(pty, "open")
func test_has_method_resize():
assert_has_method(pty, "resize")
func test_has_method_resizev():
assert_has_method(pty, "resizev")
func test_has_method_write():
assert_has_method(pty, "write")
func test_has_signal_data_received():
assert_has_signal(pty, "data_received")
func test_has_signal_exited():
assert_has_signal(pty, "exited")
func test_has_enum_Signal():
assert_true("Signal" in pty, "Expected pty to have enum Signal.")
assert_typeof(pty.Signal, typeof(Dictionary()))
var signals = {
SIGHUP = 1,
SIGINT = 2,
SIGQUIT = 3,
SIGILL = 4,
SIGTRAP = 5,
SIGABRT = 6,
SIGFPE = 8,
SIGKILL = 9,
SIGSEGV = 11,
SIGPIPE = 13,
SIGALRM = 14,
SIGTERM = 15,
}
assert_gt(
pty.Signal.size(),
signals.size() - 1,
"Expected Signal enum to have at least %d members." % signals.size()
)
for signame in signals.keys():
assert_has(pty.Signal, signame, "Expected Signal enum to have member %s." % signame)
assert_eq(
pty.Signal[signame],
signals[signame],
"Expected Signal enum member %s to have value %d." % [signame, signals[signame]]
)