Files
Grovewright/game/tests/test_net_link.gd
T
Steven Wroblewski be22e9f358 Abschlusskorrekturen M1c: Schläge der Gäste, Figuren und Fragebogen
- Der Host hält einen Schlag eines Gastes, der während der Abklingzeit
  ankommt, bis zum ersten Takt danach. Übers Internet ging sonst etwa
  jeder fünfte Schlag eines Gastes verloren.
- ENet drosselt die Figuren nie mehr; sonst konnten die Figuren eines
  von zwei Gästen dauerhaft stehen bleiben. Figuren gehen geordnet, und
  eine stille Verbindung gilt nach höchstens acht Sekunden als verloren.
- Positionen, die keine Zahlen sind, und Figuren von Spielern, die
  nicht online sind, werden verworfen.
- Ein Host mit angehaltener Simulation sagt das in der Statuszeile.
- Neue Tests: Schlag während der Abklingzeit, Zielauswahl von NetLink,
  verworfene Figuren, Neuabgleich über die Brücke.
- Der Fragebogen sagt, was jeder Rechner braucht, und die Freigabe in
  der Firewall gehört zum Host. CLAUDE.md beschreibt das Halten der
  Schläge, die Drosselung und die Zeitüberschreitung und ergänzt die
  Vertrauensregel um die Schlüssel.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-10-02 01:22:36 +02:00

216 lines
8.3 KiB
GDScript

extends TestCase
## Two NetLinks in one process, connected over the loopback interface.
## Events per link, collected while frames pass.
var _events: Dictionary[NetLink, Array] = {}
## Polls `links` for `frames` frames and collects their events.
func _pump(links: Array[NetLink], frames: int) -> void:
for frame in frames:
for link in links:
if not _events.has(link):
_events[link] = []
_events[link].append_array(link.poll(1.0 / 60.0))
await tree.process_frame
func _types(link: NetLink) -> Array:
return _events.get(link, []).map(func(event: Dictionary) -> String: return event["type"])
func _first(link: NetLink, type: String) -> Dictionary:
for event: Dictionary in _events.get(link, []):
if event["type"] == type:
return event
return {}
## The bytes of every event of `type` that `link` got, each as an Array.
func _received(link: NetLink, type: String) -> Array:
return _events.get(link, []).filter(func(event: Dictionary) -> bool: return event["type"] == type).map(
func(event: Dictionary) -> Array: return Array(event["bytes"])
)
## A host and a guest that have connected.
func _connected_pair() -> Array[NetLink]:
var host := NetLink.new()
check_eq(host.host(0, "127.0.0.1"), OK, "the host listens")
check(host.local_port() > 0, "on a port the system chose")
var guest := NetLink.new()
check_eq(guest.join("127.0.0.1", host.local_port()), OK, "the guest starts connecting")
await _pump([host, guest], 10)
return [host, guest]
func test_a_guest_connects_and_both_sides_hear_of_it() -> void:
var links := await _connected_pair()
var host := links[0]
var guest := links[1]
check(guest.is_connected_to_peers(), "the guest is connected")
check_eq(_first(guest, "connected").get("peer"), GwSession.HOST_PEER, "the guest sees the host as peer 1")
check(_first(host, "connected").get("peer", 0) > 1, "the host sees the guest")
host.close()
guest.close()
func test_messages_arrive_in_order_on_both_channels() -> void:
var links := await _connected_pair()
var host := links[0]
var guest := links[1]
var guest_peer: int = _first(host, "connected")["peer"]
_events.clear()
var big := PackedByteArray()
big.resize(50000)
big.fill(7)
host.send(guest_peer, big)
host.send(guest_peer, PackedByteArray([1]))
guest.send(GwSession.HOST_PEER, PackedByteArray([2, 3]))
guest.send_figure(GwSession.HOST_PEER, PackedByteArray([4]))
await _pump([host, guest], 20)
var received := _events[guest].filter(func(event: Dictionary) -> bool: return event["type"] == "message")
check_eq(received.size(), 2, "two messages for the guest")
if received.size() == 2:
check_eq(received[0]["bytes"].size(), 50000, "the big one first")
check_eq(received[1]["bytes"], PackedByteArray([1]), "then the small one")
check_eq(_first(host, "message").get("bytes"), PackedByteArray([2, 3]), "the host got the guest's message")
check_eq(_first(host, "figure").get("bytes"), PackedByteArray([4]), "and the position on the other channel")
host.close()
guest.close()
func test_closing_a_guest_delivers_what_was_sent_before() -> void:
var links := await _connected_pair()
var host := links[0]
var guest := links[1]
var guest_peer: int = _first(host, "connected")["peer"]
_events.clear()
host.send(guest_peer, PackedByteArray([9, 9]))
host.close_peer(guest_peer)
await _pump([host, guest], 30)
check_eq(_types(guest), ["message", "disconnected"], "the message comes before the end")
check_eq(_types(host), ["disconnected"], "the host sees the guest go")
check(not guest.is_open(), "the guest's connection is closed")
host.close()
func test_a_guest_that_quits_is_seen_leaving() -> void:
var links := await _connected_pair()
var host := links[0]
var guest := links[1]
_events.clear()
guest.close()
await _pump([host, guest], 10)
check_eq(_types(host), ["disconnected"], "the host sees the guest go")
host.close()
## Godot reports an error for every packet to a connection that is gone or
## being closed; the test runner turns each such error into a failure.
func test_nothing_is_sent_to_connections_that_are_gone_or_closing() -> void:
var host := NetLink.new()
check_eq(host.host(0, "127.0.0.1"), OK, "the host listens")
var first := NetLink.new()
var second := NetLink.new()
first.join("127.0.0.1", host.local_port())
second.join("127.0.0.1", host.local_port())
await _pump([host, first, second], 10)
var peers: Array = _events[host].map(func(event: Dictionary) -> int: return event["peer"])
check_eq(peers.size(), 2, "two guests connected")
var leaving: int = peers[0]
var staying: int = peers[1]
# Connections that never existed.
host.send(12345, PackedByteArray([1]))
host.send_figure(12345, PackedByteArray([1]))
host.send_figure(-12345, PackedByteArray([1]))
host.close_peer(12345)
# A connection that is being closed, and everybody while it closes.
host.close_peer(leaving)
for frame in 10:
host.send(leaving, PackedByteArray([2]))
host.send_figure(leaving, PackedByteArray([2]))
host.send_figure(-staying, PackedByteArray([2]))
host.send_figure(0, PackedByteArray([2]))
await _pump([host, first, second], 1)
# The connection is gone now.
host.send(leaving, PackedByteArray([3]))
host.send_figure(-leaving, PackedByteArray([3]))
await _pump([host, first, second], 5)
check(_types(first).has("disconnected"), "the first guest was closed")
check(not _types(second).has("disconnected"), "the second guest stays")
for link in [host, first, second]:
link.close()
## A packet goes to the connection it is addressed to, to every connection
## for peer 0, and to every connection but one for a negative peer.
func test_packets_go_only_where_they_are_addressed() -> void:
var host := NetLink.new()
check_eq(host.host(0, "127.0.0.1"), OK, "the host listens")
var guests: Array[NetLink] = []
var everyone: Array[NetLink] = [host]
for i in 3:
var guest := NetLink.new()
guest.join("127.0.0.1", host.local_port())
guests.append(guest)
everyone.append(guest)
await _pump(everyone, 15)
# Each guest says who it is, so that the host's id for it is known.
for i in 3:
guests[i].send(GwSession.HOST_PEER, PackedByteArray([i]))
await _pump(everyone, 10)
var peer_of: Array[int] = [0, 0, 0]
for event: Dictionary in _events[host]:
if event["type"] == "message":
peer_of[event["bytes"][0]] = event["peer"]
check(not peer_of.has(0), "the host knows all three guests")
_events.clear()
# Repeated, because a position that gets lost is no failure of the link.
for repeat in 5:
host.send(peer_of[0], PackedByteArray([10]))
host.send_figure(peer_of[2], PackedByteArray([20]))
host.send_figure(-peer_of[1], PackedByteArray([30]))
host.send_figure(0, PackedByteArray([40]))
await _pump(everyone, 2)
await _pump(everyone, 10)
check_eq(_received(guests[0], "message").size(), 5, "the first guest got the messages to it")
check_eq(_received(guests[1], "message"), [], "the second guest got none")
check_eq(_received(guests[2], "message"), [], "the third guest got none")
var first := _received(guests[0], "figure")
var second := _received(guests[1], "figure")
var third := _received(guests[2], "figure")
check(not first.has([20]), "the first guest got nothing for the third")
check(first.has([30]) and first.has([40]), "the first guest got 'all but the second' and 'all'")
check(not second.has([20]) and not second.has([30]), "the second guest got neither the one for the third nor 'all but itself'")
check(second.has([40]), "the second guest got 'all'")
check(third.has([20]) and third.has([30]) and third.has([40]), "the third guest got all three kinds")
for link in everyone:
link.close()
func test_a_guest_gives_up_when_nobody_answers() -> void:
# A port that was free a moment ago and has nobody listening now.
var probe := NetLink.new()
probe.host(0, "127.0.0.1")
var port := probe.local_port()
probe.close()
var guest := NetLink.new()
guest.connect_seconds = 0.2
check_eq(guest.join("127.0.0.1", port), OK, "connecting starts")
await _pump([guest], 30)
check_eq(_types(guest), ["failed"], "the guest gives up")
check(not guest.is_open(), "and closes")
func test_addresses_are_split_into_host_and_port() -> void:
check_eq(NetLink.split_address("192.168.1.20"), ["192.168.1.20", NetLink.DEFAULT_PORT], "default port")
check_eq(NetLink.split_address(" example.net:25000 "), ["example.net", 25000], "with port")
for text in ["", ":25000", "host:", "host:0", "host:70000", "host:port", "a:1:2"]:
check_eq(NetLink.split_address(text), [], "refused: '%s'" % text)