mirror of
https://github.com/progval/irctest.git
synced 2025-04-07 07:49:52 +00:00
Start adding support for Unreal
Not all tests pass yet, Unreal uses the protocol in ways we did not anticipate.
This commit is contained in:
55
.github/workflows/unrealircd.yml
vendored
Normal file
55
.github/workflows/unrealircd.yml
vendored
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
name: irctest with UnrealIRCd
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Set up Python 3.7
|
||||||
|
uses: actions/setup-python@v2
|
||||||
|
with:
|
||||||
|
python-version: 3.7
|
||||||
|
|
||||||
|
- name: Cache dependencies
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cache
|
||||||
|
$GITHUB_WORKSPACE/unrealircd
|
||||||
|
key: ${{ runner.os }}-unrealircd
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get install atheme-services
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install pytest -r requirements.txt
|
||||||
|
|
||||||
|
- name: Checkout UnrealIRCd
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
repository: unrealircd/unrealircd
|
||||||
|
ref: unreal52
|
||||||
|
path: unrealircd
|
||||||
|
|
||||||
|
- name: Build UnrealIRCd
|
||||||
|
run: |
|
||||||
|
cd $GITHUB_WORKSPACE/unrealircd/
|
||||||
|
cp $GITHUB_WORKSPACE/unreal_config.settings config.settings
|
||||||
|
CFLAGS=-O0 ./Config -quick
|
||||||
|
make -j 4
|
||||||
|
echo "\n\n\n\n\n\n" | make pem
|
||||||
|
make install
|
||||||
|
|
||||||
|
- name: Test with pytest
|
||||||
|
run: |
|
||||||
|
PATH=~/.local/unrealircd/bin:$PATH make unreal
|
||||||
|
|
||||||
|
|
10
Makefile
10
Makefile
@ -60,6 +60,16 @@ SOPEL_SELECTORS := \
|
|||||||
not testPlainNotAvailable \
|
not testPlainNotAvailable \
|
||||||
$(EXTRA_SELECTORS)
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
|
# testNoticeNonexistentChannel fails: https://bugs.unrealircd.org/view.php?id=5949
|
||||||
|
# test_regressions::testTagCap fails: https://bugs.unrealircd.org/view.php?id=5948
|
||||||
|
# test_messages::testLineTooLong fails: https://bugs.unrealircd.org/view.php?id=5947
|
||||||
|
UNREAL_SELECTORS := \
|
||||||
|
not Ergo \
|
||||||
|
and not testNoticeNonexistentChannel \
|
||||||
|
and not (test_regressions and testTagCap) \
|
||||||
|
and not (test_messages and testLineTooLong) \
|
||||||
|
$(EXTRA_SELECTORS)
|
||||||
|
|
||||||
.PHONY: all flakes ergo charybdis
|
.PHONY: all flakes ergo charybdis
|
||||||
|
|
||||||
all: flakes ergo inspircd limnoria sopel solanum
|
all: flakes ergo inspircd limnoria sopel solanum
|
||||||
|
@ -517,9 +517,15 @@ class BaseServerTestCase(
|
|||||||
|
|
||||||
def getRegistrationMessage(self, client: TClientName) -> Message:
|
def getRegistrationMessage(self, client: TClientName) -> Message:
|
||||||
"""Filter notices, do not send pings."""
|
"""Filter notices, do not send pings."""
|
||||||
return self.getMessage(
|
while True:
|
||||||
|
msg = self.getMessage(
|
||||||
client, synchronize=False, filter_pred=lambda m: m.command != "NOTICE"
|
client, synchronize=False, filter_pred=lambda m: m.command != "NOTICE"
|
||||||
)
|
)
|
||||||
|
if msg.command == "PING":
|
||||||
|
# Hi Unreal
|
||||||
|
self.sendLine(client, "PONG :" + msg.params[0])
|
||||||
|
else:
|
||||||
|
return msg
|
||||||
|
|
||||||
def sendLine(self, client: TClientName, line: Union[str, bytes]) -> None:
|
def sendLine(self, client: TClientName, line: Union[str, bytes]) -> None:
|
||||||
return self.clients[client].sendLine(line)
|
return self.clients[client].sendLine(line)
|
||||||
@ -565,6 +571,9 @@ class BaseServerTestCase(
|
|||||||
result.append(m)
|
result.append(m)
|
||||||
if m.command == "001":
|
if m.command == "001":
|
||||||
return result
|
return result
|
||||||
|
elif m.command == "PING":
|
||||||
|
# Hi, Unreal
|
||||||
|
self.sendLine(client, "PONG :" + m.params[0])
|
||||||
|
|
||||||
def requestCapabilities(
|
def requestCapabilities(
|
||||||
self,
|
self,
|
||||||
|
177
irctest/controllers/unrealircd.py
Normal file
177
irctest/controllers/unrealircd.py
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
from typing import Optional, Set, Type
|
||||||
|
|
||||||
|
from irctest.basecontrollers import (
|
||||||
|
BaseServerController,
|
||||||
|
DirectoryBasedController,
|
||||||
|
NotImplementedByController,
|
||||||
|
)
|
||||||
|
from irctest.irc_utils.junkdrawer import find_hostname_and_port
|
||||||
|
|
||||||
|
TEMPLATE_CONFIG = """
|
||||||
|
include "modules.default.conf";
|
||||||
|
|
||||||
|
me {{
|
||||||
|
name "My.Little.Server";
|
||||||
|
info "ExampleNET Server";
|
||||||
|
sid "001";
|
||||||
|
}}
|
||||||
|
admin {{
|
||||||
|
"Bob Smith";
|
||||||
|
"bob";
|
||||||
|
"email@example.org";
|
||||||
|
}}
|
||||||
|
class clients {{
|
||||||
|
pingfreq 90;
|
||||||
|
maxclients 1000;
|
||||||
|
sendq 200k;
|
||||||
|
recvq 8000;
|
||||||
|
}}
|
||||||
|
class servers {{
|
||||||
|
pingfreq 60;
|
||||||
|
connfreq 15; /* try to connect every 15 seconds */
|
||||||
|
maxclients 10; /* max servers */
|
||||||
|
sendq 20M;
|
||||||
|
}}
|
||||||
|
allow {{
|
||||||
|
mask *;
|
||||||
|
class clients;
|
||||||
|
maxperip 50;
|
||||||
|
{password_field}
|
||||||
|
}}
|
||||||
|
listen {{
|
||||||
|
ip {hostname};
|
||||||
|
port {port};
|
||||||
|
}}
|
||||||
|
listen {{
|
||||||
|
ip {tls_hostname};
|
||||||
|
port {tls_port};
|
||||||
|
options {{ tls; }}
|
||||||
|
tls-options {{
|
||||||
|
certificate "{pem_path}";
|
||||||
|
key "{key_path}";
|
||||||
|
}};
|
||||||
|
}}
|
||||||
|
|
||||||
|
/* Special SSL/TLS servers-only port for linking */
|
||||||
|
listen {{
|
||||||
|
ip {services_hostname};
|
||||||
|
port {services_port};
|
||||||
|
options {{ serversonly; }}
|
||||||
|
}}
|
||||||
|
|
||||||
|
link services.example.org {{
|
||||||
|
incoming {{
|
||||||
|
mask localhost;
|
||||||
|
}}
|
||||||
|
password "password";
|
||||||
|
class servers;
|
||||||
|
}}
|
||||||
|
ulines {{
|
||||||
|
services.example.org;
|
||||||
|
}}
|
||||||
|
|
||||||
|
set {{
|
||||||
|
kline-address "example@example.org";
|
||||||
|
network-name "ExampleNET";
|
||||||
|
default-server "irc.example.org";
|
||||||
|
help-channel "#Help";
|
||||||
|
cloak-keys {{ "aaaA1"; "bbbB2"; "cccC3"; }}
|
||||||
|
anti-flood {{
|
||||||
|
// Prevent throttling, especially test_buffering.py which
|
||||||
|
// triggers anti-flood with its very long lines
|
||||||
|
unknown-users {{
|
||||||
|
lag-penalty 1;
|
||||||
|
lag-penalty-bytes 10000;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
tld {{
|
||||||
|
mask *;
|
||||||
|
motd "{empty_file}";
|
||||||
|
botmotd "{empty_file}";
|
||||||
|
rules "{empty_file}";
|
||||||
|
}}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class UnrealircdController(BaseServerController, DirectoryBasedController):
|
||||||
|
software_name = "InspIRCd"
|
||||||
|
supported_sasl_mechanisms = {"PLAIN"}
|
||||||
|
supports_sts = False
|
||||||
|
|
||||||
|
def create_config(self) -> None:
|
||||||
|
super().create_config()
|
||||||
|
with self.open_file("server.conf"):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self,
|
||||||
|
hostname: str,
|
||||||
|
port: int,
|
||||||
|
*,
|
||||||
|
password: Optional[str],
|
||||||
|
ssl: bool,
|
||||||
|
run_services: bool,
|
||||||
|
valid_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
invalid_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
restricted_metadata_keys: Optional[Set[str]] = None,
|
||||||
|
) -> None:
|
||||||
|
if valid_metadata_keys or invalid_metadata_keys:
|
||||||
|
raise NotImplementedByController(
|
||||||
|
"Defining valid and invalid METADATA keys."
|
||||||
|
)
|
||||||
|
assert self.proc is None
|
||||||
|
self.port = port
|
||||||
|
self.hostname = hostname
|
||||||
|
self.create_config()
|
||||||
|
(unused_hostname, unused_port) = find_hostname_and_port()
|
||||||
|
(services_hostname, services_port) = find_hostname_and_port()
|
||||||
|
|
||||||
|
password_field = 'password "{}";'.format(password) if password else ""
|
||||||
|
|
||||||
|
self.gen_ssl()
|
||||||
|
if ssl:
|
||||||
|
(tls_hostname, tls_port) = (hostname, port)
|
||||||
|
(hostname, port) = (unused_hostname, unused_port)
|
||||||
|
else:
|
||||||
|
# Unreal refuses to start without TLS enabled
|
||||||
|
(tls_hostname, tls_port) = (unused_hostname, unused_port)
|
||||||
|
|
||||||
|
with self.open_file("empty.txt") as fd:
|
||||||
|
fd.write("\n")
|
||||||
|
|
||||||
|
assert self.directory
|
||||||
|
with self.open_file("unrealircd.conf") as fd:
|
||||||
|
fd.write(
|
||||||
|
TEMPLATE_CONFIG.format(
|
||||||
|
hostname=hostname,
|
||||||
|
port=port,
|
||||||
|
services_hostname=services_hostname,
|
||||||
|
services_port=services_port,
|
||||||
|
tls_hostname=tls_hostname,
|
||||||
|
tls_port=tls_port,
|
||||||
|
password_field=password_field,
|
||||||
|
key_path=self.key_path,
|
||||||
|
pem_path=self.pem_path,
|
||||||
|
empty_file=os.path.join(self.directory, "empty.txt"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.proc = subprocess.Popen(
|
||||||
|
[
|
||||||
|
"unrealircd",
|
||||||
|
"-F", # BOOT_NOFORK
|
||||||
|
"-f",
|
||||||
|
os.path.join(self.directory, "unrealircd.conf"),
|
||||||
|
],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
|
||||||
|
if run_services:
|
||||||
|
assert False
|
||||||
|
|
||||||
|
|
||||||
|
def get_irctest_controller_class() -> Type[UnrealircdController]:
|
||||||
|
return UnrealircdController
|
@ -33,11 +33,16 @@ class BotModeTestCase(cases.BaseServerTestCase):
|
|||||||
self.sendLine("bot", f"MODE botnick +{self._mode_char}")
|
self.sendLine("bot", f"MODE botnick +{self._mode_char}")
|
||||||
|
|
||||||
# Check echoed mode
|
# Check echoed mode
|
||||||
|
while True:
|
||||||
|
msg = self.getMessage("bot")
|
||||||
|
if msg.command != "NOTICE":
|
||||||
|
# Unreal sends the BOTMOTD here
|
||||||
self.assertMessageMatch(
|
self.assertMessageMatch(
|
||||||
self.getMessage("bot"),
|
msg,
|
||||||
command="MODE",
|
command="MODE",
|
||||||
params=["botnick", StrRe(r"\+?" + self._mode_char)],
|
params=["botnick", StrRe(r"\+?" + self._mode_char)],
|
||||||
)
|
)
|
||||||
|
break
|
||||||
|
|
||||||
def testBotMode(self):
|
def testBotMode(self):
|
||||||
self._initBot()
|
self._initBot()
|
||||||
|
@ -125,6 +125,7 @@ class CapTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
|
|||||||
cap1 = "echo-message"
|
cap1 = "echo-message"
|
||||||
cap2 = "server-time"
|
cap2 = "server-time"
|
||||||
self.addClient(1)
|
self.addClient(1)
|
||||||
|
self.connectClient("sender")
|
||||||
self.sendLine(1, "CAP LS 302")
|
self.sendLine(1, "CAP LS 302")
|
||||||
m = self.getRegistrationMessage(1)
|
m = self.getRegistrationMessage(1)
|
||||||
if not ({cap1, cap2} <= set(m.params[2].split())):
|
if not ({cap1, cap2} <= set(m.params[2].split())):
|
||||||
@ -146,7 +147,10 @@ class CapTestCase(cases.BaseServerTestCase, cases.OptionalityHelper):
|
|||||||
enabled_caps = set(cap_list.params[2].split())
|
enabled_caps = set(cap_list.params[2].split())
|
||||||
enabled_caps.discard("cap-notify") # implicitly added by some impls
|
enabled_caps.discard("cap-notify") # implicitly added by some impls
|
||||||
self.assertEqual(enabled_caps, {cap1, cap2})
|
self.assertEqual(enabled_caps, {cap1, cap2})
|
||||||
self.assertIn("time", cap_list.tags, cap_list)
|
|
||||||
|
self.sendLine(2, "PRIVMSG bar :hi")
|
||||||
|
m = self.getMessage(1)
|
||||||
|
self.assertIn("time", m.tags, m)
|
||||||
|
|
||||||
# remove the server-time cap
|
# remove the server-time cap
|
||||||
self.sendLine(1, f"CAP REQ :-{cap2}")
|
self.sendLine(1, f"CAP REQ :-{cap2}")
|
||||||
|
@ -74,9 +74,13 @@ class TagsTestCase(cases.BaseServerTestCase):
|
|||||||
@cases.mark_capabilities("message-tags")
|
@cases.mark_capabilities("message-tags")
|
||||||
def testLineTooLong(self):
|
def testLineTooLong(self):
|
||||||
self.connectClient("bar", capabilities=["message-tags"], skip_if_cap_nak=True)
|
self.connectClient("bar", capabilities=["message-tags"], skip_if_cap_nak=True)
|
||||||
|
self.connectClient(
|
||||||
|
"recver", capabilities=["message-tags"], skip_if_cap_nak=True
|
||||||
|
)
|
||||||
self.joinChannel(1, "#xyz")
|
self.joinChannel(1, "#xyz")
|
||||||
monsterMessage = "@+clientOnlyTagExample=" + "a" * 4096 + " PRIVMSG #xyz hi!"
|
monsterMessage = "@+clientOnlyTagExample=" + "a" * 4096 + " PRIVMSG #xyz hi!"
|
||||||
self.sendLine(1, monsterMessage)
|
self.sendLine(1, monsterMessage)
|
||||||
|
self.assertEqual(self.getMessages(2), [], "overflowing message was relayed")
|
||||||
replies = self.getMessages(1)
|
replies = self.getMessages(1)
|
||||||
self.assertIn(ERR_INPUTTOOLONG, set(reply.command for reply in replies))
|
self.assertIn(ERR_INPUTTOOLONG, set(reply.command for reply in replies))
|
||||||
|
|
||||||
|
@ -109,8 +109,13 @@ class RegressionsTestCase(cases.BaseServerTestCase):
|
|||||||
|
|
||||||
self.sendLine(1, "NICK valid")
|
self.sendLine(1, "NICK valid")
|
||||||
replies = {"NOTICE"}
|
replies = {"NOTICE"}
|
||||||
while replies <= {"NOTICE"}:
|
while replies <= {"NOTICE", "PING"}:
|
||||||
replies = set(msg.command for msg in self.getMessages(1, synchronize=False))
|
msgs = self.getMessages(1, synchronize=False)
|
||||||
|
for msg in msgs:
|
||||||
|
if msg.command == "PING":
|
||||||
|
# Hi Unreal
|
||||||
|
self.sendLine(1, "PONG :" + msg.params[0])
|
||||||
|
replies = set(msg.command for msg in msgs)
|
||||||
self.assertNotIn(ERR_ERRONEUSNICKNAME, replies)
|
self.assertNotIn(ERR_ERRONEUSNICKNAME, replies)
|
||||||
self.assertIn(RPL_WELCOME, replies)
|
self.assertIn(RPL_WELCOME, replies)
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user