From 805635c839738dd6963182e57c2a31a15e36c0c1 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Thu, 21 Sep 2023 09:18:23 +0200 Subject: [PATCH] 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 --- .github/workflows/test-devel.yml | 70 +++- .github/workflows/test-devel_release.yml | 6 +- .github/workflows/test-stable.yml | 70 +++- Makefile | 18 +- irctest/basecontrollers.py | 8 + irctest/cases.py | 4 + irctest/controllers/ergo.py | 3 - irctest/controllers/sable.py | 481 +++++++++++++++++++++++ irctest/irc_utils/junkdrawer.py | 2 +- irctest/server_tests/cap.py | 19 +- irctest/server_tests/chathistory.py | 65 ++- irctest/server_tests/echo_message.py | 3 + irctest/server_tests/messages.py | 1 + irctest/server_tests/who.py | 35 +- irctest/server_tests/whois.py | 20 +- make_workflows.py | 1 + workflows.yml | 31 ++ 17 files changed, 768 insertions(+), 69 deletions(-) create mode 100644 irctest/controllers/sable.py diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 89574f8..088e93d 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -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 diff --git a/.github/workflows/test-devel_release.yml b/.github/workflows/test-devel_release.yml index 87e85c7..dcff7fc 100644 --- a/.github/workflows/test-devel_release.yml +++ b/.github/workflows/test-devel_release.yml @@ -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() diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 00394b4..b8c5f1d 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -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() diff --git a/Makefile b/Makefile index bd68631..1876c51 100644 --- a/Makefile +++ b/Makefile @@ -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 \ diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index 0652fa2..b70015e 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -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 diff --git a/irctest/cases.py b/irctest/cases.py index 4ddf07f..eea0f8c 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -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: diff --git a/irctest/controllers/ergo.py b/irctest/controllers/ergo.py index 5655e82..7a75c87 100644 --- a/irctest/controllers/ergo.py +++ b/irctest/controllers/ergo.py @@ -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 diff --git a/irctest/controllers/sable.py b/irctest/controllers/sable.py new file mode 100644 index 0000000..cdd07ba --- /dev/null +++ b/irctest/controllers/sable.py @@ -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 < 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 diff --git a/irctest/irc_utils/junkdrawer.py b/irctest/irc_utils/junkdrawer.py index ecbf83f..b34de13 100644 --- a/irctest/irc_utils/junkdrawer.py +++ b/irctest/irc_utils/junkdrawer.py @@ -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]: diff --git a/irctest/server_tests/cap.py b/irctest/server_tests/cap.py index fea4ef0..a1d3dff 100644 --- a/irctest/server_tests/cap.py +++ b/irctest/server_tests/cap.py @@ -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( diff --git a/irctest/server_tests/chathistory.py b/irctest/server_tests/chathistory.py index 873661e..cb88bb1 100644 --- a/irctest/server_tests/chathistory.py +++ b/irctest/server_tests/chathistory.py @@ -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( diff --git a/irctest/server_tests/echo_message.py b/irctest/server_tests/echo_message.py index 2eb7119..cdfcb8c 100644 --- a/irctest/server_tests/echo_message.py +++ b/irctest/server_tests/echo_message.py @@ -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") diff --git a/irctest/server_tests/messages.py b/irctest/server_tests/messages.py index c0f4ec0..520fd13 100644 --- a/irctest/server_tests/messages.py +++ b/irctest/server_tests/messages.py @@ -13,6 +13,7 @@ class PrivmsgTestCase(cases.BaseServerTestCase): """""" self.connectClient("foo") self.sendLine(1, "JOIN #chan") + self.getMessages(1) # synchronize self.connectClient("bar") self.sendLine(2, "JOIN #chan") self.getMessages(2) # synchronize diff --git a/irctest/server_tests/who.py b/irctest/server_tests/who.py index 7813ee6..15bdeb3 100644 --- a/irctest/server_tests/who.py +++ b/irctest/server_tests/who.py @@ -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") diff --git a/irctest/server_tests/whois.py b/irctest/server_tests/whois.py index 2853553..51b7ad9 100644 --- a/irctest/server_tests/whois.py +++ b/irctest/server_tests/whois.py @@ -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.assertEqual(whois_user.params[1], nick) - self.assertIn(whois_user.params[2], ("~" + username, username)) + self.assertMessageMatch( + whois_user, + command=RPL_WHOISUSER, + # " * :" + 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", diff --git a/make_workflows.py b/make_workflows.py index 8c94179..8ffa6be 100644 --- a/make_workflows.py +++ b/make_workflows.py @@ -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 " ) diff --git a/workflows.yml b/workflows.yml index 87db640..b739260 100644 --- a/workflows.yml +++ b/workflows.yml @@ -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]