2 Commits

14 changed files with 131 additions and 608 deletions

View File

@ -558,7 +558,7 @@ jobs:
repository: ergochat/ergo repository: ergochat/ergo
- uses: actions/setup-go@v3 - uses: actions/setup-go@v3
with: with:
go-version: ^1.23.0 go-version: ^1.22.0
- run: go version - run: go version
- name: Build Ergo - name: Build Ergo
run: | run: |
@ -990,7 +990,6 @@ 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/
@ -1004,8 +1003,7 @@ 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 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
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

@ -65,7 +65,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
path: Bahamut path: Bahamut
ref: v2.2.4 ref: v2.2.1
repository: DALnet/Bahamut repository: DALnet/Bahamut
- name: Build Bahamut - name: Build Bahamut
run: | run: |
@ -636,7 +636,7 @@ jobs:
repository: ergochat/ergo repository: ergochat/ergo
- uses: actions/setup-go@v3 - uses: actions/setup-go@v3
with: with:
go-version: ^1.23.0 go-version: ^1.22.0
- run: go version - run: go version
- name: Build Ergo - name: Build Ergo
run: | run: |
@ -1140,7 +1140,7 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
path: sable path: sable
ref: 034c4d5dd937774099773238d8d5b8054b015607 ref: e9701e5e8d0c4f278ddd61ce7285f4918ecf99e9
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,7 +1154,6 @@ 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/
@ -1168,8 +1167,7 @@ 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 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
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()

199
Makefile
View File

@ -4,161 +4,112 @@ 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_MARKERS := \
not implementation-specific \
and not deprecated \
and not strict \
and not IRCv3 \
$(EXTRA_MARKERS)
BAHAMUT_SELECTORS := \ BAHAMUT_SELECTORS := \
(foo or not foo) \ not Ergo \
$(EXTRA_SELECTORS)
CHARYBDIS_MARKERS := \
not implementation-specific \
and not deprecated \ and not deprecated \
and not strict \ and not strict \
$(EXTRA_MARKERS) and not IRCv3 \
$(EXTRA_SELECTORS)
CHARYBDIS_SELECTORS := \ CHARYBDIS_SELECTORS := \
(foo or not foo) \ not Ergo \
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 := \
(foo or not foo) \ not deprecated \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
HYBRID_MARKERS := \
not implementation-specific \
and not deprecated \
$(EXTRA_MARKERS)
HYBRID_SELECTORS := \ HYBRID_SELECTORS := \
(foo or not foo) \ not Ergo \
and not deprecated \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
INSPIRCD_MARKERS := \
not implementation-specific \
and not deprecated \
and not strict \
$(EXTRA_MARKERS)
INSPIRCD_SELECTORS := \ INSPIRCD_SELECTORS := \
(foo or not foo) \ not Ergo \
$(EXTRA_SELECTORS)
IRCU2_MARKERS := \
not implementation-specific \
and not deprecated \ and not deprecated \
and not strict \ and not strict \
and not IRCv3 \ $(EXTRA_SELECTORS)
$(EXTRA_MARKERS)
IRCU2_SELECTORS := \ IRCU2_SELECTORS := \
(foo or not foo) \ not Ergo \
$(EXTRA_SELECTORS)
NEFARIOUS_MARKERS := \
not implementation-specific \
and not deprecated \ and not deprecated \
and not strict \ and not strict \
$(EXTRA_MARKERS) $(EXTRA_SELECTORS)
NEFARIOUS_SELECTORS := \ NEFARIOUS_SELECTORS := \
(foo or not foo) \ not Ergo \
$(EXTRA_SELECTORS)
SNIRCD_MARKERS := \
not implementation-specific \
and not deprecated \ and not deprecated \
and not strict \ and not strict \
and not IRCv3 \ $(EXTRA_SELECTORS)
$(EXTRA_MARKERS)
SNIRCD_SELECTORS := \ SNIRCD_SELECTORS := \
(foo or not foo) \ not Ergo \
$(EXTRA_SELECTORS)
IRC2_MARKERS := \
not implementation-specific \
and not deprecated \ and not deprecated \
and not strict \ and not strict \
and not IRCv3 \ $(EXTRA_SELECTORS)
$(EXTRA_MARKERS)
IRC2_SELECTORS := \ IRC2_SELECTORS := \
(foo or not foo) \ not Ergo \
$(EXTRA_SELECTORS)
MAMMON_MARKERS := \
not implementation-specific \
and not deprecated \ and not deprecated \
and not strict \ and not strict \
$(EXTRA_MARKERS) $(EXTRA_SELECTORS)
MAMMON_SELECTORS := \ MAMMON_SELECTORS := \
(foo or not foo) \ not Ergo \
$(EXTRA_SELECTORS)
NGIRCD_MARKERS := \
not implementation-specific \
and not deprecated \ and not deprecated \
and not strict \ and not strict \
$(EXTRA_MARKERS) $(EXTRA_SELECTORS)
NGIRCD_SELECTORS := \ NGIRCD_SELECTORS := \
(foo or not foo) \ not Ergo \
$(EXTRA_SELECTORS)
PLEXUS4_MARKERS := \
not implementation-specific \
and not deprecated \ and not deprecated \
$(EXTRA_MARKERS) and not strict \
PLEXUS4_SELECTORS := \
(foo or not foo) \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
LIMNORIA_MARKERS := \ PLEXUS4_SELECTORS := \
not implementation-specific \ not Ergo \
$(EXTRA_MARKERS) and not deprecated \
$(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_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
# 'SablePostgresqlHistoryTestCase and private_chathistory' disabled because Sable does not (yet?) persist private messages to postgresql # Tests marked with private_chathistory can't pass because Sable does not implement CHATHISTORY for DMs
SABLE_MARKERS := \
(Sable or not implementation-specific) \ SABLE_SELECTORS := \
not Ergo \
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 \
$(EXTRA_MARKERS) and not private_chathistory \
SABLE_SELECTORS := \ and not list and not lusers and not time and not info \
not list and not lusers and not time and not info \
and not (SablePostgresqlHistoryTestCase and private_chathistory) \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
SOLANUM_MARKERS := \ SOLANUM_SELECTORS := \
not implementation-specific \ not Ergo \
and not deprecated \ and not deprecated \
and not strict \ and not strict \
$(EXTRA_MARKERS)
SOLANUM_SELECTORS := \
(foo or not foo) \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
SOPEL_MARKERS := \ # Same as Limnoria
not implementation-specific \
$(EXTRA_MARKERS)
SOPEL_SELECTORS := \ SOPEL_SELECTORS := \
(foo or not foo) \ (foo or not foo) \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
THELOUNGE_MARKERS := \ # TheLounge can actually pass all the test so there is none to exclude.
not implementation-specific \ # `(foo or not foo)` serves as a `true` value so it doesn't break when
$(EXTRA_MARKERS) # $(EXTRA_SELECTORS) is non-empty
THELOUNGE_SELECTORS := \ THELOUNGE_SELECTORS := \
(foo or not foo) \ (foo or not foo) \
$(EXTRA_SELECTORS) $(EXTRA_SELECTORS)
@ -167,16 +118,13 @@ 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_MARKERS := \ UNREALIRCD_SELECTORS := \
not implementation-specific \ not Ergo \
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
@ -192,114 +140,107 @@ 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 and $(BAHAMUT_MARKERS)' \ -m 'services' \
-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 and $(BAHAMUT_MARKERS)' \ -m 'services' \
-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 and $(INSPIRCD_MARKERS)' \ -m 'not services' \
-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 and $(INSPIRCD_MARKERS)' \ -m 'services' \
-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 and $(INSPIRCD_MARKERS)' \ -m 'services' \
-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 $(IRCU2_MARKERS)' \ -m 'not services and not IRCv3' \
-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 and $(NEFARIOUS_MARKERS)' \ -m 'not services' \
-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 $(SNIRCD_MARKERS)' \ -m 'not services and not IRCv3' \
-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 $(IRCU2_MARKERS)' \ -m 'not services and not IRCv3' \
-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 and $(NGIRCD_MARKERS)' \ -m 'not services' \
-n 4 \ -n 4 \
-k "$(NGIRCD_SELECTORS)" -k "$(NGIRCD_SELECTORS)"
@ -307,20 +248,19 @@ 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 and $(NGIRCD_MARKERS)' \ -m 'services' \
-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 and $(NGIRCD_MARKERS)' \ -m 'services' \
-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)'
@ -328,25 +268,22 @@ 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 and $(UNREALIRCD_MARKERS)' \ -m 'not services' \
-k '$(UNREALIRCD_SELECTORS)' -k '$(UNREALIRCD_SELECTORS)'
unrealircd-5: unrealircd unrealircd-5: unrealircd
@ -355,19 +292,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 and $(UNREALIRCD_MARKERS)' \ -m 'services' \
-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 and $(UNREALIRCD_MARKERS)' \ -m 'services' \
-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 and $(UNREALIRCD_MARKERS)' \ -m 'services' \
-k '$(UNREALIRCD_SELECTORS)' -k '$(UNREALIRCD_SELECTORS)'

View File

@ -8,10 +8,8 @@ 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,
@ -69,9 +67,6 @@ 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.
@ -150,48 +145,10 @@ class _BaseController:
self._own_ports.remove((hostname, port)) self._own_ports.remove((hostname, port))
def execute( def execute(
self, self, command: Sequence[Union[str, Path]], **kwargs: Any
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
proc_name = proc_name or str(command[0]) return subprocess.Popen(command, stderr=output_to, stdout=output_to, **kwargs)
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):
@ -316,7 +273,6 @@ 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,22 +842,16 @@ 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 = { specifications = frozenset(
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

@ -33,8 +33,7 @@ TEMPLATE_CONFIG = """
class="ServerOperators" class="ServerOperators"
> >
<options casemapping="ascii" <options casemapping="ascii">
extbanformat="any">
# Disable 'NOTICE #chan :*** foo invited bar into the channel- # Disable 'NOTICE #chan :*** foo invited bar into the channel-
<security announceinvites="none"> <security announceinvites="none">

View File

@ -4,9 +4,8 @@ import shutil
import signal import signal
import subprocess import subprocess
import tempfile import tempfile
import threading
import time import time
from typing import Any, Optional, Sequence, Type from typing import Optional, Type
from irctest.basecontrollers import ( from irctest.basecontrollers import (
BaseServerController, BaseServerController,
@ -15,7 +14,6 @@ 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
@ -87,13 +85,7 @@ 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,
) )
@ -103,11 +95,10 @@ def certs_dir() -> Path:
NETWORK_CONFIG = """ NETWORK_CONFIG = """
{ {
"fanout": 2, "fanout": 1,
"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" }
] ]
@ -116,8 +107,6 @@ NETWORK_CONFIG = """
NETWORK_CONFIG_CONFIG = """ NETWORK_CONFIG_CONFIG = """
{ {
"object_expiry": 60, // 1 minute
"opers": [ "opers": [
{ {
"name": "operuser", "name": "operuser",
@ -228,58 +217,6 @@ 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,
@ -358,7 +295,7 @@ SERVICES_CONFIG = """
{ {
"target": "stdout", "target": "stdout",
"level": "debug", "level": "debug",
"modules": [ "sable" ] "modules": [ "sable_services" ]
} }
] ]
} }
@ -373,12 +310,6 @@ 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,
@ -415,11 +346,10 @@ 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 = history_hostname = services_hostname = "127.0.0.1" server1_hostname = services_hostname = "127.0.0.1"
( (
server1_management_hostname, server1_management_hostname,
@ -429,10 +359,6 @@ 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(),
@ -453,13 +379,6 @@ 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:
@ -490,28 +409,17 @@ 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()
@ -555,62 +463,11 @@ 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
@ -620,7 +477,6 @@ class SableServicesController(BaseServicesController):
self.proc = self.execute( self.proc = self.execute(
[ [
*self.faketime_cmd,
"sable_services", "sable_services",
"--foreground", "--foreground",
"--server-conf", "--server-conf",
@ -631,7 +487,6 @@ 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)
@ -640,92 +495,5 @@ 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,7 +2,6 @@
`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
@ -32,22 +31,10 @@ def skip_ngircd(f):
return newf return newf
class _BaseChathistoryTests(cases.BaseServerTestCase): @cases.mark_specifications("IRCv3")
def _wait_before_chathistory(self): @cases.mark_services
"""Hook for the Sable-specific tests that check the postgresql-based class ChathistoryTestCase(cases.BaseServerTestCase):
CHATHISTORY implementation is sound. This implementation only kicks in def validate_chathistory_batch(self, msgs, target):
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(
@ -107,13 +94,9 @@ class _BaseChathistoryTests(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")
while not (msgs := self.getMessages(bar)): 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],
@ -123,9 +106,7 @@ class _BaseChathistoryTests(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,))
while not (msgs := self.getMessages(bar)): 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",
@ -194,8 +175,6 @@ class _BaseChathistoryTests(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)
@ -246,12 +225,9 @@ class _BaseChathistoryTests(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.02) time.sleep(0.002)
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
@ -288,8 +264,6 @@ class _BaseChathistoryTests(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)
@ -334,9 +308,6 @@ class _BaseChathistoryTests(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)
@ -396,9 +367,6 @@ class _BaseChathistoryTests(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)
@ -447,8 +415,6 @@ class _BaseChathistoryTests(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)
@ -493,15 +459,15 @@ class _BaseChathistoryTests(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(user, chname) result = self.validate_chathistory_batch(self.getMessages(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(user, chname) result = self.validate_chathistory_batch(self.getMessages(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(user, chname) result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[-1:], result) self.assertEqual(echo_messages[-1:], result)
if self._supports_msgid(): if self._supports_msgid():
@ -510,7 +476,7 @@ class _BaseChathistoryTests(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(user, chname) result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[5:], result) self.assertEqual(echo_messages[5:], result)
if self._supports_timestamp(): if self._supports_timestamp():
@ -519,7 +485,7 @@ class _BaseChathistoryTests(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(user, chname) result = self.validate_chathistory_batch(self.getMessages(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):
@ -530,7 +496,7 @@ class _BaseChathistoryTests(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(user, chname) result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[:6], result) self.assertEqual(echo_messages[:6], result)
if self._supports_timestamp(): if self._supports_timestamp():
@ -539,7 +505,7 @@ class _BaseChathistoryTests(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(user, chname) result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[:6], result) self.assertEqual(echo_messages[:6], result)
self.sendLine( self.sendLine(
@ -547,7 +513,7 @@ class _BaseChathistoryTests(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(user, chname) result = self.validate_chathistory_batch(self.getMessages(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):
@ -558,7 +524,7 @@ class _BaseChathistoryTests(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(user, chname) result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[4:], result) self.assertEqual(echo_messages[4:], result)
if self._supports_timestamp(): if self._supports_timestamp():
@ -567,7 +533,7 @@ class _BaseChathistoryTests(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(user, chname) result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[4:], result) self.assertEqual(echo_messages[4:], result)
self.sendLine( self.sendLine(
@ -575,7 +541,7 @@ class _BaseChathistoryTests(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(user, chname) result = self.validate_chathistory_batch(self.getMessages(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):
@ -592,7 +558,7 @@ class _BaseChathistoryTests(cases.BaseServerTestCase):
INCLUSIVE_LIMIT, INCLUSIVE_LIMIT,
), ),
) )
result = self.validate_chathistory_batch(user, chname) result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[1:-1], result) self.assertEqual(echo_messages[1:-1], result)
self.sendLine( self.sendLine(
@ -605,7 +571,7 @@ class _BaseChathistoryTests(cases.BaseServerTestCase):
INCLUSIVE_LIMIT, INCLUSIVE_LIMIT,
), ),
) )
result = self.validate_chathistory_batch(user, chname) result = self.validate_chathistory_batch(self.getMessages(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
@ -615,7 +581,7 @@ class _BaseChathistoryTests(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(user, chname) result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[1:4], result) self.assertEqual(echo_messages[1:4], result)
self.sendLine( self.sendLine(
@ -623,7 +589,7 @@ class _BaseChathistoryTests(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(user, chname) result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[-4:-1], result) self.assertEqual(echo_messages[-4:-1], result)
if self._supports_timestamp(): if self._supports_timestamp():
@ -638,7 +604,7 @@ class _BaseChathistoryTests(cases.BaseServerTestCase):
INCLUSIVE_LIMIT, INCLUSIVE_LIMIT,
), ),
) )
result = self.validate_chathistory_batch(user, chname) result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[1:-1], result) self.assertEqual(echo_messages[1:-1], result)
self.sendLine( self.sendLine(
user, user,
@ -650,21 +616,21 @@ class _BaseChathistoryTests(cases.BaseServerTestCase):
INCLUSIVE_LIMIT, INCLUSIVE_LIMIT,
), ),
) )
result = self.validate_chathistory_batch(user, chname) result = self.validate_chathistory_batch(self.getMessages(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(user, chname) result = self.validate_chathistory_batch(self.getMessages(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(user, chname) result = self.validate_chathistory_batch(self.getMessages(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):
@ -674,7 +640,7 @@ class _BaseChathistoryTests(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(user, chname) result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual([echo_messages[7]], result) self.assertEqual([echo_messages[7]], result)
self.sendLine( self.sendLine(
@ -682,7 +648,7 @@ class _BaseChathistoryTests(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(user, chname) result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertEqual(echo_messages[6:9], result) self.assertEqual(echo_messages[6:9], result)
if self._supports_timestamp(): if self._supports_timestamp():
@ -691,7 +657,7 @@ class _BaseChathistoryTests(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(user, chname) result = self.validate_chathistory_batch(self.getMessages(user), chname)
self.assertIn(echo_messages[7], result) self.assertIn(echo_messages[7], result)
@pytest.mark.arbitrary_client_tags @pytest.mark.arbitrary_client_tags
@ -752,8 +718,6 @@ class _BaseChathistoryTests(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"
@ -850,95 +814,8 @@ class _BaseChathistoryTests(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

@ -2,6 +2,8 @@
The PRIVMSG and NOTICE commands. The PRIVMSG and NOTICE commands.
""" """
import pytest
from irctest import cases from irctest import cases
from irctest.numerics import ERR_INPUTTOOLONG from irctest.numerics import ERR_INPUTTOOLONG
from irctest.patma import ANYSTR from irctest.patma import ANYSTR
@ -123,26 +125,31 @@ class NoticeTestCase(cases.BaseServerTestCase):
class TagsTestCase(cases.BaseServerTestCase): class TagsTestCase(cases.BaseServerTestCase):
@pytest.mark.parametrize("tag_length", [4096, 10000])
@cases.mark_capabilities("message-tags") @cases.mark_capabilities("message-tags")
@cases.xfailIf( @cases.xfailIf(
lambda self: bool( lambda self, tag_length: bool(
self.controller.software_name == "UnrealIRCd" self.controller.software_name == "UnrealIRCd"
and self.controller.software_version == 5 and self.controller.software_version == 5
), ),
"UnrealIRCd <6.0.7 dropped messages with excessively large tags: " "UnrealIRCd <6.0.7 dropped messages with excessively large tags: "
"https://bugs.unrealircd.org/view.php?id=5947", "https://bugs.unrealircd.org/view.php?id=5947",
) )
def testLineTooLong(self): def testLineTooLong(self, tag_length):
self.connectClient("bar", capabilities=["message-tags"], skip_if_cap_nak=True) self.connectClient("bar", capabilities=["message-tags"], skip_if_cap_nak=True)
self.connectClient( self.connectClient(
"recver", capabilities=["message-tags"], skip_if_cap_nak=True "recver", capabilities=["message-tags"], skip_if_cap_nak=True
) )
self.joinChannel(1, "#xyz") self.joinChannel(1, "#xyz")
monsterMessage = "@+clientOnlyTagExample=" + "a" * 4096 + " PRIVMSG #xyz hi!"
monsterMessage = (
"@+clientOnlyTagExample=" + "a" * tag_length + " PRIVMSG #xyz hi!"
)
self.sendLine(1, monsterMessage) self.sendLine(1, monsterMessage)
replies = self.getMessages(1) replies = self.getMessages(1)
self.assertIn(ERR_INPUTTOOLONG, set(reply.command for reply in replies))
self.assertEqual(self.getMessages(2), [], "overflowing message was relayed") self.assertEqual(self.getMessages(2), [], "overflowing message was relayed")
if len(replies) > 0:
self.assertIn(ERR_INPUTTOOLONG, set(reply.command for reply in replies))
class LengthLimitTestCase(cases.BaseServerTestCase): class LengthLimitTestCase(cases.BaseServerTestCase):

View File

@ -221,6 +221,7 @@ class WhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase):
) )
@cases.mark_specifications("RFC2812") @cases.mark_specifications("RFC2812")
@cases.xfailIfSoftware(["Sable"], "https://github.com/Libera-Chat/sable/issues/101")
def testWhoisMissingUser(self): def testWhoisMissingUser(self):
"""Test WHOIS on a nonexistent nickname.""" """Test WHOIS on a nonexistent nickname."""
self.connectClient("qux", name="qux") self.connectClient("qux", name="qux")

View File

@ -9,7 +9,6 @@ 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),
@ -25,9 +24,6 @@ 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,12 +7,7 @@ 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

@ -97,7 +97,7 @@ software:
name: Bahamut name: Bahamut
repository: DALnet/Bahamut repository: DALnet/Bahamut
refs: refs:
stable: "v2.2.4" stable: "v2.2.1"
release: null release: null
devel: "master" devel: "master"
devel_release: null devel_release: null
@ -136,7 +136,7 @@ software:
pre_deps: pre_deps:
- uses: actions/setup-go@v3 - uses: actions/setup-go@v3
with: with:
go-version: '^1.23.0' go-version: '^1.22.0'
- run: go version - run: go version
separate_build_job: false separate_build_job: false
build_script: | build_script: |
@ -249,7 +249,7 @@ software:
name: Sable name: Sable
repository: Libera-Chat/sable repository: Libera-Chat/sable
refs: refs:
stable: 034c4d5dd937774099773238d8d5b8054b015607 stable: e9701e5e8d0c4f278ddd61ce7285f4918ecf99e9
release: null release: null
devel: master devel: master
devel_release: null devel_release: null
@ -268,9 +268,6 @@ 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/