15 Commits

Author SHA1 Message Date
1d8d78e62a sopel: Enable support for SCRAM 2022-10-28 14:26:47 +02:00
690aaf24a1 Bump flake8 version
Fixes support for importlib_metadata 5.0.0,
https://github.com/PyCQA/flake8/issues/1701
2022-10-22 12:34:46 +02:00
40385c112b add a test for AWAY :\r\n (#175) 2022-09-18 13:27:48 -04:00
9d4212504b Add tests for TIME. (#127) 2022-09-11 17:18:10 +02:00
cae3aec338 workflows: Remove special-casing of Anope 2022-09-10 15:15:29 +02:00
c1442c4301 unrealircd: Use lock around startup/shutdown instead of proot
to ensure no unrealircd instance is starting up while another clears
$PREFIX/tmp/

While proot allows full parallelism and is less error-prone, it takes
a long time to start; and segfaults on my Armbian system.
2022-09-10 14:56:20 +02:00
507f5b7426 Use pathlib to work with temporary config dirs 2022-09-10 14:17:19 +02:00
dbdadec677 test that WHO ignores +i for bare nicknames (#171) 2022-08-26 19:01:41 +02:00
6290825c64 README: Remove reference to setup.py 2022-08-20 18:05:47 +02:00
f1c9218fbb Bump Go version for Ergo 2022-08-04 21:24:48 +02:00
6b6017b40c testStarNick: Replace unreliable workaround for irc2 2022-06-27 20:54:04 +02:00
601f49a9ef Fix infinite loop when server is slow (eg. Bahamut) 2022-06-27 20:53:50 +02:00
e205cc1531 bahamut: pre-initialize entropy to avoid freezing on GH Actions 2022-06-19 16:48:26 +02:00
8a4f254a21 Reduce parallelism on other servers as well 2022-06-18 22:01:36 +02:00
81dac6f582 bahamut: lower mainloop delay, and reduce parallelism to make tests less flaky 2022-06-18 20:26:53 +02:00
28 changed files with 374 additions and 224 deletions

View File

@ -5,18 +5,22 @@ jobs:
build-anope:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
- name: Cache Anope
- name: Cache dependencies
uses: actions/cache@v2
with:
key: 3-${{ runner.os }}-anope-2.0.9
key: 3-${{ runner.os }}-anope-devel
path: '~/.cache
${{ github.workspace }}/anope
${ github.workspace }/anope
'
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Checkout Anope
uses: actions/checkout@v2
with:
@ -24,7 +28,7 @@ jobs:
ref: 2.0.9
repository: anope/anope
- name: Build Anope
run: |-
run: |
cd $GITHUB_WORKSPACE/anope/
cp $GITHUB_WORKSPACE/data/anope/* .
CFLAGS=-O0 ./Config -quick
@ -67,6 +71,7 @@ jobs:
run: |
cd $GITHUB_WORKSPACE/Bahamut/
patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch
patch src/s_bsd.c < $GITHUB_WORKSPACE/patches/bahamut_mainloop.patch
echo "#undef THROTTLE_ENABLE" >> include/config.h
libtoolize --force
aclocal
@ -541,7 +546,7 @@ jobs:
repository: ergochat/ergo
- uses: actions/setup-go@v2
with:
go-version: ^1.18.0
go-version: ^1.19.0
- run: go version
- name: Build Ergo
run: |

View File

@ -5,18 +5,22 @@ jobs:
build-anope:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
- name: Cache Anope
- name: Cache dependencies
uses: actions/cache@v2
with:
key: 3-${{ runner.os }}-anope-2.0.9
key: 3-${{ runner.os }}-anope-devel_release
path: '~/.cache
${{ github.workspace }}/anope
${ github.workspace }/anope
'
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Checkout Anope
uses: actions/checkout@v2
with:
@ -24,7 +28,7 @@ jobs:
ref: 2.0.9
repository: anope/anope
- name: Build Anope
run: |-
run: |
cd $GITHUB_WORKSPACE/anope/
cp $GITHUB_WORKSPACE/data/anope/* .
CFLAGS=-O0 ./Config -quick

View File

@ -5,18 +5,22 @@ jobs:
build-anope:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Create directories
run: cd ~/; mkdir -p .local/ go/
- name: Cache Anope
- name: Cache dependencies
uses: actions/cache@v2
with:
key: 3-${{ runner.os }}-anope-2.0.9
key: 3-${{ runner.os }}-anope-stable
path: '~/.cache
${{ github.workspace }}/anope
${ github.workspace }/anope
'
- uses: actions/checkout@v2
- name: Set up Python 3.7
uses: actions/setup-python@v2
with:
python-version: 3.7
- name: Checkout Anope
uses: actions/checkout@v2
with:
@ -24,7 +28,7 @@ jobs:
ref: 2.0.9
repository: anope/anope
- name: Build Anope
run: |-
run: |
cd $GITHUB_WORKSPACE/anope/
cp $GITHUB_WORKSPACE/data/anope/* .
CFLAGS=-O0 ./Config -quick
@ -67,6 +71,7 @@ jobs:
run: |
cd $GITHUB_WORKSPACE/Bahamut/
patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch
patch src/s_bsd.c < $GITHUB_WORKSPACE/patches/bahamut_mainloop.patch
echo "#undef THROTTLE_ENABLE" >> include/config.h
libtoolize --force
aclocal
@ -617,7 +622,7 @@ jobs:
repository: ergochat/ergo
- uses: actions/setup-go@v2
with:
go-version: ^1.18.0
go-version: ^1.19.0
- run: go version
- name: Build Ergo
run: |

View File

@ -13,7 +13,7 @@ repos:
- id: isort
- repo: https://gitlab.com/pycqa/flake8
rev: 3.8.3
rev: 5.0.4
hooks:
- id: flake8

View File

@ -122,7 +122,8 @@ bahamut:
$(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.bahamut \
-m 'not services' \
-n 10 \
-n 4 \
-vv -s \
-k '$(BAHAMUT_SELECTORS)'
bahamut-atheme:
@ -130,7 +131,6 @@ bahamut-atheme:
--controller=irctest.controllers.bahamut \
--services-controller=irctest.controllers.atheme_services \
-m 'services' \
-n 10 \
-k '$(BAHAMUT_SELECTORS)'
bahamut-anope:
@ -138,7 +138,6 @@ bahamut-anope:
--controller=irctest.controllers.bahamut \
--services-controller=irctest.controllers.anope_services \
-m 'services' \
-n 10 \
-k '$(BAHAMUT_SELECTORS)'
charybdis:
@ -182,28 +181,28 @@ ircu2:
$(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.ircu2 \
-m 'not services and not IRCv3' \
-n 10 \
-n 4 \
-k '$(IRCU2_SELECTORS)'
nefarious:
$(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.nefarious \
-m 'not services' \
-n 10 \
-n 4 \
-k '$(NEFARIOUS_SELECTORS)'
snircd:
$(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.snircd \
-m 'not services and not IRCv3' \
-n 10 \
-n 4 \
-k '$(SNIRCD_SELECTORS)'
irc2:
$(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.irc2 \
-m 'not services and not IRCv3' \
-n 10 \
-n 4 \
-k '$(IRC2_SELECTORS)'
limnoria:
@ -226,7 +225,7 @@ ngircd:
$(PYTEST) $(PYTEST_ARGS) \
--controller irctest.controllers.ngircd \
-m 'not services' \
-n 10 \
-n 4 \
-k "$(NGIRCD_SELECTORS)"
ngircd-anope:

View File

@ -23,7 +23,6 @@ cd ~
git clone https://github.com/ProgVal/irctest.git
cd irctest
pip3 install --user -r requirements.txt
python3 setup.py install --user
```
Add `~/.local/bin/` (and/or `~/go/bin/` for Ergo)

View File

@ -2,6 +2,7 @@ from __future__ import annotations
import dataclasses
import os
from pathlib import Path
import shutil
import socket
import subprocess
@ -87,7 +88,7 @@ class DirectoryBasedController(_BaseController):
"""Helper for controllers whose software configuration is based on an
arbitrary directory."""
directory: Optional[str]
directory: Optional[Path]
def __init__(self, test_config: TestCaseControllerConfig):
super().__init__(test_config)
@ -110,22 +111,21 @@ class DirectoryBasedController(_BaseController):
"""Open a file in the configuration directory."""
assert self.directory
if os.sep in name:
dir_ = os.path.join(self.directory, os.path.dirname(name))
if not os.path.isdir(dir_):
os.makedirs(dir_)
assert os.path.isdir(dir_)
return open(os.path.join(self.directory, name), mode)
dir_ = self.directory / os.path.dirname(name)
dir_.mkdir(parents=True, exist_ok=True)
assert dir_.is_dir()
return (self.directory / name).open(mode)
def create_config(self) -> None:
if not self.directory:
self.directory = tempfile.mkdtemp()
self.directory = Path(tempfile.mkdtemp())
def gen_ssl(self) -> None:
assert self.directory
self.csr_path = os.path.join(self.directory, "ssl.csr")
self.key_path = os.path.join(self.directory, "ssl.key")
self.pem_path = os.path.join(self.directory, "ssl.pem")
self.dh_path = os.path.join(self.directory, "dh.pem")
self.csr_path = self.directory / "ssl.csr"
self.key_path = self.directory / "ssl.key"
self.pem_path = self.directory / "ssl.pem"
self.dh_path = self.directory / "dh.pem"
subprocess.check_output(
[
self.openssl_bin,
@ -222,6 +222,7 @@ class BaseServerController(_BaseController):
raise NotImplementedByController("account registration")
def wait_for_port(self) -> None:
started_at = time.time()
while not self.port_open:
self.check_is_alive()
time.sleep(self._port_wait_interval)
@ -244,11 +245,16 @@ class BaseServerController(_BaseController):
# ircu2 cuts the connection without a message if registration
# is not complete.
pass
except socket.timeout:
# irc2 just keeps it open
pass
c.close()
self.port_open = True
except Exception:
continue
except ConnectionRefusedError:
if time.time() - started_at >= 60:
# waited for 60 seconds, giving up
raise
def wait_for_services(self) -> None:
assert self.services_controller

View File

@ -1,4 +1,4 @@
import os
from pathlib import Path
import shutil
import subprocess
from typing import Type
@ -101,14 +101,11 @@ class AnopeController(BaseServicesController, DirectoryBasedController):
pass
assert self.directory
services_path = shutil.which("services")
assert services_path
# Config and code need to be in the same directory, *obviously*
os.symlink(
os.path.join(
os.path.dirname(shutil.which("services")), "..", "lib" # type: ignore
),
os.path.join(self.directory, "lib"),
)
(self.directory / "lib").symlink_to(Path(services_path).parent.parent / "lib")
self.proc = subprocess.Popen(
[

View File

@ -1,4 +1,3 @@
import os
import subprocess
from typing import Optional, Type
@ -81,11 +80,11 @@ class AthemeController(BaseServicesController, DirectoryBasedController):
"atheme-services",
"-n", # don't fork
"-c",
os.path.join(self.directory, "services.conf"),
self.directory / "services.conf",
"-l",
f"/tmp/services-{server_port}.log",
"-p",
os.path.join(self.directory, "services.pid"),
self.directory / "services.pid",
"-D",
self.directory,
],

View File

@ -1,4 +1,4 @@
import os
from pathlib import Path
import shutil
import subprocess
from typing import Optional, Set, Type
@ -80,6 +80,19 @@ oper {{
"""
def initialize_entropy(directory: Path) -> None:
# https://github.com/DALnet/bahamut/blob/7fc039d403f66a954225c5dc4ad1fe683aedd794/include/dh.h#L35-L38
nb_rand_bytes = 512 // 8
# https://github.com/DALnet/bahamut/blob/7fc039d403f66a954225c5dc4ad1fe683aedd794/src/dh.c#L186
entropy_file_size = nb_rand_bytes * 4
# Not actually random; but we don't care.
entropy = b"\x00" * entropy_file_size
with (directory / ".ircd.entropy").open("wb") as fd:
fd.write(entropy)
class BahamutController(BaseServerController, DirectoryBasedController):
software_name = "Bahamut"
supported_sasl_mechanisms: Set[str] = set()
@ -121,9 +134,14 @@ class BahamutController(BaseServerController, DirectoryBasedController):
assert self.directory
# Bahamut reads some bytes from /dev/urandom on startup, which causes
# GitHub Actions to sometimes freeze and timeout.
# This initializes the entropy file so Bahamut does not need to do it itself.
initialize_entropy(self.directory)
# they are hardcoded... thankfully Bahamut reads them from the CWD.
shutil.copy(self.pem_path, os.path.join(self.directory, "ircd.crt"))
shutil.copy(self.key_path, os.path.join(self.directory, "ircd.key"))
shutil.copy(self.pem_path, self.directory / "ircd.crt")
shutil.copy(self.key_path, self.directory / "ircd.key")
with self.open_file("server.conf") as fd:
fd.write(
@ -150,7 +168,7 @@ class BahamutController(BaseServerController, DirectoryBasedController):
"ircd",
"-t", # don't fork
"-f",
os.path.join(self.directory, "server.conf"),
self.directory / "server.conf",
],
)

View File

@ -1,4 +1,3 @@
import os
import shutil
import subprocess
from typing import Optional, Set
@ -88,9 +87,9 @@ class BaseHybridController(BaseServerController, DirectoryBasedController):
self.binary_name,
"-foreground",
"-configfile",
os.path.join(self.directory, "server.conf"),
self.directory / "server.conf",
"-pidfile",
os.path.join(self.directory, "server.pid"),
self.directory / "server.pid",
],
# stderr=subprocess.DEVNULL,
)

View File

@ -185,21 +185,19 @@ class ErgoController(BaseServerController, DirectoryBasedController):
bind_address = "127.0.0.1:%s" % (port,)
listener_conf = None # plaintext
if ssl:
self.key_path = os.path.join(self.directory, "ssl.key")
self.pem_path = os.path.join(self.directory, "ssl.pem")
self.key_path = self.directory / "ssl.key"
self.pem_path = self.directory / "ssl.pem"
listener_conf = {"tls": {"cert": self.pem_path, "key": self.key_path}}
config["server"]["listeners"][bind_address] = listener_conf # type: ignore
config["datastore"]["path"] = os.path.join( # type: ignore
self.directory, "ircd.db"
)
config["datastore"]["path"] = str(self.directory / "ircd.db") # type: ignore
if password is not None:
config["server"]["password"] = hash_password(password) # type: ignore
assert self.proc is None
self._config_path = os.path.join(self.directory, "server.yml")
self._config_path = self.directory / "server.yml"
self._config = config
self._write_config()
subprocess.call(["ergo", "initdb", "--conf", self._config_path, "--quiet"])

View File

@ -1,4 +1,3 @@
import os
import shutil
import subprocess
from typing import Optional, Set, Type
@ -164,7 +163,7 @@ class InspircdController(BaseServerController, DirectoryBasedController):
"inspircd",
"--nofork",
"--config",
os.path.join(self.directory, "server.conf"),
self.directory / "server.conf",
],
stdout=subprocess.DEVNULL,
)

View File

@ -1,4 +1,3 @@
import os
import shutil
import subprocess
from typing import Optional, Set, Type
@ -68,7 +67,7 @@ class Irc2Controller(BaseServerController, DirectoryBasedController):
self.create_config()
password_field = password if password else ""
assert self.directory
pidfile = os.path.join(self.directory, "ircd.pid")
pidfile = self.directory / "ircd.pid"
with self.open_file("server.conf") as fd:
fd.write(
TEMPLATE_CONFIG.format(
@ -93,7 +92,7 @@ class Irc2Controller(BaseServerController, DirectoryBasedController):
"-p",
"on",
"-f",
os.path.join(self.directory, "server.conf"),
self.directory / "server.conf",
],
# stderr=subprocess.DEVNULL,
)

View File

@ -1,4 +1,3 @@
import os
import shutil
import subprocess
from typing import Optional, Set, Type
@ -87,7 +86,7 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController):
self.create_config()
password_field = 'password = "{}";'.format(password) if password else ""
assert self.directory
pidfile = os.path.join(self.directory, "ircd.pid")
pidfile = self.directory / "ircd.pid"
with self.open_file("server.conf") as fd:
fd.write(
TEMPLATE_CONFIG.format(
@ -110,7 +109,7 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController):
"ircd",
"-n", # don't detach
"-f",
os.path.join(self.directory, "server.conf"),
self.directory / "server.conf",
"-x",
"DEBUG",
],

View File

@ -1,4 +1,3 @@
import os
import subprocess
from typing import Optional, Type
@ -85,9 +84,7 @@ class LimnoriaController(BaseClientController, DirectoryBasedController):
)
)
assert self.directory
self.proc = subprocess.Popen(
["supybot", os.path.join(self.directory, "bot.conf")]
)
self.proc = subprocess.Popen(["supybot", self.directory / "bot.conf"])
def get_irctest_controller_class() -> Type[LimnoriaController]:

View File

@ -1,4 +1,3 @@
import os
import shutil
import subprocess
from typing import Optional, Set, Type
@ -128,7 +127,7 @@ class MammonController(BaseServerController, DirectoryBasedController):
"mammond",
"--nofork", # '--debug',
"--config",
os.path.join(self.directory, "server.yml"),
self.directory / "server.yml",
]
)

View File

@ -1,4 +1,3 @@
import os
import shutil
import subprocess
from typing import Optional, Set, Type
@ -94,7 +93,7 @@ class NgircdController(BaseServerController, DirectoryBasedController):
password_field=password_field,
key_path=self.key_path,
pem_path=self.pem_path,
empty_file=os.path.join(self.directory, "empty.txt"),
empty_file=self.directory / "empty.txt",
)
)
@ -110,7 +109,7 @@ class NgircdController(BaseServerController, DirectoryBasedController):
"ngircd",
"--nodaemon",
"--config",
os.path.join(self.directory, "server.conf"),
self.directory / "server.conf",
],
# stdout=subprocess.DEVNULL,
)

View File

@ -1,4 +1,3 @@
import os
import shutil
import subprocess
from typing import Optional, Set, Type
@ -86,7 +85,7 @@ class SnircdController(BaseServerController, DirectoryBasedController):
self.create_config()
password_field = 'password = "{}";'.format(password) if password else ""
assert self.directory
pidfile = os.path.join(self.directory, "ircd.pid")
pidfile = self.directory / "ircd.pid"
with self.open_file("server.conf") as fd:
fd.write(
TEMPLATE_CONFIG.format(
@ -109,7 +108,7 @@ class SnircdController(BaseServerController, DirectoryBasedController):
"ircd",
"-n", # don't detach
"-f",
os.path.join(self.directory, "server.conf"),
self.directory / "server.conf",
"-x",
"DEBUG",
],

View File

@ -1,4 +1,4 @@
import os
from pathlib import Path
import subprocess
import tempfile
from typing import Optional, TextIO, Type, cast
@ -27,7 +27,11 @@ auth_password = {password}
class SopelController(BaseClientController):
software_name = "Sopel"
supported_sasl_mechanisms = {"PLAIN"}
supported_sasl_mechanisms = {
"PLAIN",
"SCRAM-SHA-256",
"EXTERNAL",
}
supports_sts = False
def __init__(self, test_config: TestCaseControllerConfig):
@ -38,14 +42,14 @@ class SopelController(BaseClientController):
super().kill()
if self.filename:
try:
os.unlink(os.path.join(os.path.expanduser("~/.sopel/"), self.filename))
except OSError: #  File does not exist
(Path("~/.sopel/").expanduser() / self.filename).unlink()
except OSError: # File does not exist
pass
def open_file(self, filename: str, mode: str = "a") -> TextIO:
dir_path = os.path.expanduser("~/.sopel/")
os.makedirs(dir_path, exist_ok=True)
return cast(TextIO, open(os.path.join(dir_path, filename), mode))
dir_path = Path("~/.sopel/").expanduser()
dir_path.mkdir(parents=True, exist_ok=True)
return cast(TextIO, (dir_path / filename).open(mode))
def create_config(self) -> None:
with self.open_file(self.filename):
@ -61,6 +65,8 @@ class SopelController(BaseClientController):
# Runs a client with the config given as arguments
if tls_config is not None:
raise NotImplementedByController("TLS configuration")
if auth and len(auth.mechanisms) > 1:
raise NotImplementedByController("multiple SASL mechanisms")
assert self.proc is None
self.create_config()
with self.open_file(self.filename) as fd:
@ -70,7 +76,12 @@ class SopelController(BaseClientController):
port=port,
username=auth.username if auth else "",
password=auth.password if auth else "",
auth_method="auth_method = sasl" if auth else "",
auth_method=(
f"auth_method = sasl\n"
f"auth_target = {auth.mechanisms[0].to_string()}"
)
if auth
else "",
)
)
self.proc = subprocess.Popen(["sopel", "--quiet", "-c", self.filename])

View File

@ -1,11 +1,11 @@
import contextlib
import fcntl
import functools
import os
import pathlib
from pathlib import Path
import shutil
import signal
import subprocess
import textwrap
from typing import Optional, Set, Type
from typing import Callable, ContextManager, Iterator, Optional, Set, Type
from irctest.basecontrollers import (
BaseServerController,
@ -125,6 +125,35 @@ oper "operuser" {{
"""
def _filelock(path: Path) -> Callable[[], ContextManager]:
"""Alternative to :cls:`multiprocessing.Lock` that works with pytest-xdist"""
@contextlib.contextmanager
def f() -> Iterator[None]:
with open(path, "a") as fd:
fcntl.flock(fd, fcntl.LOCK_EX)
yield
return f
_UNREALIRCD_BIN = shutil.which("unrealircd")
if _UNREALIRCD_BIN:
_UNREALIRCD_PREFIX = Path(_UNREALIRCD_BIN).parent.parent
# Try to keep that lock file specific to this Unrealircd instance
_LOCK_PATH = _UNREALIRCD_PREFIX / "irctest-unrealircd-startstop.lock"
else:
# unrealircd not found; we are probably going to crash later anyway...
_LOCK_PATH = Path("/tmp/irctest-unrealircd-startstop.lock")
_STARTSTOP_LOCK = _filelock(_LOCK_PATH)
"""
Unreal cleans its tmp/ directory after each run, which prevents
multiple processes from starting/stopping at the same time.
"""
@functools.lru_cache()
def installed_version() -> int:
output = subprocess.check_output(["unrealircd", "-v"], universal_newlines=True)
@ -170,18 +199,6 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
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)
if installed_version() >= 6:
extras = textwrap.dedent(
@ -208,63 +225,60 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
with self.open_file("empty.txt") as fd:
fd.write("\n")
assert self.directory
password_field = 'password "{}";'.format(password) if password else ""
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"),
extras=extras,
set_extras=set_extras,
with _STARTSTOP_LOCK():
(services_hostname, services_port) = find_hostname_and_port()
(unused_hostname, unused_port) = find_hostname_and_port()
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)
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=self.directory / "empty.txt",
extras=extras,
set_extras=set_extras,
)
)
if faketime and shutil.which("faketime"):
faketime_cmd = ["faketime", "-f", faketime]
self.faketime_enabled = True
else:
faketime_cmd = []
self.proc = subprocess.Popen(
[
*faketime_cmd,
"unrealircd",
"-t",
"-F", # BOOT_NOFORK
"-f",
self.directory / "unrealircd.conf",
],
# stdout=subprocess.DEVNULL,
)
proot_cmd = []
self.using_proot = False
if shutil.which("proot"):
unrealircd_path = shutil.which("unrealircd")
if unrealircd_path:
unrealircd_prefix = pathlib.Path(unrealircd_path).parents[1]
tmpdir = os.path.join(self.directory, "tmp")
os.mkdir(tmpdir)
# Unreal cleans its tmp/ directory after each run, which prevents
# multiple processes from running at the same time.
# Using PRoot, we can isolate them, with a tmp/ directory for each
# process, so they don't interfere with each other, allowing use of
# the -n option (of pytest-xdist) to speed-up tests
proot_cmd = ["proot", "-b", f"{tmpdir}:{unrealircd_prefix}/tmp"]
self.using_proot = True
if faketime and shutil.which("faketime"):
faketime_cmd = ["faketime", "-f", faketime]
self.faketime_enabled = True
else:
faketime_cmd = []
self.proc = subprocess.Popen(
[
*proot_cmd,
*faketime_cmd,
"unrealircd",
"-t",
"-F", # BOOT_NOFORK
"-f",
os.path.join(self.directory, "unrealircd.conf"),
],
# stdout=subprocess.DEVNULL,
)
self.wait_for_port()
if run_services:
self.wait_for_port()
self.services_controller = self.services_controller_class(
self.test_config, self
)
@ -274,17 +288,13 @@ class UnrealircdController(BaseServerController, DirectoryBasedController):
server_port=services_port,
)
def kill(self) -> None:
if self.using_proot:
# Kill grandchild process, instead of killing proot, which takes more
# time (and does not seem to always work)
assert self.proc is not None
output = subprocess.check_output(
["ps", "-opid", "--no-headers", "--ppid", str(self.proc.pid)]
)
(grandchild_pid,) = [int(line) for line in output.decode().split()]
os.kill(grandchild_pid, signal.SIGKILL)
super().kill()
def kill_proc(self) -> None:
assert self.proc
with _STARTSTOP_LOCK():
self.proc.kill()
self.proc.wait(5) # wait for it to actually die
self.proc = None
def get_irctest_controller_class() -> Type[UnrealircdController]:

View File

@ -4,7 +4,13 @@ AWAY command (`RFC 2812 <https://datatracker.ietf.org/doc/html/rfc2812#section-4
"""
from irctest import cases
from irctest.numerics import RPL_AWAY, RPL_NOWAWAY, RPL_UNAWAY, RPL_USERHOST
from irctest.numerics import (
RPL_AWAY,
RPL_NOWAWAY,
RPL_UNAWAY,
RPL_USERHOST,
RPL_WHOISUSER,
)
from irctest.patma import StrRe
@ -139,3 +145,33 @@ class AwayTestCase(cases.BaseServerTestCase):
self.assertMessageMatch(
self.getMessage(2), command=RPL_USERHOST, params=["qux", StrRe(r"bar=-.*")]
)
@cases.mark_specifications("Modern")
def testAwayEmptyMessage(self):
"""
"If [AWAY] is sent with a nonempty parameter (the 'away message')
then the user is set to be away. If this command is sent with no
parameters, or with the empty string as the parameter, the user is no
longer away."
-- https://modern.ircdocs.horse/#away-message
"""
self.connectClient("bar", name="bar")
self.connectClient("qux", name="qux")
self.sendLine("bar", "AWAY :I'm not here right now")
replies = self.getMessages("bar")
self.assertIn(RPL_NOWAWAY, [msg.command for msg in replies])
self.sendLine("qux", "WHOIS bar")
replies = self.getMessages("qux")
self.assertIn(RPL_WHOISUSER, [msg.command for msg in replies])
self.assertIn(RPL_AWAY, [msg.command for msg in replies])
# empty final parameter to AWAY is treated the same as no parameter,
# i.e., the client is considered to be no longer away
self.sendLine("bar", "AWAY :")
replies = self.getMessages("bar")
self.assertIn(RPL_UNAWAY, [msg.command for msg in replies])
self.sendLine("qux", "WHOIS bar")
replies = self.getMessages("qux")
self.assertIn(RPL_WHOISUSER, [msg.command for msg in replies])
self.assertNotIn(RPL_AWAY, [msg.command for msg in replies])

View File

@ -2,10 +2,13 @@
Regression tests for bugs in `Ergo <https://ergo.chat/>`_.
"""
import time
from irctest import cases, runner
from irctest.numerics import ERR_ERRONEUSNICKNAME, ERR_NICKNAMEINUSE, RPL_WELCOME
from irctest.numerics import (
ERR_ERRONEUSNICKNAME,
ERR_NICKNAMEINUSE,
RPL_HELLO,
RPL_WELCOME,
)
from irctest.patma import ANYDICT
@ -111,8 +114,7 @@ class RegressionsTestCase(cases.BaseServerTestCase):
self.sendLine(1, "NICK *")
self.sendLine(1, "USER u s e r")
replies = {"NOTICE"}
time.sleep(2) # give time to slow servers, like irc2 to reply
while replies == {"NOTICE"}:
while replies <= {"NOTICE", RPL_HELLO}:
replies = set(msg.command for msg in self.getMessages(1, synchronize=False))
self.assertIn(ERR_ERRONEUSNICKNAME, replies)
self.assertNotIn(RPL_WELCOME, replies)

View File

@ -0,0 +1,48 @@
import math
import time
from irctest import cases
from irctest.numerics import RPL_TIME
from irctest.patma import ANYSTR, StrRe
class TimeTestCase(cases.BaseServerTestCase):
def testTime(self):
self.connectClient("user")
time_before = math.floor(time.time())
self.sendLine(1, "TIME")
msg = self.getMessage(1)
time_after = math.ceil(time.time())
if len(msg.params) == 5:
# ircu2, snircd
self.assertMessageMatch(
msg,
command=RPL_TIME,
params=["user", "My.Little.Server", StrRe("[0-9]+"), "0", ANYSTR],
)
self.assertIn(
int(msg.params[2]),
range(time_before, time_after + 1),
"Timestamp not in expected range",
)
elif len(msg.params) == 4:
# bahamut
self.assertMessageMatch(
msg,
command=RPL_TIME,
params=["user", "My.Little.Server", StrRe("[0-9]+"), ANYSTR],
)
self.assertIn(
int(msg.params[2]),
range(time_before, time_after + 1),
"Timestamp not in expected range",
)
else:
# Common case
self.assertMessageMatch(
msg, command=RPL_TIME, params=["user", "My.Little.Server", ANYSTR]
)

View File

@ -503,3 +503,34 @@ class WhoServicesTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
command=RPL_ENDOFWHO,
params=["otherNick", InsensitiveStr("coolNick"), ANYSTR],
)
class WhoInvisibleTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("Modern")
def testWhoInvisible(self):
if self.controller.software_name == "Bahamut":
raise runner.OptionalExtensionNotSupported("WHO mask")
self.connectClient("evan", name="evan")
self.sendLine("evan", "MODE evan +i")
self.getMessages("evan")
self.connectClient("shivaram", name="shivaram")
self.getMessages("shivaram")
self.sendLine("shivaram", "WHO eva*")
reply_cmds = {msg.command for msg in self.getMessages("shivaram")}
self.assertEqual(reply_cmds, {RPL_ENDOFWHO})
# invisibility should not be respected for plain nicknames, only for masks:
self.sendLine("shivaram", "WHO evan")
replies = self.getMessages("shivaram")
reply_cmds = {msg.command for msg in replies}
self.assertEqual(reply_cmds, {RPL_WHOREPLY, RPL_ENDOFWHO})
# invisibility should not be respected if the users share a channel
self.joinChannel("evan", "#test")
self.joinChannel("shivaram", "#test")
self.sendLine("shivaram", "WHO eva*")
replies = self.getMessages("shivaram")
reply_cmds = {msg.command for msg in replies}
self.assertEqual(reply_cmds, {RPL_WHOREPLY, RPL_ENDOFWHO})

View File

@ -144,11 +144,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
downloads = []
install_steps = []
for software_id in test_config.get("software", []):
if software_id == "anope":
# TODO: don't hardcode anope here
software_config = {"separate_build_job": True}
else:
software_config = config["software"][software_id]
software_config = config["software"][software_id]
env += test_config.get("env", {}).get(version_flavor.value, "") + " "
if "prefix" in software_config:
@ -245,47 +241,6 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
}
def get_build_job_anope():
return {
"runs-on": "ubuntu-latest",
"steps": [
{"uses": "actions/checkout@v2"},
{
"name": "Create directories",
"run": "cd ~/; mkdir -p .local/ go/",
},
{
"name": "Cache Anope",
"uses": "actions/cache@v2",
"with": {
"path": "~/.cache\n${{ github.workspace }}/anope\n",
"key": "3-${{ runner.os }}-anope-2.0.9",
},
},
{
"name": "Checkout Anope",
"uses": "actions/checkout@v2",
"with": {
"repository": "anope/anope",
"ref": "2.0.9",
"path": "anope",
},
},
{
"name": "Build Anope",
"run": script(
"cd $GITHUB_WORKSPACE/anope/",
"cp $GITHUB_WORKSPACE/data/anope/* .",
"CFLAGS=-O0 ./Config -quick",
"make -C build -j 4",
"make -C build install",
),
},
*upload_steps("anope"),
],
}
def upload_steps(software_id):
"""Make a tarball (to preserve permissions) and upload"""
return [
@ -326,7 +281,6 @@ def generate_workflow(config: dict, version_flavor: VersionFlavor):
}
jobs = {}
jobs["build-anope"] = get_build_job_anope()
for software_id in config["software"]:
software_config = config["software"][software_id]

View File

@ -0,0 +1,15 @@
Lower Bahamut's delay between processing incoming commands
diff --git a/src/s_bsd.c b/src/s_bsd.c
index fcc1d02..951fd8c 100644
--- a/src/s_bsd.c
+++ b/src/s_bsd.c
@@ -1458,7 +1458,7 @@ int do_client_queue(aClient *cptr)
int dolen = 0, done;
while (SBufLength(&cptr->recvQ) && !NoNewLine(cptr) &&
- ((cptr->status < STAT_UNKNOWN) || (cptr->since - timeofday < 10) ||
+ ((cptr->status < STAT_UNKNOWN) || (cptr->since - timeofday < 20) ||
IsNegoServer(cptr)))
{
/* If it's become registered as a server, just parse the whole block */

View File

@ -105,6 +105,7 @@ software:
build_script: |
cd $GITHUB_WORKSPACE/Bahamut/
patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch
patch src/s_bsd.c < $GITHUB_WORKSPACE/patches/bahamut_mainloop.patch
echo "#undef THROTTLE_ENABLE" >> include/config.h
libtoolize --force
aclocal
@ -130,7 +131,7 @@ software:
pre_deps:
- uses: actions/setup-go@v2
with:
go-version: '^1.18.0'
go-version: '^1.19.0'
- run: go version
separate_build_job: false
build_script: |
@ -300,6 +301,28 @@ software:
separate_build_job: true
build_script: *unrealircd_build_script
#############################
# Services:
anope:
name: Anope
repository: anope/anope
separate_build_job: true
path: anope
refs:
stable: "2.0.9"
release: "2.0.9"
devel: "2.0.9"
devel_release: "2.0.9"
build_script: |
cd $GITHUB_WORKSPACE/anope/
cp $GITHUB_WORKSPACE/data/anope/* .
CFLAGS=-O0 ./Config -quick
make -C build -j 4
make -C build install
#############################
# Clients: