OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_rpc_timeout.gd

96 lines · gd ref:
extends SceneTree
## Gate: a daemon call that is never answered fails, instead of waiting forever
## (hyper-games #89 §3.2 / defect 11).
##
## KeetUdsClient parked every callback in _pending, and only a reply or a
## DISCONNECT ever released it. A daemon that accepts a call and then never
## answers — keet-core's getIdentity() waiting on onlineBoot, a StateBase open
## stuck behind a missing block — therefore left the caller waiting for ever:
## the menu sat on "identity" with no error and no way to tell the user.
##
## The failure is reproduced for real, not mocked: a local TCP server accepts
## the connection and never replies. The timeout is set with set(), which an
## old client ignores silently, so on the old code the callback never fires.

var ok := true
var _server: TCPServer
var _conn: StreamPeerTCP
var _client: Node
var _t := 0.0
var _phase := 0
var _result: Array = []   # [result, err] once the callback fires
var _called_at := -1.0
var _late_calls := 0


func _p(c: bool, m: String) -> void:
	print(("ok   " if c else "FAIL: ") + m)
	if not c:
		ok = false


func _initialize() -> void:
	_server = TCPServer.new()
	var port := 47321
	var err := _server.listen(port, "127.0.0.1")
	_p(err == OK, "silent test server listening on 127.0.0.1:%d" % port)
	var ClientScript := load("res://addons/keet_social/keet_uds_client.gd")
	_client = ClientScript.new()
	_client.set("rpc_timeout_sec", 0.5)
	# Point it at the silent server AFTER _ready(): _ready() takes the transport from
	# HYPERTPS_DAEMON_TCP, which this run sets to a dead port to keep the KeetService
	# autoload off any real daemon -- set before add_child, our port was overwritten.
	_client.auto_connect = false
	_port = port
	root.add_child(_client)


var _port := 0
var _configured := false


func _process(delta: float) -> bool:
	_t += delta
	# Configure on the first frame: during _initialize() the client's _ready() has
	# not run yet, so its reconnect timer does not exist and try_connect() crashes.
	if not _configured:
		_configured = true
		_client._tcp_host = "127.0.0.1"
		_client._tcp_port = _port
		_client._peer = _client._new_peer()
		_client.try_connect()
		return false
	if _server.is_connection_available():
		_conn = _server.take_connection()   # accepted, and never answered
	if _phase == 0 and _client.is_connected:
		_phase = 1
		# A named method, not an inline multi-line lambda: those hang headless runs.
		_client.call_rpc("identity", {}, _on_reply)
	if _phase == 1 and (_t > 4.0 or not _result.is_empty()):
		_phase = 2
		_finish()
		return true
	if _t > 8.0:
		_p(false, "never connected to the silent server")
		_finish()
		return true
	return false


func _on_reply(result: Variant, err: Variant) -> void:
	if _result.is_empty():
		_result = [result, err]
		_called_at = _t
	else:
		_late_calls += 1


func _finish() -> void:
	_p(not _result.is_empty(), "an unanswered call calls back with an error instead of waiting for ever")
	if not _result.is_empty():
		_p(_result[0] == null, "result is null")
		_p(str(_result[1]).to_lower().contains("timed out"), "error says it timed out (got %s)" % str(_result[1]))
		_p(_late_calls == 0, "callback fired exactly once")
	print("GATE rpc_timeout: %s" % ("PASS" if ok else "FAIL"))
	quit(0 if ok else 1)