Files
Archipelago/test/webhost/test_port_allocation.py
T
Uriel e6e0bc3042 WebHost: Config option for custom port ranges v2 (#6009)
* Added ability to define custom port ranges the WebHost will use for game servers, instead of pure random.

* - Added better fallback to default port range when a custom range fails
- Updated config to be clearer

* Added ability to define custom port ranges the WebHost will use for game servers, instead of pure random.

* - Added better fallback to default port range when a custom range fails
- Updated config to be clearer

* Updated soft-fail message

* Removed dead import from customserver.py

* Update requirements.txt

Settings requirements to main core branch

* fix what reviewers said and add some improvements

* remove unused argument

* try fixing test with try

* use yaml lists instead of string for config

* fix value type bug on ephemeral type

* reuse sockets with websockets api instead of opening and closing them

* add used ports cache and filter used ports when looking for ports

* fix port randomizer

* Apply suggestions from code review

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* fix some reviews

* use weights for random port and remove more-itertools

* fix net_connections not working on macOS

* rename variables and functions

* lazy init `get_used_ports`

* change `game_ports` to be `tuple`

* fix last_used_ports not being updated locally

* fix random choices and move game_port conversion into tuple

* Apply suggestions from code review

Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>

* use a named tuple on parse_game_ports

* only use ranges

* do it the duck way

* this should check all usable ports before failing

* fix while loop

* add return type to weighted random

* Update WebHostLib/customserver.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* simplify tuple conversion check

* add tests

* reformat file and change `create_random_port_socket` test

* add more test cases for parse_game_ports

* try to prevent busy-looping on create random port socket when doing test

* simplify parse game port tests to one assertListEqual

* make the range lesser for port test

* reduce range on macOS

* Apply suggestions from code review

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Update WebHostLib/customserver.py

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

* remove unused import

* Update WebHostLib/customserver.py

Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>

* use generator expressions

* check for 0-tuple

* use some kind of shuffled queue

* update tests

* refactor new port handling into a class (#1)

* change time to monotonic

* Update docs/webhost configuration sample.yaml

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

* add psutil 7.2.2 as requirement

* Update WebHostLib/requirements.txt

Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>

---------

Co-authored-by: Lexipherous <jasonnlelong@gmail.com>
Co-authored-by: Duck <31627079+duckboycool@users.noreply.github.com>
Co-authored-by: black-sliver <59490463+black-sliver@users.noreply.github.com>
Co-authored-by: Doug Hoskisson <beauxq@users.noreply.github.com>
2026-06-20 17:36:18 +02:00

90 lines
4.5 KiB
Python

import os
import unittest
from socket import socket as Socket # noqa: N812
from Utils import is_macos
from WebHostLib.customserver import RandomPortSocketCreator
ci = bool(os.environ.get("CI"))
class TestPortAllocating(unittest.TestCase):
def test_parse_game_ports(self) -> None:
"""Ensure that game ports with ranges are parsed correctly"""
val = RandomPortSocketCreator._parse_game_ports(("1000-2000", "2000-5000", "1000-2000", 20, 40, "20", "0"))
self.assertCountEqual(val.valid_ports,
[*range(1000, 2001), *range(2000, 5001), *range(1000, 2001), 20, 40, 20],
"The parsed game ports are not the expected length")
self.assertTrue(val.ephemeral_allowed, "The ephemeral allowed flag is not set even though it was passed")
val = RandomPortSocketCreator._parse_game_ports(())
self.assertListEqual(val.valid_ports, [], "Empty list of game port returned something")
self.assertFalse(val.ephemeral_allowed, "Empty list returned that ephemeral is allowed")
val = RandomPortSocketCreator._parse_game_ports((0,))
self.assertListEqual(val.valid_ports, [], "Empty list of ranges returned something")
self.assertTrue(val.ephemeral_allowed, "List with just 0 is not allowing ephemeral ports")
val = RandomPortSocketCreator._parse_game_ports((1,))
self.assertListEqual(val.valid_ports, [1], "Valid ports doesn't contain the expected values")
self.assertFalse(val.ephemeral_allowed, "List with just single port returned that ephemeral is allowed")
def test_parse_game_port_errors(self) -> None:
"""Ensure that game ports with incorrect values raise the expected error"""
with self.assertRaises(ValueError, msg="Negative numbers didn't get interpreted as an invalid range"):
RandomPortSocketCreator._parse_game_ports(tuple("-50215"))
with self.assertRaises(ValueError, msg="Text got interpreted as a valid number"):
RandomPortSocketCreator._parse_game_ports(tuple("dwafawg"))
with self.assertRaises(
ValueError,
msg="A range with an extra dash at the end didn't get interpreted as an invalid number because of it's end dash"
):
RandomPortSocketCreator._parse_game_ports(tuple("20-21215-"))
with self.assertRaises(ValueError, msg="Text got interpreted as a valid number for the start of a range"):
RandomPortSocketCreator._parse_game_ports(tuple("f-21215"))
def test_random_port_socket_edge_cases(self) -> None:
"""Verify if edge cases on creation of random port socket is working fine"""
# Try giving an empty tuple and fail over it
creator = RandomPortSocketCreator(())
with self.assertRaises(OSError) as err:
creator.create("127.0.0.1")
self.assertEqual(err.exception.errno, 98, "Raised an unexpected error code")
self.assertEqual(err.exception.strerror, "No available ports", "Raised an unexpected error string")
# Try only having ephemeral ports enabled
creator = RandomPortSocketCreator(("0",))
try:
creator.create("127.0.0.1").close()
except OSError as err:
self.assertEqual(err.errno, 98, "Raised an unexpected error code")
# If it returns our error string that means something is wrong with our code
self.assertNotEqual(err.strerror, "No available ports",
"Raised an unexpected error string")
@unittest.skipUnless(ci, "can't guarantee free ports outside of CI")
def test_random_port_socket(self) -> None:
"""Verify if returned sockets use the correct port ranges"""
creator = RandomPortSocketCreator(("8080-8085",))
sockets: list[Socket] = []
for _ in range(6):
socket = creator.create("127.0.0.1")
sockets.append(socket)
_, port = socket.getsockname()
self.assertIn(port, range(8080, 8086), "Port of socket was not inside the expected range")
for s in sockets:
s.close()
sockets.clear()
creator = RandomPortSocketCreator(("30000-65535",))
length = 5_000 if is_macos else (30_000 - len(creator._get_used_ports()))
for _ in range(length):
socket = creator.create("127.0.0.1")
sockets.append(socket)
_, port = socket.getsockname()
self.assertIn(port, range(30_000, 65536), "Port of socket was not inside the expected range")
for s in sockets:
s.close()