diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index f73176b..0ba6d98 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -432,8 +432,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -470,8 +470,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -502,8 +502,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -541,8 +541,8 @@ jobs: cd $GITHUB_WORKSPACE/ergo/ make build make install - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -579,8 +579,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -611,8 +611,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -649,8 +649,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -687,8 +687,8 @@ jobs: ./configure --prefix=$HOME/.local/ --with-maxcon=1024 --enable-debug make -j 4 make install - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -714,8 +714,8 @@ jobs: - name: Install dependencies run: pip install git+https://github.com/ProgVal/Limnoria.git@testing cryptography pyxmpp2-scram - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -783,8 +783,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -821,8 +821,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -853,8 +853,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -891,8 +891,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -923,8 +923,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -949,8 +949,8 @@ jobs: python-version: 3.7 - name: Install dependencies run: pip install git+https://github.com/sopel-irc/sopel.git - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -981,8 +981,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -1013,8 +1013,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -1051,8 +1051,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -1083,8 +1083,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip diff --git a/.github/workflows/test-devel_release.yml b/.github/workflows/test-devel_release.yml index 8fa975f..d639825 100644 --- a/.github/workflows/test-devel_release.yml +++ b/.github/workflows/test-devel_release.yml @@ -117,8 +117,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -155,8 +155,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -187,8 +187,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index c4a0468..5936fe6 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -475,8 +475,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -513,8 +513,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -545,8 +545,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -577,8 +577,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -616,8 +616,8 @@ jobs: cd $GITHUB_WORKSPACE/ergo/ make build make install - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -654,8 +654,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -686,8 +686,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -724,8 +724,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -756,8 +756,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -810,8 +810,8 @@ jobs: mkdir -p $HOME/.local/bin cp $HOME/.local/sbin/ircd $HOME/.local/bin/ircd' - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -848,8 +848,8 @@ jobs: ./configure --prefix=$HOME/.local/ --with-maxcon=1024 --enable-debug make -j 4 make install - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -874,8 +874,8 @@ jobs: python-version: 3.7 - name: Install dependencies run: pip install limnoria==2022.03.17 cryptography pyxmpp2-scram - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -943,8 +943,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -981,8 +981,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -1013,8 +1013,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -1051,8 +1051,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -1083,8 +1083,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -1109,8 +1109,8 @@ jobs: python-version: 3.7 - name: Install dependencies run: pip install sopel==7.1.8 - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -1141,8 +1141,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -1173,8 +1173,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -1211,8 +1211,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -1243,8 +1243,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip diff --git a/Makefile b/Makefile index e933806..0c82a97 100644 --- a/Makefile +++ b/Makefile @@ -101,6 +101,7 @@ SOPEL_SELECTORS := \ # Tests marked with arbitrary_client_tags can't pass because Unreal whitelists which tags it relays # Tests marked with react_tag can't pass because Unreal blocks +draft/react https://github.com/unrealircd/unrealircd/pull/149 # Tests marked with private_chathistory can't pass because Unreal does not implement CHATHISTORY for DMs + UNREALIRCD_SELECTORS := \ not Ergo \ and not deprecated \ diff --git a/README.md b/README.md index 7aac195..18edbc5 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ have no side effect. Install irctest and dependencies: ``` +sudo apt install faketime # Optional, but greatly speeds up irctest/server_tests/list.py cd ~ git clone https://github.com/ProgVal/irctest.git cd irctest diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index b1464ac..e8680b3 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -189,6 +189,10 @@ class BaseServerController(_BaseController): """Character used for the 'mute' extban""" nickserv = "NickServ" + def __init__(self, *args: Any, **kwargs: Any): + super().__init__(*args, **kwargs) + self.faketime_enabled = False + def get_hostname_and_port(self) -> Tuple[str, int]: return find_hostname_and_port() @@ -202,6 +206,7 @@ class BaseServerController(_BaseController): run_services: bool, valid_metadata_keys: Optional[Set[str]], invalid_metadata_keys: Optional[Set[str]], + faketime: Optional[str], ) -> None: raise NotImplementedError() diff --git a/irctest/cases.py b/irctest/cases.py index cc15916..9a7056d 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -508,6 +508,12 @@ class BaseServerTestCase( server_support: Optional[Dict[str, Optional[str]]] run_services = False + faketime: Optional[str] = None + """If not None and the controller supports it and libfaketime is available, + runs the server using faketime and this value set as the $FAKETIME env variable. + Tests must check ``self.controller.faketime_enabled`` is True before + relying on this.""" + __new__ = object.__new__ # pytest won't collect Generic[] subclasses otherwise def setUp(self) -> None: @@ -522,6 +528,7 @@ class BaseServerTestCase( invalid_metadata_keys=self.invalid_metadata_keys, ssl=self.ssl, run_services=self.run_services, + faketime=self.faketime, ) self.clients: Dict[TClientName, client_mock.ClientMock] = {} diff --git a/irctest/controllers/bahamut.py b/irctest/controllers/bahamut.py index 01cd4dd..6a23aaa 100644 --- a/irctest/controllers/bahamut.py +++ b/irctest/controllers/bahamut.py @@ -102,6 +102,7 @@ class BahamutController(BaseServerController, DirectoryBasedController): valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, restricted_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str], ) -> None: if valid_metadata_keys or invalid_metadata_keys: raise NotImplementedByController( @@ -136,15 +137,21 @@ class BahamutController(BaseServerController, DirectoryBasedController): # pem_path=self.pem_path, ) ) + + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + self.proc = subprocess.Popen( [ - # "strace", "-f", "-e", "file", + *faketime_cmd, "ircd", "-t", # don't fork "-f", os.path.join(self.directory, "server.conf"), ], - # stdout=subprocess.DEVNULL, ) if run_services: diff --git a/irctest/controllers/base_hybrid.py b/irctest/controllers/base_hybrid.py index f91229a..e2c0418 100644 --- a/irctest/controllers/base_hybrid.py +++ b/irctest/controllers/base_hybrid.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess from typing import Optional, Set @@ -43,6 +44,7 @@ class BaseHybridController(BaseServerController, DirectoryBasedController): run_services: bool, valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str], ) -> None: if valid_metadata_keys or invalid_metadata_keys: raise NotImplementedByController( @@ -73,8 +75,16 @@ class BaseHybridController(BaseServerController, DirectoryBasedController): ) ) assert self.directory + + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + self.proc = subprocess.Popen( [ + *faketime_cmd, self.binary_name, "-foreground", "-configfile", diff --git a/irctest/controllers/ergo.py b/irctest/controllers/ergo.py index d6e80ab..3878e7b 100644 --- a/irctest/controllers/ergo.py +++ b/irctest/controllers/ergo.py @@ -1,6 +1,7 @@ import copy import json import os +import shutil import subprocess from typing import Any, Dict, Optional, Set, Type, Union @@ -155,6 +156,7 @@ class ErgoController(BaseServerController, DirectoryBasedController): valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, restricted_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str], config: Optional[Any] = None, ) -> None: if valid_metadata_keys or invalid_metadata_keys: @@ -202,8 +204,15 @@ class ErgoController(BaseServerController, DirectoryBasedController): self._write_config() subprocess.call(["ergo", "initdb", "--conf", self._config_path, "--quiet"]) subprocess.call(["ergo", "mkcerts", "--conf", self._config_path, "--quiet"]) + + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + self.proc = subprocess.Popen( - ["ergo", "run", "--conf", self._config_path, "--quiet"] + [*faketime_cmd, "ergo", "run", "--conf", self._config_path, "--quiet"] ) def wait_for_services(self) -> None: diff --git a/irctest/controllers/external_server.py b/irctest/controllers/external_server.py index 5ecbae0..e8c822a 100644 --- a/irctest/controllers/external_server.py +++ b/irctest/controllers/external_server.py @@ -42,6 +42,7 @@ class ExternalServerController(BaseServerController): valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, restricted_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str], ) -> None: pass diff --git a/irctest/controllers/inspircd.py b/irctest/controllers/inspircd.py index 402a10b..2f71c1a 100644 --- a/irctest/controllers/inspircd.py +++ b/irctest/controllers/inspircd.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess from typing import Optional, Set, Type @@ -114,6 +115,7 @@ class InspircdController(BaseServerController, DirectoryBasedController): valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, restricted_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str] = None, ) -> None: if valid_metadata_keys or invalid_metadata_keys: raise NotImplementedByController( @@ -147,8 +149,16 @@ class InspircdController(BaseServerController, DirectoryBasedController): ) ) assert self.directory + + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + self.proc = subprocess.Popen( [ + *faketime_cmd, "inspircd", "--nofork", "--config", diff --git a/irctest/controllers/irc2.py b/irctest/controllers/irc2.py index 8006ea6..69a5fb0 100644 --- a/irctest/controllers/irc2.py +++ b/irctest/controllers/irc2.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess from typing import Optional, Set, Type @@ -51,6 +52,7 @@ class Irc2Controller(BaseServerController, DirectoryBasedController): run_services: bool, valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str], ) -> None: if valid_metadata_keys or invalid_metadata_keys: raise NotImplementedByController( @@ -76,8 +78,16 @@ class Irc2Controller(BaseServerController, DirectoryBasedController): pidfile=pidfile, ) ) + + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + self.proc = subprocess.Popen( [ + *faketime_cmd, "ircd", "-s", # no iauth "-p", diff --git a/irctest/controllers/ircu2.py b/irctest/controllers/ircu2.py index 6bf9916..2be36f7 100644 --- a/irctest/controllers/ircu2.py +++ b/irctest/controllers/ircu2.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess from typing import Optional, Set, Type @@ -70,6 +71,7 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController): run_services: bool, valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str], ) -> None: if valid_metadata_keys or invalid_metadata_keys: raise NotImplementedByController( @@ -95,8 +97,16 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController): pidfile=pidfile, ) ) + + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + self.proc = subprocess.Popen( [ + *faketime_cmd, "ircd", "-n", # don't detach "-f", diff --git a/irctest/controllers/mammon.py b/irctest/controllers/mammon.py index c437139..591347e 100644 --- a/irctest/controllers/mammon.py +++ b/irctest/controllers/mammon.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess from typing import Optional, Set, Type @@ -92,6 +93,7 @@ class MammonController(BaseServerController, DirectoryBasedController): valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, restricted_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str], ) -> None: if password is not None: raise NotImplementedByController("PASS command") @@ -113,8 +115,16 @@ class MammonController(BaseServerController, DirectoryBasedController): # with self.open_file('server.yml', 'r') as fd: # print(fd.read()) assert self.directory + + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + self.proc = subprocess.Popen( [ + *faketime_cmd, "mammond", "--nofork", # '--debug', "--config", diff --git a/irctest/controllers/ngircd.py b/irctest/controllers/ngircd.py index e296899..50577dd 100644 --- a/irctest/controllers/ngircd.py +++ b/irctest/controllers/ngircd.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess from typing import Optional, Set, Type @@ -56,6 +57,7 @@ class NgircdController(BaseServerController, DirectoryBasedController): valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, restricted_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str], ) -> None: if valid_metadata_keys or invalid_metadata_keys: raise NotImplementedByController( @@ -81,6 +83,7 @@ class NgircdController(BaseServerController, DirectoryBasedController): fd.write("\n") assert self.directory + with self.open_file("server.conf") as fd: fd.write( TEMPLATE_CONFIG.format( @@ -94,8 +97,16 @@ class NgircdController(BaseServerController, DirectoryBasedController): empty_file=os.path.join(self.directory, "empty.txt"), ) ) + + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + self.proc = subprocess.Popen( [ + *faketime_cmd, "ngircd", "--nodaemon", "--config", diff --git a/irctest/controllers/snircd.py b/irctest/controllers/snircd.py index 7fa9acc..2edb85c 100644 --- a/irctest/controllers/snircd.py +++ b/irctest/controllers/snircd.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess from typing import Optional, Set, Type @@ -69,6 +70,7 @@ class SnircdController(BaseServerController, DirectoryBasedController): run_services: bool, valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str], ) -> None: if valid_metadata_keys or invalid_metadata_keys: raise NotImplementedByController( @@ -94,8 +96,16 @@ class SnircdController(BaseServerController, DirectoryBasedController): pidfile=pidfile, ) ) + + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + self.proc = subprocess.Popen( [ + *faketime_cmd, "ircd", "-n", # don't detach "-f", diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py index 529ed80..a24bcc0 100644 --- a/irctest/controllers/unrealircd.py +++ b/irctest/controllers/unrealircd.py @@ -156,6 +156,7 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, restricted_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str], ) -> None: if valid_metadata_keys or invalid_metadata_keys: raise NotImplementedByController( @@ -192,6 +193,7 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): fd.write("\n") assert self.directory + with self.open_file("unrealircd.conf") as fd: fd.write( TEMPLATE_CONFIG.format( @@ -225,9 +227,16 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): 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 diff --git a/irctest/numerics.py b/irctest/numerics.py index 28006fe..8aafb08 100644 --- a/irctest/numerics.py +++ b/irctest/numerics.py @@ -66,6 +66,7 @@ RPL_WHOISIDLE = "317" RPL_ENDOFWHOIS = "318" RPL_WHOISCHANNELS = "319" RPL_WHOISSPECIAL = "320" +RPL_LISTSTART = "321" RPL_LIST = "322" RPL_LISTEND = "323" RPL_CHANNELMODEIS = "324" diff --git a/irctest/server_tests/list.py b/irctest/server_tests/list.py index 7a902e3..2649076 100644 --- a/irctest/server_tests/list.py +++ b/irctest/server_tests/list.py @@ -3,39 +3,60 @@ The LIST command (`RFC 1459 `__, `RFC 2812 `__, `Modern `__) - -TODO: check with Modern """ +import time + + from irctest import cases +from irctest import cases, runner +from irctest.numerics import RPL_LIST, RPL_LISTEND, RPL_LISTSTART -class ListTestCase(cases.BaseServerTestCase): + +class _BasedListTestCase(cases.BaseServerTestCase): + def _parseChanList(self, client): + channels = set() + while True: + m = self.getMessage(client) + if m.command == RPL_LISTEND: + break + if m.command == RPL_LIST: + if m.params[1].startswith("&"): + # skip local pseudo-channels listed by ngircd and ircu + continue + channels.add(m.params[1]) + + return channels + + +class ListTestCase(_BasedListTestCase): @cases.mark_specifications("RFC1459", "RFC2812") @cases.xfailIfSoftware(["irc2"], "irc2 deprecated LIST") def testListEmpty(self): """ + """ self.connectClient("foo") self.connectClient("bar") self.getMessages(1) self.sendLine(2, "LIST") m = self.getMessage(2) - if m.command == "321": - # skip RPL_LISTSTART + if m.command == RPL_LISTSTART: + # skip m = self.getMessage(2) # skip local pseudo-channels listed by ngircd and ircu - while m.command == "322" and m.params[1].startswith("&"): + while m.command == RPL_LIST and m.params[1].startswith("&"): m = self.getMessage(2) self.assertNotEqual( m.command, - "322", # RPL_LIST + RPL_LIST, "LIST response gives (at least) one channel, whereas there " "is none.", ) self.assertMessageMatch( m, - command="323", # RPL_LISTEND + command=RPL_LISTEND, fail_msg="Second reply to LIST is not 322 (RPL_LIST) " "or 323 (RPL_LISTEND), or but: {msg}", ) @@ -46,6 +67,8 @@ class ListTestCase(cases.BaseServerTestCase): """When a channel exists, LIST should get it in a reply. + + """ self.connectClient("foo") self.connectClient("bar") @@ -53,34 +76,335 @@ class ListTestCase(cases.BaseServerTestCase): self.getMessages(1) self.sendLine(2, "LIST") m = self.getMessage(2) - if m.command == "321": - # skip RPL_LISTSTART + if m.command == RPL_LISTSTART: + # skip m = self.getMessage(2) self.assertNotEqual( m.command, - "323", # RPL_LISTEND + RPL_LISTEND, fail_msg="LIST response ended (ie. 323, aka RPL_LISTEND) " "without listing any channel, whereas there is one.", ) self.assertMessageMatch( m, - command="322", # RPL_LIST + command=RPL_LIST, fail_msg="Second reply to LIST is not 322 (RPL_LIST), " "nor 323 (RPL_LISTEND) but: {msg}", ) m = self.getMessage(2) # skip local pseudo-channels listed by ngircd and ircu - while m.command == "322" and m.params[1].startswith("&"): + while m.command == RPL_LIST and m.params[1].startswith("&"): m = self.getMessage(2) self.assertNotEqual( m.command, - "322", # RPL_LIST + RPL_LIST, fail_msg="LIST response gives (at least) two channels, " "whereas there is only one.", ) self.assertMessageMatch( m, - command="323", # RPL_LISTEND + command=RPL_LISTEND, fail_msg="Third reply to LIST is not 322 (RPL_LIST) " "or 323 (RPL_LISTEND), or but: {msg}", ) + + @cases.mark_isupport("ELIST") + @cases.mark_specifications("Modern") + def testListMask(self): + """ + "M: Searching based on mask." + -- + -- https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.8 + """ + self.connectClient("foo") + + if "M" not in self.server_support.get("ELIST", ""): + raise runner.OptionalExtensionNotSupported("ELIST=M") + + self.connectClient("bar") + self.sendLine(1, "JOIN #chan1") + self.getMessages(1) + self.sendLine(1, "JOIN #chan2") + self.getMessages(1) + + self.sendLine(2, "LIST *an1") + self.assertEqual(self._parseChanList(2), {"#chan1"}) + + self.sendLine(2, "LIST *an2") + self.assertEqual(self._parseChanList(2), {"#chan2"}) + + self.sendLine(2, "LIST #c*n2") + self.assertEqual(self._parseChanList(2), {"#chan2"}) + + self.sendLine(2, "LIST *an3") + self.assertEqual(self._parseChanList(2), set()) + + self.sendLine(2, "LIST #ch*") + self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) + + @cases.mark_isupport("ELIST") + @cases.mark_specifications("Modern") + def testListNotMask(self): + """ + " N: Searching based on a non-matching mask. i.e., the opposite of M." + -- + -- https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.8 + """ + self.connectClient("foo") + + if "N" not in self.server_support.get("ELIST", ""): + raise runner.OptionalExtensionNotSupported("ELIST=N") + + self.sendLine(1, "JOIN #chan1") + self.getMessages(1) + self.sendLine(1, "JOIN #chan2") + self.getMessages(1) + + self.connectClient("bar") + + self.sendLine(2, "LIST !*an1") + self.assertEqual(self._parseChanList(2), {"#chan2"}) + + self.sendLine(2, "LIST !*an2") + self.assertEqual(self._parseChanList(2), {"#chan1"}) + + self.sendLine(2, "LIST !#c*n2") + self.assertEqual(self._parseChanList(2), {"#chan1"}) + + self.sendLine(2, "LIST !*an3") + self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) + + self.sendLine(2, "LIST !#ch*") + self.assertEqual(self._parseChanList(2), set()) + + @cases.mark_isupport("ELIST") + @cases.mark_specifications("Modern") + def testListUsers(self): + """ + "U: Searching based on user count within the channel, via the "val" modifiers to search for a channel that has less or more than val users, + respectively." + -- + -- https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.8 + """ + self.connectClient("foo") + + if "M" not in self.server_support.get("ELIST", ""): + raise runner.OptionalExtensionNotSupported("ELIST=M") + + self.sendLine(1, "JOIN #chan1") + self.getMessages(1) + self.sendLine(1, "JOIN #chan2") + self.getMessages(1) + + self.connectClient("bar") + self.sendLine(2, "JOIN #chan2") + self.getMessages(2) + + self.connectClient("baz") + + self.sendLine(3, "LIST >0") + self.assertEqual(self._parseChanList(3), {"#chan1", "#chan2"}) + + self.sendLine(3, "LIST <1") + self.assertEqual(self._parseChanList(3), set()) + + self.sendLine(3, "LIST <100") + self.assertEqual(self._parseChanList(3), {"#chan1", "#chan2"}) + + self.sendLine(3, "LIST >1") + self.assertEqual(self._parseChanList(3), {"#chan2"}) + + self.sendLine(3, "LIST <2") + self.assertEqual(self._parseChanList(3), {"#chan1"}) + + self.sendLine(3, "LIST <100") + self.assertEqual(self._parseChanList(3), {"#chan1", "#chan2"}) + + +class FaketimeListTestCase(_BasedListTestCase): + faketime = "+1y x30" # for every wall clock second, 1 minute passed for the server + + def _sleep_minutes(self, n): + for _ in range(n): + if self.controller.faketime_enabled: + # From the server's point of view, 1 min will pass + time.sleep(2) + else: + time.sleep(60) + + # reply to pings + self.getMessages(1) + self.getMessages(2) + + @cases.mark_isupport("ELIST") + @cases.mark_specifications("Modern") + def testListCreationTime(self): + """ + " C: Searching based on channel creation time, via the "Cval" + modifiers to search for a channel creation time that is higher or lower + than val." + -- + -- https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.8 + + Unfortunately, this is ambiguous, because "val" is a time delta (in minutes), + not a timestamp. + + On InspIRCd and Charybdis/Solanum, "C minutes ago + + On UnrealIRCd, Plexus, and Hybrid, it is interpreted as "the channel's creation + time is a timestamp lower than minutes ago" (ie. the exact opposite) + + "C: Searching based on channel creation time, via the "Cval" + modifiers to search for a channel that was created either less than `val` + minutes ago, or more than `val` minutes ago, respectively" + -- https://github.com/ircdocs/modern-irc/pull/171 + """ + self.connectClient("foo") + + if "C" not in self.server_support.get("ELIST", ""): + raise runner.OptionalExtensionNotSupported("ELIST=C") + + self.connectClient("bar") + self.sendLine(1, "JOIN #chan1") + self.getMessages(1) + + # Helps debugging + self.sendLine(1, "TIME") + self.getMessages(1) + + self._sleep_minutes(2) + + # Helps debugging + self.sendLine(1, "TIME") + self.getMessages(1) + + self.sendLine(1, "JOIN #chan2") + self.getMessages(1) + + self._sleep_minutes(1) + + if self.controller.software_name in ("UnrealIRCd", "Plexus4", "Hybrid"): + self.sendLine(2, "LIST C<2") + self.assertEqual(self._parseChanList(2), {"#chan1"}) + + self.sendLine(2, "LIST C>2") + self.assertEqual(self._parseChanList(2), {"#chan2"}) + + self.sendLine(2, "LIST C>0") + self.assertEqual(self._parseChanList(2), set()) + + self.sendLine(2, "LIST C<0") + self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) + + self.sendLine(2, "LIST C>10") + self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) + elif self.controller.software_name in ("Solanum", "Charybdis", "InspIRCd"): + self.sendLine(2, "LIST C>2") + self.assertEqual(self._parseChanList(2), {"#chan1"}) + + self.sendLine(2, "LIST C<2") + self.assertEqual(self._parseChanList(2), {"#chan2"}) + + self.sendLine(2, "LIST C<0") + if self.controller.software_name == "InspIRCd": + self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) + else: + self.assertEqual(self._parseChanList(2), set()) + + self.sendLine(2, "LIST C>0") + self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) + + self.sendLine(2, "LIST C<10") + self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) + else: + assert False, f"{self.controller.software_name} not supported" + + @cases.mark_isupport("ELIST") + @cases.mark_specifications("Modern") + def testListTopicTime(self): + """ + "T: Searching based on topic time, via the "Tval" + modifiers to search for a topic time that is lower or higher than + val respectively." + -- + -- https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.8 + + See testListCreationTime's docstring for comments on this. + + "T: Searching based on topic set time, via the "Tval" modifiers + to search for a topic time that was set less than `val` minutes ago, or more + than `val` minutes ago, respectively." + -- https://github.com/ircdocs/modern-irc/pull/171 + """ + self.connectClient("foo") + + if "T" not in self.server_support.get("ELIST", ""): + raise runner.OptionalExtensionNotSupported("ELIST=T") + + self.connectClient("bar") + self.sendLine(1, "JOIN #chan1") + self.sendLine(1, "JOIN #chan2") + self.getMessages(1) + + self.sendLine(1, "TOPIC #chan1 :First channel") + self.getMessages(1) + + # Helps debugging + self.sendLine(1, "TIME") + self.getMessages(1) + + self._sleep_minutes(2) + + # Helps debugging + self.sendLine(1, "TIME") + self.getMessages(1) + + self.sendLine(1, "TOPIC #chan2 :Second channel") + self.getMessages(1) + + self._sleep_minutes(1) + + if self.controller.software_name in ("UnrealIRCd",): + self.sendLine(1, "LIST T<2") + self.assertEqual(self._parseChanList(1), {"#chan1"}) + + self.sendLine(1, "LIST T>2") + self.assertEqual(self._parseChanList(1), {"#chan2"}) + + self.sendLine(1, "LIST T>0") + self.assertEqual(self._parseChanList(1), set()) + + self.sendLine(1, "LIST T<0") + self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"}) + + self.sendLine(1, "LIST T>10") + self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"}) + elif self.controller.software_name in ( + "Solanum", + "Charybdis", + "InspIRCd", + "Plexus4", + "Hybrid", + ): + self.sendLine(1, "LIST T>2") + self.assertEqual(self._parseChanList(1), {"#chan1"}) + + self.sendLine(1, "LIST T<2") + self.assertEqual(self._parseChanList(1), {"#chan2"}) + + self.sendLine(1, "LIST T<0") + if self.controller.software_name == "InspIRCd": + # Insp internally represents "LIST T>0" like "LIST" + self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"}) + else: + self.assertEqual(self._parseChanList(1), set()) + + self.sendLine(1, "LIST T>0") + self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"}) + + self.sendLine(1, "LIST T<10") + self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"}) + else: + assert False, f"{self.controller.software_name} not supported" diff --git a/irctest/specifications.py b/irctest/specifications.py index 8a629c7..16257fa 100644 --- a/irctest/specifications.py +++ b/irctest/specifications.py @@ -50,6 +50,7 @@ class Capabilities(enum.Enum): @enum.unique class IsupportTokens(enum.Enum): BOT = "BOT" + ELIST = "ELIST" PREFIX = "PREFIX" MONITOR = "MONITOR" STATUSMSG = "STATUSMSG" diff --git a/make_workflows.py b/make_workflows.py index 62ab72b..6d95675 100644 --- a/make_workflows.py +++ b/make_workflows.py @@ -208,8 +208,8 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs): *unpack, *install_steps, { - "name": "Install Atheme", - "run": "sudo apt-get install atheme-services", + "name": "Install system dependencies", + "run": "sudo apt-get install atheme-services faketime", }, { "name": "Install irctest dependencies", diff --git a/pytest.ini b/pytest.ini index 2aa6a82..1bebd29 100644 --- a/pytest.ini +++ b/pytest.ini @@ -32,6 +32,7 @@ markers = # isupport tokens BOT + ELIST MONITOR PREFIX STATUSMSG