From b274cad65b36d72db7afab582cc0811b2c16ea89 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 27 Oct 2024 18:17:57 +0100 Subject: [PATCH 01/17] Add tests for Sable's postgresql chathistory backend --- Makefile | 29 ++++--- irctest/basecontrollers.py | 3 + irctest/cases.py | 10 ++- irctest/controllers/sable.py | 129 +++++++++++++++++++++++++++- irctest/server_tests/chathistory.py | 120 +++++++++++++++++++++++++- irctest/specifications.py | 4 + pytest.ini | 5 ++ 7 files changed, 277 insertions(+), 23 deletions(-) diff --git a/Makefile b/Makefile index dc245869..cde2edc9 100644 --- a/Makefile +++ b/Makefile @@ -8,71 +8,72 @@ PYTEST_ARGS ?= EXTRA_SELECTORS ?= BAHAMUT_SELECTORS := \ - not Ergo \ + not implementation-specific \ and not deprecated \ and not strict \ and not IRCv3 \ $(EXTRA_SELECTORS) CHARYBDIS_SELECTORS := \ - not Ergo \ + not implementation-specific \ and not deprecated \ and not strict \ $(EXTRA_SELECTORS) ERGO_SELECTORS := \ + (Ergo or not implementation-specific) \ not deprecated \ $(EXTRA_SELECTORS) HYBRID_SELECTORS := \ - not Ergo \ + not implementation-specific \ and not deprecated \ $(EXTRA_SELECTORS) INSPIRCD_SELECTORS := \ - not Ergo \ + not implementation-specific \ and not deprecated \ and not strict \ $(EXTRA_SELECTORS) IRCU2_SELECTORS := \ - not Ergo \ + not implementation-specific \ and not deprecated \ and not strict \ $(EXTRA_SELECTORS) NEFARIOUS_SELECTORS := \ - not Ergo \ + not implementation-specific \ and not deprecated \ and not strict \ $(EXTRA_SELECTORS) SNIRCD_SELECTORS := \ - not Ergo \ + not implementation-specific \ and not deprecated \ and not strict \ $(EXTRA_SELECTORS) IRC2_SELECTORS := \ - not Ergo \ + not implementation-specific \ and not deprecated \ and not strict \ $(EXTRA_SELECTORS) MAMMON_SELECTORS := \ - not Ergo \ + not implementation-specific \ and not deprecated \ and not strict \ $(EXTRA_SELECTORS) NGIRCD_SELECTORS := \ - not Ergo \ + not implementation-specific \ and not deprecated \ and not strict \ $(EXTRA_SELECTORS) PLEXUS4_SELECTORS := \ - not Ergo \ + not implementation-specific \ and not deprecated \ $(EXTRA_SELECTORS) @@ -87,7 +88,7 @@ LIMNORIA_SELECTORS := \ # Tests marked with private_chathistory can't pass because Sable does not implement CHATHISTORY for DMs SABLE_SELECTORS := \ - not Ergo \ + (Sable or not implementation-specific) \ and not deprecated \ and not strict \ and not arbitrary_client_tags \ @@ -97,7 +98,7 @@ SABLE_SELECTORS := \ $(EXTRA_SELECTORS) SOLANUM_SELECTORS := \ - not Ergo \ + not implementation-specific \ and not deprecated \ and not strict \ $(EXTRA_SELECTORS) @@ -119,7 +120,7 @@ THELOUNGE_SELECTORS := \ # Tests marked with private_chathistory can't pass because Unreal does not implement CHATHISTORY for DMs UNREALIRCD_SELECTORS := \ - not Ergo \ + not implementation-specific \ and not deprecated \ and not strict \ and not arbitrary_client_tags \ diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index 156d5c3f..11126e77 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -67,6 +67,9 @@ class TestCaseControllerConfig: This should be used as little as possible, using the other attributes instead; as they are work with any controller.""" + sable_history_server: bool = False + """Whether to start Sable's long-term history server""" + class _BaseController: """Base class for software controllers. diff --git a/irctest/cases.py b/irctest/cases.py index 4d14b3a2..7ab0e690 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -842,16 +842,22 @@ def mark_services(cls: TClass) -> TClass: def mark_specifications( *specifications_str: str, deprecated: bool = False, strict: bool = False ) -> Callable[[TCallable], TCallable]: - specifications = frozenset( + specifications = { Specifications.from_name(s) if isinstance(s, str) else s for s in specifications_str - ) + } if None in specifications: raise ValueError("Invalid set of specifications: {}".format(specifications)) + is_implementation_specific = all( + spec.is_implementation_specific() for spec in specifications + ) + def decorator(f: TCallable) -> TCallable: for specification in specifications: f = getattr(pytest.mark, specification.value)(f) + if is_implementation_specific: + f = getattr(pytest.mark, "implementation-specific")(f) if strict: f = pytest.mark.strict(f) if deprecated: diff --git a/irctest/controllers/sable.py b/irctest/controllers/sable.py index 200ea6fa..7155dcf0 100644 --- a/irctest/controllers/sable.py +++ b/irctest/controllers/sable.py @@ -5,7 +5,7 @@ import subprocess import tempfile import time -from typing import Optional, Type +from typing import Optional, Sequence, Type from irctest.basecontrollers import ( BaseServerController, @@ -85,7 +85,7 @@ def certs_dir() -> Path: 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"], + ["bash", "gen_certs.sh", "My.Little.Server", "My.Little.History", "My.Little.Services"], cwd=certs_dir.name, check=True, ) @@ -99,6 +99,7 @@ def certs_dir() -> Path: "ca_file": "%(certs_dir)s/ca_cert.pem", "peers": [ + { "name": "My.Little.History", "address": "%(history_hostname)s:%(history_port)s", "fingerprint": "%(history_cert_sha1)s" }, { "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" } ] @@ -107,7 +108,7 @@ def certs_dir() -> Path: NETWORK_CONFIG_CONFIG = """ { - "object_expiry": 300, + "object_expiry": 60, // 1 minute "opers": [ { @@ -219,6 +220,58 @@ def certs_dir() -> Path: } """ +HISTORY_SERVER_CONFIG = """ +{ + "server_id": 50, + "server_name": "My.Little.History", + + "management": { + "address": "%(history_management_hostname)s:%(history_management_port)s", + "client_ca": "%(certs_dir)s/ca_cert.pem", + "authorised_fingerprints": [ + { "name": "user1", "fingerprint": "435bc6db9f22e84ba5d9652432154617c9509370" } + ] + }, + + "server": { + "database": "%(history_db_url)s", + "auto_run_migrations": true, + }, + + "event_log": { + "event_expiry": 300, // five minutes, for local testing + }, + + "tls_config": { + "key_file": "%(certs_dir)s/My.Little.History.key", + "cert_file": "%(certs_dir)s/My.Little.History.pem" + }, + + "node_config": { + "listen_addr": "%(history_hostname)s:%(history_port)s", + "cert_file": "%(certs_dir)s/My.Little.History.pem", + "key_file": "%(certs_dir)s/My.Little.History.key" + }, + + "log": { + "dir": "log/services/", + + "module-levels": { + "": "debug", + "sable_history": "trace", + }, + + "targets": [ + { + "target": "stdout", + "level": "trace", + "modules": [ "sable_history" ] + } + ] + } +} +""" + SERVICES_CONFIG = """ { "server_id": 99, @@ -348,10 +401,11 @@ def run( (server1_hostname, server1_port) = self.get_hostname_and_port() (services_hostname, services_port) = self.get_hostname_and_port() + (history_hostname, history_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_hostname = history_hostname = services_hostname = "127.0.0.1" ( server1_management_hostname, @@ -361,6 +415,10 @@ def run( services_management_hostname, services_management_port, ) = self.get_hostname_and_port() + ( + history_management_hostname, + history_management_port, + ) = self.get_hostname_and_port() self.template_vars = dict( certs_dir=certs_dir(), @@ -381,6 +439,13 @@ def run( services_management_hostname=services_management_hostname, services_management_port=services_management_port, services_alias_users=SERVICES_ALIAS_USERS if run_services else "", + history_hostname=history_hostname, + history_port=history_port, + history_cert_sha1=(certs_dir() / "My.Little.History.pem.sha1") + .read_text() + .strip(), + history_management_hostname=history_management_hostname, + history_management_port=history_management_port, ) with self.open_file("configs/network.conf") as fd: @@ -416,12 +481,22 @@ def run( if run_services: self.services_controller = SableServicesController(self.test_config, self) + self.services_controller.faketime_cmd = faketime_cmd self.services_controller.run( protocol="sable", server_hostname=services_hostname, server_port=services_port, ) + if self.test_config.sable_history_server: + self.history_controller = SableHistoryController(self.test_config, self) + self.history_controller.faketime_cmd = faketime_cmd + self.history_controller.run( + protocol="sable", + server_hostname=history_hostname, + server_port=history_port, + ) + def kill_proc(self) -> None: os.killpg(self.pgroup_id, signal.SIGKILL) super().kill_proc() @@ -470,6 +545,8 @@ class SableServicesController(BaseServicesController): server_controller: SableController software_name = "Sable Services" + faketime_cmd: Sequence[str] + def run(self, protocol: str, server_hostname: str, server_port: int) -> None: assert protocol == "sable" assert self.server_controller.directory is not None @@ -479,6 +556,7 @@ def run(self, protocol: str, server_hostname: str, server_port: int) -> None: self.proc = self.execute( [ + *self.faketime_cmd, "sable_services", "--foreground", "--server-conf", @@ -497,5 +575,48 @@ def kill_proc(self) -> None: super().kill_proc() +class SableHistoryController(BaseServicesController): + server_controller: SableController + software_name = "Sable History Server" + faketime_cmd: Sequence[str] + + def run(self, protocol: str, server_hostname: str, server_port: int) -> None: + assert protocol == "sable" + assert self.server_controller.directory is not None + history_db_url=os.environ.get("PIFPAF_POSTGRESQL_URL") or os.environ.get("IRCTEST_POSTGRESQL_URL") + assert history_db_url, ( + "Cannot find a postgresql database to use as backend for sable_history. " + "Either set the IRCTEST_POSTGRESQL_URL env var to a libpq URL, or " + "run `pip3 install pifpaf` and wrap irctest in a pifpaf call (ie. " + "pifpaf run postgresql -- pytest --controller=irctest.controllers.sable ...)" + ) + + with self.server_controller.open_file("configs/history_server.conf") as fd: + fd.write(HISTORY_SERVER_CONFIG % { + **self.server_controller.template_vars, + "history_db_url": history_db_url, + }) + + self.proc = self.execute( + [ + *self.faketime_cmd, + "sable_history", + "--foreground", + "--server-conf", + self.server_controller.directory / "configs/history_server.conf", + "--network-conf", + self.server_controller.directory / "configs/network.conf", + ], + cwd=self.server_controller.directory, + preexec_fn=os.setsid, + env={"RUST_BACKTRACE": "1", **os.environ}, + ) + 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/server_tests/chathistory.py b/irctest/server_tests/chathistory.py index 71521b60..e8b92942 100644 --- a/irctest/server_tests/chathistory.py +++ b/irctest/server_tests/chathistory.py @@ -2,6 +2,7 @@ `IRCv3 draft chathistory `_ """ +import dataclasses import functools import secrets import time @@ -31,9 +32,18 @@ def newf(self, *args, **kwargs): return newf -@cases.mark_specifications("IRCv3") -@cases.mark_services -class ChathistoryTestCase(cases.BaseServerTestCase): +class _BaseChathistoryTests(cases.BaseServerTestCase): + def _wait_before_chathistory(self): + """Hook for the Sable-specific tests that check the postgresql-based + CHATHISTORY implementation is sound. This implementation only kicks in + after the in-memory history is cleared, which happens after a 5 min timeout; + and this gives a chance to :class:``SablePostgresqlHistoryTestCase`` to + wait this timeout. + + For other tests, this does nothing. + """ + raise NotImplementedError("_BaseChathistoryTests._wait_before_chathistory") + def validate_chathistory_batch(self, msgs, target): (start, *inner_msgs, end) = msgs @@ -94,6 +104,8 @@ def testInvalidTargets(self): self.joinChannel(qux, real_chname) self.getMessages(qux) + self._wait_before_chathistory() + # test a nonexistent channel self.sendLine(bar, "CHATHISTORY LATEST #nonexistent_channel * 10") msgs = self.getMessages(bar) @@ -175,6 +187,8 @@ def testMessagesToSelf(self): messages.append(echo.to_history_message()) self.assertEqual(echo.to_history_message(), delivery.to_history_message()) + self._wait_before_chathistory() + self.sendLine(bar, "CHATHISTORY LATEST %s * 10" % (bar,)) replies = [msg for msg in self.getMessages(bar) if msg.command == "PRIVMSG"] self.assertEqual([msg.to_history_message() for msg in replies], messages) @@ -228,6 +242,9 @@ def testChathistory(self, subcommand): time.sleep(0.002) self.validate_echo_messages(NUM_MESSAGES, echo_messages) + + self._wait_before_chathistory() + self.validate_chathistory(subcommand, echo_messages, 1, chname) @skip_ngircd @@ -264,6 +281,8 @@ def testChathistoryNoEventPlayback(self): ) time.sleep(0.002) + self._wait_before_chathistory() + self.validate_echo_messages(NUM_MESSAGES, echo_messages) self.sendLine(1, "CHATHISTORY LATEST %s * 100" % chname) (batch_open, *messages, batch_close) = self.getMessages(1) @@ -308,6 +327,9 @@ def testChathistoryEventPlayback(self, subcommand): time.sleep(0.002) self.validate_echo_messages(NUM_MESSAGES * 2, echo_messages) + + self._wait_before_chathistory() + self.validate_chathistory(subcommand, echo_messages, 1, chname) @pytest.mark.parametrize("subcommand", SUBCOMMANDS) @@ -367,6 +389,9 @@ def testChathistoryDMs(self, subcommand): self.getMessages(2) self.validate_echo_messages(NUM_MESSAGES, echo_messages) + + self._wait_before_chathistory() + self.validate_chathistory(subcommand, echo_messages, 1, c2) self.validate_chathistory(subcommand, echo_messages, 2, c1) @@ -415,6 +440,8 @@ def testChathistoryDMs(self, subcommand): ] self.assertEqual(results, new_convo) + self._wait_before_chathistory() + # additional messages with c3 should not show up in the c1-c2 history: self.validate_chathistory(subcommand, echo_messages, 1, c2) self.validate_chathistory(subcommand, echo_messages, 2, c1) @@ -718,6 +745,8 @@ def validate_tagmsg(msg, target, msgid): self.assertEqual(len(relay), 1) validate_tagmsg(relay[0], chname, msgid) + self._wait_before_chathistory() + self.sendLine(1, "CHATHISTORY LATEST %s * 10" % (chname,)) history_tagmsgs = [ msg for msg in self.getMessages(1) if msg.command == "TAGMSG" @@ -814,8 +843,93 @@ def validate_msg(msg): validate_msg(relay) +@cases.mark_specifications("IRCv3") +@cases.mark_services +class ChathistoryTestCase(_BaseChathistoryTests): + def _wait_before_chathistory(self): + """does nothing""" + pass + + assert {f"_validate_chathistory_{cmd}" for cmd in SUBCOMMANDS} == { meth_name for meth_name in dir(ChathistoryTestCase) if meth_name.startswith("_validate_chathistory_") }, "ChathistoryTestCase.validate_chathistory and SUBCOMMANDS are out of sync" + + +@cases.mark_specifications("Sable") +@cases.mark_services +class SablePostgresqlHistoryTestCase(_BaseChathistoryTests): + faketime = "+1y x15" # for every wall clock second, 15 seconds pass for the server + + @staticmethod + def config() -> cases.TestCaseControllerConfig: + return dataclasses.replace( # type: ignore[no-any-return] + _BaseChathistoryTests.config(), + sable_history_server=True, + ) + + def _wait_before_chathistory(self): + """waits 6 seconds which appears to be a 1.5 min to Sable; which goes over + the 1 min timeout for in-memory history""" + assert self.controller.faketime_enabled, "faketime is not installed" + time.sleep(7) + + +@cases.mark_specifications("Sable") +@cases.mark_services +class SableExpiringHistoryTestCase(cases.BaseServerTestCase): + # for every wall clock second, 10 seconds pass for the server. + # At x15, sable_history does not always have time to persist messages (wtf?) + # at x30, links between nodes timeout. + faketime = "+1y x10" + + def _wait_before_chathistory(self): + """waits 6 seconds which appears to be a 1.5 min to Sable; which goes over + the 1 min timeout for in-memory history""" + assert self.controller.faketime_enabled, "faketime is not installed" + time.sleep(15) + + def testChathistoryExpired(self): + """Checks that Sable forgets about messages if the history server is not available""" + 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) + + self.sendLine(1, f"PRIVMSG {chname} :this is a message") + self.getMessages(1) + + self._wait_before_chathistory() + + self.sendLine(1, f"CHATHISTORY LATEST {chname} * 10") + + # Sable processes CHATHISTORY asynchronously, which can be pretty slow as it + # sends cross-server requests. This means we can't just rely on a PING-PONG + # or the usual time.sleep(self.controller.sync_sleep_time) to make sure + # the ircd replied to us + time.sleep(self.controller.sync_sleep_time * 10) + + (start, *middle, end) = self.getMessages(1) + self.assertMessageMatch( + start, command="BATCH", params=[StrRe(r"\+.*"), "chathistory", chname] + ) + batch_tag = start.params[0][1:] + self.assertMessageMatch(end, command="BATCH", params=["-" + batch_tag]) + self.assertEqual( + len(middle), 0, f"Got messages that should be expired: {middle}" + ) diff --git a/irctest/specifications.py b/irctest/specifications.py index 9c4617bc..41f82b4c 100644 --- a/irctest/specifications.py +++ b/irctest/specifications.py @@ -9,6 +9,7 @@ class Specifications(enum.Enum): RFC2812 = "RFC2812" IRCv3 = "IRCv3" # Mark with capabilities whenever possible Ergo = "Ergo" + Sable = "Sable" Ircdocs = "ircdocs" """Any document on ircdocs.horse (especially defs.ircdocs.horse), @@ -24,6 +25,9 @@ def from_name(cls, name: str) -> Specifications: return spec raise ValueError(name) + def is_implementation_specific(self) -> bool: + return self in (Specifications.Ergo, Specifications.Sable) + @enum.unique class Capabilities(enum.Enum): diff --git a/pytest.ini b/pytest.ini index 375f2bb3..1c13017f 100644 --- a/pytest.ini +++ b/pytest.ini @@ -7,7 +7,12 @@ markers = IRCv3 modern ircdocs + + # implementations for which we have specific test get two markers: + # the implementation name and 'implementation-specific' + implementation-specific Ergo + Sable # misc marks strict From f350ff0b96ccd4bcb0656375582d06c864e24f8f Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 27 Oct 2024 18:35:54 +0100 Subject: [PATCH 02/17] Make tests less flaky on Sable --- irctest/server_tests/chathistory.py | 76 +++++++++++++++-------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/irctest/server_tests/chathistory.py b/irctest/server_tests/chathistory.py index e8b92942..ec4046b1 100644 --- a/irctest/server_tests/chathistory.py +++ b/irctest/server_tests/chathistory.py @@ -44,7 +44,10 @@ def _wait_before_chathistory(self): """ raise NotImplementedError("_BaseChathistoryTests._wait_before_chathistory") - def validate_chathistory_batch(self, msgs, target): + def validate_chathistory_batch(self, user, target): + # may need to try again for Sable, as it has a pretty high latency here + while not (msgs := self.getMessages(user)): + pass (start, *inner_msgs, end) = msgs self.assertMessageMatch( @@ -239,7 +242,7 @@ def testChathistory(self, subcommand): echo_messages.extend( msg.to_history_message() for msg in self.getMessages(1) ) - time.sleep(0.002) + time.sleep(0.02) self.validate_echo_messages(NUM_MESSAGES, echo_messages) @@ -486,15 +489,15 @@ def validate_chathistory(self, subcommand, echo_messages, user, chname): def _validate_chathistory_LATEST(self, echo_messages, user, chname): INCLUSIVE_LIMIT = len(echo_messages) * 2 self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, INCLUSIVE_LIMIT)) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual(echo_messages, result) self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, 5)) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual(echo_messages[-5:], result) self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, 1)) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual(echo_messages[-1:], result) if self._supports_msgid(): @@ -503,7 +506,7 @@ def _validate_chathistory_LATEST(self, echo_messages, user, chname): "CHATHISTORY LATEST %s msgid=%s %d" % (chname, echo_messages[4].msgid, INCLUSIVE_LIMIT), ) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual(echo_messages[5:], result) if self._supports_timestamp(): @@ -512,7 +515,7 @@ def _validate_chathistory_LATEST(self, echo_messages, user, chname): "CHATHISTORY LATEST %s timestamp=%s %d" % (chname, echo_messages[4].time, INCLUSIVE_LIMIT), ) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual(echo_messages[5:], result) def _validate_chathistory_BEFORE(self, echo_messages, user, chname): @@ -523,7 +526,7 @@ def _validate_chathistory_BEFORE(self, echo_messages, user, chname): "CHATHISTORY BEFORE %s msgid=%s %d" % (chname, echo_messages[6].msgid, INCLUSIVE_LIMIT), ) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual(echo_messages[:6], result) if self._supports_timestamp(): @@ -532,7 +535,7 @@ def _validate_chathistory_BEFORE(self, echo_messages, user, chname): "CHATHISTORY BEFORE %s timestamp=%s %d" % (chname, echo_messages[6].time, INCLUSIVE_LIMIT), ) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual(echo_messages[:6], result) self.sendLine( @@ -540,7 +543,7 @@ def _validate_chathistory_BEFORE(self, echo_messages, user, chname): "CHATHISTORY BEFORE %s timestamp=%s %d" % (chname, echo_messages[6].time, 2), ) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual(echo_messages[4:6], result) def _validate_chathistory_AFTER(self, echo_messages, user, chname): @@ -551,7 +554,7 @@ def _validate_chathistory_AFTER(self, echo_messages, user, chname): "CHATHISTORY AFTER %s msgid=%s %d" % (chname, echo_messages[3].msgid, INCLUSIVE_LIMIT), ) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual(echo_messages[4:], result) if self._supports_timestamp(): @@ -560,7 +563,7 @@ def _validate_chathistory_AFTER(self, echo_messages, user, chname): "CHATHISTORY AFTER %s timestamp=%s %d" % (chname, echo_messages[3].time, INCLUSIVE_LIMIT), ) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual(echo_messages[4:], result) self.sendLine( @@ -568,7 +571,7 @@ def _validate_chathistory_AFTER(self, echo_messages, user, chname): "CHATHISTORY AFTER %s timestamp=%s %d" % (chname, echo_messages[3].time, 3), ) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual(echo_messages[4:7], result) def _validate_chathistory_BETWEEN(self, echo_messages, user, chname): @@ -585,7 +588,7 @@ def _validate_chathistory_BETWEEN(self, echo_messages, user, chname): INCLUSIVE_LIMIT, ), ) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual(echo_messages[1:-1], result) self.sendLine( @@ -598,7 +601,7 @@ def _validate_chathistory_BETWEEN(self, echo_messages, user, chname): INCLUSIVE_LIMIT, ), ) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual(echo_messages[1:-1], result) # BETWEEN forwards and backwards with a limit, should get @@ -608,7 +611,7 @@ def _validate_chathistory_BETWEEN(self, echo_messages, user, chname): "CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d" % (chname, echo_messages[0].msgid, echo_messages[-1].msgid, 3), ) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual(echo_messages[1:4], result) self.sendLine( @@ -616,7 +619,7 @@ def _validate_chathistory_BETWEEN(self, echo_messages, user, chname): "CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d" % (chname, echo_messages[-1].msgid, echo_messages[0].msgid, 3), ) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual(echo_messages[-4:-1], result) if self._supports_timestamp(): @@ -631,7 +634,7 @@ def _validate_chathistory_BETWEEN(self, echo_messages, user, chname): INCLUSIVE_LIMIT, ), ) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual(echo_messages[1:-1], result) self.sendLine( user, @@ -643,21 +646,21 @@ def _validate_chathistory_BETWEEN(self, echo_messages, user, chname): INCLUSIVE_LIMIT, ), ) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual(echo_messages[1:-1], result) self.sendLine( user, "CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d" % (chname, echo_messages[0].time, echo_messages[-1].time, 3), ) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual(echo_messages[1:4], result) self.sendLine( user, "CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d" % (chname, echo_messages[-1].time, echo_messages[0].time, 3), ) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual(echo_messages[-4:-1], result) def _validate_chathistory_AROUND(self, echo_messages, user, chname): @@ -667,7 +670,7 @@ def _validate_chathistory_AROUND(self, echo_messages, user, chname): "CHATHISTORY AROUND %s msgid=%s %d" % (chname, echo_messages[7].msgid, 1), ) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual([echo_messages[7]], result) self.sendLine( @@ -675,7 +678,7 @@ def _validate_chathistory_AROUND(self, echo_messages, user, chname): "CHATHISTORY AROUND %s msgid=%s %d" % (chname, echo_messages[7].msgid, 3), ) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertEqual(echo_messages[6:9], result) if self._supports_timestamp(): @@ -684,7 +687,7 @@ def _validate_chathistory_AROUND(self, echo_messages, user, chname): "CHATHISTORY AROUND %s timestamp=%s %d" % (chname, echo_messages[7].time, 3), ) - result = self.validate_chathistory_batch(self.getMessages(user), chname) + result = self.validate_chathistory_batch(user, chname) self.assertIn(echo_messages[7], result) @pytest.mark.arbitrary_client_tags @@ -861,7 +864,10 @@ def _wait_before_chathistory(self): @cases.mark_specifications("Sable") @cases.mark_services class SablePostgresqlHistoryTestCase(_BaseChathistoryTests): - faketime = "+1y x15" # for every wall clock second, 15 seconds pass for the server + # for every wall clock second, 10 seconds pass for the server. + # At x15, sable_history does not always have time to persist messages (wtf?) + # at x30, links between nodes timeout. + faketime = "+1y x10" @staticmethod def config() -> cases.TestCaseControllerConfig: @@ -871,18 +877,15 @@ def config() -> cases.TestCaseControllerConfig: ) def _wait_before_chathistory(self): - """waits 6 seconds which appears to be a 1.5 min to Sable; which goes over + """waits 15 seconds which appears to be a 1.5 min to Sable; which goes over the 1 min timeout for in-memory history""" assert self.controller.faketime_enabled, "faketime is not installed" - time.sleep(7) + time.sleep(15) @cases.mark_specifications("Sable") @cases.mark_services class SableExpiringHistoryTestCase(cases.BaseServerTestCase): - # for every wall clock second, 10 seconds pass for the server. - # At x15, sable_history does not always have time to persist messages (wtf?) - # at x30, links between nodes timeout. faketime = "+1y x10" def _wait_before_chathistory(self): @@ -918,13 +921,14 @@ def testChathistoryExpired(self): self.sendLine(1, f"CHATHISTORY LATEST {chname} * 10") - # Sable processes CHATHISTORY asynchronously, which can be pretty slow as it - # sends cross-server requests. This means we can't just rely on a PING-PONG - # or the usual time.sleep(self.controller.sync_sleep_time) to make sure - # the ircd replied to us - time.sleep(self.controller.sync_sleep_time * 10) + while not (messages := self.getMessages(1)): + # Sable processes CHATHISTORY asynchronously, which can be pretty slow as it + # sends cross-server requests. This means we can't just rely on a PING-PONG + # or the usual time.sleep(self.controller.sync_sleep_time) to make sure + # the ircd replied to us + time.sleep(self.controller.sync_sleep_time) - (start, *middle, end) = self.getMessages(1) + (start, *middle, end) = messages self.assertMessageMatch( start, command="BATCH", params=[StrRe(r"\+.*"), "chathistory", chname] ) From 396841ebff540338f664a159c6eee159c143a6f4 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Mon, 28 Oct 2024 20:22:40 +0100 Subject: [PATCH 03/17] Another workaround for Sable's latency with the history server --- irctest/server_tests/chathistory.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/irctest/server_tests/chathistory.py b/irctest/server_tests/chathistory.py index ec4046b1..9ee117a4 100644 --- a/irctest/server_tests/chathistory.py +++ b/irctest/server_tests/chathistory.py @@ -111,7 +111,9 @@ def testInvalidTargets(self): # test a nonexistent channel self.sendLine(bar, "CHATHISTORY LATEST #nonexistent_channel * 10") - msgs = self.getMessages(bar) + while not (msgs := self.getMessages(bar)): + # need to retry when Sable has the history server on + pass msgs = [msg for msg in msgs if msg.command != "MODE"] # :NickServ MODE +r self.assertMessageMatch( msgs[0], @@ -121,7 +123,9 @@ def testInvalidTargets(self): # as should a real channel to which one is not joined: self.sendLine(bar, "CHATHISTORY LATEST %s * 10" % (real_chname,)) - msgs = self.getMessages(bar) + while not (msgs := self.getMessages(bar)): + # need to retry when Sable has the history server on + pass self.assertMessageMatch( msgs[0], command="FAIL", From 827c6a6df40923d834d888194c14ba858a8d23fa Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 10 Nov 2024 17:08:01 +0100 Subject: [PATCH 04/17] Actually print logs from sable_services and sable_history --- irctest/basecontrollers.py | 35 +++++++++++++++++++++++++++++++++-- irctest/controllers/sable.py | 7 +++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index 11126e77..d15a9d38 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -8,9 +8,11 @@ import shutil import socket import subprocess +import sys import tempfile import textwrap import time +import threading from typing import ( IO, Any, @@ -148,10 +150,39 @@ def kill(self) -> None: self._own_ports.remove((hostname, port)) def execute( - self, command: Sequence[Union[str, Path]], **kwargs: Any + self, command: Sequence[Union[str, Path]], proc_name: Optional[str], **kwargs: Any ) -> subprocess.Popen: output_to = None if self.debug_mode else subprocess.DEVNULL - return subprocess.Popen(command, stderr=output_to, stdout=output_to, **kwargs) + proc_name = proc_name or command[0] + kwargs.setdefault("stdout", output_to) + kwargs.setdefault("stderr", output_to) + stream_stdout = stream_stderr = None + if kwargs["stdout"] in (None, subprocess.STDOUT): + kwargs["stdout"] = subprocess.PIPE + def stream_stdout(): + for line in proc.stdout: + prefix = f"{time.time():.3f} {proc_name} ".encode() + try: + sys.stdout.buffer.write(prefix + line) + except ValueError: + # "I/O operation on closed file" + pass + if kwargs["stderr"] in (subprocess.STDOUT, None): + kwargs["stdout"] = subprocess.PIPE + def stream_stderr(): + for line in proc.stdout: + prefix = f"{time.time():.3f} {proc_name} ".encode() + try: + sys.stdout.buffer.write(prefix + line) + except ValueError: + # "I/O operation on closed file" + pass + proc = subprocess.Popen(command, **kwargs) + if stream_stdout is not None: + threading.Thread(target=stream_stdout, name="stream_stdout").start() + if stream_stderr is not None: + threading.Thread(target=stream_stderr, name="stream_stderr").start() + return proc class DirectoryBasedController(_BaseController): diff --git a/irctest/controllers/sable.py b/irctest/controllers/sable.py index 7155dcf0..e4a64008 100644 --- a/irctest/controllers/sable.py +++ b/irctest/controllers/sable.py @@ -265,7 +265,7 @@ def certs_dir() -> Path: { "target": "stdout", "level": "trace", - "modules": [ "sable_history" ] + "modules": [ "sable" ] } ] } @@ -350,7 +350,7 @@ def certs_dir() -> Path: { "target": "stdout", "level": "debug", - "modules": [ "sable_services" ] + "modules": [ "sable" ] } ] } @@ -476,6 +476,7 @@ def run( cwd=self.directory, preexec_fn=os.setsid, env={"RUST_BACKTRACE": "1", **os.environ}, + proc_name="sable_ircd ", ) self.pgroup_id = os.getpgid(self.proc.pid) @@ -567,6 +568,7 @@ def run(self, protocol: str, server_hostname: str, server_port: int) -> None: cwd=self.server_controller.directory, preexec_fn=os.setsid, env={"RUST_BACKTRACE": "1", **os.environ}, + proc_name="sable_services", ) self.pgroup_id = os.getpgid(self.proc.pid) @@ -610,6 +612,7 @@ def run(self, protocol: str, server_hostname: str, server_port: int) -> None: cwd=self.server_controller.directory, preexec_fn=os.setsid, env={"RUST_BACKTRACE": "1", **os.environ}, + proc_name="sable_history ", ) self.pgroup_id = os.getpgid(self.proc.pid) From 0dc5a0fddac41edd4d8e3a52bcc962d858b243a0 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 10 Nov 2024 18:52:07 +0100 Subject: [PATCH 05/17] Need fanout > 1 when more than 2 nodes. And restore faketime config to previous value, as that wasn't the right fix --- irctest/controllers/sable.py | 2 +- irctest/server_tests/chathistory.py | 19 ++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/irctest/controllers/sable.py b/irctest/controllers/sable.py index e4a64008..271b079f 100644 --- a/irctest/controllers/sable.py +++ b/irctest/controllers/sable.py @@ -95,7 +95,7 @@ def certs_dir() -> Path: NETWORK_CONFIG = """ { - "fanout": 1, + "fanout": 2, "ca_file": "%(certs_dir)s/ca_cert.pem", "peers": [ diff --git a/irctest/server_tests/chathistory.py b/irctest/server_tests/chathistory.py index 9ee117a4..2a7a2a1a 100644 --- a/irctest/server_tests/chathistory.py +++ b/irctest/server_tests/chathistory.py @@ -868,10 +868,9 @@ def _wait_before_chathistory(self): @cases.mark_specifications("Sable") @cases.mark_services class SablePostgresqlHistoryTestCase(_BaseChathistoryTests): - # for every wall clock second, 10 seconds pass for the server. - # At x15, sable_history does not always have time to persist messages (wtf?) + # for every wall clock second, 15 seconds pass for the server. # at x30, links between nodes timeout. - faketime = "+1y x10" + faketime = "+1y x15" @staticmethod def config() -> cases.TestCaseControllerConfig: @@ -881,22 +880,24 @@ def config() -> cases.TestCaseControllerConfig: ) def _wait_before_chathistory(self): - """waits 15 seconds which appears to be a 1.5 min to Sable; which goes over - the 1 min timeout for in-memory history""" + """waits 6 seconds which appears to be a 1.5 min to Sable; which goes over + the 1 min timeout for in-memory history (+ 1 min because the cleanup job + only runs every min)""" assert self.controller.faketime_enabled, "faketime is not installed" - time.sleep(15) + time.sleep(8) @cases.mark_specifications("Sable") @cases.mark_services class SableExpiringHistoryTestCase(cases.BaseServerTestCase): - faketime = "+1y x10" + faketime = "+1y x15" def _wait_before_chathistory(self): """waits 6 seconds which appears to be a 1.5 min to Sable; which goes over - the 1 min timeout for in-memory history""" + the 1 min timeout for in-memory history (+ 1 min because the cleanup job + only runs every min)""" assert self.controller.faketime_enabled, "faketime is not installed" - time.sleep(15) + time.sleep(8) def testChathistoryExpired(self): """Checks that Sable forgets about messages if the history server is not available""" From a9c87eae911abf78ac1709507a2a7f128b2a1ccf Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 17 Nov 2024 17:45:05 +0100 Subject: [PATCH 06/17] Bump Sable --- .github/workflows/test-stable.yml | 2 +- workflows.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 3e5c9117..8188186e 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -1140,7 +1140,7 @@ jobs: uses: actions/checkout@v4 with: path: sable - ref: e9701e5e8d0c4f278ddd61ce7285f4918ecf99e9 + ref: aee63093bb6ee250b24aabb843b863ab0a327e7a repository: Libera-Chat/sable - name: Install rust toolchain uses: actions-rs/toolchain@v1 diff --git a/workflows.yml b/workflows.yml index 1995b871..a7d129ce 100644 --- a/workflows.yml +++ b/workflows.yml @@ -249,7 +249,7 @@ software: name: Sable repository: Libera-Chat/sable refs: - stable: e9701e5e8d0c4f278ddd61ce7285f4918ecf99e9 + stable: aee63093bb6ee250b24aabb843b863ab0a327e7a release: null devel: master devel_release: null From 895849ed937ce671a886770ddff316ab3d0df320 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 17 Nov 2024 18:05:49 +0100 Subject: [PATCH 07/17] Fix lint --- irctest/basecontrollers.py | 21 +++++++++++++++------ irctest/controllers/sable.py | 19 +++++++++++++------ 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index d15a9d38..0f394ecb 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -11,8 +11,8 @@ import sys import tempfile import textwrap -import time import threading +import time from typing import ( IO, Any, @@ -150,16 +150,21 @@ def kill(self) -> None: self._own_ports.remove((hostname, port)) def execute( - self, command: Sequence[Union[str, Path]], proc_name: Optional[str], **kwargs: Any + self, + command: Sequence[Union[str, Path]], + proc_name: Optional[str] = None, + **kwargs: Any, ) -> subprocess.Popen: output_to = None if self.debug_mode else subprocess.DEVNULL - proc_name = proc_name or command[0] + proc_name = proc_name or str(command[0]) kwargs.setdefault("stdout", output_to) kwargs.setdefault("stderr", output_to) stream_stdout = stream_stderr = None if kwargs["stdout"] in (None, subprocess.STDOUT): kwargs["stdout"] = subprocess.PIPE - def stream_stdout(): + + def stream_stdout() -> None: + assert proc.stdout is not None # for mypy for line in proc.stdout: prefix = f"{time.time():.3f} {proc_name} ".encode() try: @@ -167,16 +172,20 @@ def stream_stdout(): except ValueError: # "I/O operation on closed file" pass + if kwargs["stderr"] in (subprocess.STDOUT, None): kwargs["stdout"] = subprocess.PIPE - def stream_stderr(): - for line in proc.stdout: + + def stream_stderr() -> None: + assert proc.stderr is not None # for mypy + for line in proc.stderr: prefix = f"{time.time():.3f} {proc_name} ".encode() try: sys.stdout.buffer.write(prefix + line) except ValueError: # "I/O operation on closed file" pass + proc = subprocess.Popen(command, **kwargs) if stream_stdout is not None: threading.Thread(target=stream_stdout, name="stream_stdout").start() diff --git a/irctest/controllers/sable.py b/irctest/controllers/sable.py index 271b079f..df6313a0 100644 --- a/irctest/controllers/sable.py +++ b/irctest/controllers/sable.py @@ -85,7 +85,13 @@ def certs_dir() -> Path: 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.History", "My.Little.Services"], + [ + "bash", + "gen_certs.sh", + "My.Little.Server", + "My.Little.History", + "My.Little.Services", + ], cwd=certs_dir.name, check=True, ) @@ -585,7 +591,9 @@ class SableHistoryController(BaseServicesController): def run(self, protocol: str, server_hostname: str, server_port: int) -> None: assert protocol == "sable" assert self.server_controller.directory is not None - history_db_url=os.environ.get("PIFPAF_POSTGRESQL_URL") or os.environ.get("IRCTEST_POSTGRESQL_URL") + history_db_url = os.environ.get("PIFPAF_POSTGRESQL_URL") or os.environ.get( + "IRCTEST_POSTGRESQL_URL" + ) assert history_db_url, ( "Cannot find a postgresql database to use as backend for sable_history. " "Either set the IRCTEST_POSTGRESQL_URL env var to a libpq URL, or " @@ -594,10 +602,9 @@ def run(self, protocol: str, server_hostname: str, server_port: int) -> None: ) with self.server_controller.open_file("configs/history_server.conf") as fd: - fd.write(HISTORY_SERVER_CONFIG % { - **self.server_controller.template_vars, - "history_db_url": history_db_url, - }) + vals = dict(self.server_controller.template_vars) + vals["history_db_url"] = history_db_url + fd.write(HISTORY_SERVER_CONFIG % vals) self.proc = self.execute( [ From a1bb20323ac35e5bf3879f8d363229ed04c49841 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 17 Nov 2024 18:10:52 +0100 Subject: [PATCH 08/17] Start postgresql --- .github/workflows/test-devel.yml | 4 +++- .github/workflows/test-stable.yml | 4 +++- workflows.yml | 3 +++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 41203ef8..680e3e3e 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -990,6 +990,7 @@ jobs: cache-on-failure: true workspaces: sable -> target - run: rustc --version + - run: sudo systemctl start postgresql.service - name: Build Sable run: | cd $GITHUB_WORKSPACE/sable/ @@ -1003,7 +1004,8 @@ jobs: - env: IRCTEST_DEBUG_LOGS: ${{ runner.debug }} name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml --timeout 300' PATH=$HOME/.local/bin:$PATH PATH=$GITHUB_WORKSPACE/sable/target/debug/sbin:$GITHUB_WORKSPACE/sable/target/debug/bin:$GITHUB_WORKSPACE/sable/target/debug:$PATH + run: PYTEST_ARGS='--junit-xml pytest.xml --timeout 300' PATH=$HOME/.local/bin:$PATH + IRCTEST_POSTGRESQL_URL=postgresql:// 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() diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 8188186e..2987c85f 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -1154,6 +1154,7 @@ jobs: cache-on-failure: true workspaces: sable -> target - run: rustc --version + - run: sudo systemctl start postgresql.service - name: Build Sable run: | cd $GITHUB_WORKSPACE/sable/ @@ -1167,7 +1168,8 @@ jobs: - env: IRCTEST_DEBUG_LOGS: ${{ runner.debug }} name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml --timeout 300' PATH=$HOME/.local/bin:$PATH PATH=$GITHUB_WORKSPACE/sable/target/debug/sbin:$GITHUB_WORKSPACE/sable/target/debug/bin:$GITHUB_WORKSPACE/sable/target/debug:$PATH + run: PYTEST_ARGS='--junit-xml pytest.xml --timeout 300' PATH=$HOME/.local/bin:$PATH + IRCTEST_POSTGRESQL_URL=postgresql:// 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() diff --git a/workflows.yml b/workflows.yml index a7d129ce..cf257d7e 100644 --- a/workflows.yml +++ b/workflows.yml @@ -268,6 +268,9 @@ software: workspaces: "sable -> target" cache-on-failure: true - run: rustc --version + - run: start postgresql + run: "sudo systemctl start postgresql.service" + env: "IRCTEST_POSTGRESQL_URL=postgresql://" separate_build_job: false build_script: | cd $GITHUB_WORKSPACE/sable/ From a0a859d4faa619753382b560e11b3f771c8509ff Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Tue, 19 Nov 2024 18:01:32 +0100 Subject: [PATCH 09/17] Fix stderr/stdout typo --- irctest/basecontrollers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index 0f394ecb..58a1afb9 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -174,7 +174,7 @@ def stream_stdout() -> None: pass if kwargs["stderr"] in (subprocess.STDOUT, None): - kwargs["stdout"] = subprocess.PIPE + kwargs["stderr"] = subprocess.PIPE def stream_stderr() -> None: assert proc.stderr is not None # for mypy From 388506c3d45a60d448d71519420d9b41ca13878b Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 8 Dec 2024 09:36:07 +0100 Subject: [PATCH 10/17] Fix typo in Ergo selectors --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c92ad707..2b03f623 100644 --- a/Makefile +++ b/Makefile @@ -22,7 +22,7 @@ CHARYBDIS_SELECTORS := \ ERGO_SELECTORS := \ (Ergo or not implementation-specific) \ - not deprecated \ + and not deprecated \ $(EXTRA_SELECTORS) HYBRID_SELECTORS := \ From e0b4aec24a9b362d0f7835cc044448c1b1c5d902 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 8 Dec 2024 09:39:22 +0100 Subject: [PATCH 11/17] Bump sable --- .github/workflows/test-stable.yml | 2 +- workflows.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 2987c85f..b4e97334 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -1140,7 +1140,7 @@ jobs: uses: actions/checkout@v4 with: path: sable - ref: aee63093bb6ee250b24aabb843b863ab0a327e7a + ref: 0576ef4e572a973d577536875526897d43dc37a3 repository: Libera-Chat/sable - name: Install rust toolchain uses: actions-rs/toolchain@v1 diff --git a/workflows.yml b/workflows.yml index cf257d7e..766722b5 100644 --- a/workflows.yml +++ b/workflows.yml @@ -249,7 +249,7 @@ software: name: Sable repository: Libera-Chat/sable refs: - stable: aee63093bb6ee250b24aabb843b863ab0a327e7a + stable: 0576ef4e572a973d577536875526897d43dc37a3 release: null devel: master devel_release: null From 4cfb7665c6f4cdf03b1ab996fbd4982565e5cabc Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 8 Dec 2024 13:25:59 +0100 Subject: [PATCH 12/17] Use -m instead of -k to filter markers -k uses substring match, so -k Sable matched ErgoUtf8NickDisabledTestCase --- Makefile | 141 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 102 insertions(+), 39 deletions(-) diff --git a/Makefile b/Makefile index 2b03f623..ea5d3fa5 100644 --- a/Makefile +++ b/Makefile @@ -4,110 +4,159 @@ PYTEST ?= python3 -m pytest # pytest-xdist is installed) PYTEST_ARGS ?= +# Will be appended at the end of the -m argument to pytest +EXTRA_MARKERS ?= + # Will be appended at the end of the -k argument to pytest EXTRA_SELECTORS ?= -BAHAMUT_SELECTORS := \ +BAHAMUT_MARKERS := \ not implementation-specific \ and not deprecated \ and not strict \ and not IRCv3 \ + $(EXTRA_MARKERS) +BAHAMUT_SELECTORS := \ + (foo or not foo) \ $(EXTRA_SELECTORS) -CHARYBDIS_SELECTORS := \ +CHARYBDIS_MARKERS := \ not implementation-specific \ and not deprecated \ and not strict \ + $(EXTRA_MARKERS) +CHARYBDIS_SELECTORS := \ + (foo or not foo) \ $(EXTRA_SELECTORS) -ERGO_SELECTORS := \ +ERGO_MARKERS := \ (Ergo or not implementation-specific) \ and not deprecated \ + $(EXTRA_MARKERS) +ERGO_SELECTORS := \ + (foo or not foo) \ $(EXTRA_SELECTORS) -HYBRID_SELECTORS := \ +HYBRID_MARKERS := \ not implementation-specific \ and not deprecated \ + $(EXTRA_MARKERS) +HYBRID_SELECTORS := \ + (foo or not foo) \ $(EXTRA_SELECTORS) -INSPIRCD_SELECTORS := \ +INSPIRCD_MARKERS := \ not implementation-specific \ and not deprecated \ and not strict \ + $(EXTRA_MARKERS) +INSPIRCD_SELECTORS := \ + (foo or not foo) \ $(EXTRA_SELECTORS) -IRCU2_SELECTORS := \ +IRCU2_MARKERS := \ not implementation-specific \ and not deprecated \ and not strict \ + and not IRCv3 \ + $(EXTRA_MARKERS) +IRCU2_SELECTORS := \ + (foo or not foo) \ $(EXTRA_SELECTORS) -NEFARIOUS_SELECTORS := \ +NEFARIOUS_MARKERS := \ not implementation-specific \ and not deprecated \ and not strict \ + $(EXTRA_MARKERS) +NEFARIOUS_SELECTORS := \ + (foo or not foo) \ $(EXTRA_SELECTORS) -SNIRCD_SELECTORS := \ +SNIRCD_MARKERS := \ not implementation-specific \ and not deprecated \ and not strict \ + and not IRCv3 \ + $(EXTRA_MARKERS) +SNIRCD_SELECTORS := \ + (foo or not foo) \ $(EXTRA_SELECTORS) -IRC2_SELECTORS := \ +IRC2_MARKERS := \ not implementation-specific \ and not deprecated \ and not strict \ + and not IRCv3 \ + $(EXTRA_MARKERS) +IRC2_SELECTORS := \ + (foo or not foo) \ $(EXTRA_SELECTORS) -MAMMON_SELECTORS := \ +MAMMON_MARKERS := \ not implementation-specific \ and not deprecated \ and not strict \ + $(EXTRA_MARKERS) +MAMMON_SELECTORS := \ + (foo or not foo) \ $(EXTRA_SELECTORS) -NGIRCD_SELECTORS := \ +NGIRCD_MARKERS := \ not implementation-specific \ and not deprecated \ and not strict \ + $(EXTRA_MARKERS) +NGIRCD_SELECTORS := \ + (foo or not foo) \ $(EXTRA_SELECTORS) -PLEXUS4_SELECTORS := \ +PLEXUS4_MARKERS := \ not implementation-specific \ and not deprecated \ + $(EXTRA_MARKERS) +PLEXUS4_SELECTORS := \ + (foo or not foo) \ $(EXTRA_SELECTORS) -# Limnoria can actually pass all the test so there is none to exclude. -# `(foo or not foo)` serves as a `true` value so it doesn't break when -# $(EXTRA_SELECTORS) is non-empty +LIMNORIA_MARKERS := \ + not implementation-specific \ + $(EXTRA_MARKERS) LIMNORIA_SELECTORS := \ (foo or not foo) \ $(EXTRA_SELECTORS) # Tests marked with arbitrary_client_tags or react_tag can't pass because Sable does not support client tags yet -SABLE_SELECTORS := \ +SABLE_MARKERS := \ (Sable or not implementation-specific) \ and not deprecated \ and not strict \ and not arbitrary_client_tags \ and not react_tag \ - and not list and not lusers and not time and not info \ + $(EXTRA_MARKERS) +SABLE_SELECTORS := \ + not list and not lusers and not time and not info \ $(EXTRA_SELECTORS) -SOLANUM_SELECTORS := \ +SOLANUM_MARKERS := \ not implementation-specific \ and not deprecated \ and not strict \ + $(EXTRA_MARKERS) +SOLANUM_SELECTORS := \ + (foo or not foo) \ $(EXTRA_SELECTORS) -# Same as Limnoria +SOPEL_MARKERS := \ + not implementation-specific \ + $(EXTRA_MARKERS) SOPEL_SELECTORS := \ (foo or not foo) \ $(EXTRA_SELECTORS) -# TheLounge can actually pass all the test so there is none to exclude. -# `(foo or not foo)` serves as a `true` value so it doesn't break when -# $(EXTRA_SELECTORS) is non-empty +THELOUNGE_MARKERS := \ + not implementation-specific \ + $(EXTRA_MARKERS) THELOUNGE_SELECTORS := \ (foo or not foo) \ $(EXTRA_SELECTORS) @@ -116,13 +165,16 @@ THELOUNGE_SELECTORS := \ # 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 := \ +UNREALIRCD_MARKERS := \ not implementation-specific \ and not deprecated \ and not strict \ and not arbitrary_client_tags \ and not react_tag \ and not private_chathistory \ + $(EXTRA_MARKERS) +UNREALIRCD_SELECTORS := \ + (foo or not foo) \ $(EXTRA_SELECTORS) .PHONY: all flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sable sopel solanum unrealircd @@ -138,107 +190,114 @@ bahamut: -m 'not services' \ -n 4 \ -vv -s \ + -m 'not services and $(BAHAMUT_MARKERS)' -k '$(BAHAMUT_SELECTORS)' bahamut-atheme: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.bahamut \ --services-controller=irctest.controllers.atheme_services \ - -m 'services' \ + -m 'services and $(BAHAMUT_MARKERS)' \ -k '$(BAHAMUT_SELECTORS)' bahamut-anope: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.bahamut \ --services-controller=irctest.controllers.anope_services \ - -m 'services' \ + -m 'services and $(BAHAMUT_MARKERS)' \ -k '$(BAHAMUT_SELECTORS)' charybdis: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.charybdis \ --services-controller=irctest.controllers.atheme_services \ + -m '$(CHARYBDIS_MARKERS)' -k '$(CHARYBDIS_SELECTORS)' ergo: $(PYTEST) $(PYTEST_ARGS) \ --controller irctest.controllers.ergo \ + -m '$(ERGO_MARKERS)' -k "$(ERGO_SELECTORS)" hybrid: $(PYTEST) $(PYTEST_ARGS) \ --controller irctest.controllers.hybrid \ --services-controller=irctest.controllers.anope_services \ + -m '$(HYBRID_MARKERS)' -k "$(HYBRID_SELECTORS)" inspircd: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.inspircd \ - -m 'not services' \ + -m 'not services and $(INSPIRCD_MARKERS)' \ -k '$(INSPIRCD_SELECTORS)' inspircd-atheme: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.inspircd \ --services-controller=irctest.controllers.atheme_services \ - -m 'services' \ + -m 'services and $(INSPIRCD_MARKERS)' \ -k '$(INSPIRCD_SELECTORS)' inspircd-anope: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.inspircd \ --services-controller=irctest.controllers.anope_services \ - -m 'services' \ + -m 'services and $(INSPIRCD_MARKERS)' \ -k '$(INSPIRCD_SELECTORS)' ircu2: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.ircu2 \ - -m 'not services and not IRCv3' \ + -m 'not services and $(IRCU2_MARKERS)' \ -n 4 \ -k '$(IRCU2_SELECTORS)' nefarious: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.nefarious \ - -m 'not services' \ + -m 'not services and $(NEFARIOUS_MARKERS)' \ -n 4 \ -k '$(NEFARIOUS_SELECTORS)' snircd: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.snircd \ - -m 'not services and not IRCv3' \ + -m 'not services and $(SNIRCD_MARKERS)' \ -n 4 \ -k '$(SNIRCD_SELECTORS)' irc2: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.irc2 \ - -m 'not services and not IRCv3' \ + -m 'not services and $(IRCU2_MARKERS)' \ -n 4 \ -k '$(IRC2_SELECTORS)' limnoria: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.limnoria \ + -m '$(LIMNORIA_MARKERS)' \ -k '$(LIMNORIA_SELECTORS)' mammon: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.mammon \ + -m '$(MAMMON_MARKERS)' \ -k '$(MAMMON_SELECTORS)' plexus4: $(PYTEST) $(PYTEST_ARGS) \ --controller irctest.controllers.plexus4 \ --services-controller=irctest.controllers.anope_services \ + -m '$(PLEXUS4_MARKERS)' \ -k "$(PLEXUS4_SELECTORS)" ngircd: $(PYTEST) $(PYTEST_ARGS) \ --controller irctest.controllers.ngircd \ - -m 'not services' \ + -m 'not services and $(NGIRCD_MARKERS)' \ -n 4 \ -k "$(NGIRCD_SELECTORS)" @@ -246,19 +305,20 @@ ngircd-anope: $(PYTEST) $(PYTEST_ARGS) \ --controller irctest.controllers.ngircd \ --services-controller=irctest.controllers.anope_services \ - -m 'services' \ + -m 'services and $(NGIRCD_MARKERS)' \ -k "$(NGIRCD_SELECTORS)" ngircd-atheme: $(PYTEST) $(PYTEST_ARGS) \ --controller irctest.controllers.ngircd \ --services-controller=irctest.controllers.atheme_services \ - -m 'services' \ + -m 'services and $(NGIRCD_MARKERS)' \ -k "$(NGIRCD_SELECTORS)" sable: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.sable \ + -m '$(SABLE_MARKERS)' \ -n 20 \ -k '$(SABLE_SELECTORS)' @@ -266,22 +326,25 @@ solanum: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.solanum \ --services-controller=irctest.controllers.atheme_services \ + -m '$(SOLANUM_MARKERS)' \ -k '$(SOLANUM_SELECTORS)' sopel: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.sopel \ + -m '$(SOPEL_MARKERS)' \ -k '$(SOPEL_SELECTORS)' thelounge: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.thelounge \ + -m '$(THELOUNGE_MARKERS)' \ -k '$(THELOUNGE_SELECTORS)' unrealircd: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.unrealircd \ - -m 'not services' \ + -m 'not services and $(UNREALIRCD_MARKERS)' \ -k '$(UNREALIRCD_SELECTORS)' unrealircd-5: unrealircd @@ -290,19 +353,19 @@ unrealircd-atheme: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.unrealircd \ --services-controller=irctest.controllers.atheme_services \ - -m 'services' \ + -m 'services and $(UNREALIRCD_MARKERS)' \ -k '$(UNREALIRCD_SELECTORS)' unrealircd-anope: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.unrealircd \ --services-controller=irctest.controllers.anope_services \ - -m 'services' \ + -m 'services and $(UNREALIRCD_MARKERS)' \ -k '$(UNREALIRCD_SELECTORS)' unrealircd-dlk: pifpaf run mysql -- $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.unrealircd \ --services-controller=irctest.controllers.dlk_services \ - -m 'services' \ + -m 'services and $(UNREALIRCD_MARKERS)' \ -k '$(UNREALIRCD_SELECTORS)' From c97753da4e010baf2079a7493ebe4460aff531d5 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 8 Dec 2024 16:09:42 +0100 Subject: [PATCH 13/17] Don't run DM tests from SablePostgresqlHistoryTestCase --- Makefile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Makefile b/Makefile index ea5d3fa5..b270e809 100644 --- a/Makefile +++ b/Makefile @@ -127,6 +127,7 @@ LIMNORIA_SELECTORS := \ $(EXTRA_SELECTORS) # Tests marked with arbitrary_client_tags or react_tag can't pass because Sable does not support client tags yet +# 'SablePostgresqlHistoryTestCase and private_chathistory' disabled because Sable does not (yet?) persist private messages to postgresql SABLE_MARKERS := \ (Sable or not implementation-specific) \ and not deprecated \ @@ -136,6 +137,7 @@ SABLE_MARKERS := \ $(EXTRA_MARKERS) SABLE_SELECTORS := \ not list and not lusers and not time and not info \ + and not (SablePostgresqlHistoryTestCase and private_chathistory) \ $(EXTRA_SELECTORS) SOLANUM_MARKERS := \ From b843581e3f102c379e88b7edd31db61567ec7897 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 8 Dec 2024 17:23:30 +0100 Subject: [PATCH 14/17] sable: Rely on SASL PLAIN instead of NickServ to check for services availability --- irctest/controllers/sable.py | 37 ++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/irctest/controllers/sable.py b/irctest/controllers/sable.py index df6313a0..eb28be29 100644 --- a/irctest/controllers/sable.py +++ b/irctest/controllers/sable.py @@ -14,6 +14,7 @@ NotImplementedByController, ) from irctest.cases import BaseServerTestCase +from irctest.client_mock import ClientMock from irctest.exceptions import NoMessageException from irctest.patma import ANYSTR @@ -554,6 +555,42 @@ class SableServicesController(BaseServicesController): faketime_cmd: Sequence[str] + def wait_for_services(self) -> None: + """Overrides the default implementation, as it relies on + ``PRIVMSG NickServ: HELP``, which always succeeds on Sable. + + Instead, this relies on SASL PLAIN availability.""" + if self.services_up: + # Don't check again if they are already available + return + self.server_controller.wait_for_port() + + c = ClientMock(name="chkSASL", show_io=True) + c.connect(self.server_controller.hostname, self.server_controller.port) + + def wait() -> None: + while True: + c.sendLine("CAP LS 302") + for msg in c.getMessages(synchronize=False): + if msg.command == "CAP": + assert msg.params[-2] == "LS", msg + for cap in msg.params[-1].split(): + if cap.startswith("sasl="): + mechanisms = cap.split("=", 1)[1].split(",") + if "PLAIN" in mechanisms: + return + else: + if msg.params[0] == "*": + # End of CAP LS + time.sleep(self.server_controller.sync_sleep_time) + + wait() + + c.sendLine("QUIT") + c.getMessages() + c.disconnect() + self.services_up = True + def run(self, protocol: str, server_hostname: str, server_port: int) -> None: assert protocol == "sable" assert self.server_controller.directory is not None From a1437277d554891211f700222533db9c4886591d Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 8 Dec 2024 21:15:17 +0100 Subject: [PATCH 15/17] Wait for sable_history to be up --- .github/workflows/test-stable.yml | 2 +- irctest/basecontrollers.py | 1 + irctest/controllers/sable.py | 64 ++++++++++++++++++++++++++++++- workflows.yml | 2 +- 4 files changed, 66 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index b4e97334..9bc70d6a 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -1140,7 +1140,7 @@ jobs: uses: actions/checkout@v4 with: path: sable - ref: 0576ef4e572a973d577536875526897d43dc37a3 + ref: 034c4d5dd937774099773238d8d5b8054b015607 repository: Libera-Chat/sable - name: Install rust toolchain uses: actions-rs/toolchain@v1 diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index 58a1afb9..a7036d0f 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -316,6 +316,7 @@ class BaseServerController(_BaseController): def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) self.faketime_enabled = False + self.services_controller = None def run( self, diff --git a/irctest/controllers/sable.py b/irctest/controllers/sable.py index eb28be29..f41c3997 100644 --- a/irctest/controllers/sable.py +++ b/irctest/controllers/sable.py @@ -4,8 +4,9 @@ import signal import subprocess import tempfile +import threading import time -from typing import Optional, Sequence, Type +from typing import Any, Optional, Sequence, Type from irctest.basecontrollers import ( BaseServerController, @@ -372,6 +373,12 @@ class SableController(BaseServerController, DirectoryBasedController): """Sable processes commands very quickly, but responses for commands changing the state may be sent after later commands for messages which don't.""" + history_controller: Optional[BaseServicesController] = None + + def __init__(self, *args: Any, **kwargs: Any): + super().__init__(*args, **kwargs) + self.history_controller = None + def run( self, hostname: str, @@ -548,6 +555,19 @@ def registerUser( case.sendLine(client, "QUIT") case.assertDisconnected(client) + def wait_for_services(self) -> None: + # FIXME: this isn't called when sable_history is enabled but sable_services + # isn't. This doesn't happen with the existing tests so this isn't an issue yet + if self.services_controller is not None: + t1 = threading.Thread(target=self.services_controller.wait_for_services) + t1.start() + if self.history_controller is not None: + t2 = threading.Thread(target=self.history_controller.wait_for_services) + t2.start() + t2.join() + if self.services_controller is not None: + t1.join() + class SableServicesController(BaseServicesController): server_controller: SableController @@ -660,6 +680,48 @@ def run(self, protocol: str, server_hostname: str, server_port: int) -> None: ) self.pgroup_id = os.getpgid(self.proc.pid) + def wait_for_services(self) -> None: + """Overrides the default implementation, as it relies on + ``PRIVMSG NickServ: HELP``, which always succeeds on Sable. + + Instead, this relies on SASL PLAIN availability.""" + if self.services_up: + # Don't check again if they are already available + return + self.server_controller.wait_for_port() + + c = ClientMock(name="chkHist", show_io=True) + c.connect(self.server_controller.hostname, self.server_controller.port) + c.sendLine("NICK chkHist") + c.sendLine("USER chk chk chk chk") + time.sleep(self.server_controller.sync_sleep_time) + got_end_of_motd = False + while not got_end_of_motd: + for msg in c.getMessages(synchronize=False): + if msg.command == "PING": + c.sendLine("PONG :" + msg.params[0]) + if msg.command in ("376", "422"): # RPL_ENDOFMOTD / ERR_NOMOTD + got_end_of_motd = True + + def wait() -> None: + timeout = time.time() + 10 + while time.time() < timeout: + c.sendLine("LINKS") + time.sleep(self.server_controller.sync_sleep_time) + for msg in c.getMessages(synchronize=False): + if msg.command == "364": # RPL_LINKS + if msg.params[2] == "My.Little.History": + return + + raise Exception("History server is not available") + + wait() + + c.sendLine("QUIT") + c.getMessages() + c.disconnect() + self.services_up = True + def kill_proc(self) -> None: os.killpg(self.pgroup_id, signal.SIGKILL) super().kill_proc() diff --git a/workflows.yml b/workflows.yml index 766722b5..e3eaedb3 100644 --- a/workflows.yml +++ b/workflows.yml @@ -249,7 +249,7 @@ software: name: Sable repository: Libera-Chat/sable refs: - stable: 0576ef4e572a973d577536875526897d43dc37a3 + stable: 034c4d5dd937774099773238d8d5b8054b015607 release: null devel: master devel_release: null From 622527ea120da874d334734e405c31ad16748a62 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 8 Dec 2024 21:46:16 +0100 Subject: [PATCH 16/17] Enable debug logs --- .github/workflows/test-devel.yml | 2 +- .github/workflows/test-stable.yml | 2 +- workflows.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 680e3e3e..55246c60 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -1005,7 +1005,7 @@ jobs: IRCTEST_DEBUG_LOGS: ${{ runner.debug }} name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml --timeout 300' PATH=$HOME/.local/bin:$PATH - IRCTEST_POSTGRESQL_URL=postgresql:// PATH=$GITHUB_WORKSPACE/sable/target/debug/sbin:$GITHUB_WORKSPACE/sable/target/debug/bin:$GITHUB_WORKSPACE/sable/target/debug:$PATH + IRCTEST_POSTGRESQL_URL=postgresql:// IRCTEST_DEBUG_LOGS=1 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() diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 9bc70d6a..9016106f 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -1169,7 +1169,7 @@ jobs: IRCTEST_DEBUG_LOGS: ${{ runner.debug }} name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml --timeout 300' PATH=$HOME/.local/bin:$PATH - IRCTEST_POSTGRESQL_URL=postgresql:// PATH=$GITHUB_WORKSPACE/sable/target/debug/sbin:$GITHUB_WORKSPACE/sable/target/debug/bin:$GITHUB_WORKSPACE/sable/target/debug:$PATH + IRCTEST_POSTGRESQL_URL=postgresql:// IRCTEST_DEBUG_LOGS=1 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() diff --git a/workflows.yml b/workflows.yml index e3eaedb3..be8830b5 100644 --- a/workflows.yml +++ b/workflows.yml @@ -270,7 +270,7 @@ software: - run: rustc --version - run: start postgresql run: "sudo systemctl start postgresql.service" - env: "IRCTEST_POSTGRESQL_URL=postgresql://" + env: "IRCTEST_POSTGRESQL_URL=postgresql:// IRCTEST_DEBUG_LOGS=1" separate_build_job: false build_script: | cd $GITHUB_WORKSPACE/sable/ From 6f1f54b9b8482f8191e0d6ca6a79108143497391 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 8 Dec 2024 22:09:47 +0100 Subject: [PATCH 17/17] Host is required --- .github/workflows/test-devel.yml | 2 +- .github/workflows/test-stable.yml | 2 +- workflows.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 55246c60..ae88351f 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -1005,7 +1005,7 @@ jobs: IRCTEST_DEBUG_LOGS: ${{ runner.debug }} name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml --timeout 300' PATH=$HOME/.local/bin:$PATH - IRCTEST_POSTGRESQL_URL=postgresql:// IRCTEST_DEBUG_LOGS=1 PATH=$GITHUB_WORKSPACE/sable/target/debug/sbin:$GITHUB_WORKSPACE/sable/target/debug/bin:$GITHUB_WORKSPACE/sable/target/debug:$PATH + IRCTEST_POSTGRESQL_URL=postgresql://localhost IRCTEST_DEBUG_LOGS=1 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() diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 9016106f..d1732ad1 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -1169,7 +1169,7 @@ jobs: IRCTEST_DEBUG_LOGS: ${{ runner.debug }} name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml --timeout 300' PATH=$HOME/.local/bin:$PATH - IRCTEST_POSTGRESQL_URL=postgresql:// IRCTEST_DEBUG_LOGS=1 PATH=$GITHUB_WORKSPACE/sable/target/debug/sbin:$GITHUB_WORKSPACE/sable/target/debug/bin:$GITHUB_WORKSPACE/sable/target/debug:$PATH + IRCTEST_POSTGRESQL_URL=postgresql://localhost IRCTEST_DEBUG_LOGS=1 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() diff --git a/workflows.yml b/workflows.yml index be8830b5..4c64127a 100644 --- a/workflows.yml +++ b/workflows.yml @@ -270,7 +270,7 @@ software: - run: rustc --version - run: start postgresql run: "sudo systemctl start postgresql.service" - env: "IRCTEST_POSTGRESQL_URL=postgresql:// IRCTEST_DEBUG_LOGS=1" + env: "IRCTEST_POSTGRESQL_URL=postgresql://localhost IRCTEST_DEBUG_LOGS=1" separate_build_job: false build_script: | cd $GITHUB_WORKSPACE/sable/