Add Sable (#229)

* [WIP] Add support for Sable

* tweak sable controller

* echo_message: Add missing synchronization for Sable

* update sable

* whois: Simplify test

* WHO: Remove test for oper flag from testWhoChan

So it won't fail on Sable, which hides oper status

* WHO: Skip/xfail tests for Sable as needed

* Skip NakWhole when multi-prefix is not supported

* [WIP] Run Sable on CI

* working-directory is not setable on actions

* this isn't ergo

* this really isn't ergo

* minimize rust install and cache cargo deps

* Need to specify packages to install...

* Phony target

* Give up on 'cargo install', it seems to ignore the cache

* try again to cache the target dir

* This isn't Solanum

* Comment out BaseServicesController

* Parallelize Sable tests

* target is relative...

* sigh

* Fix prefix

* Re-add the other software

* chathistory: Test TOPIC is not sent unless event-playable is enabled

* sable: Dynamically generate certificates

This allows using custom server/services names

* sable: Enable services

* sable: Add support for account registration

Sable doesn't support REGISTER via NickServ

* sable: Lower log verbosity

* Fix lint

* Re-add Sable to CI

* Fix/skip tests on Sable

* Kill sable_services' subprocesses

* Bump Sable to include the labeled-response fix

* Bump Sable to the channel-rename downgrade fix
This commit is contained in:
2023-09-21 09:18:23 +02:00
committed by GitHub
parent e1ff9fd7fe
commit 805635c839
17 changed files with 768 additions and 69 deletions

View File

@ -402,6 +402,7 @@ jobs:
- test-ngircd-anope
- test-ngircd-atheme
- test-plexus4
- test-sable
- test-solanum
- test-sopel
- test-thelounge
@ -570,7 +571,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/go/sbin:~/go/bin:$PATH
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/go/sbin:~/go/bin:~/go:$PATH
make ergo
timeout-minutes: 30
- if: always()
@ -642,7 +643,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH
make inspircd
timeout-minutes: 30
- if: always()
@ -681,7 +682,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH make
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH make
inspircd-anope
timeout-minutes: 30
- if: always()
@ -819,7 +820,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:~/.local/:$PATH
make ngircd
timeout-minutes: 30
- if: always()
@ -858,7 +859,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH make
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:~/.local/:$PATH make
ngircd-anope
timeout-minutes: 30
- if: always()
@ -891,7 +892,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:~/.local/:$PATH
make ngircd-atheme
timeout-minutes: 30
- if: always()
@ -939,6 +940,53 @@ jobs:
with:
name: pytest-results_plexus4_devel
path: pytest.xml
test-sable:
needs: []
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: 3.11
- name: Checkout Sable
uses: actions/checkout@v3
with:
path: sable
ref: master
repository: Libera-Chat/sable
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
override: true
profile: minimal
toolchain: nightly
- name: Enable Cargo cache
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
workspaces: sable -> target
- run: rustc --version
- name: Build Sable
run: |
cd $GITHUB_WORKSPACE/sable/
cargo build
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=$GITHUB_WORKSPACE/sable/target/debug/sbin:$GITHUB_WORKSPACE/sable/target/debug/bin:$GITHUB_WORKSPACE/sable/target/debug:$PATH
make sable
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
with:
name: pytest-results_sable_devel
path: pytest.xml
test-solanum:
needs:
- build-solanum
@ -1061,7 +1109,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:~/.local/unrealircd:$PATH
make unrealircd
timeout-minutes: 30
- if: always()
@ -1094,7 +1142,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:~/.local/unrealircd:$PATH
make unrealircd-5
timeout-minutes: 30
- if: always()
@ -1133,7 +1181,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH make
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:~/.local/unrealircd:$PATH make
unrealircd-anope
timeout-minutes: 30
- if: always()
@ -1166,7 +1214,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:~/.local/unrealircd:$PATH
make unrealircd-atheme
timeout-minutes: 30
- if: always()
@ -1210,7 +1258,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:~/.local/unrealircd:$PATH
IRCTEST_DLK_PATH="${{ github.workspace }}/Dlk-Services" IRCTEST_WP_CLI_PATH="${{
github.workspace }}/wp-cli.phar" IRCTEST_WP_ZIP_PATH="${{ github.workspace
}}/wordpress-latest.zip" make unrealircd-dlk

View File

@ -132,7 +132,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH
make inspircd
timeout-minutes: 30
- if: always()
@ -171,7 +171,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH make
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH make
inspircd-anope
timeout-minutes: 30
- if: always()
@ -204,7 +204,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH
make inspircd-atheme
timeout-minutes: 30
- if: always()

View File

@ -446,6 +446,7 @@ jobs:
- test-ngircd-anope
- test-ngircd-atheme
- test-plexus4
- test-sable
- test-solanum
- test-sopel
- test-thelounge
@ -646,7 +647,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/go/sbin:~/go/bin:$PATH
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/go/sbin:~/go/bin:~/go:$PATH
make ergo
timeout-minutes: 30
- if: always()
@ -718,7 +719,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH
make inspircd
timeout-minutes: 30
- if: always()
@ -757,7 +758,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH make
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH make
inspircd-anope
timeout-minutes: 30
- if: always()
@ -790,7 +791,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH
make inspircd-atheme
timeout-minutes: 30
- if: always()
@ -977,7 +978,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:~/.local/:$PATH
make ngircd
timeout-minutes: 30
- if: always()
@ -1016,7 +1017,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH make
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:~/.local/:$PATH make
ngircd-anope
timeout-minutes: 30
- if: always()
@ -1049,7 +1050,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:~/.local/:$PATH
make ngircd-atheme
timeout-minutes: 30
- if: always()
@ -1097,6 +1098,53 @@ jobs:
with:
name: pytest-results_plexus4_stable
path: pytest.xml
test-sable:
needs: []
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: 3.11
- name: Checkout Sable
uses: actions/checkout@v3
with:
path: sable
ref: ff1179512a79eba57ca468a5f83af84ecce08a5b
repository: Libera-Chat/sable
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
override: true
profile: minimal
toolchain: nightly
- name: Enable Cargo cache
uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true
workspaces: sable -> target
- run: rustc --version
- name: Build Sable
run: |
cd $GITHUB_WORKSPACE/sable/
cargo build
- name: Install system dependencies
run: sudo apt-get install atheme-services faketime
- name: Install irctest dependencies
run: |-
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=$GITHUB_WORKSPACE/sable/target/debug/sbin:$GITHUB_WORKSPACE/sable/target/debug/bin:$GITHUB_WORKSPACE/sable/target/debug:$PATH
make sable
timeout-minutes: 30
- if: always()
name: Publish results
uses: actions/upload-artifact@v3
with:
name: pytest-results_sable_stable
path: pytest.xml
test-solanum:
needs:
- build-solanum
@ -1219,7 +1267,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:~/.local/unrealircd:$PATH
make unrealircd
timeout-minutes: 30
- if: always()
@ -1252,7 +1300,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:~/.local/unrealircd:$PATH
make unrealircd-5
timeout-minutes: 30
- if: always()
@ -1291,7 +1339,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH make
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:~/.local/unrealircd:$PATH make
unrealircd-anope
timeout-minutes: 30
- if: always()
@ -1324,7 +1372,7 @@ jobs:
python -m pip install --upgrade pip
pip install pytest pytest-xdist -r requirements.txt
- name: Test with pytest
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH
run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:~/.local/unrealircd:$PATH
make unrealircd-atheme
timeout-minutes: 30
- if: always()

View File

@ -87,6 +87,13 @@ LIMNORIA_SELECTORS := \
(foo or not foo) \
$(EXTRA_SELECTORS)
SABLE_SELECTORS := \
not Ergo \
and not deprecated \
and not strict \
and not whowas and not list and not lusers and not userhost and not time and not info \
$(EXTRA_SELECTORS)
SOLANUM_SELECTORS := \
not Ergo \
and not deprecated \
@ -118,9 +125,9 @@ UNREALIRCD_SELECTORS := \
and not private_chathistory \
$(EXTRA_SELECTORS)
.PHONY: all flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sopel solanum unrealircd
.PHONY: all flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sable sopel solanum unrealircd
all: flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sopel solanum unrealircd
all: flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sable sopel solanum unrealircd
flakes:
find irctest/ -name "*.py" -not -path "irctest/scram/*" -print0 | xargs -0 pyflakes3
@ -249,6 +256,13 @@ ngircd-atheme:
-m 'services' \
-k "$(NGIRCD_SELECTORS)"
sable:
$(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.sable \
-n 20 \
-m 'not services' \
-k '$(SABLE_SELECTORS)'
solanum:
$(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.solanum \

View File

@ -68,6 +68,7 @@ class _BaseController:
supports_sts: bool
supported_sasl_mechanisms: Set[str]
proc: Optional[subprocess.Popen]
_used_ports: Set[Tuple[str, int]]
@ -248,6 +249,12 @@ class BaseServerController(_BaseController):
extban_mute_char: Optional[str] = None
"""Character used for the 'mute' extban"""
nickserv = "NickServ"
sync_sleep_time = 0.0
"""How many seconds to sleep before clients synchronously get messages.
This can be 0 for servers answering all commands in order (all but Sable as of
this writing), as irctest emits a PING, waits for a PONG, and captures all messages
between the two."""
def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
@ -350,6 +357,7 @@ class BaseServicesController(_BaseController):
c.connect(self.server_controller.hostname, self.server_controller.port)
c.sendLine("NICK chkNS")
c.sendLine("USER chk chk chk chk")
time.sleep(self.server_controller.sync_sleep_time)
for msg in c.getMessages(synchronize=False):
if msg.command == "PING":
# Hi Unreal

View File

@ -585,9 +585,13 @@ class BaseServerTestCase(
del self.clients[name]
def getMessages(self, client: TClientName, **kwargs: Any) -> List[Message]:
if kwargs.get("synchronize", True):
time.sleep(self.controller.sync_sleep_time)
return self.clients[client].getMessages(**kwargs)
def getMessage(self, client: TClientName, **kwargs: Any) -> Message:
if kwargs.get("synchronize", True):
time.sleep(self.controller.sync_sleep_time)
return self.clients[client].getMessage(**kwargs)
def getRegistrationMessage(self, client: TClientName) -> Message:

View File

@ -211,9 +211,6 @@ class ErgoController(BaseServerController, DirectoryBasedController):
username: str,
password: Optional[str] = None,
) -> None:
# XXX: Move this somewhere else when
# https://github.com/ircv3/ircv3-specifications/pull/152 becomes
# part of the specification
if not case.run_services:
# Ergo does not actually need this, but other controllers do, so we
# are checking it here as well for tests that aren't tested with other

View File

@ -0,0 +1,481 @@
import os
from pathlib import Path
import shutil
import signal
import subprocess
import tempfile
import time
from typing import Optional, Type
from irctest.basecontrollers import (
BaseServerController,
BaseServicesController,
DirectoryBasedController,
NotImplementedByController,
)
from irctest.cases import BaseServerTestCase
from irctest.exceptions import NoMessageException
from irctest.patma import ANYSTR
GEN_CERTS = """
mkdir -p useless_openssl_data/
cat > openssl.cnf <<EOF
[ ca ]
default_ca = CA_default # The default ca section
[ CA_default ]
new_certs_dir = useless_openssl_data/
database = useless_openssl_data/db
policy = policy_anything
serial = useless_openssl_data/serial
copy_extensions = copy
email_in_dn = no
rand_serial = no
[ policy_anything ]
countryName = optional
stateOrProvinceName = optional
localityName = optional
organizationName = optional
organizationalUnitName = optional
commonName = supplied
emailAddress = optional
[ usr_cert ]
subjectAltName=subject:copy
EOF
rm -f useless_openssl_data/db
touch useless_openssl_data/db
echo 01 > useless_openssl_data/serial
# Generate CA
openssl req -x509 -nodes -newkey rsa:2048 -batch \
-subj "/CN=Test CA" \
-outform PEM -out ca_cert.pem \
-keyout ca_cert.key
for server in $*; do
openssl genrsa -traditional \
-out $server.key \
2048
openssl req -nodes -batch -new \
-addext "subjectAltName = DNS:$server" \
-key $server.key \
-outform PEM -out server_$server.req
openssl ca -config openssl.cnf -days 3650 -md sha512 -batch \
-subj /CN=$server \
-keyfile ca_cert.key -cert ca_cert.pem \
-in server_$server.req \
-out $server.pem
openssl x509 -sha1 -in $server.pem -fingerprint -noout \
| sed "s/.*=//" | sed "s/://g" | tr '[:upper:]' '[:lower:]' > $server.pem.sha1
done
rm -r useless_openssl_data/
"""
_certs_dir = None
def certs_dir() -> Path:
global _certs_dir
if _certs_dir is None:
certs_dir = tempfile.TemporaryDirectory()
(Path(certs_dir.name) / "gen_certs.sh").write_text(GEN_CERTS)
subprocess.run(
["bash", "gen_certs.sh", "My.Little.Server", "My.Little.Services"],
cwd=certs_dir.name,
check=True,
)
_certs_dir = certs_dir
return Path(_certs_dir.name)
NETWORK_CONFIG = """
{
"fanout": 1,
"ca_file": "%(certs_dir)s/ca_cert.pem",
"peers": [
{ "name": "My.Little.Services", "address": "%(services_hostname)s:%(services_port)s", "fingerprint": "%(services_cert_sha1)s" },
{ "name": "My.Little.Server", "address": "%(server1_hostname)s:%(server1_port)s", "fingerprint": "%(server1_cert_sha1)s" }
]
}
"""
NETWORK_CONFIG_CONFIG = """
{
"opers": [
{
"name": "operuser",
// echo -n "operpassword" | openssl passwd -6 -stdin
"hash": "$6$z5yA.OfGliDoi/R2$BgSsguS6bxAsPSCygDisgDw5JZuo5.88eU3Hyc7/4OaNpeKIxWGjOggeHzOl0xLiZg1vfwxXjOTFN14wG5vNI."
}
],
"alias_users": [
{
"nick": "ChanServ",
"user": "ChanServ",
"host": "services.",
"realname": "Channel services compatibility layer",
"command_alias": "CS"
},
{
"nick": "NickServ",
"user": "NickServ",
"host": "services.",
"realname": "Account services compatibility layer",
"command_alias": "NS"
}
],
"default_roles": {
"builtin:op": [
"always_send",
"op_self", "op_grant", "voice_self", "voice_grant",
"receive_op", "receive_voice", "receive_opmod",
"topic", "kick", "set_simple_mode", "set_key",
"rename",
"ban_view", "ban_add", "ban_remove_any",
"quiet_view", "quiet_add", "quiet_remove_any",
"exempt_view", "exempt_add", "exempt_remove_any",
"invite_self", "invite_other",
"invex_view", "invex_add", "invex_remove_any"
],
"builtin:voice": [
"always_send",
"voice_self",
"receive_voice",
"ban_view", "quiet_view"
],
"builtin:all": [
"ban_view", "quiet_view"
]
},
"debug_mode": true
}
"""
SERVER_CONFIG = """
{
"server_id": 1,
"server_name": "My.Little.Server",
"management": {
"address": "%(server1_management_hostname)s:%(server1_management_port)s",
"client_ca": "%(certs_dir)s/ca_cert.pem",
"authorised_fingerprints": [
{ "name": "user1", "fingerprint": "435bc6db9f22e84ba5d9652432154617c9509370" },
],
},
"server": {
"listeners": [
{ "address": "%(c2s_hostname)s:%(c2s_port)s" },
],
},
"event_log": {
"event_expiry": 300, // five minutes, for local testing
},
"tls_config": {
"key_file": "%(certs_dir)s/My.Little.Server.key",
"cert_file": "%(certs_dir)s/My.Little.Server.pem",
},
"node_config": {
"listen_addr": "%(server1_hostname)s:%(server1_port)s",
"cert_file": "%(certs_dir)s/My.Little.Server.pem",
"key_file": "%(certs_dir)s/My.Little.Server.key",
},
"log": {
"dir": "log/server1/",
"module-levels": {
"": "debug",
"sable_ircd": "trace",
},
"targets": [
{
"target": "stdout",
"level": "trace",
"modules": [ "sable", "audit", "client_listener" ],
},
],
},
}
"""
SERVICES_CONFIG = """
{
"server_id": 99,
"server_name": "My.Little.Services",
"management": {
"address": "%(services_management_hostname)s:%(services_management_port)s",
"client_ca": "%(certs_dir)s/ca_cert.pem",
"authorised_fingerprints": [
{ "name": "user1", "fingerprint": "435bc6db9f22e84ba5d9652432154617c9509370" }
]
},
"server": {
"database": "test_database.json",
"default_roles": {
"builtin:founder": [
"founder", "access_view", "access_edit", "role_view", "role_edit",
"op_self", "op_grant",
"voice_self", "voice_grant",
"always_send",
"invite_self", "invite_other",
"receive_op", "receive_voice", "receive_opmod",
"topic", "kick", "set_simple_mode", "set_key",
"rename",
"ban_view", "ban_add", "ban_remove_any",
"quiet_view", "quiet_add", "quiet_remove_any",
"exempt_view", "exempt_add", "exempt_remove_any",
"invex_view", "invex_add", "invex_remove_any"
],
"builtin:op": [
"always_send",
"receive_op", "receive_voice", "receive_opmod",
"topic", "kick", "set_simple_mode", "set_key",
"rename",
"ban_view", "ban_add", "ban_remove_any",
"quiet_view", "quiet_add", "quiet_remove_any",
"exempt_view", "exempt_add", "exempt_remove_any",
"invex_view", "invex_add", "invex_remove_any"
],
"builtin:voice": [
"always_send", "voice_self", "receive_voice"
]
}
},
"event_log": {
"event_expiry": 300, // five minutes, for local testing
},
"tls_config": {
"key_file": "%(certs_dir)s/My.Little.Services.key",
"cert_file": "%(certs_dir)s/My.Little.Services.pem"
},
"node_config": {
"listen_addr": "%(services_hostname)s:%(services_port)s",
"cert_file": "%(certs_dir)s/My.Little.Services.pem",
"key_file": "%(certs_dir)s/My.Little.Services.key"
},
"log": {
"dir": "log/services/",
"module-levels": {
"": "debug"
},
"targets": [
{
"target": "stdout",
"level": "debug",
"modules": [ "sable_services" ]
}
]
}
}
"""
class SableController(BaseServerController, DirectoryBasedController):
software_name = "Sable"
supported_sasl_mechanisms = {"PLAIN"}
sync_sleep_time = 0.1
"""Sable processes commands very quickly, but responses for commands changing the
state may be sent after later commands for messages which don't."""
def run(
self,
hostname: str,
port: int,
*,
password: Optional[str],
ssl: bool,
run_services: bool,
faketime: Optional[str],
) -> None:
if password is not None:
raise NotImplementedByController("PASS command")
if ssl:
raise NotImplementedByController("SSL")
assert self.proc is None
self.port = port
self.create_config()
assert self.directory
(self.directory / "configs").mkdir()
c2s_hostname = hostname
c2s_port = port
del hostname, port
# base controller expects this to check for NickServ presence itself
self.hostname = c2s_hostname
self.port = c2s_port
(server1_hostname, server1_port) = self.get_hostname_and_port()
(services_hostname, services_port) = self.get_hostname_and_port()
# Sable requires inbound connections to match the configured hostname,
# so we can't configure 0.0.0.0
server1_hostname = services_hostname = "127.0.0.1"
(
server1_management_hostname,
server1_management_port,
) = self.get_hostname_and_port()
(
services_management_hostname,
services_management_port,
) = self.get_hostname_and_port()
self.template_vars = dict(
certs_dir=certs_dir(),
c2s_hostname=c2s_hostname,
c2s_port=c2s_port,
server1_hostname=server1_hostname,
server1_port=server1_port,
server1_cert_sha1=(certs_dir() / "My.Little.Server.pem.sha1")
.read_text()
.strip(),
server1_management_hostname=server1_management_hostname,
server1_management_port=server1_management_port,
services_hostname=services_hostname,
services_port=services_port,
services_cert_sha1=(certs_dir() / "My.Little.Services.pem.sha1")
.read_text()
.strip(),
services_management_hostname=services_management_hostname,
services_management_port=services_management_port,
)
with self.open_file("configs/network.conf") as fd:
fd.write(NETWORK_CONFIG % self.template_vars)
with self.open_file("configs/network_config.conf") as fd:
fd.write(NETWORK_CONFIG_CONFIG % self.template_vars)
with self.open_file("configs/server1.conf") as fd:
fd.write(SERVER_CONFIG % self.template_vars)
if faketime and shutil.which("faketime"):
faketime_cmd = ["faketime", "-f", faketime]
self.faketime_enabled = True
else:
faketime_cmd = []
self.proc = subprocess.Popen(
[
*faketime_cmd,
"sable_ircd",
"--foreground",
"--server-conf",
self.directory / "configs/server1.conf",
"--network-conf",
self.directory / "configs/network.conf",
"--bootstrap-network",
self.directory / "configs/network_config.conf",
],
cwd=self.directory,
preexec_fn=os.setsid,
)
self.pgroup_id = os.getpgid(self.proc.pid)
if run_services:
self.services_controller = SableServicesController(self.test_config, self)
self.services_controller.run(
protocol="sable",
server_hostname=services_hostname,
server_port=services_port,
)
def kill_proc(self) -> None:
os.killpg(self.pgroup_id, signal.SIGKILL)
super().kill_proc()
def registerUser(
self,
case: BaseServerTestCase, # type: ignore
username: str,
password: Optional[str] = None,
) -> None:
# XXX: Move this somewhere else when
# https://github.com/ircv3/ircv3-specifications/pull/152 becomes
# part of the specification
if not case.run_services:
raise ValueError(
"Attempted to register a nick, but `run_services` it not True."
)
assert password
client = case.addClient(show_io=True)
case.sendLine(client, "NICK " + username)
case.sendLine(client, "USER r e g :user")
while case.getRegistrationMessage(client).command != "001":
pass
case.getMessages(client)
case.sendLine(
client,
f"REGISTER * * {password}",
)
for _ in range(100):
time.sleep(0.1)
try:
msg = case.getMessage(client)
except NoMessageException:
continue
case.assertMessageMatch(
msg, command="REGISTER", params=["SUCCESS", username, ANYSTR]
)
break
else:
raise NoMessageException()
case.sendLine(client, "QUIT")
case.assertDisconnected(client)
class SableServicesController(BaseServicesController):
server_controller: SableController
software_name = "Sable Services"
def run(self, protocol: str, server_hostname: str, server_port: int) -> None:
assert protocol == "sable"
assert self.server_controller.directory is not None
with self.server_controller.open_file("configs/services.conf") as fd:
fd.write(SERVICES_CONFIG % self.server_controller.template_vars)
self.proc = subprocess.Popen(
[
"sable_services",
"--foreground",
"--server-conf",
self.server_controller.directory / "configs/services.conf",
"--network-conf",
self.server_controller.directory / "configs/network.conf",
],
cwd=self.server_controller.directory,
preexec_fn=os.setsid,
)
self.pgroup_id = os.getpgid(self.proc.pid)
def kill_proc(self) -> None:
os.killpg(self.pgroup_id, signal.SIGKILL)
super().kill_proc()
def get_irctest_controller_class() -> Type[SableController]:
return SableController

View File

@ -13,7 +13,7 @@ def ircv3_timestamp_to_unixtime(timestamp: str) -> float:
def random_name(base: str) -> str:
return base + "-" + secrets.token_hex(8)
return base + "-" + secrets.token_hex(5)
def find_hostname_and_port() -> Tuple[str, int]:

View File

@ -56,6 +56,10 @@ class CapTestCase(cases.BaseServerTestCase):
)
@cases.mark_specifications("IRCv3")
@cases.xfailIfSoftware(
["Sable"],
"does not support multi-prefix",
)
def testReqOne(self):
"""Tests requesting a single capability"""
self.addClient(1)
@ -89,8 +93,8 @@ class CapTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("IRCv3")
@cases.xfailIfSoftware(
["ngIRCd"],
"ngIRCd does not support userhost-in-names",
["ngIRCd", "Sable"],
"does not support userhost-in-names",
)
def testReqTwo(self):
"""Tests requesting two capabilities at once"""
@ -131,8 +135,8 @@ class CapTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("IRCv3")
@cases.xfailIfSoftware(
["ngIRCd"],
"ngIRCd does not support userhost-in-names",
["ngIRCd", "Sable"],
"does not support userhost-in-names",
)
def testReqOneThenOne(self):
"""Tests requesting two capabilities in different messages"""
@ -183,8 +187,8 @@ class CapTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("IRCv3")
@cases.xfailIfSoftware(
["ngIRCd"],
"ngIRCd does not support userhost-in-names",
["ngIRCd", "Sable"],
"does not support userhost-in-names",
)
def testReqPostRegistration(self):
"""Tests requesting more capabilities after CAP END"""
@ -300,7 +304,8 @@ class CapTestCase(cases.BaseServerTestCase):
""" # noqa
self.addClient(1)
self.sendLine(1, "CAP LS 302")
self.assertIn("multi-prefix", self.getCapLs(1))
if "multi-prefix" not in self.getCapLs(1):
raise CapabilityNotSupported("multi-prefix")
self.sendLine(1, "CAP REQ :foo multi-prefix bar")
m = self.getRegistrationMessage(1)
self.assertMessageMatch(

View File

@ -46,7 +46,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
result = []
for msg in inner_msgs:
if (
msg.command == "PRIVMSG"
msg.command in ("PRIVMSG", "TOPIC")
and batch_tag is not None
and msg.tags.get("batch") == batch_tag
):
@ -220,6 +220,47 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
self.validate_echo_messages(NUM_MESSAGES, echo_messages)
self.validate_chathistory(subcommand, echo_messages, 1, chname)
@skip_ngircd
def testChathistoryNoEventPlayback(self):
"""Tests that non-messages don't appear in the chat history when event-playback
is not enabled."""
self.connectClient(
"bar",
capabilities=[
"message-tags",
"server-time",
"echo-message",
"batch",
"labeled-response",
"sasl",
CHATHISTORY_CAP,
],
skip_if_cap_nak=True,
)
chname = "#chan" + secrets.token_hex(12)
self.joinChannel(1, chname)
self.getMessages(1)
self.getMessages(1)
NUM_MESSAGES = 10
echo_messages = []
for i in range(NUM_MESSAGES):
self.sendLine(1, "TOPIC %s :this is topic %d" % (chname, i))
self.getMessages(1)
self.sendLine(1, "PRIVMSG %s :this is message %d" % (chname, i))
echo_messages.extend(
msg.to_history_message() for msg in self.getMessages(1)
)
time.sleep(0.002)
self.validate_echo_messages(NUM_MESSAGES, echo_messages)
self.sendLine(1, "CHATHISTORY LATEST %s * 100" % chname)
(batch_open, *messages, batch_close) = self.getMessages(1)
self.assertMessageMatch(batch_open, command="BATCH")
self.assertMessageMatch(batch_close, command="BATCH")
self.assertEqual([msg for msg in messages if msg.command != "PRIVMSG"], [])
@pytest.mark.parametrize("subcommand", SUBCOMMANDS)
@skip_ngircd
def testChathistoryEventPlayback(self, subcommand):
@ -244,21 +285,27 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
NUM_MESSAGES = 10
echo_messages = []
for i in range(NUM_MESSAGES):
self.sendLine(1, "TOPIC %s :this is topic %d" % (chname, i))
echo_messages.extend(
msg.to_history_message() for msg in self.getMessages(1)
)
time.sleep(0.002)
self.sendLine(1, "PRIVMSG %s :this is message %d" % (chname, i))
echo_messages.extend(
msg.to_history_message() for msg in self.getMessages(1)
)
time.sleep(0.002)
self.validate_echo_messages(NUM_MESSAGES, echo_messages)
self.validate_echo_messages(NUM_MESSAGES * 2, echo_messages)
self.validate_chathistory(subcommand, echo_messages, 1, chname)
@pytest.mark.parametrize("subcommand", SUBCOMMANDS)
@pytest.mark.private_chathistory
@skip_ngircd
def testChathistoryDMs(self, subcommand):
c1 = "foo" + secrets.token_hex(12)
c2 = "bar" + secrets.token_hex(12)
c1 = random_name("foo")
c2 = random_name("bar")
self.controller.registerUser(self, c1, "sesame1")
self.controller.registerUser(self, c2, "sesame2")
self.connectClient(
@ -313,7 +360,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
self.validate_chathistory(subcommand, echo_messages, 1, c2)
self.validate_chathistory(subcommand, echo_messages, 2, c1)
c3 = "baz" + secrets.token_hex(12)
c3 = random_name("baz")
self.connectClient(
c3,
capabilities=[
@ -583,8 +630,8 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
@pytest.mark.arbitrary_client_tags
@skip_ngircd
def testChathistoryTagmsg(self):
c1 = "foo" + secrets.token_hex(12)
c2 = "bar" + secrets.token_hex(12)
c1 = random_name("foo")
c2 = random_name("bar")
chname = "#chan" + secrets.token_hex(12)
self.controller.registerUser(self, c1, "sesame1")
self.controller.registerUser(self, c2, "sesame2")
@ -683,8 +730,8 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
@skip_ngircd
def testChathistoryDMClientOnlyTags(self):
# regression test for Ergo #1411
c1 = "foo" + secrets.token_hex(12)
c2 = "bar" + secrets.token_hex(12)
c1 = random_name("foo")
c2 = random_name("bar")
self.controller.registerUser(self, c1, "sesame1")
self.controller.registerUser(self, c2, "sesame2")
self.connectClient(

View File

@ -32,6 +32,9 @@ class EchoMessageTestCase(cases.BaseServerTestCase):
self.sendLine(1, "JOIN #chan")
# Synchronize
self.getMessages(1)
if not solo:
self.connectClient("qux", capabilities=capabilities)
self.sendLine(2, "JOIN #chan")

View File

@ -13,6 +13,7 @@ class PrivmsgTestCase(cases.BaseServerTestCase):
"""<https://tools.ietf.org/html/rfc2812#section-3.3.1>"""
self.connectClient("foo")
self.sendLine(1, "JOIN #chan")
self.getMessages(1) # synchronize
self.connectClient("bar")
self.sendLine(2, "JOIN #chan")
self.getMessages(2) # synchronize

View File

@ -87,7 +87,7 @@ class BaseWhoTestCase:
class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
@cases.mark_specifications("Modern")
def testWhoStar(self):
if self.controller.software_name == "Bahamut":
if self.controller.software_name in ("Bahamut", "Sable"):
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
@ -118,7 +118,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
)
@cases.mark_specifications("Modern")
def testWhoNick(self, mask):
if "*" in mask and self.controller.software_name == "Bahamut":
if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"):
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
@ -148,7 +148,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
ids=["username", "realname-mask", "hostname"],
)
def testWhoUsernameRealName(self, mask):
if "*" in mask and self.controller.software_name == "Bahamut":
if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"):
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
@ -201,7 +201,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
)
@cases.mark_specifications("Modern")
def testWhoNickAway(self, mask):
if "*" in mask and self.controller.software_name == "Bahamut":
if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"):
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
@ -228,9 +228,14 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
@pytest.mark.parametrize(
"mask", ["coolNick", "coolnick", "coolni*"], ids=["exact", "casefolded", "mask"]
)
@cases.xfailIfSoftware(
["Sable"],
"Sable does not advertise oper status in WHO: "
"https://github.com/Libera-Chat/sable/pull/77",
)
@cases.mark_specifications("Modern")
def testWhoNickOper(self, mask):
if "*" in mask and self.controller.software_name == "Bahamut":
if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"):
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
@ -262,9 +267,14 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
@pytest.mark.parametrize(
"mask", ["coolNick", "coolnick", "coolni*"], ids=["exact", "casefolded", "mask"]
)
@cases.xfailIfSoftware(
["Sable"],
"Sable does not advertise oper status in WHO: "
"https://github.com/Libera-Chat/sable/pull/77",
)
@cases.mark_specifications("Modern")
def testWhoNickAwayAndOper(self, mask):
if "*" in mask and self.controller.software_name == "Bahamut":
if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"):
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
@ -298,18 +308,11 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
@pytest.mark.parametrize("mask", ["#chan", "#CHAN"], ids=["exact", "casefolded"])
@cases.mark_specifications("Modern")
def testWhoChan(self, mask):
if "*" in mask and self.controller.software_name == "Bahamut":
if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"):
raise runner.OptionalExtensionNotSupported("WHO mask")
self._init()
self.sendLine(1, "OPER operuser operpassword")
self.assertIn(
RPL_YOUREOPER,
[m.command for m in self.getMessages(1)],
fail_msg="OPER failed",
)
self.sendLine(1, "AWAY :be right back")
self.getMessages(1)
self.getMessages(2)
@ -335,7 +338,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
StrRe(host_re),
"My.Little.Server",
"coolNick",
"G*@",
"G@",
StrRe(realname_regexp(self.realname)),
],
)
@ -589,7 +592,7 @@ class WhoServicesTestCase(BaseWhoTestCase, cases.BaseServerTestCase):
class WhoInvisibleTestCase(cases.BaseServerTestCase):
@cases.mark_specifications("Modern")
def testWhoInvisible(self):
if self.controller.software_name == "Bahamut":
if self.controller.software_name in ("Bahamut", "Sable"):
raise runner.OptionalExtensionNotSupported("WHO mask")
self.connectClient("evan", name="evan")

View File

@ -195,18 +195,26 @@ class WhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase):
self.connectClient("otherNick")
self.getMessages(2)
self.sendLine(2, f"WHOIS {server} coolnick")
self.sendLine(2, f"WHOIS {server} {nick}")
messages = self.getMessages(2)
whois_user = messages[0]
self.assertEqual(whois_user.command, RPL_WHOISUSER)
self.assertMessageMatch(
whois_user,
command=RPL_WHOISUSER,
# "<client> <nick> <username> <host> * :<realname>"
self.assertEqual(whois_user.params[1], nick)
self.assertIn(whois_user.params[2], ("~" + username, username))
params=[
"otherNick",
nick,
StrRe("~?" + username),
ANYSTR,
ANYSTR,
realname,
],
)
# dumb regression test for oragono/oragono#355:
self.assertNotIn(
whois_user.params[3], [nick, username, "~" + username, realname]
)
self.assertEqual(whois_user.params[5], realname)
@pytest.mark.parametrize(
"away,oper",

View File

@ -151,6 +151,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs):
env += (
f"PATH={software_config['prefix']}/sbin"
f":{software_config['prefix']}/bin"
f":{software_config['prefix']}"
f":$PATH "
)

View File

@ -250,6 +250,34 @@ software:
make -j 4
make install
sable:
name: Sable
repository: Libera-Chat/sable
refs:
stable: ff1179512a79eba57ca468a5f83af84ecce08a5b
release: null
devel: master
devel_release: null
path: sable
prefix: "$GITHUB_WORKSPACE/sable/target/debug"
pre_deps:
- name: Install rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: nightly
profile: minimal
override: true
- name: Enable Cargo cache
uses: Swatinem/rust-cache@v2
with:
workspaces: "sable -> target"
cache-on-failure: true
- run: rustc --version
separate_build_job: false
build_script: |
cd $GITHUB_WORKSPACE/sable/
cargo build
snircd:
name: snircd
repository: quakenet/snircd
@ -454,6 +482,9 @@ tests:
nefarious:
software: [nefarious]
sable:
software: [sable]
# doesn't build because it can't find liblex for some reason
#snircd:
# software: [snircd]