18 Commits

Author SHA1 Message Date
6f1f54b9b8 Host is required 2024-12-08 22:09:47 +01:00
622527ea12 Enable debug logs 2024-12-08 21:46:16 +01:00
a1437277d5 Wait for sable_history to be up 2024-12-08 21:44:28 +01:00
b843581e3f sable: Rely on SASL PLAIN instead of NickServ to check for services availability 2024-12-08 17:46:17 +01:00
c97753da4e Don't run DM tests from SablePostgresqlHistoryTestCase 2024-12-08 16:10:50 +01:00
4cfb7665c6 Use -m instead of -k to filter markers
-k uses substring match, so -k Sable matched ErgoUtf8NickDisabledTestCase
2024-12-08 14:54:19 +01:00
e0b4aec24a Bump sable 2024-12-08 09:42:08 +01:00
388506c3d4 Fix typo in Ergo selectors 2024-12-08 09:36:07 +01:00
a0a859d4fa Fix stderr/stdout typo 2024-11-19 18:01:32 +01:00
a1bb20323a Start postgresql 2024-11-17 18:11:38 +01:00
895849ed93 Fix lint 2024-11-17 18:11:38 +01:00
4983db7cad Merge branch 'master' into sable-pg-history 2024-11-17 18:00:47 +01:00
a9c87eae91 Bump Sable 2024-11-17 17:45:05 +01:00
0dc5a0fdda Need fanout > 1 when more than 2 nodes.
And restore faketime config to previous value, as that wasn't the right fix
2024-11-10 18:52:07 +01:00
827c6a6df4 Actually print logs from sable_services and sable_history 2024-11-10 17:08:01 +01:00
396841ebff Another workaround for Sable's latency with the history server 2024-10-28 20:23:07 +01:00
f350ff0b96 Make tests less flaky on Sable 2024-10-27 18:35:54 +01:00
b274cad65b Add tests for Sable's postgresql chathistory backend 2024-10-27 18:17:57 +01:00
10 changed files with 589 additions and 104 deletions

View File

@ -990,6 +990,7 @@ jobs:
cache-on-failure: true cache-on-failure: true
workspaces: sable -> target workspaces: sable -> target
- run: rustc --version - run: rustc --version
- run: sudo systemctl start postgresql.service
- name: Build Sable - name: Build Sable
run: | run: |
cd $GITHUB_WORKSPACE/sable/ cd $GITHUB_WORKSPACE/sable/
@ -1003,7 +1004,8 @@ jobs:
- env: - env:
IRCTEST_DEBUG_LOGS: ${{ runner.debug }} IRCTEST_DEBUG_LOGS: ${{ runner.debug }}
name: Test with pytest 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://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 make sable
timeout-minutes: 30 timeout-minutes: 30
- if: always() - if: always()

View File

@ -1140,7 +1140,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
path: sable path: sable
ref: 52397dc9e0f27c3ed197f984c00f06639870716d ref: 034c4d5dd937774099773238d8d5b8054b015607
repository: Libera-Chat/sable repository: Libera-Chat/sable
- name: Install rust toolchain - name: Install rust toolchain
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
@ -1154,6 +1154,7 @@ jobs:
cache-on-failure: true cache-on-failure: true
workspaces: sable -> target workspaces: sable -> target
- run: rustc --version - run: rustc --version
- run: sudo systemctl start postgresql.service
- name: Build Sable - name: Build Sable
run: | run: |
cd $GITHUB_WORKSPACE/sable/ cd $GITHUB_WORKSPACE/sable/
@ -1167,7 +1168,8 @@ jobs:
- env: - env:
IRCTEST_DEBUG_LOGS: ${{ runner.debug }} IRCTEST_DEBUG_LOGS: ${{ runner.debug }}
name: Test with pytest 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://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 make sable
timeout-minutes: 30 timeout-minutes: 30
- if: always() - if: always()

188
Makefile
View File

@ -4,109 +4,161 @@ PYTEST ?= python3 -m pytest
# pytest-xdist is installed) # pytest-xdist is installed)
PYTEST_ARGS ?= 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 # Will be appended at the end of the -k argument to pytest
EXTRA_SELECTORS ?= EXTRA_SELECTORS ?=
BAHAMUT_SELECTORS := \ BAHAMUT_MARKERS := \
not Ergo \ not implementation-specific \
and not deprecated \ and not deprecated \
and not strict \ and not strict \
and not IRCv3 \ and not IRCv3 \
$(EXTRA_MARKERS)
BAHAMUT_SELECTORS := \
(foo or not foo) \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
CHARYBDIS_MARKERS := \
not implementation-specific \
and not deprecated \
and not strict \
$(EXTRA_MARKERS)
CHARYBDIS_SELECTORS := \ CHARYBDIS_SELECTORS := \
not Ergo \ (foo or not foo) \
and not deprecated \
and not strict \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
ERGO_MARKERS := \
(Ergo or not implementation-specific) \
and not deprecated \
$(EXTRA_MARKERS)
ERGO_SELECTORS := \ ERGO_SELECTORS := \
not deprecated \ (foo or not foo) \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
HYBRID_MARKERS := \
not implementation-specific \
and not deprecated \
$(EXTRA_MARKERS)
HYBRID_SELECTORS := \ HYBRID_SELECTORS := \
not Ergo \ (foo or not foo) \
and not deprecated \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
INSPIRCD_MARKERS := \
not implementation-specific \
and not deprecated \
and not strict \
$(EXTRA_MARKERS)
INSPIRCD_SELECTORS := \ INSPIRCD_SELECTORS := \
not Ergo \ (foo or not foo) \
and not deprecated \
and not strict \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
IRCU2_MARKERS := \
not implementation-specific \
and not deprecated \
and not strict \
and not IRCv3 \
$(EXTRA_MARKERS)
IRCU2_SELECTORS := \ IRCU2_SELECTORS := \
not Ergo \ (foo or not foo) \
and not deprecated \
and not strict \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
NEFARIOUS_MARKERS := \
not implementation-specific \
and not deprecated \
and not strict \
$(EXTRA_MARKERS)
NEFARIOUS_SELECTORS := \ NEFARIOUS_SELECTORS := \
not Ergo \ (foo or not foo) \
and not deprecated \
and not strict \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
SNIRCD_MARKERS := \
not implementation-specific \
and not deprecated \
and not strict \
and not IRCv3 \
$(EXTRA_MARKERS)
SNIRCD_SELECTORS := \ SNIRCD_SELECTORS := \
not Ergo \ (foo or not foo) \
and not deprecated \
and not strict \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
IRC2_MARKERS := \
not implementation-specific \
and not deprecated \
and not strict \
and not IRCv3 \
$(EXTRA_MARKERS)
IRC2_SELECTORS := \ IRC2_SELECTORS := \
not Ergo \ (foo or not foo) \
and not deprecated \
and not strict \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
MAMMON_MARKERS := \
not implementation-specific \
and not deprecated \
and not strict \
$(EXTRA_MARKERS)
MAMMON_SELECTORS := \ MAMMON_SELECTORS := \
not Ergo \ (foo or not foo) \
and not deprecated \
and not strict \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
NGIRCD_MARKERS := \
not implementation-specific \
and not deprecated \
and not strict \
$(EXTRA_MARKERS)
NGIRCD_SELECTORS := \ NGIRCD_SELECTORS := \
not Ergo \ (foo or not foo) \
and not deprecated \
and not strict \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
PLEXUS4_MARKERS := \
not implementation-specific \
and not deprecated \
$(EXTRA_MARKERS)
PLEXUS4_SELECTORS := \ PLEXUS4_SELECTORS := \
not Ergo \ (foo or not foo) \
and not deprecated \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
# Limnoria can actually pass all the test so there is none to exclude. LIMNORIA_MARKERS := \
# `(foo or not foo)` serves as a `true` value so it doesn't break when not implementation-specific \
# $(EXTRA_SELECTORS) is non-empty $(EXTRA_MARKERS)
LIMNORIA_SELECTORS := \ LIMNORIA_SELECTORS := \
(foo or not foo) \ (foo or not foo) \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
# Tests marked with arbitrary_client_tags or react_tag can't pass because Sable does not support client tags yet # Tests marked with arbitrary_client_tags or react_tag can't pass because Sable does not support client tags yet
SABLE_SELECTORS := \ # 'SablePostgresqlHistoryTestCase and private_chathistory' disabled because Sable does not (yet?) persist private messages to postgresql
not Ergo \ SABLE_MARKERS := \
(Sable or not implementation-specific) \
and not deprecated \ and not deprecated \
and not strict \ and not strict \
and not arbitrary_client_tags \ and not arbitrary_client_tags \
and not react_tag \ 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 \
and not (SablePostgresqlHistoryTestCase and private_chathistory) \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
SOLANUM_SELECTORS := \ SOLANUM_MARKERS := \
not Ergo \ not implementation-specific \
and not deprecated \ and not deprecated \
and not strict \ and not strict \
$(EXTRA_MARKERS)
SOLANUM_SELECTORS := \
(foo or not foo) \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
# Same as Limnoria SOPEL_MARKERS := \
not implementation-specific \
$(EXTRA_MARKERS)
SOPEL_SELECTORS := \ SOPEL_SELECTORS := \
(foo or not foo) \ (foo or not foo) \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
# TheLounge can actually pass all the test so there is none to exclude. THELOUNGE_MARKERS := \
# `(foo or not foo)` serves as a `true` value so it doesn't break when not implementation-specific \
# $(EXTRA_SELECTORS) is non-empty $(EXTRA_MARKERS)
THELOUNGE_SELECTORS := \ THELOUNGE_SELECTORS := \
(foo or not foo) \ (foo or not foo) \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
@ -115,13 +167,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 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 # Tests marked with private_chathistory can't pass because Unreal does not implement CHATHISTORY for DMs
UNREALIRCD_SELECTORS := \ UNREALIRCD_MARKERS := \
not Ergo \ not implementation-specific \
and not deprecated \ and not deprecated \
and not strict \ and not strict \
and not arbitrary_client_tags \ and not arbitrary_client_tags \
and not react_tag \ and not react_tag \
and not private_chathistory \ and not private_chathistory \
$(EXTRA_MARKERS)
UNREALIRCD_SELECTORS := \
(foo or not foo) \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
.PHONY: all flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sable sopel solanum unrealircd .PHONY: all flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sable sopel solanum unrealircd
@ -137,107 +192,114 @@ bahamut:
-m 'not services' \ -m 'not services' \
-n 4 \ -n 4 \
-vv -s \ -vv -s \
-m 'not services and $(BAHAMUT_MARKERS)'
-k '$(BAHAMUT_SELECTORS)' -k '$(BAHAMUT_SELECTORS)'
bahamut-atheme: bahamut-atheme:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.bahamut \ --controller=irctest.controllers.bahamut \
--services-controller=irctest.controllers.atheme_services \ --services-controller=irctest.controllers.atheme_services \
-m 'services' \ -m 'services and $(BAHAMUT_MARKERS)' \
-k '$(BAHAMUT_SELECTORS)' -k '$(BAHAMUT_SELECTORS)'
bahamut-anope: bahamut-anope:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.bahamut \ --controller=irctest.controllers.bahamut \
--services-controller=irctest.controllers.anope_services \ --services-controller=irctest.controllers.anope_services \
-m 'services' \ -m 'services and $(BAHAMUT_MARKERS)' \
-k '$(BAHAMUT_SELECTORS)' -k '$(BAHAMUT_SELECTORS)'
charybdis: charybdis:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.charybdis \ --controller=irctest.controllers.charybdis \
--services-controller=irctest.controllers.atheme_services \ --services-controller=irctest.controllers.atheme_services \
-m '$(CHARYBDIS_MARKERS)'
-k '$(CHARYBDIS_SELECTORS)' -k '$(CHARYBDIS_SELECTORS)'
ergo: ergo:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller irctest.controllers.ergo \ --controller irctest.controllers.ergo \
-m '$(ERGO_MARKERS)'
-k "$(ERGO_SELECTORS)" -k "$(ERGO_SELECTORS)"
hybrid: hybrid:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller irctest.controllers.hybrid \ --controller irctest.controllers.hybrid \
--services-controller=irctest.controllers.anope_services \ --services-controller=irctest.controllers.anope_services \
-m '$(HYBRID_MARKERS)'
-k "$(HYBRID_SELECTORS)" -k "$(HYBRID_SELECTORS)"
inspircd: inspircd:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.inspircd \ --controller=irctest.controllers.inspircd \
-m 'not services' \ -m 'not services and $(INSPIRCD_MARKERS)' \
-k '$(INSPIRCD_SELECTORS)' -k '$(INSPIRCD_SELECTORS)'
inspircd-atheme: inspircd-atheme:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.inspircd \ --controller=irctest.controllers.inspircd \
--services-controller=irctest.controllers.atheme_services \ --services-controller=irctest.controllers.atheme_services \
-m 'services' \ -m 'services and $(INSPIRCD_MARKERS)' \
-k '$(INSPIRCD_SELECTORS)' -k '$(INSPIRCD_SELECTORS)'
inspircd-anope: inspircd-anope:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.inspircd \ --controller=irctest.controllers.inspircd \
--services-controller=irctest.controllers.anope_services \ --services-controller=irctest.controllers.anope_services \
-m 'services' \ -m 'services and $(INSPIRCD_MARKERS)' \
-k '$(INSPIRCD_SELECTORS)' -k '$(INSPIRCD_SELECTORS)'
ircu2: ircu2:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.ircu2 \ --controller=irctest.controllers.ircu2 \
-m 'not services and not IRCv3' \ -m 'not services and $(IRCU2_MARKERS)' \
-n 4 \ -n 4 \
-k '$(IRCU2_SELECTORS)' -k '$(IRCU2_SELECTORS)'
nefarious: nefarious:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.nefarious \ --controller=irctest.controllers.nefarious \
-m 'not services' \ -m 'not services and $(NEFARIOUS_MARKERS)' \
-n 4 \ -n 4 \
-k '$(NEFARIOUS_SELECTORS)' -k '$(NEFARIOUS_SELECTORS)'
snircd: snircd:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.snircd \ --controller=irctest.controllers.snircd \
-m 'not services and not IRCv3' \ -m 'not services and $(SNIRCD_MARKERS)' \
-n 4 \ -n 4 \
-k '$(SNIRCD_SELECTORS)' -k '$(SNIRCD_SELECTORS)'
irc2: irc2:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.irc2 \ --controller=irctest.controllers.irc2 \
-m 'not services and not IRCv3' \ -m 'not services and $(IRCU2_MARKERS)' \
-n 4 \ -n 4 \
-k '$(IRC2_SELECTORS)' -k '$(IRC2_SELECTORS)'
limnoria: limnoria:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.limnoria \ --controller=irctest.controllers.limnoria \
-m '$(LIMNORIA_MARKERS)' \
-k '$(LIMNORIA_SELECTORS)' -k '$(LIMNORIA_SELECTORS)'
mammon: mammon:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.mammon \ --controller=irctest.controllers.mammon \
-m '$(MAMMON_MARKERS)' \
-k '$(MAMMON_SELECTORS)' -k '$(MAMMON_SELECTORS)'
plexus4: plexus4:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller irctest.controllers.plexus4 \ --controller irctest.controllers.plexus4 \
--services-controller=irctest.controllers.anope_services \ --services-controller=irctest.controllers.anope_services \
-m '$(PLEXUS4_MARKERS)' \
-k "$(PLEXUS4_SELECTORS)" -k "$(PLEXUS4_SELECTORS)"
ngircd: ngircd:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller irctest.controllers.ngircd \ --controller irctest.controllers.ngircd \
-m 'not services' \ -m 'not services and $(NGIRCD_MARKERS)' \
-n 4 \ -n 4 \
-k "$(NGIRCD_SELECTORS)" -k "$(NGIRCD_SELECTORS)"
@ -245,19 +307,20 @@ ngircd-anope:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller irctest.controllers.ngircd \ --controller irctest.controllers.ngircd \
--services-controller=irctest.controllers.anope_services \ --services-controller=irctest.controllers.anope_services \
-m 'services' \ -m 'services and $(NGIRCD_MARKERS)' \
-k "$(NGIRCD_SELECTORS)" -k "$(NGIRCD_SELECTORS)"
ngircd-atheme: ngircd-atheme:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller irctest.controllers.ngircd \ --controller irctest.controllers.ngircd \
--services-controller=irctest.controllers.atheme_services \ --services-controller=irctest.controllers.atheme_services \
-m 'services' \ -m 'services and $(NGIRCD_MARKERS)' \
-k "$(NGIRCD_SELECTORS)" -k "$(NGIRCD_SELECTORS)"
sable: sable:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.sable \ --controller=irctest.controllers.sable \
-m '$(SABLE_MARKERS)' \
-n 20 \ -n 20 \
-k '$(SABLE_SELECTORS)' -k '$(SABLE_SELECTORS)'
@ -265,22 +328,25 @@ solanum:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.solanum \ --controller=irctest.controllers.solanum \
--services-controller=irctest.controllers.atheme_services \ --services-controller=irctest.controllers.atheme_services \
-m '$(SOLANUM_MARKERS)' \
-k '$(SOLANUM_SELECTORS)' -k '$(SOLANUM_SELECTORS)'
sopel: sopel:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.sopel \ --controller=irctest.controllers.sopel \
-m '$(SOPEL_MARKERS)' \
-k '$(SOPEL_SELECTORS)' -k '$(SOPEL_SELECTORS)'
thelounge: thelounge:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.thelounge \ --controller=irctest.controllers.thelounge \
-m '$(THELOUNGE_MARKERS)' \
-k '$(THELOUNGE_SELECTORS)' -k '$(THELOUNGE_SELECTORS)'
unrealircd: unrealircd:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.unrealircd \ --controller=irctest.controllers.unrealircd \
-m 'not services' \ -m 'not services and $(UNREALIRCD_MARKERS)' \
-k '$(UNREALIRCD_SELECTORS)' -k '$(UNREALIRCD_SELECTORS)'
unrealircd-5: unrealircd unrealircd-5: unrealircd
@ -289,19 +355,19 @@ unrealircd-atheme:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.unrealircd \ --controller=irctest.controllers.unrealircd \
--services-controller=irctest.controllers.atheme_services \ --services-controller=irctest.controllers.atheme_services \
-m 'services' \ -m 'services and $(UNREALIRCD_MARKERS)' \
-k '$(UNREALIRCD_SELECTORS)' -k '$(UNREALIRCD_SELECTORS)'
unrealircd-anope: unrealircd-anope:
$(PYTEST) $(PYTEST_ARGS) \ $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.unrealircd \ --controller=irctest.controllers.unrealircd \
--services-controller=irctest.controllers.anope_services \ --services-controller=irctest.controllers.anope_services \
-m 'services' \ -m 'services and $(UNREALIRCD_MARKERS)' \
-k '$(UNREALIRCD_SELECTORS)' -k '$(UNREALIRCD_SELECTORS)'
unrealircd-dlk: unrealircd-dlk:
pifpaf run mysql -- $(PYTEST) $(PYTEST_ARGS) \ pifpaf run mysql -- $(PYTEST) $(PYTEST_ARGS) \
--controller=irctest.controllers.unrealircd \ --controller=irctest.controllers.unrealircd \
--services-controller=irctest.controllers.dlk_services \ --services-controller=irctest.controllers.dlk_services \
-m 'services' \ -m 'services and $(UNREALIRCD_MARKERS)' \
-k '$(UNREALIRCD_SELECTORS)' -k '$(UNREALIRCD_SELECTORS)'

View File

@ -8,8 +8,10 @@ from pathlib import Path
import shutil import shutil
import socket import socket
import subprocess import subprocess
import sys
import tempfile import tempfile
import textwrap import textwrap
import threading
import time import time
from typing import ( from typing import (
IO, IO,
@ -67,6 +69,9 @@ class TestCaseControllerConfig:
This should be used as little as possible, using the other attributes instead; This should be used as little as possible, using the other attributes instead;
as they are work with any controller.""" as they are work with any controller."""
sable_history_server: bool = False
"""Whether to start Sable's long-term history server"""
class _BaseController: class _BaseController:
"""Base class for software controllers. """Base class for software controllers.
@ -145,10 +150,48 @@ class _BaseController:
self._own_ports.remove((hostname, port)) self._own_ports.remove((hostname, port))
def execute( def execute(
self, command: Sequence[Union[str, Path]], **kwargs: Any self,
command: Sequence[Union[str, Path]],
proc_name: Optional[str] = None,
**kwargs: Any,
) -> subprocess.Popen: ) -> subprocess.Popen:
output_to = None if self.debug_mode else subprocess.DEVNULL 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 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() -> None:
assert proc.stdout is not None # for mypy
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["stderr"] = subprocess.PIPE
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()
if stream_stderr is not None:
threading.Thread(target=stream_stderr, name="stream_stderr").start()
return proc
class DirectoryBasedController(_BaseController): class DirectoryBasedController(_BaseController):
@ -273,6 +316,7 @@ class BaseServerController(_BaseController):
def __init__(self, *args: Any, **kwargs: Any): def __init__(self, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.faketime_enabled = False self.faketime_enabled = False
self.services_controller = None
def run( def run(
self, self,

View File

@ -842,16 +842,22 @@ def mark_services(cls: TClass) -> TClass:
def mark_specifications( def mark_specifications(
*specifications_str: str, deprecated: bool = False, strict: bool = False *specifications_str: str, deprecated: bool = False, strict: bool = False
) -> Callable[[TCallable], TCallable]: ) -> Callable[[TCallable], TCallable]:
specifications = frozenset( specifications = {
Specifications.from_name(s) if isinstance(s, str) else s Specifications.from_name(s) if isinstance(s, str) else s
for s in specifications_str for s in specifications_str
) }
if None in specifications: if None in specifications:
raise ValueError("Invalid set of specifications: {}".format(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: def decorator(f: TCallable) -> TCallable:
for specification in specifications: for specification in specifications:
f = getattr(pytest.mark, specification.value)(f) f = getattr(pytest.mark, specification.value)(f)
if is_implementation_specific:
f = getattr(pytest.mark, "implementation-specific")(f)
if strict: if strict:
f = pytest.mark.strict(f) f = pytest.mark.strict(f)
if deprecated: if deprecated:

View File

@ -4,8 +4,9 @@ import shutil
import signal import signal
import subprocess import subprocess
import tempfile import tempfile
import threading
import time import time
from typing import Optional, Type from typing import Any, Optional, Sequence, Type
from irctest.basecontrollers import ( from irctest.basecontrollers import (
BaseServerController, BaseServerController,
@ -14,6 +15,7 @@ from irctest.basecontrollers import (
NotImplementedByController, NotImplementedByController,
) )
from irctest.cases import BaseServerTestCase from irctest.cases import BaseServerTestCase
from irctest.client_mock import ClientMock
from irctest.exceptions import NoMessageException from irctest.exceptions import NoMessageException
from irctest.patma import ANYSTR from irctest.patma import ANYSTR
@ -85,7 +87,13 @@ def certs_dir() -> Path:
certs_dir = tempfile.TemporaryDirectory() certs_dir = tempfile.TemporaryDirectory()
(Path(certs_dir.name) / "gen_certs.sh").write_text(GEN_CERTS) (Path(certs_dir.name) / "gen_certs.sh").write_text(GEN_CERTS)
subprocess.run( 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, cwd=certs_dir.name,
check=True, check=True,
) )
@ -95,10 +103,11 @@ def certs_dir() -> Path:
NETWORK_CONFIG = """ NETWORK_CONFIG = """
{ {
"fanout": 1, "fanout": 2,
"ca_file": "%(certs_dir)s/ca_cert.pem", "ca_file": "%(certs_dir)s/ca_cert.pem",
"peers": [ "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.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" } { "name": "My.Little.Server", "address": "%(server1_hostname)s:%(server1_port)s", "fingerprint": "%(server1_cert_sha1)s" }
] ]
@ -107,7 +116,7 @@ NETWORK_CONFIG = """
NETWORK_CONFIG_CONFIG = """ NETWORK_CONFIG_CONFIG = """
{ {
"object_expiry": 300, "object_expiry": 60, // 1 minute
"opers": [ "opers": [
{ {
@ -219,6 +228,58 @@ SERVER_CONFIG = """
} }
""" """
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" ]
}
]
}
}
"""
SERVICES_CONFIG = """ SERVICES_CONFIG = """
{ {
"server_id": 99, "server_id": 99,
@ -297,7 +358,7 @@ SERVICES_CONFIG = """
{ {
"target": "stdout", "target": "stdout",
"level": "debug", "level": "debug",
"modules": [ "sable_services" ] "modules": [ "sable" ]
} }
] ]
} }
@ -312,6 +373,12 @@ class SableController(BaseServerController, DirectoryBasedController):
"""Sable processes commands very quickly, but responses for commands changing the """Sable processes commands very quickly, but responses for commands changing the
state may be sent after later commands for messages which don't.""" 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( def run(
self, self,
hostname: str, hostname: str,
@ -348,10 +415,11 @@ class SableController(BaseServerController, DirectoryBasedController):
(server1_hostname, server1_port) = self.get_hostname_and_port() (server1_hostname, server1_port) = self.get_hostname_and_port()
(services_hostname, services_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, # Sable requires inbound connections to match the configured hostname,
# so we can't configure 0.0.0.0 # 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, server1_management_hostname,
@ -361,6 +429,10 @@ class SableController(BaseServerController, DirectoryBasedController):
services_management_hostname, services_management_hostname,
services_management_port, services_management_port,
) = self.get_hostname_and_port() ) = self.get_hostname_and_port()
(
history_management_hostname,
history_management_port,
) = self.get_hostname_and_port()
self.template_vars = dict( self.template_vars = dict(
certs_dir=certs_dir(), certs_dir=certs_dir(),
@ -381,6 +453,13 @@ class SableController(BaseServerController, DirectoryBasedController):
services_management_hostname=services_management_hostname, services_management_hostname=services_management_hostname,
services_management_port=services_management_port, services_management_port=services_management_port,
services_alias_users=SERVICES_ALIAS_USERS if run_services else "", 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: with self.open_file("configs/network.conf") as fd:
@ -411,17 +490,28 @@ class SableController(BaseServerController, DirectoryBasedController):
cwd=self.directory, cwd=self.directory,
preexec_fn=os.setsid, preexec_fn=os.setsid,
env={"RUST_BACKTRACE": "1", **os.environ}, env={"RUST_BACKTRACE": "1", **os.environ},
proc_name="sable_ircd ",
) )
self.pgroup_id = os.getpgid(self.proc.pid) self.pgroup_id = os.getpgid(self.proc.pid)
if run_services: if run_services:
self.services_controller = SableServicesController(self.test_config, self) self.services_controller = SableServicesController(self.test_config, self)
self.services_controller.faketime_cmd = faketime_cmd
self.services_controller.run( self.services_controller.run(
protocol="sable", protocol="sable",
server_hostname=services_hostname, server_hostname=services_hostname,
server_port=services_port, 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: def kill_proc(self) -> None:
os.killpg(self.pgroup_id, signal.SIGKILL) os.killpg(self.pgroup_id, signal.SIGKILL)
super().kill_proc() super().kill_proc()
@ -465,11 +555,62 @@ class SableController(BaseServerController, DirectoryBasedController):
case.sendLine(client, "QUIT") case.sendLine(client, "QUIT")
case.assertDisconnected(client) 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): class SableServicesController(BaseServicesController):
server_controller: SableController server_controller: SableController
software_name = "Sable Services" software_name = "Sable Services"
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: def run(self, protocol: str, server_hostname: str, server_port: int) -> None:
assert protocol == "sable" assert protocol == "sable"
assert self.server_controller.directory is not None assert self.server_controller.directory is not None
@ -479,6 +620,7 @@ class SableServicesController(BaseServicesController):
self.proc = self.execute( self.proc = self.execute(
[ [
*self.faketime_cmd,
"sable_services", "sable_services",
"--foreground", "--foreground",
"--server-conf", "--server-conf",
@ -489,6 +631,7 @@ class SableServicesController(BaseServicesController):
cwd=self.server_controller.directory, cwd=self.server_controller.directory,
preexec_fn=os.setsid, preexec_fn=os.setsid,
env={"RUST_BACKTRACE": "1", **os.environ}, env={"RUST_BACKTRACE": "1", **os.environ},
proc_name="sable_services",
) )
self.pgroup_id = os.getpgid(self.proc.pid) self.pgroup_id = os.getpgid(self.proc.pid)
@ -497,5 +640,92 @@ class SableServicesController(BaseServicesController):
super().kill_proc() 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:
vals = dict(self.server_controller.template_vars)
vals["history_db_url"] = history_db_url
fd.write(HISTORY_SERVER_CONFIG % vals)
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},
proc_name="sable_history ",
)
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()
def get_irctest_controller_class() -> Type[SableController]: def get_irctest_controller_class() -> Type[SableController]:
return SableController return SableController

View File

@ -2,6 +2,7 @@
`IRCv3 draft chathistory <https://ircv3.net/specs/extensions/chathistory>`_ `IRCv3 draft chathistory <https://ircv3.net/specs/extensions/chathistory>`_
""" """
import dataclasses
import functools import functools
import secrets import secrets
import time import time
@ -31,10 +32,22 @@ def skip_ngircd(f):
return newf return newf
@cases.mark_specifications("IRCv3") class _BaseChathistoryTests(cases.BaseServerTestCase):
@cases.mark_services def _wait_before_chathistory(self):
class ChathistoryTestCase(cases.BaseServerTestCase): """Hook for the Sable-specific tests that check the postgresql-based
def validate_chathistory_batch(self, msgs, target): 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, 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 (start, *inner_msgs, end) = msgs
self.assertMessageMatch( self.assertMessageMatch(
@ -94,9 +107,13 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
self.joinChannel(qux, real_chname) self.joinChannel(qux, real_chname)
self.getMessages(qux) self.getMessages(qux)
self._wait_before_chathistory()
# test a nonexistent channel # test a nonexistent channel
self.sendLine(bar, "CHATHISTORY LATEST #nonexistent_channel * 10") 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 msgs = [msg for msg in msgs if msg.command != "MODE"] # :NickServ MODE +r
self.assertMessageMatch( self.assertMessageMatch(
msgs[0], msgs[0],
@ -106,7 +123,9 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
# as should a real channel to which one is not joined: # as should a real channel to which one is not joined:
self.sendLine(bar, "CHATHISTORY LATEST %s * 10" % (real_chname,)) 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( self.assertMessageMatch(
msgs[0], msgs[0],
command="FAIL", command="FAIL",
@ -175,6 +194,8 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
messages.append(echo.to_history_message()) messages.append(echo.to_history_message())
self.assertEqual(echo.to_history_message(), delivery.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,)) self.sendLine(bar, "CHATHISTORY LATEST %s * 10" % (bar,))
replies = [msg for msg in self.getMessages(bar) if msg.command == "PRIVMSG"] replies = [msg for msg in self.getMessages(bar) if msg.command == "PRIVMSG"]
self.assertEqual([msg.to_history_message() for msg in replies], messages) self.assertEqual([msg.to_history_message() for msg in replies], messages)
@ -225,9 +246,12 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
echo_messages.extend( echo_messages.extend(
msg.to_history_message() for msg in self.getMessages(1) 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) self.validate_echo_messages(NUM_MESSAGES, echo_messages)
self._wait_before_chathistory()
self.validate_chathistory(subcommand, echo_messages, 1, chname) self.validate_chathistory(subcommand, echo_messages, 1, chname)
@skip_ngircd @skip_ngircd
@ -264,6 +288,8 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
) )
time.sleep(0.002) time.sleep(0.002)
self._wait_before_chathistory()
self.validate_echo_messages(NUM_MESSAGES, echo_messages) self.validate_echo_messages(NUM_MESSAGES, echo_messages)
self.sendLine(1, "CHATHISTORY LATEST %s * 100" % chname) self.sendLine(1, "CHATHISTORY LATEST %s * 100" % chname)
(batch_open, *messages, batch_close) = self.getMessages(1) (batch_open, *messages, batch_close) = self.getMessages(1)
@ -308,6 +334,9 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
time.sleep(0.002) time.sleep(0.002)
self.validate_echo_messages(NUM_MESSAGES * 2, echo_messages) self.validate_echo_messages(NUM_MESSAGES * 2, echo_messages)
self._wait_before_chathistory()
self.validate_chathistory(subcommand, echo_messages, 1, chname) self.validate_chathistory(subcommand, echo_messages, 1, chname)
@pytest.mark.parametrize("subcommand", SUBCOMMANDS) @pytest.mark.parametrize("subcommand", SUBCOMMANDS)
@ -367,6 +396,9 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
self.getMessages(2) self.getMessages(2)
self.validate_echo_messages(NUM_MESSAGES, echo_messages) 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, 1, c2)
self.validate_chathistory(subcommand, echo_messages, 2, c1) self.validate_chathistory(subcommand, echo_messages, 2, c1)
@ -415,6 +447,8 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
] ]
self.assertEqual(results, new_convo) self.assertEqual(results, new_convo)
self._wait_before_chathistory()
# additional messages with c3 should not show up in the c1-c2 history: # 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, 1, c2)
self.validate_chathistory(subcommand, echo_messages, 2, c1) self.validate_chathistory(subcommand, echo_messages, 2, c1)
@ -459,15 +493,15 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
def _validate_chathistory_LATEST(self, echo_messages, user, chname): def _validate_chathistory_LATEST(self, echo_messages, user, chname):
INCLUSIVE_LIMIT = len(echo_messages) * 2 INCLUSIVE_LIMIT = len(echo_messages) * 2
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, INCLUSIVE_LIMIT)) 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.assertEqual(echo_messages, result)
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, 5)) 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.assertEqual(echo_messages[-5:], result)
self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, 1)) 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) self.assertEqual(echo_messages[-1:], result)
if self._supports_msgid(): if self._supports_msgid():
@ -476,7 +510,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY LATEST %s msgid=%s %d" "CHATHISTORY LATEST %s msgid=%s %d"
% (chname, echo_messages[4].msgid, INCLUSIVE_LIMIT), % (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) self.assertEqual(echo_messages[5:], result)
if self._supports_timestamp(): if self._supports_timestamp():
@ -485,7 +519,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY LATEST %s timestamp=%s %d" "CHATHISTORY LATEST %s timestamp=%s %d"
% (chname, echo_messages[4].time, INCLUSIVE_LIMIT), % (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) self.assertEqual(echo_messages[5:], result)
def _validate_chathistory_BEFORE(self, echo_messages, user, chname): def _validate_chathistory_BEFORE(self, echo_messages, user, chname):
@ -496,7 +530,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY BEFORE %s msgid=%s %d" "CHATHISTORY BEFORE %s msgid=%s %d"
% (chname, echo_messages[6].msgid, INCLUSIVE_LIMIT), % (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) self.assertEqual(echo_messages[:6], result)
if self._supports_timestamp(): if self._supports_timestamp():
@ -505,7 +539,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY BEFORE %s timestamp=%s %d" "CHATHISTORY BEFORE %s timestamp=%s %d"
% (chname, echo_messages[6].time, INCLUSIVE_LIMIT), % (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.assertEqual(echo_messages[:6], result)
self.sendLine( self.sendLine(
@ -513,7 +547,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY BEFORE %s timestamp=%s %d" "CHATHISTORY BEFORE %s timestamp=%s %d"
% (chname, echo_messages[6].time, 2), % (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) self.assertEqual(echo_messages[4:6], result)
def _validate_chathistory_AFTER(self, echo_messages, user, chname): def _validate_chathistory_AFTER(self, echo_messages, user, chname):
@ -524,7 +558,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY AFTER %s msgid=%s %d" "CHATHISTORY AFTER %s msgid=%s %d"
% (chname, echo_messages[3].msgid, INCLUSIVE_LIMIT), % (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) self.assertEqual(echo_messages[4:], result)
if self._supports_timestamp(): if self._supports_timestamp():
@ -533,7 +567,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY AFTER %s timestamp=%s %d" "CHATHISTORY AFTER %s timestamp=%s %d"
% (chname, echo_messages[3].time, INCLUSIVE_LIMIT), % (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.assertEqual(echo_messages[4:], result)
self.sendLine( self.sendLine(
@ -541,7 +575,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY AFTER %s timestamp=%s %d" "CHATHISTORY AFTER %s timestamp=%s %d"
% (chname, echo_messages[3].time, 3), % (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) self.assertEqual(echo_messages[4:7], result)
def _validate_chathistory_BETWEEN(self, echo_messages, user, chname): def _validate_chathistory_BETWEEN(self, echo_messages, user, chname):
@ -558,7 +592,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
INCLUSIVE_LIMIT, 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.assertEqual(echo_messages[1:-1], result)
self.sendLine( self.sendLine(
@ -571,7 +605,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
INCLUSIVE_LIMIT, 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.assertEqual(echo_messages[1:-1], result)
# BETWEEN forwards and backwards with a limit, should get # BETWEEN forwards and backwards with a limit, should get
@ -581,7 +615,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d" "CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
% (chname, echo_messages[0].msgid, echo_messages[-1].msgid, 3), % (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.assertEqual(echo_messages[1:4], result)
self.sendLine( self.sendLine(
@ -589,7 +623,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d" "CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d"
% (chname, echo_messages[-1].msgid, echo_messages[0].msgid, 3), % (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) self.assertEqual(echo_messages[-4:-1], result)
if self._supports_timestamp(): if self._supports_timestamp():
@ -604,7 +638,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
INCLUSIVE_LIMIT, 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.assertEqual(echo_messages[1:-1], result)
self.sendLine( self.sendLine(
user, user,
@ -616,21 +650,21 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
INCLUSIVE_LIMIT, 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.assertEqual(echo_messages[1:-1], result)
self.sendLine( self.sendLine(
user, user,
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d" "CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
% (chname, echo_messages[0].time, echo_messages[-1].time, 3), % (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.assertEqual(echo_messages[1:4], result)
self.sendLine( self.sendLine(
user, user,
"CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d" "CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d"
% (chname, echo_messages[-1].time, echo_messages[0].time, 3), % (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) self.assertEqual(echo_messages[-4:-1], result)
def _validate_chathistory_AROUND(self, echo_messages, user, chname): def _validate_chathistory_AROUND(self, echo_messages, user, chname):
@ -640,7 +674,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY AROUND %s msgid=%s %d" "CHATHISTORY AROUND %s msgid=%s %d"
% (chname, echo_messages[7].msgid, 1), % (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.assertEqual([echo_messages[7]], result)
self.sendLine( self.sendLine(
@ -648,7 +682,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY AROUND %s msgid=%s %d" "CHATHISTORY AROUND %s msgid=%s %d"
% (chname, echo_messages[7].msgid, 3), % (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) self.assertEqual(echo_messages[6:9], result)
if self._supports_timestamp(): if self._supports_timestamp():
@ -657,7 +691,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
"CHATHISTORY AROUND %s timestamp=%s %d" "CHATHISTORY AROUND %s timestamp=%s %d"
% (chname, echo_messages[7].time, 3), % (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) self.assertIn(echo_messages[7], result)
@pytest.mark.arbitrary_client_tags @pytest.mark.arbitrary_client_tags
@ -718,6 +752,8 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
self.assertEqual(len(relay), 1) self.assertEqual(len(relay), 1)
validate_tagmsg(relay[0], chname, msgid) validate_tagmsg(relay[0], chname, msgid)
self._wait_before_chathistory()
self.sendLine(1, "CHATHISTORY LATEST %s * 10" % (chname,)) self.sendLine(1, "CHATHISTORY LATEST %s * 10" % (chname,))
history_tagmsgs = [ history_tagmsgs = [
msg for msg in self.getMessages(1) if msg.command == "TAGMSG" msg for msg in self.getMessages(1) if msg.command == "TAGMSG"
@ -814,8 +850,95 @@ class ChathistoryTestCase(cases.BaseServerTestCase):
validate_msg(relay) 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} == { assert {f"_validate_chathistory_{cmd}" for cmd in SUBCOMMANDS} == {
meth_name meth_name
for meth_name in dir(ChathistoryTestCase) for meth_name in dir(ChathistoryTestCase)
if meth_name.startswith("_validate_chathistory_") if meth_name.startswith("_validate_chathistory_")
}, "ChathistoryTestCase.validate_chathistory and SUBCOMMANDS are out of sync" }, "ChathistoryTestCase.validate_chathistory and SUBCOMMANDS are out of sync"
@cases.mark_specifications("Sable")
@cases.mark_services
class SablePostgresqlHistoryTestCase(_BaseChathistoryTests):
# for every wall clock second, 15 seconds pass for the server.
# at x30, links between nodes timeout.
faketime = "+1y x15"
@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 (+ 1 min because the cleanup job
only runs every min)"""
assert self.controller.faketime_enabled, "faketime is not installed"
time.sleep(8)
@cases.mark_specifications("Sable")
@cases.mark_services
class SableExpiringHistoryTestCase(cases.BaseServerTestCase):
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 (+ 1 min because the cleanup job
only runs every min)"""
assert self.controller.faketime_enabled, "faketime is not installed"
time.sleep(8)
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")
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) = messages
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}"
)

View File

@ -9,6 +9,7 @@ class Specifications(enum.Enum):
RFC2812 = "RFC2812" RFC2812 = "RFC2812"
IRCv3 = "IRCv3" # Mark with capabilities whenever possible IRCv3 = "IRCv3" # Mark with capabilities whenever possible
Ergo = "Ergo" Ergo = "Ergo"
Sable = "Sable"
Ircdocs = "ircdocs" Ircdocs = "ircdocs"
"""Any document on ircdocs.horse (especially defs.ircdocs.horse), """Any document on ircdocs.horse (especially defs.ircdocs.horse),
@ -24,6 +25,9 @@ class Specifications(enum.Enum):
return spec return spec
raise ValueError(name) raise ValueError(name)
def is_implementation_specific(self) -> bool:
return self in (Specifications.Ergo, Specifications.Sable)
@enum.unique @enum.unique
class Capabilities(enum.Enum): class Capabilities(enum.Enum):

View File

@ -7,7 +7,12 @@ markers =
IRCv3 IRCv3
modern modern
ircdocs ircdocs
# implementations for which we have specific test get two markers:
# the implementation name and 'implementation-specific'
implementation-specific
Ergo Ergo
Sable
# misc marks # misc marks
strict strict

View File

@ -249,7 +249,7 @@ software:
name: Sable name: Sable
repository: Libera-Chat/sable repository: Libera-Chat/sable
refs: refs:
stable: 52397dc9e0f27c3ed197f984c00f06639870716d stable: 034c4d5dd937774099773238d8d5b8054b015607
release: null release: null
devel: master devel: master
devel_release: null devel_release: null
@ -268,6 +268,9 @@ software:
workspaces: "sable -> target" workspaces: "sable -> target"
cache-on-failure: true cache-on-failure: true
- run: rustc --version - run: rustc --version
- run: start postgresql
run: "sudo systemctl start postgresql.service"
env: "IRCTEST_POSTGRESQL_URL=postgresql://localhost IRCTEST_DEBUG_LOGS=1"
separate_build_job: false separate_build_job: false
build_script: | build_script: |
cd $GITHUB_WORKSPACE/sable/ cd $GITHUB_WORKSPACE/sable/