From da005d7d2492bf31c4bdeb46108240766c69d0ad Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Mon, 21 Feb 2022 21:43:22 +0100 Subject: [PATCH 001/143] Add tests for WHOX. (#131) --- irctest/cases.py | 17 ++-- irctest/numerics.py | 1 + irctest/server_tests/who.py | 162 +++++++++++++++++++++++++++++++++++- irctest/specifications.py | 1 + pytest.ini | 1 + 5 files changed, 172 insertions(+), 10 deletions(-) diff --git a/irctest/cases.py b/irctest/cases.py index 49a1b81..f4caa9d 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -647,6 +647,16 @@ class BaseServerTestCase( else: raise + def authenticateClient( + self, client: TClientName, account: str, password: str + ) -> None: + self.sendLine(client, "AUTHENTICATE PLAIN") + m = self.getRegistrationMessage(client) + self.assertMessageMatch(m, command="AUTHENTICATE", params=["+"]) + self.sendLine(client, sasl_plain_blob(account, password)) + m = self.getRegistrationMessage(client) + self.assertIn(m.command, ["900", "903"], str(m)) + def connectClient( self, nick: str, @@ -670,12 +680,7 @@ class BaseServerTestCase( if password is not None: if "sasl" not in (capabilities or ()): raise ValueError("Used 'password' option without sasl capbilitiy") - self.sendLine(client, "AUTHENTICATE PLAIN") - m = self.getRegistrationMessage(client) - self.assertMessageMatch(m, command="AUTHENTICATE", params=["+"]) - self.sendLine(client, sasl_plain_blob(account or nick, password)) - m = self.getRegistrationMessage(client) - self.assertIn(m.command, ["900", "903"], str(m)) + self.authenticateClient(client, account or nick, password) self.sendLine(client, "NICK {}".format(nick)) self.sendLine(client, "USER %s * * :Realname" % (ident,)) diff --git a/irctest/numerics.py b/irctest/numerics.py index 6193243..28006fe 100644 --- a/irctest/numerics.py +++ b/irctest/numerics.py @@ -86,6 +86,7 @@ RPL_ENDOFEXCEPTLIST = "349" RPL_VERSION = "351" RPL_WHOREPLY = "352" RPL_NAMREPLY = "353" +RPL_WHOSPCRPL = "354" RPL_LINKS = "364" RPL_ENDOFLINKS = "365" RPL_ENDOFNAMES = "366" diff --git a/irctest/server_tests/who.py b/irctest/server_tests/who.py index bfb8cb4..c526b0d 100644 --- a/irctest/server_tests/who.py +++ b/irctest/server_tests/who.py @@ -2,8 +2,8 @@ import re import pytest -from irctest import cases -from irctest.numerics import RPL_ENDOFWHO, RPL_WHOREPLY, RPL_YOUREOPER +from irctest import cases, runner +from irctest.numerics import RPL_ENDOFWHO, RPL_WHOREPLY, RPL_WHOSPCRPL, RPL_YOUREOPER from irctest.patma import ANYSTR, InsensitiveStr, StrRe @@ -15,15 +15,22 @@ def realname_regexp(realname): ) -class WhoTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): - def _init(self): +class BaseWhoTestCase: + def _init(self, auth=False): self.nick = "coolNick" self.username = "myusernam" # may be truncated if longer than this self.realname = "My UniqueReal Name" self.addClient() + if auth: + self.controller.registerUser(self, "coolAcct", "sesame") + self.requestCapabilities(1, ["sasl"], skip_if_cap_nak=True) + self.authenticateClient(1, "coolAcct", "sesame") self.sendLine(1, f"NICK {self.nick}") self.sendLine(1, f"USER {self.username} 0 * :{self.realname}") + if auth: + self.sendLine(1, "CAP END") + self.getRegistrationMessage(1) self.skipToWelcome(1) self.sendLine(1, "JOIN #chan") @@ -69,6 +76,8 @@ class WhoTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): ], ) + +class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase, cases.OptionalityHelper): @cases.mark_specifications("Modern") def testWhoStar(self): self._init() @@ -323,3 +332,148 @@ class WhoTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): command=RPL_ENDOFWHO, params=["otherNick", InsensitiveStr(mask), ANYSTR], ) + + @cases.mark_specifications("IRCv3") + @cases.mark_isupport("WHOX") + def testWhoxFull(self): + """https://github.com/ircv3/ircv3-specifications/pull/482""" + self._testWhoxFull("%tcuihsnfdlaor,123") + + @cases.mark_specifications("IRCv3") + @cases.mark_isupport("WHOX") + def testWhoxFullReversed(self): + """https://github.com/ircv3/ircv3-specifications/pull/482""" + self._testWhoxFull("%" + "".join(reversed("tcuihsnfdlaor")) + ",123") + + def _testWhoxFull(self, chars): + self._init() + if "WHOX" not in self.server_support: + raise runner.IsupportTokenNotSupported("WHOX") + + self.sendLine(2, f"WHO coolNick {chars}") + messages = self.getMessages(2) + + self.assertEqual(len(messages), 2, "Unexpected number of messages") + + (reply, end) = messages + + self.assertMessageMatch( + reply, + command=RPL_WHOSPCRPL, + params=[ + "otherNick", + "123", + StrRe(r"(#chan|\*)"), + StrRe("~?myusernam"), + ANYSTR, + ANYSTR, + "My.Little.Server", + "coolNick", + StrRe("H@?"), + ANYSTR, # hopcount + StrRe("[0-9]"), # seconds idle + "0", # account name + ANYSTR, # op level + "My UniqueReal Name", + ], + ) + + self.assertMessageMatch( + end, + command=RPL_ENDOFWHO, + params=["otherNick", InsensitiveStr("coolNick"), ANYSTR], + ) + + def testWhoxToken(self): + """https://github.com/ircv3/ircv3-specifications/pull/482""" + self._init() + if "WHOX" not in self.server_support: + raise runner.IsupportTokenNotSupported("WHOX") + + self.sendLine(2, "WHO coolNick %tn,321") + messages = self.getMessages(2) + + self.assertEqual(len(messages), 2, "Unexpected number of messages") + + (reply, end) = messages + + self.assertMessageMatch( + reply, + command=RPL_WHOSPCRPL, + params=[ + "otherNick", + "321", + "coolNick", + ], + ) + + self.assertMessageMatch( + end, + command=RPL_ENDOFWHO, + params=["otherNick", InsensitiveStr("coolNick"), ANYSTR], + ) + + +@cases.mark_services +class WhoServicesTestCase( + BaseWhoTestCase, cases.BaseServerTestCase, cases.OptionalityHelper +): + @cases.mark_specifications("IRCv3") + @cases.mark_isupport("WHOX") + def testWhoxAccount(self): + self._init(auth=True) + if "WHOX" not in self.server_support: + raise runner.IsupportTokenNotSupported("WHOX") + + self.sendLine(2, "WHO coolNick %na") + messages = self.getMessages(2) + + self.assertEqual(len(messages), 2, "Unexpected number of messages") + + (reply, end) = messages + + self.assertMessageMatch( + reply, + command=RPL_WHOSPCRPL, + params=[ + "otherNick", + "coolNick", + "coolAcct", + ], + ) + + self.assertMessageMatch( + end, + command=RPL_ENDOFWHO, + params=["otherNick", InsensitiveStr("coolNick"), ANYSTR], + ) + + @cases.mark_specifications("IRCv3") + @cases.mark_isupport("WHOX") + def testWhoxNoAccount(self): + self._init(auth=False) + if "WHOX" not in self.server_support: + raise runner.IsupportTokenNotSupported("WHOX") + + self.sendLine(2, "WHO coolNick %na") + messages = self.getMessages(2) + + self.assertEqual(len(messages), 2, "Unexpected number of messages") + + (reply, end) = messages + + self.assertMessageMatch( + reply, + command=RPL_WHOSPCRPL, + params=[ + "otherNick", + "coolNick", + "0", + ], + ) + + self.assertMessageMatch( + end, + command=RPL_ENDOFWHO, + params=["otherNick", InsensitiveStr("coolNick"), ANYSTR], + ) diff --git a/irctest/specifications.py b/irctest/specifications.py index 71a5bcf..745895a 100644 --- a/irctest/specifications.py +++ b/irctest/specifications.py @@ -53,6 +53,7 @@ class IsupportTokens(enum.Enum): MONITOR = "MONITOR" STATUSMSG = "STATUSMSG" TARGMAX = "TARGMAX" + WHOX = "WHOX" @classmethod def from_name(cls, name: str) -> IsupportTokens: diff --git a/pytest.ini b/pytest.ini index 221add7..6774b93 100644 --- a/pytest.ini +++ b/pytest.ini @@ -35,5 +35,6 @@ markers = MONITOR STATUSMSG TARGMAX + WHOX python_classes = *TestCase Test* From 7e112359a2e64767eb5769dcf2f8e8f11ab4512e Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Fri, 4 Mar 2022 15:58:05 -0500 Subject: [PATCH 002/143] secret channel test (#135) * silent.py tests for channels with mode +s appearing in LIST only when the user is connected to that channel * Added assertions for exact content of lines with command RPL_LIST and checks for exact number of RPL_LIST replies * fix linter errors * only validate the first two parameters of RPL_LIST * rename to secret channel test, add citation * ignore ngircd pseudo-channel * attempt to fix charybdis/solanum and ircu issues * review fixes Co-authored-by: William Rehwinkel --- irctest/controllers/charybdis.py | 3 ++ irctest/server_tests/chmodes/secret.py | 55 ++++++++++++++++++++++++++ irctest/server_tests/list.py | 8 ++-- 3 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 irctest/server_tests/chmodes/secret.py diff --git a/irctest/controllers/charybdis.py b/irctest/controllers/charybdis.py index 489d36b..e07685e 100644 --- a/irctest/controllers/charybdis.py +++ b/irctest/controllers/charybdis.py @@ -12,6 +12,9 @@ serverinfo {{ general {{ throttle_count = 100; # We need to connect lots of clients quickly + # disable throttling for LIST and similar: + pace_wait_simple = 0 second; + pace_wait = 0 second; sasl_service = "SaslServ"; }}; diff --git a/irctest/server_tests/chmodes/secret.py b/irctest/server_tests/chmodes/secret.py new file mode 100644 index 0000000..15efaff --- /dev/null +++ b/irctest/server_tests/chmodes/secret.py @@ -0,0 +1,55 @@ +from irctest import cases +from irctest.numerics import RPL_LIST + + +class SecretChannelTestCase(cases.BaseServerTestCase): + @cases.mark_specifications("RFC1459", "Modern") + def testSecretChannelListCommand(self): + """ + + + "Likewise, secret channels are not listed + at all unless the client is a member of the channel in question." + + + "A channel that is set to secret will not show up in responses to + the LIST or NAMES command unless the client sending the command is + joined to the channel." + """ + + def get_listed_channels(replies): + channels = set() + for reply in replies: + # skip pseudo-channels (&SERVER, &NOTICES) listed by ngircd + # and ircu: + if reply.command == RPL_LIST and reply.params[1].startswith("#"): + channels.add(reply.params[1]) + return channels + + # test that a silent channel is shown in list if the user is in the channel. + self.connectClient("first", name="first") + self.joinChannel("first", "#gen") + self.getMessages("first") + self.sendLine("first", "MODE #gen +s") + # run command LIST + self.sendLine("first", "LIST") + replies = self.getMessages("first") + self.assertEqual(get_listed_channels(replies), {"#gen"}) + + # test that another client would not see the secret + # channel. + self.connectClient("second", name="second") + self.getMessages("second") + self.sendLine("second", "LIST") + replies = self.getMessages("second") + # RPL_LIST 322 should NOT be present this time. + self.assertEqual(get_listed_channels(replies), set()) + + # Second client will join the secret channel + # and call command LIST. The channel SHOULD + # appear this time. + self.joinChannel("second", "#gen") + self.sendLine("second", "LIST") + replies = self.getMessages("second") + # Should be only one line with command RPL_LIST + self.assertEqual(get_listed_channels(replies), {"#gen"}) diff --git a/irctest/server_tests/list.py b/irctest/server_tests/list.py index 0310903..9529cb9 100644 --- a/irctest/server_tests/list.py +++ b/irctest/server_tests/list.py @@ -15,8 +15,8 @@ class ListTestCase(cases.BaseServerTestCase): if m.command == "321": # skip RPL_LISTSTART m = self.getMessage(2) - while m.command == "322" and m.params[1] == "&SERVER": - # ngircd adds this pseudo-channel + # skip local pseudo-channels listed by ngircd and ircu + while m.command == "322" and m.params[1].startswith("&"): m = self.getMessage(2) self.assertNotEqual( m.command, @@ -58,8 +58,8 @@ class ListTestCase(cases.BaseServerTestCase): "nor 323 (RPL_LISTEND) but: {msg}", ) m = self.getMessage(2) - while m.command == "322" and m.params[1] == "&SERVER": - # ngircd adds this pseudo-channel + # skip local pseudo-channels listed by ngircd and ircu + while m.command == "322" and m.params[1].startswith("&"): m = self.getMessage(2) self.assertNotEqual( m.command, From 66c457f6ced70cf73c771bf96338c5fb6c060e93 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 5 Mar 2022 09:47:29 +0100 Subject: [PATCH 003/143] patma: Fix repr() inconsistencies and add tests --- irctest/patma.py | 8 ++-- irctest/self_tests/cases.py | 75 +++++++++++++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 7 deletions(-) diff --git a/irctest/patma.py b/irctest/patma.py index 5fc99df..f198601 100644 --- a/irctest/patma.py +++ b/irctest/patma.py @@ -24,7 +24,7 @@ class AnyOptStr(Operator): """Wildcard matching any string as well as None""" def __repr__(self) -> str: - return "AnyOptStr" + return "AnyOptStr()" @dataclasses.dataclass(frozen=True) @@ -59,7 +59,7 @@ class RemainingKeys(Operator): key: Operator def __repr__(self) -> str: - return f"Keys({self.key!r})" + return f"RemainingKeys({self.key!r})" ANYSTR = AnyStr() @@ -77,9 +77,9 @@ class ListRemainder: def __repr__(self) -> str: if self.min_length: - return f"*ListRemainder({self.item!r}, min_length={self.min_length})" + return f"ListRemainder({self.item!r}, min_length={self.min_length})" else: - return f"*ListRemainder({self.item!r})" + return f"ListRemainder({self.item!r})" ANYLIST = [ListRemainder(ANYSTR)] diff --git a/irctest/self_tests/cases.py b/irctest/self_tests/cases.py index 9279bdb..22a2286 100644 --- a/irctest/self_tests/cases.py +++ b/irctest/self_tests/cases.py @@ -16,7 +16,7 @@ from irctest.patma import ( ) # fmt: off -MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str]]] = [ +MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [ ( # the specification: dict( @@ -36,6 +36,11 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str]]] = [ [ "PRIVMSG #chan hello2", "PRIVMSG #chan2 hello", + ], + # and they each error with: + [ + "expected params to match ['#chan', 'hello'], got ['#chan', 'hello2']", + "expected params to match ['#chan', 'hello'], got ['#chan2', 'hello']", ] ), ( @@ -58,6 +63,11 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str]]] = [ [ "PRIVMSG #chan :hi", "PRIVMSG #chan2 hello", + ], + # and they each error with: + [ + "expected params to match ['#chan', StrRe(r'hello.*')], got ['#chan', 'hi']", + "expected params to match ['#chan', StrRe(r'hello.*')], got ['#chan2', 'hello']", ] ), ( @@ -76,6 +86,12 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str]]] = [ "PRIVMSG #chan :hi", ":foo2!baz@qux PRIVMSG #chan hello", "@tag1=bar :foo2!baz@qux PRIVMSG #chan :hello", + ], + # and they each error with: + [ + "expected nick to be foo, got None instead", + "expected nick to be foo, got foo2 instead", + "expected nick to be foo, got foo2 instead", ] ), ( @@ -96,6 +112,13 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str]]] = [ "@tag1=value1 PRIVMSG #chan :hello", "PRIVMSG #chan hello", ":foo!baz@qux PRIVMSG #chan hello", + ], + # and they each error with: + [ + "expected tags to match {'tag1': 'bar'}, got {'tag1': 'bar', 'tag2': ''}", + "expected tags to match {'tag1': 'bar'}, got {'tag1': 'value1'}", + "expected tags to match {'tag1': 'bar'}, got {}", + "expected tags to match {'tag1': 'bar'}, got {}", ] ), ( @@ -116,6 +139,12 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str]]] = [ "@tag1=bar;tag2= PRIVMSG #chan :hello", "PRIVMSG #chan hello", ":foo!baz@qux PRIVMSG #chan hello", + ], + # and they each error with: + [ + "expected tags to match {'tag1': AnyStr}, got {'tag1': 'bar', 'tag2': ''}", + "expected tags to match {'tag1': AnyStr}, got {}", + "expected tags to match {'tag1': AnyStr}, got {}", ] ), ( @@ -138,6 +167,14 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str]]] = [ "PRIVMSG #chan hello2", "PRIVMSG #chan2 hello", ":foo!baz@qux PRIVMSG #chan hello", + ], + # and they each error with: + [ + "expected command to be PRIVMSG, got PRIVMG", + "expected tags to match {'tag1': 'bar', RemainingKeys(AnyStr): AnyOptStr()}, got {'tag1': 'value1'}", + "expected params to match ['#chan', 'hello'], got ['#chan', 'hello2']", + "expected params to match ['#chan', 'hello'], got ['#chan2', 'hello']", + "expected tags to match {'tag1': 'bar', RemainingKeys(AnyStr): AnyOptStr()}, got {}", ] ), ( @@ -159,6 +196,13 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str]]] = [ "@tag1=value1 PRIVMSG #chan :hello", "@tag1=bar;tag2= PRIVMSG #chan :hello", "@tag1=bar;tag2=baz PRIVMSG #chan :hello", + ], + # and they each error with: + [ + "expected command to be PRIVMSG, got PRIVMG", + "expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): AnyOptStr()}, got {'tag1': 'value1'}", + "expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): AnyOptStr()}, got {'tag1': 'bar', 'tag2': ''}", + "expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): AnyOptStr()}, got {'tag1': 'bar', 'tag2': 'baz'}", ] ), ( @@ -176,6 +220,11 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str]]] = [ [ "005 nick", "005 nick BAR=2", + ], + # and they each error with: + [ + "expected params to match ['nick', 'FOO=1', ListRemainder(AnyStr)], got ['nick']", + "expected params to match ['nick', 'FOO=1', ListRemainder(AnyStr)], got ['nick', 'BAR=2']", ] ), ( @@ -193,6 +242,10 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str]]] = [ # and does not match: [ "005 nick", + ], + # and they each error with: + [ + "expected params to match ['nick', ListRemainder(AnyStr, min_length=1)], got ['nick']", ] ), ( @@ -211,6 +264,11 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str]]] = [ [ "005 nick", "005 nick foo=1", + ], + # and they each error with: + [ + "expected params to match ['nick', ListRemainder(StrRe(r'[A-Z]+=.*'), min_length=1)], got ['nick']", + "expected params to match ['nick', ListRemainder(StrRe(r'[A-Z]+=.*'), min_length=1)], got ['nick', 'foo=1']", ] ), ] @@ -222,7 +280,7 @@ class IrcTestCaseTestCase(cases._IrcTestCase): "spec,msg", [ pytest.param(spec, msg, id=f"{spec}-{msg}") - for (spec, positive_matches, _) in MESSAGE_SPECS + for (spec, positive_matches, _, _) in MESSAGE_SPECS for msg in positive_matches ], ) @@ -235,7 +293,7 @@ class IrcTestCaseTestCase(cases._IrcTestCase): "spec,msg", [ pytest.param(spec, msg, id=f"{spec}-{msg}") - for (spec, _, negative_matches) in MESSAGE_SPECS + for (spec, _, negative_matches, _) in MESSAGE_SPECS for msg in negative_matches ], ) @@ -244,3 +302,14 @@ class IrcTestCaseTestCase(cases._IrcTestCase): assert not self.messageEqual(parse_message(msg), **spec), msg with pytest.raises(AssertionError): self.assertMessageMatch(parse_message(msg), **spec), msg + + @pytest.mark.parametrize( + "spec,msg,error_string", + [ + pytest.param(spec, msg, error_string, id=error_string) + for (spec, _, negative_matches, error_stringgexps) in MESSAGE_SPECS + for (msg, error_string) in zip(negative_matches, error_stringgexps) + ], + ) + def test_message_matching_negative_message(self, spec, msg, error_string): + self.assertIn(error_string, self.messageDiffers(parse_message(msg), **spec)) From 2a4e71eccde3174b872f4896d6c750906cc9d6de Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 5 Mar 2022 09:52:18 +0100 Subject: [PATCH 004/143] patma: Fix inconsistencies between ANYSTR and AnyOptStr --- irctest/patma.py | 19 ++++++++++------- irctest/self_tests/cases.py | 26 +++++++++++------------ irctest/server_tests/labeled_responses.py | 6 +++--- 3 files changed, 27 insertions(+), 24 deletions(-) diff --git a/irctest/patma.py b/irctest/patma.py index f198601..2ae27e8 100644 --- a/irctest/patma.py +++ b/irctest/patma.py @@ -13,18 +13,18 @@ class Operator: pass -class AnyStr(Operator): +class _AnyStr(Operator): """Wildcard matching any string""" def __repr__(self) -> str: - return "AnyStr" + return "ANYSTR" -class AnyOptStr(Operator): +class _AnyOptStr(Operator): """Wildcard matching any string as well as None""" def __repr__(self) -> str: - return "AnyOptStr()" + return "ANYOPTSTR" @dataclasses.dataclass(frozen=True) @@ -62,10 +62,13 @@ class RemainingKeys(Operator): return f"RemainingKeys({self.key!r})" -ANYSTR = AnyStr() +ANYSTR = _AnyStr() """Singleton, spares two characters""" -ANYDICT = {RemainingKeys(ANYSTR): AnyOptStr()} +ANYOPTSTR = _AnyOptStr() +"""Singleton, spares two characters""" + +ANYDICT = {RemainingKeys(ANYSTR): ANYOPTSTR} """Matches any dictionary; useful to compare tags dict, eg. `match_dict(got_tags, {"label": "foo", **ANYDICT})`""" @@ -87,9 +90,9 @@ ANYLIST = [ListRemainder(ANYSTR)] def match_string(got: Optional[str], expected: Union[str, Operator, None]) -> bool: - if isinstance(expected, AnyOptStr): + if isinstance(expected, _AnyOptStr): return True - elif isinstance(expected, AnyStr) and got is not None: + elif isinstance(expected, _AnyStr) and got is not None: return True elif isinstance(expected, StrRe): if got is None or not re.match(expected.regexp, got): diff --git a/irctest/self_tests/cases.py b/irctest/self_tests/cases.py index 22a2286..296cfa0 100644 --- a/irctest/self_tests/cases.py +++ b/irctest/self_tests/cases.py @@ -7,8 +7,8 @@ from irctest.irc_utils.message_parser import parse_message from irctest.patma import ( ANYDICT, ANYLIST, + ANYOPTSTR, ANYSTR, - AnyOptStr, ListRemainder, NotStrRe, RemainingKeys, @@ -142,9 +142,9 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [ ], # and they each error with: [ - "expected tags to match {'tag1': AnyStr}, got {'tag1': 'bar', 'tag2': ''}", - "expected tags to match {'tag1': AnyStr}, got {}", - "expected tags to match {'tag1': AnyStr}, got {}", + "expected tags to match {'tag1': ANYSTR}, got {'tag1': 'bar', 'tag2': ''}", + "expected tags to match {'tag1': ANYSTR}, got {}", + "expected tags to match {'tag1': ANYSTR}, got {}", ] ), ( @@ -171,16 +171,16 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [ # and they each error with: [ "expected command to be PRIVMSG, got PRIVMG", - "expected tags to match {'tag1': 'bar', RemainingKeys(AnyStr): AnyOptStr()}, got {'tag1': 'value1'}", + "expected tags to match {'tag1': 'bar', RemainingKeys(ANYSTR): ANYOPTSTR}, got {'tag1': 'value1'}", "expected params to match ['#chan', 'hello'], got ['#chan', 'hello2']", "expected params to match ['#chan', 'hello'], got ['#chan2', 'hello']", - "expected tags to match {'tag1': 'bar', RemainingKeys(AnyStr): AnyOptStr()}, got {}", + "expected tags to match {'tag1': 'bar', RemainingKeys(ANYSTR): ANYOPTSTR}, got {}", ] ), ( # the specification: dict( - tags={"tag1": "bar", RemainingKeys(NotStrRe("tag2")): AnyOptStr()}, + tags={"tag1": "bar", RemainingKeys(NotStrRe("tag2")): ANYOPTSTR}, command="PRIVMSG", params=["#chan", "hello"], ), @@ -200,9 +200,9 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [ # and they each error with: [ "expected command to be PRIVMSG, got PRIVMG", - "expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): AnyOptStr()}, got {'tag1': 'value1'}", - "expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): AnyOptStr()}, got {'tag1': 'bar', 'tag2': ''}", - "expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): AnyOptStr()}, got {'tag1': 'bar', 'tag2': 'baz'}", + "expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): ANYOPTSTR}, got {'tag1': 'value1'}", + "expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): ANYOPTSTR}, got {'tag1': 'bar', 'tag2': ''}", + "expected tags to match {'tag1': 'bar', RemainingKeys(NotStrRe(r'tag2')): ANYOPTSTR}, got {'tag1': 'bar', 'tag2': 'baz'}", ] ), ( @@ -223,8 +223,8 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [ ], # and they each error with: [ - "expected params to match ['nick', 'FOO=1', ListRemainder(AnyStr)], got ['nick']", - "expected params to match ['nick', 'FOO=1', ListRemainder(AnyStr)], got ['nick', 'BAR=2']", + "expected params to match ['nick', 'FOO=1', ListRemainder(ANYSTR)], got ['nick']", + "expected params to match ['nick', 'FOO=1', ListRemainder(ANYSTR)], got ['nick', 'BAR=2']", ] ), ( @@ -245,7 +245,7 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [ ], # and they each error with: [ - "expected params to match ['nick', ListRemainder(AnyStr, min_length=1)], got ['nick']", + "expected params to match ['nick', ListRemainder(ANYSTR, min_length=1)], got ['nick']", ] ), ( diff --git a/irctest/server_tests/labeled_responses.py b/irctest/server_tests/labeled_responses.py index acef9b9..f1c2374 100644 --- a/irctest/server_tests/labeled_responses.py +++ b/irctest/server_tests/labeled_responses.py @@ -11,7 +11,7 @@ import pytest from irctest import cases from irctest.numerics import ERR_UNKNOWNCOMMAND -from irctest.patma import ANYDICT, AnyOptStr, NotStrRe, RemainingKeys, StrRe +from irctest.patma import ANYDICT, ANYOPTSTR, NotStrRe, RemainingKeys, StrRe class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): @@ -299,7 +299,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper tags={ "+draft/reply": msgid, "+draft/react": "l😃l", - RemainingKeys(NotStrRe("label")): AnyOptStr(), + RemainingKeys(NotStrRe("label")): ANYOPTSTR, }, ) self.assertNotIn( @@ -367,7 +367,7 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper tags={ "+draft/reply": msgid, "+draft/react": "l😃l", - RemainingKeys(NotStrRe("label")): AnyOptStr(), + RemainingKeys(NotStrRe("label")): ANYOPTSTR, }, fail_msg="No TAGMSG received by the target after sending one out", ) From 8356ace0144297885f6d51e334763df5223b933f Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 5 Mar 2022 09:53:24 +0100 Subject: [PATCH 005/143] Shorten ListRemainder's repr() when possible. --- irctest/patma.py | 2 ++ irctest/self_tests/cases.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/irctest/patma.py b/irctest/patma.py index 2ae27e8..30ce278 100644 --- a/irctest/patma.py +++ b/irctest/patma.py @@ -81,6 +81,8 @@ class ListRemainder: def __repr__(self) -> str: if self.min_length: return f"ListRemainder({self.item!r}, min_length={self.min_length})" + elif self.item is ANYSTR: + return "*ANYLIST" else: return f"ListRemainder({self.item!r})" diff --git a/irctest/self_tests/cases.py b/irctest/self_tests/cases.py index 296cfa0..edc4c05 100644 --- a/irctest/self_tests/cases.py +++ b/irctest/self_tests/cases.py @@ -223,8 +223,8 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [ ], # and they each error with: [ - "expected params to match ['nick', 'FOO=1', ListRemainder(ANYSTR)], got ['nick']", - "expected params to match ['nick', 'FOO=1', ListRemainder(ANYSTR)], got ['nick', 'BAR=2']", + "expected params to match ['nick', 'FOO=1', *ANYLIST], got ['nick']", + "expected params to match ['nick', 'FOO=1', *ANYLIST], got ['nick', 'BAR=2']", ] ), ( From ee8f60d6c2de43067d26bd6334165958ad26cdbe Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Wed, 9 Mar 2022 20:01:34 +0100 Subject: [PATCH 006/143] Add test for ISUPPORT PREFIX. (#128) --- irctest/server_tests/isupport.py | 58 ++++++++++++++++++++++++++++++++ irctest/specifications.py | 1 + pytest.ini | 1 + 3 files changed, 60 insertions(+) diff --git a/irctest/server_tests/isupport.py b/irctest/server_tests/isupport.py index 4a68a1a..40f087d 100644 --- a/irctest/server_tests/isupport.py +++ b/irctest/server_tests/isupport.py @@ -4,6 +4,64 @@ from irctest import cases, runner class IsupportTestCase(cases.BaseServerTestCase): + @cases.mark_specifications("Modern") + @cases.mark_isupport("PREFIX") + def testPrefix(self): + """https://modern.ircdocs.horse/#prefix-parameter""" + self.connectClient("foo") + + if "PREFIX" not in self.server_support: + raise runner.NotImplementedByController("PREFIX") + + if self.server_support["PREFIX"] == "": + # "The value is OPTIONAL and when it is not specified indicates that no + # prefixes are supported." + return + + m = re.match( + r"\((?P[a-zA-Z]+)\)(?P\S+)", self.server_support["PREFIX"] + ) + self.assertTrue( + m, + f"PREFIX={self.server_support['PREFIX']} does not have the expected " + f"format.", + ) + + modes = m.group("modes") + prefixes = m.group("prefixes") + + # "There is a one-to-one mapping between prefixes and channel modes." + self.assertEqual( + len(modes), len(prefixes), "Mismatched length of prefix and channel modes." + ) + + # "The prefixes in this parameter are in descending order, from the prefix + # that gives the most privileges to the prefix that gives the least." + self.assertLess(modes.index("o"), modes.index("v"), "'o' is not before 'v'") + if "h" in modes: + self.assertLess(modes.index("o"), modes.index("h"), "'o' is not before 'h'") + self.assertLess(modes.index("h"), modes.index("v"), "'h' is not before 'v'") + if "q" in modes: + self.assertLess(modes.index("q"), modes.index("o"), "'q' is not before 'o'") + + # Not technically in the spec, but it would be very confusing not to follow + # these conventions. + mode_to_prefix = dict(zip(modes, prefixes)) + self.assertEqual(mode_to_prefix["o"], "@", "Prefix char for mode +o is not @") + self.assertEqual(mode_to_prefix["v"], "+", "Prefix char for mode +v is not +") + if "h" in modes: + self.assertEqual( + mode_to_prefix["h"], "%", "Prefix char for mode +h is not %" + ) + if "q" in modes: + self.assertEqual( + mode_to_prefix["q"], "~", "Prefix char for mode +q is not ~" + ) + if "a" in modes: + self.assertEqual( + mode_to_prefix["a"], "&", "Prefix char for mode +a is not &" + ) + @cases.mark_specifications("Modern", "ircdocs") @cases.mark_isupport("TARGMAX") def testTargmax(self): diff --git a/irctest/specifications.py b/irctest/specifications.py index 745895a..8a629c7 100644 --- a/irctest/specifications.py +++ b/irctest/specifications.py @@ -50,6 +50,7 @@ class Capabilities(enum.Enum): @enum.unique class IsupportTokens(enum.Enum): BOT = "BOT" + PREFIX = "PREFIX" MONITOR = "MONITOR" STATUSMSG = "STATUSMSG" TARGMAX = "TARGMAX" diff --git a/pytest.ini b/pytest.ini index 6774b93..19518e3 100644 --- a/pytest.ini +++ b/pytest.ini @@ -33,6 +33,7 @@ markers = # isupport tokens BOT MONITOR + PREFIX STATUSMSG TARGMAX WHOX From 69c5dca4b9b2bcff9dc25dde488db4463e68f88f Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 19 Mar 2022 16:09:27 +0100 Subject: [PATCH 007/143] Add client tests for SASL with non-ASCII passwords (#137) --- irctest/client_tests/sasl.py | 14 ++++++++------ irctest/controllers/limnoria.py | 14 ++++++++++---- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/irctest/client_tests/sasl.py b/irctest/client_tests/sasl.py index c157059..7cc3781 100644 --- a/irctest/client_tests/sasl.py +++ b/irctest/client_tests/sasl.py @@ -84,8 +84,9 @@ class SaslTestCase(cases.BaseClientTestCase, cases.OptionalityHelper): m = self.getMessage() self.assertMessageMatch(m, command="CAP") + @pytest.mark.parametrize("pattern", ["barbaz", "éèà"]) @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") - def testPlainLarge(self): + def testPlainLarge(self, pattern): """Test the client splits large AUTHENTICATE messages whose payload is not a multiple of 400. @@ -94,10 +95,10 @@ class SaslTestCase(cases.BaseClientTestCase, cases.OptionalityHelper): auth = authentication.Authentication( mechanisms=[authentication.Mechanisms.plain], username="foo", - password="bar" * 200, + password=pattern * 100, ) authstring = base64.b64encode( - b"\x00".join([b"foo", b"foo", b"bar" * 200]) + b"\x00".join([b"foo", b"foo", pattern.encode() * 100]) ).decode() m = self.negotiateCapabilities(["sasl"], auth=auth) self.assertEqual(m, Message({}, None, "AUTHENTICATE", ["PLAIN"])) @@ -114,7 +115,8 @@ class SaslTestCase(cases.BaseClientTestCase, cases.OptionalityHelper): self.assertEqual(m, Message({}, None, "CAP", ["END"])) @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") - def testPlainLargeMultiple(self): + @pytest.mark.parametrize("pattern", ["quux", "éè"]) + def testPlainLargeMultiple(self, pattern): """Test the client splits large AUTHENTICATE messages whose payload is a multiple of 400. @@ -123,10 +125,10 @@ class SaslTestCase(cases.BaseClientTestCase, cases.OptionalityHelper): auth = authentication.Authentication( mechanisms=[authentication.Mechanisms.plain], username="foo", - password="quux" * 148, + password=pattern * 148, ) authstring = base64.b64encode( - b"\x00".join([b"foo", b"foo", b"quux" * 148]) + b"\x00".join([b"foo", b"foo", pattern.encode() * 148]) ).decode() m = self.negotiateCapabilities(["sasl"], auth=auth) self.assertEqual(m, Message({}, None, "AUTHENTICATE", ["PLAIN"])) diff --git a/irctest/controllers/limnoria.py b/irctest/controllers/limnoria.py index 78b005f..5b38e21 100644 --- a/irctest/controllers/limnoria.py +++ b/irctest/controllers/limnoria.py @@ -55,13 +55,19 @@ class LimnoriaController(BaseClientController, DirectoryBasedController): # Runs a client with the config given as arguments assert self.proc is None self.create_config() + + username = password = "" + mechanisms = "" if auth: mechanisms = " ".join(mech.to_string() for mech in auth.mechanisms) if auth.ecdsa_key: with self.open_file("ecdsa_key.pem") as fd: fd.write(auth.ecdsa_key) - else: - mechanisms = "" + + if auth.username: + username = auth.username.encode("unicode_escape").decode() + if auth.password: + password = auth.password.encode("unicode_escape").decode() with self.open_file("bot.conf") as fd: fd.write( TEMPLATE_CONFIG.format( @@ -69,8 +75,8 @@ class LimnoriaController(BaseClientController, DirectoryBasedController): loglevel="CRITICAL", hostname=hostname, port=port, - username=auth.username if auth else "", - password=auth.password if auth else "", + username=username, + password=password, mechanisms=mechanisms.lower(), enable_tls=tls_config.enable if tls_config else "False", trusted_fingerprints=" ".join(tls_config.trusted_fingerprints) From 2af62461bc358d3de426a9df2ba976ba804242d1 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 19 Mar 2022 16:34:39 +0100 Subject: [PATCH 008/143] Add test for mismatch on both command and param --- irctest/self_tests/cases.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/irctest/self_tests/cases.py b/irctest/self_tests/cases.py index edc4c05..eda4b5d 100644 --- a/irctest/self_tests/cases.py +++ b/irctest/self_tests/cases.py @@ -271,6 +271,25 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [ "expected params to match ['nick', ListRemainder(StrRe(r'[A-Z]+=.*'), min_length=1)], got ['nick', 'foo=1']", ] ), + ( + # the specification: + dict( + command="PING", + params=["abc"] + ), + # matches: + [ + "PING abc", + ], + # and does not match: + [ + "PONG def" + ], + # and they each error with: + [ + "expected command to be PING, got PONG" + ] + ), ] # fmt: on From c47b057546791e92eb133f7a0bb3ea037c5e688e Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 19 Mar 2022 16:53:14 +0100 Subject: [PATCH 009/143] Fix inconsistent arg order --- irctest/server_tests/whois.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irctest/server_tests/whois.py b/irctest/server_tests/whois.py index b948e88..8a0ef3a 100644 --- a/irctest/server_tests/whois.py +++ b/irctest/server_tests/whois.py @@ -200,7 +200,7 @@ class WhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase, cases.Optionality """Tests all numerics are in the exhaustive list defined in the Modern spec. """ - self._testWhoisNumerics(authenticate=False, away=away, oper=oper) + self._testWhoisNumerics(oper=oper, authenticate=False, away=away) @cases.mark_services From 7b38c2be8a20845abab3242c8a6cc35d66f6cef0 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 19 Mar 2022 20:20:50 +0100 Subject: [PATCH 010/143] Add tests for WHOWAS. (#138) --- .github/workflows/test-devel.yml | 5 +- .github/workflows/test-devel_release.yml | 2 +- .github/workflows/test-stable.yml | 5 +- Makefile | 4 + README.md | 2 +- irctest/controllers/ngircd.py | 3 + irctest/server_tests/whowas.py | 204 ++++++++++++++++++ .../bahamut_localhost.patch | 0 .../inspircd_mainloop.patch | 0 patches/ngircd_whowas_delay.patch | 19 ++ workflows.yml | 5 +- 11 files changed, 241 insertions(+), 8 deletions(-) create mode 100644 irctest/server_tests/whowas.py rename bahamut_localhost.patch => patches/bahamut_localhost.patch (100%) rename inspircd_mainloop.patch => patches/inspircd_mainloop.patch (100%) create mode 100644 patches/ngircd_whowas_delay.patch diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index f3c4d38..640e680 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -66,7 +66,7 @@ jobs: - name: Build Bahamut run: | cd $GITHUB_WORKSPACE/Bahamut/ - patch src/s_user.c < $GITHUB_WORKSPACE/bahamut_localhost.patch + patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch echo "#undef THROTTLE_ENABLE" >> include/config.h libtoolize --force aclocal @@ -144,7 +144,7 @@ jobs: - name: Build InspIRCd run: | cd $GITHUB_WORKSPACE/inspircd/ - patch src/inspircd.cpp < $GITHUB_WORKSPACE/inspircd_mainloop.patch + patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch ./configure --prefix=$HOME/.local/inspircd --development make -j 4 make install @@ -184,6 +184,7 @@ jobs: - name: Build ngircd run: | cd $GITHUB_WORKSPACE/ngircd + patch src/ngircd/client.c < $GITHUB_WORKSPACE/patches/ngircd_whowas_delay.patch ./autogen.sh ./configure --prefix=$HOME/.local/ make -j 4 diff --git a/.github/workflows/test-devel_release.yml b/.github/workflows/test-devel_release.yml index 92fc3cc..0b25abb 100644 --- a/.github/workflows/test-devel_release.yml +++ b/.github/workflows/test-devel_release.yml @@ -57,7 +57,7 @@ jobs: - name: Build InspIRCd run: | cd $GITHUB_WORKSPACE/inspircd/ - patch src/inspircd.cpp < $GITHUB_WORKSPACE/inspircd_mainloop.patch + patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch ./configure --prefix=$HOME/.local/inspircd --development make -j 4 make install diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 1df34a6..c908907 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -66,7 +66,7 @@ jobs: - name: Build Bahamut run: | cd $GITHUB_WORKSPACE/Bahamut/ - patch src/s_user.c < $GITHUB_WORKSPACE/bahamut_localhost.patch + patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch echo "#undef THROTTLE_ENABLE" >> include/config.h libtoolize --force aclocal @@ -184,7 +184,7 @@ jobs: - name: Build InspIRCd run: | cd $GITHUB_WORKSPACE/inspircd/ - patch src/inspircd.cpp < $GITHUB_WORKSPACE/inspircd_mainloop.patch + patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch ./configure --prefix=$HOME/.local/inspircd --development make -j 4 make install @@ -224,6 +224,7 @@ jobs: - name: Build ngircd run: | cd $GITHUB_WORKSPACE/ngircd + patch src/ngircd/client.c < $GITHUB_WORKSPACE/patches/ngircd_whowas_delay.patch ./autogen.sh ./configure --prefix=$HOME/.local/ make -j 4 diff --git a/Makefile b/Makefile index 4b76e4d..0f572bf 100644 --- a/Makefile +++ b/Makefile @@ -55,6 +55,7 @@ HYBRID_SELECTORS := \ # testNoticeNonexistentChannel fails because of https://github.com/inspircd/inspircd/issues/1849 # testBotPrivateMessage and testBotChannelMessage fail because https://github.com/inspircd/inspircd/pull/1910 is not released yet # testNamesInvalidChannel and testNamesNonexistingChannel fail because https://github.com/inspircd/inspircd/pull/1922 is not released yet. +# WHOWAS tests fail because https://github.com/inspircd/inspircd/pull/1967 and https://github.com/inspircd/inspircd/pull/1968 are not released yet INSPIRCD_SELECTORS := \ not Ergo \ and not deprecated \ @@ -62,6 +63,7 @@ INSPIRCD_SELECTORS := \ and not testNoticeNonexistentChannel \ and not testBotPrivateMessage and not testBotChannelMessage \ and not testNamesInvalidChannel and not testNamesNonexistingChannel \ + and not whowas \ $(EXTRA_SELECTORS) # buffering tests fail because ircu2 discards the whole buffer on long lines (TODO: refine how we exclude these tests) @@ -72,6 +74,7 @@ INSPIRCD_SELECTORS := \ # testKickDefaultComment fails because it uses the nick of the kickee rather than the kicker. # testEmptyRealname fails because it uses a default value instead of ERR_NEEDMOREPARAMS. # HelpTestCase fails because it returns NOTICEs instead of numerics +# testWhowasCountZero fails: https://github.com/UndernetIRC/ircu2/pull/19 IRCU2_SELECTORS := \ not Ergo \ and not deprecated \ @@ -84,6 +87,7 @@ IRCU2_SELECTORS := \ and not testKickDefaultComment \ and not testEmptyRealname \ and not HelpTestCase \ + and not testWhowasCountZero \ $(EXTRA_SELECTORS) # same justification as ircu2 diff --git a/README.md b/README.md index c5503af..7aac195 100644 --- a/README.md +++ b/README.md @@ -111,7 +111,7 @@ git clone https://github.com/inspircd/inspircd.git cd inspircd # optional, makes tests run considerably faster -patch src/inspircd.cpp < ~/irctest/inspircd_mainloop.patch +patch src/inspircd.cpp < ~/irctest/patches/inspircd_mainloop.patch ./configure --prefix=$HOME/.local/ --development make -j 4 diff --git a/irctest/controllers/ngircd.py b/irctest/controllers/ngircd.py index 5d0d845..17b3540 100644 --- a/irctest/controllers/ngircd.py +++ b/irctest/controllers/ngircd.py @@ -26,6 +26,9 @@ TEMPLATE_CONFIG = """ Passive = yes # don't connect to it ServiceMask = *Serv +[Options] + MorePrivacy = no # by default, always replies to WHOWAS with ERR_WASNOSUCHNICK + [Operator] Name = operuser Password = operpassword diff --git a/irctest/server_tests/whowas.py b/irctest/server_tests/whowas.py new file mode 100644 index 0000000..c3de2c9 --- /dev/null +++ b/irctest/server_tests/whowas.py @@ -0,0 +1,204 @@ +from irctest import cases +from irctest.exceptions import ConnectionClosed +from irctest.numerics import ( + RPL_ENDOFWHOWAS, + RPL_WHOISACTUALLY, + RPL_WHOISSERVER, + RPL_WHOWASUSER, +) +from irctest.patma import ANYSTR, StrRe + + +class WhowasTestCase(cases.BaseServerTestCase): + @cases.mark_specifications("RFC1459", "RFC2812") + def testWhowasNumerics(self): + """ + https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 + https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 + """ + self.connectClient("nick1") + + self.connectClient("nick2") + self.sendLine(2, "QUIT :bye") + try: + self.getMessages(2) + except ConnectionClosed: + pass + + self.sendLine(1, "WHOWAS nick2") + + messages = [] + for _ in range(10): + messages.extend(self.getMessages(1)) + if RPL_ENDOFWHOWAS in (m.command for m in messages): + break + + last_message = messages.pop() + + self.assertMessageMatch( + last_message, + command=RPL_ENDOFWHOWAS, + params=["nick1", "nick2", ANYSTR], + fail_msg=f"Last message was not RPL_ENDOFWHOWAS ({RPL_ENDOFWHOWAS})", + ) + + unexpected_messages = [] + + # Straight from the RFCs + for m in messages: + if m.command == RPL_WHOWASUSER: + host_re = "[0-9A-Za-z_:.-]+" + self.assertMessageMatch( + m, + params=[ + "nick1", + "nick2", + StrRe("~?username"), + StrRe(host_re), + "*", + "Realname", + ], + ) + elif m.command == RPL_WHOISSERVER: + self.assertMessageMatch( + m, params=["nick1", "nick2", "My.Little.Server", ANYSTR] + ) + elif m.command == RPL_WHOISACTUALLY: + # Technically not allowed by the RFCs, but Solanum uses it. + # Not checking the syntax here; WhoisTestCase does it. + pass + else: + unexpected_messages.append(m) + + self.assertEqual( + unexpected_messages, [], fail_msg="Unexpected numeric messages: {got}" + ) + + def _testWhowasMultiple(self, second_result, whowas_command): + """ + "The history is searched backward, returning the most recent entry first." + -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 + -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 + """ + # TODO: this test assumes the order is always: RPL_WHOWASUSER, then + # optional RPL_WHOISACTUALLY, then RPL_WHOISSERVER; but the RFCs + # don't specify the order. + self.connectClient("nick1") + + self.connectClient("nick2", ident="ident2") + self.sendLine(2, "QUIT :bye") + try: + self.getMessages(2) + except ConnectionClosed: + pass + + self.connectClient("nick2", ident="ident3") + self.sendLine(3, "QUIT :bye") + try: + self.getMessages(3) + except ConnectionClosed: + pass + + self.sendLine(1, whowas_command) + + messages = self.getMessages(1) + + # nick2 with ident3 + self.assertMessageMatch( + messages.pop(0), + command=RPL_WHOWASUSER, + params=[ + "nick1", + "nick2", + StrRe("~?ident3"), + ANYSTR, + "*", + "Realname", + ], + ) + while messages[0].command in (RPL_WHOISACTUALLY, RPL_WHOISSERVER): + # don't care + messages.pop(0) + + if second_result: + # nick2 with ident2 + self.assertMessageMatch( + messages.pop(0), + command=RPL_WHOWASUSER, + params=[ + "nick1", + "nick2", + StrRe("~?ident2"), + ANYSTR, + "*", + "Realname", + ], + ) + if messages[0].command == RPL_WHOISACTUALLY: + # don't care + messages.pop(0) + while messages[0].command in (RPL_WHOISACTUALLY, RPL_WHOISSERVER): + # don't care + messages.pop(0) + + self.assertMessageMatch( + messages.pop(0), + command=RPL_ENDOFWHOWAS, + params=["nick1", "nick2", ANYSTR], + fail_msg=f"Last message was not RPL_ENDOFWHOWAS ({RPL_ENDOFWHOWAS})", + ) + + @cases.mark_specifications("RFC1459", "RFC2812") + def testWhowasMultiple(self): + """ + "The history is searched backward, returning the most recent entry first." + -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 + -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 + """ + self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2") + + @cases.mark_specifications("RFC1459", "RFC2812") + def testWhowasCount1(self): + """ + "If there are multiple entries, up to replies will be returned" + -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 + -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 + """ + self._testWhowasMultiple(second_result=False, whowas_command="WHOWAS nick2 1") + + @cases.mark_specifications("RFC1459", "RFC2812") + def testWhowasCount2(self): + """ + "If there are multiple entries, up to replies will be returned" + -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 + -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 + """ + self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 2") + + @cases.mark_specifications("RFC1459", "RFC2812") + def testWhowasCountNegative(self): + """ + "If a non-positive number is passed as being , then a full search + is done." + -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 + -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 + """ + self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 -1") + + @cases.mark_specifications("RFC1459", "RFC2812") + def testWhowasCountZero(self): + """ + "If a non-positive number is passed as being , then a full search + is done." + -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 + -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 + """ + self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 0") + + @cases.mark_specifications("RFC2812", deprecated=True) + def testWhowasWildcard(self): + """ + "Wildcards are allowed in the parameter." + -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 + """ + self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS *ck2") diff --git a/bahamut_localhost.patch b/patches/bahamut_localhost.patch similarity index 100% rename from bahamut_localhost.patch rename to patches/bahamut_localhost.patch diff --git a/inspircd_mainloop.patch b/patches/inspircd_mainloop.patch similarity index 100% rename from inspircd_mainloop.patch rename to patches/inspircd_mainloop.patch diff --git a/patches/ngircd_whowas_delay.patch b/patches/ngircd_whowas_delay.patch new file mode 100644 index 0000000..80e322e --- /dev/null +++ b/patches/ngircd_whowas_delay.patch @@ -0,0 +1,19 @@ +ngIRCd skips WHOWAS entries for users that were connected for less +than 30 seconds. + +To avoid waiting 30s in every WHOWAS test, we need to remove this. + +diff --git a/src/ngircd/client.c b/src/ngircd/client.c +index 67c02604..66e8e540 100644 +--- a/src/ngircd/client.c ++++ b/src/ngircd/client.c +@@ -1490,9 +1490,6 @@ Client_RegisterWhowas( CLIENT *Client ) + return; + + now = time(NULL); +- /* Don't register clients that were connected less than 30 seconds. */ +- if( now - Client->starttime < 30 ) +- return; + + slot = Last_Whowas + 1; + if( slot >= MAX_WHOWAS || slot < 0 ) slot = 0; diff --git a/workflows.yml b/workflows.yml index 4909a0d..3ce954b 100644 --- a/workflows.yml +++ b/workflows.yml @@ -104,7 +104,7 @@ software: separate_build_job: true build_script: | cd $GITHUB_WORKSPACE/Bahamut/ - patch src/s_user.c < $GITHUB_WORKSPACE/bahamut_localhost.patch + patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch echo "#undef THROTTLE_ENABLE" >> include/config.h libtoolize --force aclocal @@ -152,7 +152,7 @@ software: separate_build_job: true build_script: &inspircd_build_script | cd $GITHUB_WORKSPACE/inspircd/ - patch src/inspircd.cpp < $GITHUB_WORKSPACE/inspircd_mainloop.patch + patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch ./configure --prefix=$HOME/.local/inspircd --development make -j 4 make install @@ -217,6 +217,7 @@ software: separate_build_job: true build_script: | cd $GITHUB_WORKSPACE/ngircd + patch src/ngircd/client.c < $GITHUB_WORKSPACE/patches/ngircd_whowas_delay.patch ./autogen.sh ./configure --prefix=$HOME/.local/ make -j 4 From b63ead9546f229a85c3fb980b2e4a17d7805c001 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 19 Mar 2022 21:39:26 +0100 Subject: [PATCH 011/143] Bump versions used on the CI. (#140) --- .github/workflows/test-stable.yml | 22 +++++++++++----------- Makefile | 3 --- workflows.yml | 22 +++++++++++----------- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index c908907..0e66d6d 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -61,7 +61,7 @@ jobs: uses: actions/checkout@v2 with: path: Bahamut - ref: v2.2.0 + ref: v2.2.1 repository: DALnet/Bahamut - name: Build Bahamut run: | @@ -149,7 +149,7 @@ jobs: uses: actions/checkout@v2 with: path: ircd-hybrid - ref: 8.2.38 + ref: 8.2.39 repository: ircd-hybrid/ircd-hybrid - name: Build Hybrid run: | @@ -179,7 +179,7 @@ jobs: uses: actions/checkout@v2 with: path: inspircd - ref: v3.10.0 + ref: v3.12.0 repository: inspircd/inspircd - name: Build InspIRCd run: | @@ -257,10 +257,10 @@ jobs: with: python-version: 3.7 - name: clone - run: 'curl https://gitlab.com/rizon/plexus4/-/archive/403a967e3677a2a8420b504f451e7557259e0790/plexus4-403a967e3677a2a8420b504f451e7557259e0790.tar.gz - | tar -zx + run: 'curl https://gitlab.com/rizon/plexus4/-/archive/20211115_0-611/plexus4-20211115_0-611.tar + | tar -x - mv plexus4* plexus4' + mv plexus* plexus4' - name: build run: 'cd $GITHUB_WORKSPACE/plexus4 @@ -302,7 +302,7 @@ jobs: uses: actions/checkout@v2 with: path: solanum - ref: e370888264da666a1bd9faac86cd5f2aa06084f4 + ref: 492d560ee13e71dc35403fd676e58c2d5bdcf2a9 repository: solanum-ircd/solanum - name: Build Solanum run: | @@ -342,7 +342,7 @@ jobs: uses: actions/checkout@v2 with: path: unrealircd - ref: d77f42e4bef388ae344256eeef9a8000345ae381 + ref: daa0c11f285c7123ba9fa2966dee2d1a17729f1e repository: unrealircd/unrealircd - name: Build UnrealIRCd 6 run: | @@ -386,7 +386,7 @@ jobs: uses: actions/checkout@v2 with: path: unrealircd - ref: 94993a03ca8d3c193c0295c33af39270c3f9d27d + ref: 6604856973f713a494f83d38992d7d61ce6b9db4 repository: unrealircd/unrealircd - name: Build UnrealIRCd 5 run: | @@ -876,7 +876,7 @@ jobs: with: python-version: 3.7 - name: Install dependencies - run: pip install limnoria==2021.10.09 cryptography pyxmpp2-scram + run: pip install limnoria==2022.03.17 cryptography pyxmpp2-scram - name: Install Atheme run: sudo apt-get install atheme-services - name: Install irctest dependencies @@ -1074,7 +1074,7 @@ jobs: with: python-version: 3.7 - name: Install dependencies - run: pip install sopel==7.1.1 + run: pip install sopel==7.1.8 - name: Install Atheme run: sudo apt-get install atheme-services - name: Install irctest dependencies diff --git a/Makefile b/Makefile index 0f572bf..64a5ba2 100644 --- a/Makefile +++ b/Makefile @@ -52,9 +52,7 @@ HYBRID_SELECTORS := \ and not deprecated \ $(EXTRA_SELECTORS) -# testNoticeNonexistentChannel fails because of https://github.com/inspircd/inspircd/issues/1849 # testBotPrivateMessage and testBotChannelMessage fail because https://github.com/inspircd/inspircd/pull/1910 is not released yet -# testNamesInvalidChannel and testNamesNonexistingChannel fail because https://github.com/inspircd/inspircd/pull/1922 is not released yet. # WHOWAS tests fail because https://github.com/inspircd/inspircd/pull/1967 and https://github.com/inspircd/inspircd/pull/1968 are not released yet INSPIRCD_SELECTORS := \ not Ergo \ @@ -62,7 +60,6 @@ INSPIRCD_SELECTORS := \ and not strict \ and not testNoticeNonexistentChannel \ and not testBotPrivateMessage and not testBotChannelMessage \ - and not testNamesInvalidChannel and not testNamesNonexistingChannel \ and not whowas \ $(EXTRA_SELECTORS) diff --git a/workflows.yml b/workflows.yml index 3ce954b..0bce533 100644 --- a/workflows.yml +++ b/workflows.yml @@ -27,7 +27,7 @@ software: name: Hybrid repository: ircd-hybrid/ircd-hybrid refs: - stable: "8.2.38" + stable: "8.2.39" release: null devel: "8.2.x" devel_release: null @@ -47,8 +47,8 @@ software: stable: - name: clone run: |- - curl https://gitlab.com/rizon/plexus4/-/archive/403a967e3677a2a8420b504f451e7557259e0790/plexus4-403a967e3677a2a8420b504f451e7557259e0790.tar.gz | tar -zx - mv plexus4* plexus4 + curl https://gitlab.com/rizon/plexus4/-/archive/20211115_0-611/plexus4-20211115_0-611.tar | tar -x + mv plexus* plexus4 - name: build run: |- cd $GITHUB_WORKSPACE/plexus4 @@ -77,7 +77,7 @@ software: refs: # Actually Solanum doesn't have releases; so we just bump this # commit hash from time to time - stable: e370888264da666a1bd9faac86cd5f2aa06084f4 + stable: 492d560ee13e71dc35403fd676e58c2d5bdcf2a9 release: null devel: main devel_release: null @@ -96,7 +96,7 @@ software: name: Bahamut repository: DALnet/Bahamut refs: - stable: "v2.2.0" + stable: "v2.2.1" release: null devel: "master" devel_release: null @@ -142,7 +142,7 @@ software: name: InspIRCd repository: inspircd/inspircd refs: &inspircd_refs - stable: v3.10.0 + stable: v3.12.0 release: null devel: master devel_release: insp3 @@ -250,8 +250,8 @@ software: name: UnrealIRCd 6 repository: unrealircd/unrealircd refs: - stable: d77f42e4bef388ae344256eeef9a8000345ae381 # 6.0.0 + 2 commits - release: 893bf864f6c616e891d84916d27e342c252f31aaa # 6.0.0 + stable: daa0c11f285c7123ba9fa2966dee2d1a17729f1e # 6.0.2 + a few commits + release: 29fd2e772a6b4b9107daa4e3c237df454b055810 # 6.0.2 devel: unreal60_dev devel_release: null path: unrealircd @@ -272,7 +272,7 @@ software: name: UnrealIRCd 5 repository: unrealircd/unrealircd refs: - stable: 94993a03ca8d3c193c0295c33af39270c3f9d27d # 5.2.1-rc1 + stable: 6604856973f713a494f83d38992d7d61ce6b9db4 # 5.2.4 release: null devel: unreal52 devel_release: @@ -290,7 +290,7 @@ software: install_steps: stable: - name: Install dependencies - run: pip install limnoria==2021.10.09 cryptography pyxmpp2-scram + run: pip install limnoria==2022.03.17 cryptography pyxmpp2-scram release: - name: Install dependencies run: pip install limnoria cryptography pyxmpp2-scram @@ -305,7 +305,7 @@ software: install_steps: stable: - name: Install dependencies - run: pip install sopel==7.1.1 + run: pip install sopel==7.1.8 release: - name: Install dependencies run: pip install sopel From f606c075f7d222fe8a2173ee6bbcceaf6dd5764a Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 19 Mar 2022 22:12:25 +0100 Subject: [PATCH 012/143] Add tests for error cases of WHOWAS. (#139) --- Makefile | 2 ++ irctest/server_tests/whowas.py | 63 ++++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/Makefile b/Makefile index 64a5ba2..8347077 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,7 @@ BAHAMUT_SELECTORS := \ # AccountTagTestCase.testInvite fails because https://github.com/solanum-ircd/solanum/issues/166 # testKickDefaultComment fails because it uses the nick of the kickee rather than the kicker. # testWhoisNumerics[oper] fails because charybdis uses RPL_WHOISSPECIAL instead of RPL_WHOISOPERATOR +# testWhowasNoSuchNick fails because of a typo (solved in https://github.com/solanum-ircd/solanum/commit/08b7b6bd7e60a760ad47b58cbe8075b45d66166f) CHARYBDIS_SELECTORS := \ not Ergo \ and not deprecated \ @@ -37,6 +38,7 @@ CHARYBDIS_SELECTORS := \ and not testKickDefaultComment \ and not (AccountTagTestCase and testInvite) \ and not (testWhoisNumerics and oper) \ + and not testWhowasNoSuchNick \ $(EXTRA_SELECTORS) # testInfoNosuchserver does not apply to Ergo: Ergo ignores the optional argument diff --git a/irctest/server_tests/whowas.py b/irctest/server_tests/whowas.py index c3de2c9..4fab64d 100644 --- a/irctest/server_tests/whowas.py +++ b/irctest/server_tests/whowas.py @@ -1,6 +1,8 @@ from irctest import cases from irctest.exceptions import ConnectionClosed from irctest.numerics import ( + ERR_NONICKNAMEGIVEN, + ERR_WASNOSUCHNICK, RPL_ENDOFWHOWAS, RPL_WHOISACTUALLY, RPL_WHOISSERVER, @@ -202,3 +204,64 @@ class WhowasTestCase(cases.BaseServerTestCase): -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 """ self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS *ck2") + + @cases.mark_specifications("RFC1459", "RFC2812", deprecated=True) + def testWhowasNoParam(self): + """ + https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 + https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 + + and: + + "At the end of all reply batches, there must be RPL_ENDOFWHOWAS + (even if there was only one reply and it was an error)." + -- https://datatracker.ietf.org/doc/html/rfc1459#page-50 + -- https://datatracker.ietf.org/doc/html/rfc2812#page-45 + """ + # But no one seems to follow this. Most implementations use ERR_NEEDMOREPARAMS + # instead of ERR_NONICKNAMEGIVEN; and I couldn't find any that returns + # RPL_ENDOFWHOWAS either way. + self.connectClient("nick1") + + self.sendLine(1, "WHOWAS") + + self.assertMessageMatch( + self.getMessage(1), + command=ERR_NONICKNAMEGIVEN, + params=["nick1", ANYSTR], + ) + + self.assertMessageMatch( + self.getMessage(1), + command=RPL_ENDOFWHOWAS, + params=["nick1", "nick2", ANYSTR], + ) + + @cases.mark_specifications("RFC1459", "RFC2812") + def testWhowasNoSuchNick(self): + """ + https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 + https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 + + and: + + "At the end of all reply batches, there must be RPL_ENDOFWHOWAS + (even if there was only one reply and it was an error)." + -- https://datatracker.ietf.org/doc/html/rfc1459#page-50 + -- https://datatracker.ietf.org/doc/html/rfc2812#page-45 + """ + self.connectClient("nick1") + + self.sendLine(1, "WHOWAS nick2") + + self.assertMessageMatch( + self.getMessage(1), + command=ERR_WASNOSUCHNICK, + params=["nick1", "nick2", ANYSTR], + ) + + self.assertMessageMatch( + self.getMessage(1), + command=RPL_ENDOFWHOWAS, + params=["nick1", "nick2", ANYSTR], + ) From 256a8641ec61227e1005eb281b1c37e2cf00cbc1 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 20 Mar 2022 11:36:51 +0100 Subject: [PATCH 013/143] Add test for multi-target WHOWAS (#141) * Add test for multi-target WHOWAS I don't think anyone implements it; let's see * Skip on Bahamut --- Makefile | 2 + irctest/server_tests/whowas.py | 82 +++++++++++++++++++++++++++++++++- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 8347077..eef477a 100644 --- a/Makefile +++ b/Makefile @@ -14,6 +14,7 @@ ANOPE_SELECTORS := \ # buffering tests cannot pass because of issues with UTF-8 handling: https://github.com/DALnet/bahamut/issues/196 # mask tests in test_who.py fail because they are not implemented. # some HelpTestCase::*[HELP] tests fail because Bahamut forwards /HELP to HelpServ (but not /HELPOP) +# testWhowasMultiTarget fails because Bahamut returns the results in query order instead of chronological order BAHAMUT_SELECTORS := \ not Ergo \ and not deprecated \ @@ -23,6 +24,7 @@ BAHAMUT_SELECTORS := \ and not (testWho and not whois and mask) \ and not testWhoStar \ and (not HelpTestCase or HELPOP) \ + and not testWhowasMultiTarget \ $(EXTRA_SELECTORS) # testQuitErrors is very flaky diff --git a/irctest/server_tests/whowas.py b/irctest/server_tests/whowas.py index 4fab64d..2550432 100644 --- a/irctest/server_tests/whowas.py +++ b/irctest/server_tests/whowas.py @@ -1,4 +1,6 @@ -from irctest import cases +import pytest + +from irctest import cases, runner from irctest.exceptions import ConnectionClosed from irctest.numerics import ( ERR_NONICKNAMEGIVEN, @@ -265,3 +267,81 @@ class WhowasTestCase(cases.BaseServerTestCase): command=RPL_ENDOFWHOWAS, params=["nick1", "nick2", ANYSTR], ) + + @cases.mark_specifications("RFC2812") + @cases.mark_isupport("TARGMAX") + @pytest.mark.parametrize("targets", ["nick2,nick3", "nick3,nick2"]) + def testWhowasMultiTarget(self, targets): + """ + https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 + """ + self.connectClient("nick1") + + targmax = dict( + item.split(":", 1) + for item in self.server_support.get("TARGMAX", "").split(",") + if item + ) + if targmax.get("WHOWAS", "1") == "1": + raise runner.NotImplementedByController("Multi-target WHOWAS") + + self.connectClient("nick2", ident="ident2") + self.sendLine(2, "QUIT :bye") + try: + self.getMessages(2) + except ConnectionClosed: + pass + + self.connectClient("nick3", ident="ident3") + self.sendLine(3, "QUIT :bye") + try: + self.getMessages(3) + except ConnectionClosed: + pass + + self.sendLine(1, f"WHOWAS {targets}") + + messages = self.getMessages(1) + + self.assertMessageMatch( + messages.pop(0), + command=RPL_WHOWASUSER, + params=[ + "nick1", + "nick3", + StrRe("~?ident3"), + ANYSTR, + "*", + "Realname", + ], + ) + while messages[0].command in (RPL_WHOISACTUALLY, RPL_WHOISSERVER): + # don't care + messages.pop(0) + + # nick2 with ident2 + self.assertMessageMatch( + messages.pop(0), + command=RPL_WHOWASUSER, + params=[ + "nick1", + "nick2", + StrRe("~?ident2"), + ANYSTR, + "*", + "Realname", + ], + ) + if messages[0].command == RPL_WHOISACTUALLY: + # don't care + messages.pop(0) + while messages[0].command in (RPL_WHOISACTUALLY, RPL_WHOISSERVER): + # don't care + messages.pop(0) + + self.assertMessageMatch( + messages.pop(0), + command=RPL_ENDOFWHOWAS, + params=["nick1", targets, ANYSTR], + fail_msg=f"Last message was not RPL_ENDOFWHOWAS ({RPL_ENDOFWHOWAS})", + ) From 7608ea5145b4da6c892b81eb42e171152899144a Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 20 Mar 2022 22:07:07 +0100 Subject: [PATCH 014/143] Fix flaky LUSERS tests on Unreal --- irctest/controllers/unrealircd.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py index 5e8b103..236f56f 100644 --- a/irctest/controllers/unrealircd.py +++ b/irctest/controllers/unrealircd.py @@ -106,6 +106,10 @@ tld {{ rules "{empty_file}"; }} +files {{ + tunefile "{empty_file}"; +}} + oper "operuser" {{ password = "operpassword"; mask *; From 491f92ca605b9da408f8c6be67937b2172e99f0b Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Wed, 23 Mar 2022 21:26:41 +0100 Subject: [PATCH 015/143] Use proot with unreal, to make it parallelizable (#146) --- irctest/controllers/unrealircd.py | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py index 236f56f..af397c1 100644 --- a/irctest/controllers/unrealircd.py +++ b/irctest/controllers/unrealircd.py @@ -1,5 +1,8 @@ import functools import os +import pathlib +import shutil +import signal import subprocess import textwrap from typing import Optional, Set, Type @@ -205,8 +208,26 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): extras=extras, ) ) + + proot_cmd = [] + self.using_proot = False + if shutil.which("proot"): + unrealircd_path = shutil.which("unrealircd") + if unrealircd_path: + unrealircd_prefix = pathlib.Path(unrealircd_path).parents[1] + tmpdir = os.path.join(self.directory, "tmp") + os.mkdir(tmpdir) + # Unreal cleans its tmp/ directory after each run, which prevents + # multiple processes from running at the same time. + # Using PRoot, we can isolate them, with a tmp/ directory for each + # process, so they don't interfere with each other, allowing use of + # the -n option (of pytest-xdist) to speed-up tests + proot_cmd = ["proot", "-b", f"{tmpdir}:{unrealircd_prefix}/tmp"] + self.using_proot = True + self.proc = subprocess.Popen( [ + *proot_cmd, "unrealircd", "-t", "-F", # BOOT_NOFORK @@ -227,6 +248,18 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): server_port=services_port, ) + def kill(self) -> None: + if self.using_proot: + # Kill grandchild process, instead of killing proot, which takes more + # time (and does not seem to always work) + assert self.proc is not None + output = subprocess.check_output( + ["ps", "-opid", "--no-headers", "--ppid", str(self.proc.pid)] + ) + (grandchild_pid,) = [int(line) for line in output.decode().split()] + os.kill(grandchild_pid, signal.SIGKILL) + super().kill() + def get_irctest_controller_class() -> Type[UnrealircdController]: return UnrealircdController From 3f483243d9c553785b8eeced52d4b8c31791daa7 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 27 Mar 2022 17:07:29 +0200 Subject: [PATCH 016/143] Minor readability improvement --- irctest/cases.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/irctest/cases.py b/irctest/cases.py index f4caa9d..4dbd43a 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -539,13 +539,10 @@ class BaseServerTestCase( if self.run_services: self.controller.wait_for_services() if not name: - new_name: int = ( - max( - [int(name) for name in self.clients if isinstance(name, (int, str))] - + [0] - ) - + 1 - ) + used_ids: List[int] = [ + int(name) for name in self.clients if isinstance(name, (int, str)) + ] + new_name = max(used_ids + [0]) + 1 name = cast(TClientName, new_name) show_io = show_io if show_io is not None else self.show_io self.clients[name] = client_mock.ClientMock(name=name, show_io=show_io) From 72a12ff5ce9cb644ad58194f1670992ca85e5318 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 20 Mar 2022 14:05:45 +0100 Subject: [PATCH 017/143] Add support for 'faketime', to avoid long sleeps in upcoming ELIST tests --- .github/workflows/test-devel.yml | 76 ++++++++++---------- .github/workflows/test-devel_release.yml | 12 ++-- .github/workflows/test-stable.yml | 88 ++++++++++++------------ README.md | 1 + irctest/basecontrollers.py | 5 ++ irctest/cases.py | 7 ++ irctest/controllers/bahamut.py | 11 ++- irctest/controllers/base_hybrid.py | 10 +++ irctest/controllers/ergo.py | 11 ++- irctest/controllers/external_server.py | 1 + irctest/controllers/inspircd.py | 10 +++ irctest/controllers/irc2.py | 10 +++ irctest/controllers/ircu2.py | 10 +++ irctest/controllers/mammon.py | 10 +++ irctest/controllers/ngircd.py | 11 +++ irctest/controllers/snircd.py | 10 +++ irctest/controllers/unrealircd.py | 13 ++++ make_workflows.py | 4 +- 18 files changed, 207 insertions(+), 93 deletions(-) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 640e680..5d18ad2 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -435,8 +435,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -473,8 +473,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -505,8 +505,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -544,8 +544,8 @@ jobs: cd $GITHUB_WORKSPACE/ergo/ make build make install - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -582,8 +582,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -614,8 +614,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -652,8 +652,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -690,8 +690,8 @@ jobs: ./configure --prefix=$HOME/.local/ --with-maxcon=1024 --enable-debug make -j 4 make install - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -717,8 +717,8 @@ jobs: - name: Install dependencies run: pip install git+https://github.com/ProgVal/Limnoria.git@testing cryptography pyxmpp2-scram - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -749,8 +749,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -787,8 +787,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -819,8 +819,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -857,8 +857,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -889,8 +889,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -915,8 +915,8 @@ jobs: python-version: 3.7 - name: Install dependencies run: pip install git+https://github.com/sopel-irc/sopel.git - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -947,8 +947,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -979,8 +979,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -1017,8 +1017,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -1049,8 +1049,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip diff --git a/.github/workflows/test-devel_release.yml b/.github/workflows/test-devel_release.yml index 0b25abb..02d2524 100644 --- a/.github/workflows/test-devel_release.yml +++ b/.github/workflows/test-devel_release.yml @@ -121,8 +121,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -159,8 +159,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -191,8 +191,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 0e66d6d..7e0a1a3 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -478,8 +478,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -516,8 +516,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -548,8 +548,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -580,8 +580,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -619,8 +619,8 @@ jobs: cd $GITHUB_WORKSPACE/ergo/ make build make install - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -657,8 +657,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -689,8 +689,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -727,8 +727,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -759,8 +759,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -813,8 +813,8 @@ jobs: mkdir -p $HOME/.local/bin cp $HOME/.local/sbin/ircd $HOME/.local/bin/ircd' - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -851,8 +851,8 @@ jobs: ./configure --prefix=$HOME/.local/ --with-maxcon=1024 --enable-debug make -j 4 make install - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -877,8 +877,8 @@ jobs: python-version: 3.7 - name: Install dependencies run: pip install limnoria==2022.03.17 cryptography pyxmpp2-scram - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -909,8 +909,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -947,8 +947,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -979,8 +979,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -1017,8 +1017,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -1049,8 +1049,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -1075,8 +1075,8 @@ jobs: python-version: 3.7 - name: Install dependencies run: pip install sopel==7.1.8 - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -1107,8 +1107,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -1139,8 +1139,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -1177,8 +1177,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip @@ -1209,8 +1209,8 @@ jobs: path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip diff --git a/README.md b/README.md index 7aac195..18edbc5 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ have no side effect. Install irctest and dependencies: ``` +sudo apt install faketime # Optional, but greatly speeds up irctest/server_tests/list.py cd ~ git clone https://github.com/ProgVal/irctest.git cd irctest diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index b1464ac..e8680b3 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -189,6 +189,10 @@ class BaseServerController(_BaseController): """Character used for the 'mute' extban""" nickserv = "NickServ" + def __init__(self, *args: Any, **kwargs: Any): + super().__init__(*args, **kwargs) + self.faketime_enabled = False + def get_hostname_and_port(self) -> Tuple[str, int]: return find_hostname_and_port() @@ -202,6 +206,7 @@ class BaseServerController(_BaseController): run_services: bool, valid_metadata_keys: Optional[Set[str]], invalid_metadata_keys: Optional[Set[str]], + faketime: Optional[str], ) -> None: raise NotImplementedError() diff --git a/irctest/cases.py b/irctest/cases.py index 4dbd43a..a259b7f 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -508,6 +508,12 @@ class BaseServerTestCase( server_support: Optional[Dict[str, Optional[str]]] run_services = False + faketime: Optional[str] = None + """If not None and the controller supports it and libfaketime is available, + runs the server using faketime and this value set as the $FAKETIME env variable. + Tests must check ``self.controller.faketime_enabled`` is True before + relying on this.""" + __new__ = object.__new__ # pytest won't collect Generic[] subclasses otherwise def setUp(self) -> None: @@ -522,6 +528,7 @@ class BaseServerTestCase( invalid_metadata_keys=self.invalid_metadata_keys, ssl=self.ssl, run_services=self.run_services, + faketime=self.faketime, ) self.clients: Dict[TClientName, client_mock.ClientMock] = {} diff --git a/irctest/controllers/bahamut.py b/irctest/controllers/bahamut.py index 01cd4dd..6a23aaa 100644 --- a/irctest/controllers/bahamut.py +++ b/irctest/controllers/bahamut.py @@ -102,6 +102,7 @@ class BahamutController(BaseServerController, DirectoryBasedController): valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, restricted_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str], ) -> None: if valid_metadata_keys or invalid_metadata_keys: raise NotImplementedByController( @@ -136,15 +137,21 @@ class BahamutController(BaseServerController, DirectoryBasedController): # pem_path=self.pem_path, ) ) + + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + self.proc = subprocess.Popen( [ - # "strace", "-f", "-e", "file", + *faketime_cmd, "ircd", "-t", # don't fork "-f", os.path.join(self.directory, "server.conf"), ], - # stdout=subprocess.DEVNULL, ) if run_services: diff --git a/irctest/controllers/base_hybrid.py b/irctest/controllers/base_hybrid.py index f91229a..e2c0418 100644 --- a/irctest/controllers/base_hybrid.py +++ b/irctest/controllers/base_hybrid.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess from typing import Optional, Set @@ -43,6 +44,7 @@ class BaseHybridController(BaseServerController, DirectoryBasedController): run_services: bool, valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str], ) -> None: if valid_metadata_keys or invalid_metadata_keys: raise NotImplementedByController( @@ -73,8 +75,16 @@ class BaseHybridController(BaseServerController, DirectoryBasedController): ) ) assert self.directory + + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + self.proc = subprocess.Popen( [ + *faketime_cmd, self.binary_name, "-foreground", "-configfile", diff --git a/irctest/controllers/ergo.py b/irctest/controllers/ergo.py index d6e80ab..3878e7b 100644 --- a/irctest/controllers/ergo.py +++ b/irctest/controllers/ergo.py @@ -1,6 +1,7 @@ import copy import json import os +import shutil import subprocess from typing import Any, Dict, Optional, Set, Type, Union @@ -155,6 +156,7 @@ class ErgoController(BaseServerController, DirectoryBasedController): valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, restricted_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str], config: Optional[Any] = None, ) -> None: if valid_metadata_keys or invalid_metadata_keys: @@ -202,8 +204,15 @@ class ErgoController(BaseServerController, DirectoryBasedController): self._write_config() subprocess.call(["ergo", "initdb", "--conf", self._config_path, "--quiet"]) subprocess.call(["ergo", "mkcerts", "--conf", self._config_path, "--quiet"]) + + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + self.proc = subprocess.Popen( - ["ergo", "run", "--conf", self._config_path, "--quiet"] + [*faketime_cmd, "ergo", "run", "--conf", self._config_path, "--quiet"] ) def wait_for_services(self) -> None: diff --git a/irctest/controllers/external_server.py b/irctest/controllers/external_server.py index 5ecbae0..e8c822a 100644 --- a/irctest/controllers/external_server.py +++ b/irctest/controllers/external_server.py @@ -42,6 +42,7 @@ class ExternalServerController(BaseServerController): valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, restricted_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str], ) -> None: pass diff --git a/irctest/controllers/inspircd.py b/irctest/controllers/inspircd.py index 1cd40ba..24167aa 100644 --- a/irctest/controllers/inspircd.py +++ b/irctest/controllers/inspircd.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess from typing import Optional, Set, Type @@ -114,6 +115,7 @@ class InspircdController(BaseServerController, DirectoryBasedController): valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, restricted_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str] = None, ) -> None: if valid_metadata_keys or invalid_metadata_keys: raise NotImplementedByController( @@ -147,8 +149,16 @@ class InspircdController(BaseServerController, DirectoryBasedController): ) ) assert self.directory + + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + self.proc = subprocess.Popen( [ + *faketime_cmd, "inspircd", "--nofork", "--config", diff --git a/irctest/controllers/irc2.py b/irctest/controllers/irc2.py index 56defc8..d901c0d 100644 --- a/irctest/controllers/irc2.py +++ b/irctest/controllers/irc2.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess from typing import Optional, Set, Type @@ -51,6 +52,7 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController): run_services: bool, valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str], ) -> None: if valid_metadata_keys or invalid_metadata_keys: raise NotImplementedByController( @@ -76,8 +78,16 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController): pidfile=pidfile, ) ) + + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + self.proc = subprocess.Popen( [ + *faketime_cmd, "ircd", "-s", # no iauth "-p", diff --git a/irctest/controllers/ircu2.py b/irctest/controllers/ircu2.py index 592cfd2..1b87338 100644 --- a/irctest/controllers/ircu2.py +++ b/irctest/controllers/ircu2.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess from typing import Optional, Set, Type @@ -69,6 +70,7 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController): run_services: bool, valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str], ) -> None: if valid_metadata_keys or invalid_metadata_keys: raise NotImplementedByController( @@ -94,8 +96,16 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController): pidfile=pidfile, ) ) + + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + self.proc = subprocess.Popen( [ + *faketime_cmd, "ircd", "-n", # don't detach "-f", diff --git a/irctest/controllers/mammon.py b/irctest/controllers/mammon.py index c437139..591347e 100644 --- a/irctest/controllers/mammon.py +++ b/irctest/controllers/mammon.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess from typing import Optional, Set, Type @@ -92,6 +93,7 @@ class MammonController(BaseServerController, DirectoryBasedController): valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, restricted_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str], ) -> None: if password is not None: raise NotImplementedByController("PASS command") @@ -113,8 +115,16 @@ class MammonController(BaseServerController, DirectoryBasedController): # with self.open_file('server.yml', 'r') as fd: # print(fd.read()) assert self.directory + + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + self.proc = subprocess.Popen( [ + *faketime_cmd, "mammond", "--nofork", # '--debug', "--config", diff --git a/irctest/controllers/ngircd.py b/irctest/controllers/ngircd.py index 17b3540..0999863 100644 --- a/irctest/controllers/ngircd.py +++ b/irctest/controllers/ngircd.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess from typing import Optional, Set, Type @@ -56,6 +57,7 @@ class NgircdController(BaseServerController, DirectoryBasedController): valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, restricted_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str], ) -> None: if valid_metadata_keys or invalid_metadata_keys: raise NotImplementedByController( @@ -81,6 +83,7 @@ class NgircdController(BaseServerController, DirectoryBasedController): fd.write("\n") assert self.directory + with self.open_file("server.conf") as fd: fd.write( TEMPLATE_CONFIG.format( @@ -94,8 +97,16 @@ class NgircdController(BaseServerController, DirectoryBasedController): empty_file=os.path.join(self.directory, "empty.txt"), ) ) + + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + self.proc = subprocess.Popen( [ + *faketime_cmd, "ngircd", "--nodaemon", "--config", diff --git a/irctest/controllers/snircd.py b/irctest/controllers/snircd.py index 7fa9acc..2edb85c 100644 --- a/irctest/controllers/snircd.py +++ b/irctest/controllers/snircd.py @@ -1,4 +1,5 @@ import os +import shutil import subprocess from typing import Optional, Set, Type @@ -69,6 +70,7 @@ class SnircdController(BaseServerController, DirectoryBasedController): run_services: bool, valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str], ) -> None: if valid_metadata_keys or invalid_metadata_keys: raise NotImplementedByController( @@ -94,8 +96,16 @@ class SnircdController(BaseServerController, DirectoryBasedController): pidfile=pidfile, ) ) + + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + self.proc = subprocess.Popen( [ + *faketime_cmd, "ircd", "-n", # don't detach "-f", diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py index af397c1..d9db353 100644 --- a/irctest/controllers/unrealircd.py +++ b/irctest/controllers/unrealircd.py @@ -1,8 +1,12 @@ import functools import os +<<<<<<< HEAD import pathlib import shutil import signal +======= +import shutil +>>>>>>> 96e6642 (Add support for 'faketime', to avoid long sleeps in upcoming ELIST tests) import subprocess import textwrap from typing import Optional, Set, Type @@ -156,6 +160,7 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): valid_metadata_keys: Optional[Set[str]] = None, invalid_metadata_keys: Optional[Set[str]] = None, restricted_metadata_keys: Optional[Set[str]] = None, + faketime: Optional[str], ) -> None: if valid_metadata_keys or invalid_metadata_keys: raise NotImplementedByController( @@ -192,6 +197,7 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): fd.write("\n") assert self.directory + with self.open_file("unrealircd.conf") as fd: fd.write( TEMPLATE_CONFIG.format( @@ -225,9 +231,16 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): proot_cmd = ["proot", "-b", f"{tmpdir}:{unrealircd_prefix}/tmp"] self.using_proot = True + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + self.proc = subprocess.Popen( [ *proot_cmd, + *faketime_cmd, "unrealircd", "-t", "-F", # BOOT_NOFORK diff --git a/make_workflows.py b/make_workflows.py index 4a74f90..3e1895a 100644 --- a/make_workflows.py +++ b/make_workflows.py @@ -209,8 +209,8 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs): *unpack, *install_steps, { - "name": "Install Atheme", - "run": "sudo apt-get install atheme-services", + "name": "Install system dependencies", + "run": "sudo apt-get install atheme-services faketime", }, { "name": "Install irctest dependencies", From a9a7a2a1875c7ae0153c8a43635d55f676c6d81f Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 20 Mar 2022 14:06:54 +0100 Subject: [PATCH 018/143] list: Modernize tests a bit --- irctest/numerics.py | 1 + irctest/server_tests/list.py | 28 ++++++++++++++++------------ 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/irctest/numerics.py b/irctest/numerics.py index 28006fe..8aafb08 100644 --- a/irctest/numerics.py +++ b/irctest/numerics.py @@ -66,6 +66,7 @@ RPL_WHOISIDLE = "317" RPL_ENDOFWHOIS = "318" RPL_WHOISCHANNELS = "319" RPL_WHOISSPECIAL = "320" +RPL_LISTSTART = "321" RPL_LIST = "322" RPL_LISTEND = "323" RPL_CHANNELMODEIS = "324" diff --git a/irctest/server_tests/list.py b/irctest/server_tests/list.py index 9529cb9..3ef89d0 100644 --- a/irctest/server_tests/list.py +++ b/irctest/server_tests/list.py @@ -1,4 +1,5 @@ from irctest import cases +from irctest.numerics import RPL_LIST, RPL_LISTEND, RPL_LISTSTART class ListTestCase(cases.BaseServerTestCase): @@ -6,26 +7,27 @@ class ListTestCase(cases.BaseServerTestCase): def testListEmpty(self): """ + """ self.connectClient("foo") self.connectClient("bar") self.getMessages(1) self.sendLine(2, "LIST") m = self.getMessage(2) - if m.command == "321": - # skip RPL_LISTSTART + if m.command == RPL_LISTSTART: + # skip m = self.getMessage(2) # skip local pseudo-channels listed by ngircd and ircu - while m.command == "322" and m.params[1].startswith("&"): + while m.command == RPL_LIST and m.params[1].startswith("&"): m = self.getMessage(2) self.assertNotEqual( m.command, - "322", # RPL_LIST + RPL_LIST, "LIST response gives (at least) one channel, whereas there " "is none.", ) self.assertMessageMatch( m, - command="323", # RPL_LISTEND + command=RPL_LISTEND, fail_msg="Second reply to LIST is not 322 (RPL_LIST) " "or 323 (RPL_LISTEND), or but: {msg}", ) @@ -35,6 +37,8 @@ class ListTestCase(cases.BaseServerTestCase): """When a channel exists, LIST should get it in a reply. + + """ self.connectClient("foo") self.connectClient("bar") @@ -42,34 +46,34 @@ class ListTestCase(cases.BaseServerTestCase): self.getMessages(1) self.sendLine(2, "LIST") m = self.getMessage(2) - if m.command == "321": - # skip RPL_LISTSTART + if m.command == RPL_LISTSTART: + # skip m = self.getMessage(2) self.assertNotEqual( m.command, - "323", # RPL_LISTEND + RPL_LISTEND, fail_msg="LIST response ended (ie. 323, aka RPL_LISTEND) " "without listing any channel, whereas there is one.", ) self.assertMessageMatch( m, - command="322", # RPL_LIST + command=RPL_LIST, fail_msg="Second reply to LIST is not 322 (RPL_LIST), " "nor 323 (RPL_LISTEND) but: {msg}", ) m = self.getMessage(2) # skip local pseudo-channels listed by ngircd and ircu - while m.command == "322" and m.params[1].startswith("&"): + while m.command == RPL_LIST and m.params[1].startswith("&"): m = self.getMessage(2) self.assertNotEqual( m.command, - "322", # RPL_LIST + RPL_LIST, fail_msg="LIST response gives (at least) two channels, " "whereas there is only one.", ) self.assertMessageMatch( m, - command="323", # RPL_LISTEND + command=RPL_LISTEND, fail_msg="Third reply to LIST is not 322 (RPL_LIST) " "or 323 (RPL_LISTEND), or but: {msg}", ) From af001fad2e1b5acc1b2ece2ccbe47b6761e83885 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 20 Mar 2022 14:07:46 +0100 Subject: [PATCH 019/143] Add tests for ELIST --- Makefile | 2 + irctest/controllers/plexus4.py | 2 +- irctest/controllers/unrealircd.py | 4 - irctest/server_tests/list.py | 323 +++++++++++++++++++++++++++++- irctest/specifications.py | 1 + pytest.ini | 1 + 6 files changed, 326 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index eef477a..0052a74 100644 --- a/Makefile +++ b/Makefile @@ -179,6 +179,7 @@ SOPEL_SELECTORS := \ # testChathistory[AROUND] fails: https://bugs.unrealircd.org/view.php?id=5953 # testWhoAllOpers fails because Unreal skips results when the mask is too broad # HELP and HELPOP tests fail because Unreal uses custom numerics https://github.com/unrealircd/unrealircd/pull/184 +# testListTopicTime fails because Unreal mistakenly advertises it as available https://github.com/unrealircd/unrealircd/pull/193 UNREALIRCD_SELECTORS := \ not Ergo \ and not deprecated \ @@ -194,6 +195,7 @@ UNREALIRCD_SELECTORS := \ and not (testChathistory and (between or around)) \ and not testWhoAllOpers \ and not HelpTestCase \ + and not testListTopicTime \ $(EXTRA_SELECTORS) .PHONY: all flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon limnoria sopel solanum unrealircd diff --git a/irctest/controllers/plexus4.py b/irctest/controllers/plexus4.py index 395b4e4..a968e56 100644 --- a/irctest/controllers/plexus4.py +++ b/irctest/controllers/plexus4.py @@ -74,7 +74,7 @@ operator {{ class Plexus4Controller(BaseHybridController): - software_name = "Hybrid" + software_name = "Plexus4" binary_name = "ircd" services_protocol = "plexus" diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py index d9db353..a149fd4 100644 --- a/irctest/controllers/unrealircd.py +++ b/irctest/controllers/unrealircd.py @@ -1,12 +1,8 @@ import functools import os -<<<<<<< HEAD import pathlib import shutil import signal -======= -import shutil ->>>>>>> 96e6642 (Add support for 'faketime', to avoid long sleeps in upcoming ELIST tests) import subprocess import textwrap from typing import Optional, Set, Type diff --git a/irctest/server_tests/list.py b/irctest/server_tests/list.py index 3ef89d0..18a8b3c 100644 --- a/irctest/server_tests/list.py +++ b/irctest/server_tests/list.py @@ -1,8 +1,26 @@ -from irctest import cases +import time + +from irctest import cases, runner from irctest.numerics import RPL_LIST, RPL_LISTEND, RPL_LISTSTART -class ListTestCase(cases.BaseServerTestCase): +class _BasedListTestCase(cases.BaseServerTestCase): + def _parseChanList(self, client): + channels = set() + while True: + m = self.getMessage(client) + if m.command == RPL_LISTEND: + break + if m.command == RPL_LIST: + if m.params[1].startswith("&"): + # skip local pseudo-channels listed by ngircd and ircu + continue + channels.add(m.params[1]) + + return channels + + +class ListTestCase(_BasedListTestCase): @cases.mark_specifications("RFC1459", "RFC2812") def testListEmpty(self): """ @@ -77,3 +95,304 @@ class ListTestCase(cases.BaseServerTestCase): fail_msg="Third reply to LIST is not 322 (RPL_LIST) " "or 323 (RPL_LISTEND), or but: {msg}", ) + + @cases.mark_isupport("ELIST") + @cases.mark_specifications("Modern") + def testListMask(self): + """ + "M: Searching based on mask." + -- + -- https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.8 + """ + self.connectClient("foo") + + if "M" not in self.server_support.get("ELIST", ""): + raise runner.OptionalExtensionNotSupported("ELIST=M") + + self.connectClient("bar") + self.sendLine(1, "JOIN #chan1") + self.getMessages(1) + self.sendLine(1, "JOIN #chan2") + self.getMessages(1) + + self.sendLine(2, "LIST *an1") + self.assertEqual(self._parseChanList(2), {"#chan1"}) + + self.sendLine(2, "LIST *an2") + self.assertEqual(self._parseChanList(2), {"#chan2"}) + + self.sendLine(2, "LIST #c*n2") + self.assertEqual(self._parseChanList(2), {"#chan2"}) + + self.sendLine(2, "LIST *an3") + self.assertEqual(self._parseChanList(2), set()) + + self.sendLine(2, "LIST #ch*") + self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) + + @cases.mark_isupport("ELIST") + @cases.mark_specifications("Modern") + def testListNotMask(self): + """ + " N: Searching based on a non-matching mask. i.e., the opposite of M." + -- + -- https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.8 + """ + self.connectClient("foo") + + if "N" not in self.server_support.get("ELIST", ""): + raise runner.OptionalExtensionNotSupported("ELIST=N") + + self.sendLine(1, "JOIN #chan1") + self.getMessages(1) + self.sendLine(1, "JOIN #chan2") + self.getMessages(1) + + self.connectClient("bar") + + self.sendLine(2, "LIST !*an1") + self.assertEqual(self._parseChanList(2), {"#chan2"}) + + self.sendLine(2, "LIST !*an2") + self.assertEqual(self._parseChanList(2), {"#chan1"}) + + self.sendLine(2, "LIST !#c*n2") + self.assertEqual(self._parseChanList(2), {"#chan1"}) + + self.sendLine(2, "LIST !*an3") + self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) + + self.sendLine(2, "LIST !#ch*") + self.assertEqual(self._parseChanList(2), set()) + + @cases.mark_isupport("ELIST") + @cases.mark_specifications("Modern") + def testListUsers(self): + """ + "U: Searching based on user count within the channel, via the "val" modifiers to search for a channel that has less or more than val users, + respectively." + -- + -- https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.8 + """ + self.connectClient("foo") + + if "M" not in self.server_support.get("ELIST", ""): + raise runner.OptionalExtensionNotSupported("ELIST=M") + + self.sendLine(1, "JOIN #chan1") + self.getMessages(1) + self.sendLine(1, "JOIN #chan2") + self.getMessages(1) + + self.connectClient("bar") + self.sendLine(2, "JOIN #chan2") + self.getMessages(2) + + self.connectClient("baz") + + self.sendLine(3, "LIST >0") + self.assertEqual(self._parseChanList(3), {"#chan1", "#chan2"}) + + self.sendLine(3, "LIST <1") + self.assertEqual(self._parseChanList(3), set()) + + self.sendLine(3, "LIST <100") + self.assertEqual(self._parseChanList(3), {"#chan1", "#chan2"}) + + self.sendLine(3, "LIST >1") + self.assertEqual(self._parseChanList(3), {"#chan2"}) + + self.sendLine(3, "LIST <2") + self.assertEqual(self._parseChanList(3), {"#chan1"}) + + self.sendLine(3, "LIST <100") + self.assertEqual(self._parseChanList(3), {"#chan1", "#chan2"}) + + +class FaketimeListTestCase(_BasedListTestCase): + faketime = "+1y x30" # for every wall clock second, 1 minute passed for the server + + def _sleep_minutes(self, n): + for _ in range(n): + if self.controller.faketime_enabled: + # From the server's point of view, 1 min will pass + time.sleep(2) + else: + time.sleep(60) + + # reply to pings + self.getMessages(1) + self.getMessages(2) + + @cases.mark_isupport("ELIST") + @cases.mark_specifications("Modern") + def testListCreationTime(self): + """ + " C: Searching based on channel creation time, via the "Cval" + modifiers to search for a channel creation time that is higher or lower + than val." + -- + -- https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.8 + + Unfortunately, this is ambiguous, because "val" is a time delta (in minutes), + not a timestamp. + + On InspIRCd and Charybdis/Solanum, "C minutes ago + + On UnrealIRCd, Plexus, and Hybrid, it is interpreted as "the channel's creation + time is a timestamp lower than minutes ago" (ie. the exact opposite) + + "C: Searching based on channel creation time, via the "Cval" + modifiers to search for a channel that was created either less than `val` + minutes ago, or more than `val` minutes ago, respectively" + -- https://github.com/ircdocs/modern-irc/pull/171 + """ + self.connectClient("foo") + + if "C" not in self.server_support.get("ELIST", ""): + raise runner.OptionalExtensionNotSupported("ELIST=C") + + self.connectClient("bar") + self.sendLine(1, "JOIN #chan1") + self.getMessages(1) + + # Helps debugging + self.sendLine(1, "TIME") + self.getMessages(1) + + self._sleep_minutes(2) + + # Helps debugging + self.sendLine(1, "TIME") + self.getMessages(1) + + self.sendLine(1, "JOIN #chan2") + self.getMessages(1) + + self._sleep_minutes(1) + + if self.controller.software_name in ("UnrealIRCd", "Plexus4", "Hybrid"): + self.sendLine(2, "LIST C<2") + self.assertEqual(self._parseChanList(2), {"#chan1"}) + + self.sendLine(2, "LIST C>2") + self.assertEqual(self._parseChanList(2), {"#chan2"}) + + self.sendLine(2, "LIST C>0") + self.assertEqual(self._parseChanList(2), set()) + + self.sendLine(2, "LIST C<0") + self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) + + self.sendLine(2, "LIST C>10") + self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) + elif self.controller.software_name in ("Solanum", "Charybdis", "InspIRCd"): + self.sendLine(2, "LIST C>2") + self.assertEqual(self._parseChanList(2), {"#chan1"}) + + self.sendLine(2, "LIST C<2") + self.assertEqual(self._parseChanList(2), {"#chan2"}) + + self.sendLine(2, "LIST C<0") + if self.controller.software_name == "InspIRCd": + self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) + else: + self.assertEqual(self._parseChanList(2), set()) + + self.sendLine(2, "LIST C>0") + self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) + + self.sendLine(2, "LIST C<10") + self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) + else: + assert False, f"{self.controller.software_name} not supported" + + @cases.mark_isupport("ELIST") + @cases.mark_specifications("Modern") + def testListTopicTime(self): + """ + "T: Searching based on topic time, via the "Tval" + modifiers to search for a topic time that is lower or higher than + val respectively." + -- + -- https://datatracker.ietf.org/doc/html/draft-hardy-irc-isupport-00#section-4.8 + + See testListCreationTime's docstring for comments on this. + + "T: Searching based on topic set time, via the "Tval" modifiers + to search for a topic time that was set less than `val` minutes ago, or more + than `val` minutes ago, respectively." + -- https://github.com/ircdocs/modern-irc/pull/171 + """ + self.connectClient("foo") + + if "T" not in self.server_support.get("ELIST", ""): + raise runner.OptionalExtensionNotSupported("ELIST=T") + + self.connectClient("bar") + self.sendLine(1, "JOIN #chan1") + self.sendLine(1, "JOIN #chan2") + self.getMessages(1) + + self.sendLine(1, "TOPIC #chan1 :First channel") + self.getMessages(1) + + # Helps debugging + self.sendLine(1, "TIME") + self.getMessages(1) + + self._sleep_minutes(2) + + # Helps debugging + self.sendLine(1, "TIME") + self.getMessages(1) + + self.sendLine(1, "TOPIC #chan2 :Second channel") + self.getMessages(1) + + self._sleep_minutes(1) + + if self.controller.software_name in ("UnrealIRCd",): + self.sendLine(1, "LIST T<2") + self.assertEqual(self._parseChanList(1), {"#chan1"}) + + self.sendLine(1, "LIST T>2") + self.assertEqual(self._parseChanList(1), {"#chan2"}) + + self.sendLine(1, "LIST T>0") + self.assertEqual(self._parseChanList(1), set()) + + self.sendLine(1, "LIST T<0") + self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"}) + + self.sendLine(1, "LIST T>10") + self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"}) + elif self.controller.software_name in ( + "Solanum", + "Charybdis", + "InspIRCd", + "Plexus4", + "Hybrid", + ): + self.sendLine(1, "LIST T>2") + self.assertEqual(self._parseChanList(1), {"#chan1"}) + + self.sendLine(1, "LIST T<2") + self.assertEqual(self._parseChanList(1), {"#chan2"}) + + self.sendLine(1, "LIST T<0") + if self.controller.software_name == "InspIRCd": + # Insp internally represents "LIST T>0" like "LIST" + self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"}) + else: + self.assertEqual(self._parseChanList(1), set()) + + self.sendLine(1, "LIST T>0") + self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"}) + + self.sendLine(1, "LIST T<10") + self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"}) + else: + assert False, f"{self.controller.software_name} not supported" diff --git a/irctest/specifications.py b/irctest/specifications.py index 8a629c7..16257fa 100644 --- a/irctest/specifications.py +++ b/irctest/specifications.py @@ -50,6 +50,7 @@ class Capabilities(enum.Enum): @enum.unique class IsupportTokens(enum.Enum): BOT = "BOT" + ELIST = "ELIST" PREFIX = "PREFIX" MONITOR = "MONITOR" STATUSMSG = "STATUSMSG" diff --git a/pytest.ini b/pytest.ini index 19518e3..34ae065 100644 --- a/pytest.ini +++ b/pytest.ini @@ -32,6 +32,7 @@ markers = # isupport tokens BOT + ELIST MONITOR PREFIX STATUSMSG From f52f21897bc7f37075fca4e9c2a0121b9edf9392 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Wed, 30 Mar 2022 20:32:56 +0200 Subject: [PATCH 020/143] Bump Go version --- .github/workflows/test-devel.yml | 2 +- .github/workflows/test-stable.yml | 2 +- workflows.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 640e680..4b94249 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -537,7 +537,7 @@ jobs: repository: ergochat/ergo - uses: actions/setup-go@v2 with: - go-version: ^1.17.0 + go-version: ^1.18.0 - run: go version - name: Build Ergo run: | diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 0e66d6d..7603d76 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -612,7 +612,7 @@ jobs: repository: ergochat/ergo - uses: actions/setup-go@v2 with: - go-version: ^1.17.0 + go-version: ^1.18.0 - run: go version - name: Build Ergo run: | diff --git a/workflows.yml b/workflows.yml index 0bce533..6b19581 100644 --- a/workflows.yml +++ b/workflows.yml @@ -130,7 +130,7 @@ software: pre_deps: - uses: actions/setup-go@v2 with: - go-version: '^1.17.0' + go-version: '^1.18.0' - run: go version separate_build_job: false build_script: | From 9a19416731ae71452bb59718df52ad596b019b64 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Thu, 31 Mar 2022 15:53:51 +0200 Subject: [PATCH 021/143] INVITE: Fix misunderstanding of the RFCs (#148) They make the first argument of numerics implicit, so there is actually no difference with Modern --- Makefile | 12 ++-- irctest/server_tests/invite.py | 125 +++++++++------------------------ 2 files changed, 41 insertions(+), 96 deletions(-) diff --git a/Makefile b/Makefile index eef477a..c0d9bc7 100644 --- a/Makefile +++ b/Makefile @@ -49,10 +49,10 @@ ERGO_SELECTORS := \ and not testInfoNosuchserver \ $(EXTRA_SELECTORS) -# testInviteUnoppedModern is the only strict test that Hybrid fails +# testInviteUnopped is the only strict test that Hybrid fails HYBRID_SELECTORS := \ not Ergo \ - and not testInviteUnoppedModern \ + and not testInviteUnopped \ and not deprecated \ $(EXTRA_SELECTORS) @@ -138,12 +138,12 @@ NGIRCD_SELECTORS := \ and (not HelpTestCase or HELPOP) \ $(EXTRA_SELECTORS) -# testInviteUnoppedModern is the only strict test that Plexus4 fails -# testInviteInviteOnlyModern fails because Plexus4 allows non-op to invite if (and only if) the channel is not invite-only +# testInviteUnopped is the only strict test that Plexus4 fails +# testInviteInviteOnly fails because Plexus4 allows non-op to invite if (and only if) the channel is not invite-only PLEXUS4_SELECTORS := \ not Ergo \ - and not testInviteUnoppedModern \ - and not testInviteInviteOnlyModern \ + and not testInviteUnopped \ + and not testInviteInviteOnly \ and not deprecated \ $(EXTRA_SELECTORS) diff --git a/irctest/server_tests/invite.py b/irctest/server_tests/invite.py index 935780b..8c07371 100644 --- a/irctest/server_tests/invite.py +++ b/irctest/server_tests/invite.py @@ -110,7 +110,7 @@ class InviteTestCase(cases.BaseServerTestCase): "got this instead: {msg}", ) - def _testInvite(self, opped, invite_only, modern): + def _testInvite(self, opped, invite_only): """ "Only the user inviting and the user being invited will receive notification of the invitation." @@ -163,23 +163,14 @@ class InviteTestCase(cases.BaseServerTestCase): ) self.sendLine(1, "INVITE bar #chan") - if modern: - self.assertMessageMatch( - self.getMessage(1), - command=RPL_INVITING, - params=["foo", "bar", "#chan"], - fail_msg=f"After “foo” invited “bar” to a channel, “foo” should have " - f"received “{RPL_INVITING} foo #chan bar” but got this instead: " - f"{{msg}}", - ) - else: - self.assertMessageMatch( - self.getMessage(1), - command=RPL_INVITING, - params=["#chan", "bar"], - fail_msg=f"After “foo” invited “bar” to a channel, “foo” should have " - f"received “{RPL_INVITING} #chan bar” but got this instead: {{msg}}", - ) + self.assertMessageMatch( + self.getMessage(1), + command=RPL_INVITING, + params=["foo", "bar", "#chan"], + fail_msg=f"After “foo” invited “bar” to a channel, “foo” should have " + f"received “{RPL_INVITING} foo #chan bar” but got this instead: " + f"{{msg}}", + ) messages = self.getMessages(2) self.assertNotEqual( @@ -197,24 +188,14 @@ class InviteTestCase(cases.BaseServerTestCase): ) @pytest.mark.parametrize("invite_only", [True, False]) - @cases.mark_specifications("Modern") - def testInviteModern(self, invite_only): - self._testInvite(opped=True, invite_only=invite_only, modern=True) + @cases.mark_specifications("RFC1459", "RFC2812", "Modern") + def testInvite(self, invite_only): + self._testInvite(opped=True, invite_only=invite_only) - @pytest.mark.parametrize("invite_only", [True, False]) - @cases.mark_specifications("RFC1459", "RFC2812", deprecated=True) - def testInviteRfc(self, invite_only): - self._testInvite(opped=True, invite_only=invite_only, modern=False) - - @cases.mark_specifications("Modern", strict=True) - def testInviteUnoppedModern(self): + @cases.mark_specifications("RFC1459", "RFC2812", "Modern", strict=True) + def testInviteUnopped(self): """Tests invites from unopped users on not-invite-only chans.""" - self._testInvite(opped=False, invite_only=False, modern=True) - - @cases.mark_specifications("RFC1459", "RFC2812", deprecated=True, strict=True) - def testInviteUnoppedRfc(self, opped, invite_only): - """Tests invites from unopped users on not-invite-only chans.""" - self._testInvite(opped=False, invite_only=False, modern=False) + self._testInvite(opped=False, invite_only=False) @cases.mark_specifications("RFC2812", "Modern") def testInviteNoNotificationForOtherMembers(self): @@ -248,7 +229,8 @@ class InviteTestCase(cases.BaseServerTestCase): "were notified: {got}", ) - def _testInviteInviteOnly(self, modern): + @cases.mark_specifications("RFC1459", "RFC2812", "Modern") + def testInviteInviteOnly(self): """ "To invite a user to a channel which is invite only (MODE +i), the client sending the invite must be recognised as being a @@ -288,35 +270,17 @@ class InviteTestCase(cases.BaseServerTestCase): ) self.sendLine(1, "INVITE bar #chan") - if modern: - self.assertMessageMatch( - self.getMessage(1), - command=ERR_CHANOPRIVSNEEDED, - params=["foo", "#chan", ANYSTR], - fail_msg=f"After “foo” invited “bar” to a channel to an invite-only " - f"channel without being opped, “foo” should have received " - f"“{ERR_CHANOPRIVSNEEDED} foo #chan :*” but got this instead: {{msg}}", - ) - else: - self.assertMessageMatch( - self.getMessage(1), - command=ERR_CHANOPRIVSNEEDED, - params=["#chan", ANYSTR], - fail_msg=f"After “foo” invited “bar” to a channel to an invite-only " - f"channel without being opped, “foo” should have received " - f"“{ERR_CHANOPRIVSNEEDED} #chan :*” but got this instead: {{msg}}", - ) - - @cases.mark_specifications("Modern") - def testInviteInviteOnlyModern(self): - self._testInviteInviteOnly(modern=True) - - @cases.mark_specifications("RFC1459", "RFC2812", deprecated=True) - def testInviteInviteOnlyRfc(self): - self._testInviteInviteOnly(modern=False) + self.assertMessageMatch( + self.getMessage(1), + command=ERR_CHANOPRIVSNEEDED, + params=["foo", "#chan", ANYSTR], + fail_msg=f"After “foo” invited “bar” to a channel to an invite-only " + f"channel without being opped, “foo” should have received " + f"“{ERR_CHANOPRIVSNEEDED} foo #chan :*” but got this instead: {{msg}}", + ) @cases.mark_specifications("RFC2812", "Modern") - def _testInviteOnlyFromUsersInChannel(self, modern): + def testInviteOnlyFromUsersInChannel(self): """ "if the channel exists, only members of the channel are allowed to invite other users" @@ -349,26 +313,15 @@ class InviteTestCase(cases.BaseServerTestCase): self.getMessages(3) self.sendLine(1, "INVITE bar #chan") - if modern: - self.assertMessageMatch( - self.getMessage(1), - command=ERR_NOTONCHANNEL, - params=["foo", "#chan", ANYSTR], - fail_msg=f"After “foo” invited “bar” to a channel it is not on " - f"#chan, “foo” should have received " - f"“ERR_NOTONCHANNEL ({ERR_NOTONCHANNEL}) foo #chan :*” but " - f"got this instead: {{msg}}", - ) - else: - self.assertMessageMatch( - self.getMessage(1), - command=ERR_NOTONCHANNEL, - params=["#chan", ANYSTR], - fail_msg=f"After “foo” invited “bar” to a channel it is not on " - f"#chan, “foo” should have received " - f"“ERR_NOTONCHANNEL ({ERR_NOTONCHANNEL}) #chan :*” but " - f"got this instead: {{msg}}", - ) + self.assertMessageMatch( + self.getMessage(1), + command=ERR_NOTONCHANNEL, + params=["foo", "#chan", ANYSTR], + fail_msg=f"After “foo” invited “bar” to a channel it is not on " + f"#chan, “foo” should have received " + f"“ERR_NOTONCHANNEL ({ERR_NOTONCHANNEL}) foo #chan :*” but " + f"got this instead: {{msg}}", + ) messages = self.getMessages(2) self.assertEqual( @@ -378,14 +331,6 @@ class InviteTestCase(cases.BaseServerTestCase): "not in #chan, “bar” received something.", ) - @cases.mark_specifications("Modern") - def testInviteOnlyFromUsersInChannelModern(self): - self._testInviteOnlyFromUsersInChannel(modern=True) - - @cases.mark_specifications("RFC2812", deprecated=True) - def testInviteOnlyFromUsersInChannelRfc(self): - self._testInviteOnlyFromUsersInChannel(modern=False) - @cases.mark_specifications("Modern") def testInviteAlreadyInChannel(self): """ From ebd7edcc744591674b53c588f05d63785d8d495c Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 9 Apr 2022 08:59:50 +0200 Subject: [PATCH 022/143] workflows: Replace spaces from artifact names It made them impractical to use as file names. --- .github/workflows/test-devel.yml | 38 ++++++++++---------- .github/workflows/test-devel_release.yml | 6 ++-- .github/workflows/test-stable.yml | 44 ++++++++++++------------ make_workflows.py | 2 +- 4 files changed, 45 insertions(+), 45 deletions(-) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 4b94249..e3c95a3 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -448,7 +448,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results bahamut (devel) + name: pytest-results_bahamut_devel path: pytest.xml test-bahamut-anope: needs: @@ -486,7 +486,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results bahamut-anope (devel) + name: pytest-results_bahamut-anope_devel path: pytest.xml test-bahamut-atheme: needs: @@ -518,7 +518,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results bahamut-atheme (devel) + name: pytest-results_bahamut-atheme_devel path: pytest.xml test-ergo: needs: [] @@ -557,7 +557,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results ergo (devel) + name: pytest-results_ergo_devel path: pytest.xml test-hybrid: needs: @@ -595,7 +595,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results hybrid (devel) + name: pytest-results_hybrid_devel path: pytest.xml test-inspircd: needs: @@ -627,7 +627,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results inspircd (devel) + name: pytest-results_inspircd_devel path: pytest.xml test-inspircd-anope: needs: @@ -665,7 +665,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results inspircd-anope (devel) + name: pytest-results_inspircd-anope_devel path: pytest.xml test-ircu2: needs: [] @@ -703,7 +703,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results ircu2 (devel) + name: pytest-results_ircu2_devel path: pytest.xml test-limnoria: needs: [] @@ -730,7 +730,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results limnoria (devel) + name: pytest-results_limnoria_devel path: pytest.xml test-ngircd: needs: @@ -762,7 +762,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results ngircd (devel) + name: pytest-results_ngircd_devel path: pytest.xml test-ngircd-anope: needs: @@ -800,7 +800,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results ngircd-anope (devel) + name: pytest-results_ngircd-anope_devel path: pytest.xml test-ngircd-atheme: needs: @@ -832,7 +832,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results ngircd-atheme (devel) + name: pytest-results_ngircd-atheme_devel path: pytest.xml test-plexus4: needs: @@ -870,7 +870,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results plexus4 (devel) + name: pytest-results_plexus4_devel path: pytest.xml test-solanum: needs: @@ -902,7 +902,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results solanum (devel) + name: pytest-results_solanum_devel path: pytest.xml test-sopel: needs: [] @@ -928,7 +928,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results sopel (devel) + name: pytest-results_sopel_devel path: pytest.xml test-unrealircd: needs: @@ -960,7 +960,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results unrealircd (devel) + name: pytest-results_unrealircd_devel path: pytest.xml test-unrealircd-5: needs: @@ -992,7 +992,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results unrealircd-5 (devel) + name: pytest-results_unrealircd-5_devel path: pytest.xml test-unrealircd-anope: needs: @@ -1030,7 +1030,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results unrealircd-anope (devel) + name: pytest-results_unrealircd-anope_devel path: pytest.xml test-unrealircd-atheme: needs: @@ -1062,7 +1062,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results unrealircd-atheme (devel) + name: pytest-results_unrealircd-atheme_devel path: pytest.xml name: irctest with devel versions 'on': diff --git a/.github/workflows/test-devel_release.yml b/.github/workflows/test-devel_release.yml index 0b25abb..eb50191 100644 --- a/.github/workflows/test-devel_release.yml +++ b/.github/workflows/test-devel_release.yml @@ -134,7 +134,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results inspircd (devel_release) + name: pytest-results_inspircd_devel_release path: pytest.xml test-inspircd-anope: needs: @@ -172,7 +172,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results inspircd-anope (devel_release) + name: pytest-results_inspircd-anope_devel_release path: pytest.xml test-inspircd-atheme: needs: @@ -204,7 +204,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results inspircd-atheme (devel_release) + name: pytest-results_inspircd-atheme_devel_release path: pytest.xml name: irctest with devel_release versions 'on': diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 7603d76..f311420 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -491,7 +491,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results bahamut (stable) + name: pytest-results_bahamut_stable path: pytest.xml test-bahamut-anope: needs: @@ -529,7 +529,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results bahamut-anope (stable) + name: pytest-results_bahamut-anope_stable path: pytest.xml test-bahamut-atheme: needs: @@ -561,7 +561,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results bahamut-atheme (stable) + name: pytest-results_bahamut-atheme_stable path: pytest.xml test-charybdis: needs: @@ -593,7 +593,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results charybdis (stable) + name: pytest-results_charybdis_stable path: pytest.xml test-ergo: needs: [] @@ -632,7 +632,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results ergo (stable) + name: pytest-results_ergo_stable path: pytest.xml test-hybrid: needs: @@ -670,7 +670,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results hybrid (stable) + name: pytest-results_hybrid_stable path: pytest.xml test-inspircd: needs: @@ -702,7 +702,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results inspircd (stable) + name: pytest-results_inspircd_stable path: pytest.xml test-inspircd-anope: needs: @@ -740,7 +740,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results inspircd-anope (stable) + name: pytest-results_inspircd-anope_stable path: pytest.xml test-inspircd-atheme: needs: @@ -772,7 +772,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results inspircd-atheme (stable) + name: pytest-results_inspircd-atheme_stable path: pytest.xml test-irc2: needs: [] @@ -826,7 +826,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results irc2 (stable) + name: pytest-results_irc2_stable path: pytest.xml test-ircu2: needs: [] @@ -864,7 +864,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results ircu2 (stable) + name: pytest-results_ircu2_stable path: pytest.xml test-limnoria: needs: [] @@ -890,7 +890,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results limnoria (stable) + name: pytest-results_limnoria_stable path: pytest.xml test-ngircd: needs: @@ -922,7 +922,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results ngircd (stable) + name: pytest-results_ngircd_stable path: pytest.xml test-ngircd-anope: needs: @@ -960,7 +960,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results ngircd-anope (stable) + name: pytest-results_ngircd-anope_stable path: pytest.xml test-ngircd-atheme: needs: @@ -992,7 +992,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results ngircd-atheme (stable) + name: pytest-results_ngircd-atheme_stable path: pytest.xml test-plexus4: needs: @@ -1030,7 +1030,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results plexus4 (stable) + name: pytest-results_plexus4_stable path: pytest.xml test-solanum: needs: @@ -1062,7 +1062,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results solanum (stable) + name: pytest-results_solanum_stable path: pytest.xml test-sopel: needs: [] @@ -1088,7 +1088,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results sopel (stable) + name: pytest-results_sopel_stable path: pytest.xml test-unrealircd: needs: @@ -1120,7 +1120,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results unrealircd (stable) + name: pytest-results_unrealircd_stable path: pytest.xml test-unrealircd-5: needs: @@ -1152,7 +1152,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results unrealircd-5 (stable) + name: pytest-results_unrealircd-5_stable path: pytest.xml test-unrealircd-anope: needs: @@ -1190,7 +1190,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results unrealircd-anope (stable) + name: pytest-results_unrealircd-anope_stable path: pytest.xml test-unrealircd-atheme: needs: @@ -1222,7 +1222,7 @@ jobs: name: Publish results uses: actions/upload-artifact@v2 with: - name: pytest results unrealircd-atheme (stable) + name: pytest-results_unrealircd-atheme_stable path: pytest.xml name: irctest with stable versions 'on': diff --git a/make_workflows.py b/make_workflows.py index 4a74f90..1468be0 100644 --- a/make_workflows.py +++ b/make_workflows.py @@ -237,7 +237,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs): "if": "always()", "uses": "actions/upload-artifact@v2", "with": { - "name": f"pytest results {test_id} ({version_flavor.value})", + "name": f"pytest-results_{test_id}_{version_flavor.value}", "path": "pytest.xml", }, }, From 3083aeeb24bd2d5cd0feec6eeedc13b93e5aea10 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 10 Apr 2022 02:39:30 -0400 Subject: [PATCH 023/143] fix processing of multiline CAP LS 302 output (#153) connectClient implicitly assumed that the CAP LS 302 output would be a single registration message. This caused incorrect skipping of some tests with `skip_if_cap_nak=True`, for example RegisterEmailVerifiedTestCase.testAfterConnect on Ergo. Technically there is no need for connectClient to send CAP LS before CAP REQ; however, this provides additional test coverage for the syntactic correctness of the CAP LS output in multiple server configurations, so we might as well keep it. --- irctest/cases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irctest/cases.py b/irctest/cases.py index 4dbd43a..0a64c1a 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -672,7 +672,7 @@ class BaseServerTestCase( client = self.addClient(name, show_io=show_io) if capabilities: self.sendLine(client, "CAP LS 302") - m = self.getRegistrationMessage(client) + self.getCapLs(client) self.requestCapabilities(client, capabilities, skip_if_cap_nak) if password is not None: if "sasl" not in (capabilities or ()): From edf3e5904baf9670bf339d2170e36f7ed8697665 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 10 Apr 2022 10:40:39 +0200 Subject: [PATCH 024/143] Produce a dashboard website after running tests (#152) --- .github/deploy_to_netlify.py | 99 +++++++ .github/workflows/test-devel.yml | 40 ++- .github/workflows/test-devel_release.yml | 40 ++- .github/workflows/test-stable.yml | 40 ++- irctest/dashboard/format.py | 322 +++++++++++++++++++++++ irctest/dashboard/github_download.py | 86 ++++++ irctest/dashboard/style.css | 60 +++++ make_workflows.py | 54 ++-- mypy.ini | 3 + pytest.ini | 3 + 10 files changed, 653 insertions(+), 94 deletions(-) create mode 100755 .github/deploy_to_netlify.py create mode 100644 irctest/dashboard/format.py create mode 100644 irctest/dashboard/github_download.py create mode 100644 irctest/dashboard/style.css diff --git a/.github/deploy_to_netlify.py b/.github/deploy_to_netlify.py new file mode 100755 index 0000000..e24d3ee --- /dev/null +++ b/.github/deploy_to_netlify.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 + +import json +import os +import re +import subprocess +import urllib.request + +with open(os.environ["GITHUB_EVENT_PATH"]) as fd: + github_event = json.load(fd) + +context_suffix = "" + +command = ["netlify", "deploy", "--dir=dashboard/"] +if "pull_request" in github_event and "number" in github_event: + pr_number = github_event["number"] + sha = github_event["after"] + # Aliases can't exceed 37 chars + command.extend(["--alias", f"pr-{pr_number}-{sha[0:10]}"]) + context_suffix = " (pull_request)" +else: + ref = github_event["ref"] + m = re.match("refs/heads/(.*)", ref) + if m: + branch = m.group(1) + sha = github_event["head_commit"]["id"] + + command.extend(["--alias", f"br-{branch[0:23]}-{sha[0:10]}"]) + + if branch in ("main", "master"): + command.extend(["--prod"]) + else: + context_suffix = " (push)" + else: + # TODO + pass + + +proc = subprocess.run(command, capture_output=True) + +output = proc.stdout.decode() +assert proc.returncode == 0, (output, proc.stderr.decode()) + +m = re.search("https://[^ ]*--[^ ]*netlify.app", output) +assert m +netlify_site_url = m.group(0) +target_url = f"{netlify_site_url}/index.xhtml" + + +def send_status() -> None: + statuses_url = github_event["repository"]["statuses_url"].format(sha=sha) + + payload = { + "state": "success", + "context": f"Dashboard{context_suffix}", + "description": "Table of all test results", + "target_url": target_url, + } + request = urllib.request.Request( + statuses_url, + data=json.dumps(payload).encode(), + headers={ + "Authorization": f'token {os.environ["GITHUB_TOKEN"]}', + "Content-Type": "text/json", + "Accept": "application/vnd.github+json", + }, + ) + + response = urllib.request.urlopen(request) + + assert response.status == 201, response.read() + + +send_status() + + +def send_pr_comment() -> None: + comments_url = github_event["pull_request"]["_links"]["comments"]["href"] + + payload = { + "body": f"[Test results]({target_url})", + } + request = urllib.request.Request( + comments_url, + data=json.dumps(payload).encode(), + headers={ + "Authorization": f'token {os.environ["GITHUB_TOKEN"]}', + "Content-Type": "text/json", + "Accept": "application/vnd.github+json", + }, + ) + + response = urllib.request.urlopen(request) + + assert response.status == 201, response.read() + + +if "pull_request" in github_event: + send_pr_comment() diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index e3c95a3..b3c82c4 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -369,7 +369,7 @@ jobs: retention-days: 1 publish-test-results: if: success() || failure() - name: Publish Unit Tests Results + name: Publish Dashboard needs: - test-bahamut - test-bahamut-anope @@ -397,27 +397,23 @@ jobs: uses: actions/download-artifact@v2 with: path: artifacts - - if: github.event_name == 'pull_request' - name: Publish Unit Test Results - uses: actions/github-script@v4 - with: - result-encoding: string - script: | - let body = ''; - const options = {}; - options.listeners = { - stdout: (data) => { - body += data.toString(); - } - }; - await exec.exec('bash', ['-c', 'shopt -s globstar; python3 report.py artifacts/**/*.xml'], options); - github.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: body, - }); - return body; + - name: Install dashboard dependencies + run: |- + python -m pip install --upgrade pip + pip install defusedxml + - name: Generate dashboard + run: |- + shopt -s globstar + python3 -m irctest.dashboard.format dashboard/ artifacts/**/*.xml + echo '/ /index.xhtml' > dashboard/_redirects + - name: Install netlify-cli + run: npm i -g netlify-cli + - env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + name: Deploy to Netlify + run: ./.github/deploy_to_netlify.py test-bahamut: needs: - build-bahamut diff --git a/.github/workflows/test-devel_release.yml b/.github/workflows/test-devel_release.yml index eb50191..3e0fc2e 100644 --- a/.github/workflows/test-devel_release.yml +++ b/.github/workflows/test-devel_release.yml @@ -71,7 +71,7 @@ jobs: retention-days: 1 publish-test-results: if: success() || failure() - name: Publish Unit Tests Results + name: Publish Dashboard needs: - test-inspircd - test-inspircd-anope @@ -83,27 +83,23 @@ jobs: uses: actions/download-artifact@v2 with: path: artifacts - - if: github.event_name == 'pull_request' - name: Publish Unit Test Results - uses: actions/github-script@v4 - with: - result-encoding: string - script: | - let body = ''; - const options = {}; - options.listeners = { - stdout: (data) => { - body += data.toString(); - } - }; - await exec.exec('bash', ['-c', 'shopt -s globstar; python3 report.py artifacts/**/*.xml'], options); - github.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: body, - }); - return body; + - name: Install dashboard dependencies + run: |- + python -m pip install --upgrade pip + pip install defusedxml + - name: Generate dashboard + run: |- + shopt -s globstar + python3 -m irctest.dashboard.format dashboard/ artifacts/**/*.xml + echo '/ /index.xhtml' > dashboard/_redirects + - name: Install netlify-cli + run: npm i -g netlify-cli + - env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + name: Deploy to Netlify + run: ./.github/deploy_to_netlify.py test-inspircd: needs: - build-inspircd diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index f311420..02989aa 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -409,7 +409,7 @@ jobs: retention-days: 1 publish-test-results: if: success() || failure() - name: Publish Unit Tests Results + name: Publish Dashboard needs: - test-bahamut - test-bahamut-anope @@ -440,27 +440,23 @@ jobs: uses: actions/download-artifact@v2 with: path: artifacts - - if: github.event_name == 'pull_request' - name: Publish Unit Test Results - uses: actions/github-script@v4 - with: - result-encoding: string - script: | - let body = ''; - const options = {}; - options.listeners = { - stdout: (data) => { - body += data.toString(); - } - }; - await exec.exec('bash', ['-c', 'shopt -s globstar; python3 report.py artifacts/**/*.xml'], options); - github.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: body, - }); - return body; + - name: Install dashboard dependencies + run: |- + python -m pip install --upgrade pip + pip install defusedxml + - name: Generate dashboard + run: |- + shopt -s globstar + python3 -m irctest.dashboard.format dashboard/ artifacts/**/*.xml + echo '/ /index.xhtml' > dashboard/_redirects + - name: Install netlify-cli + run: npm i -g netlify-cli + - env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }} + name: Deploy to Netlify + run: ./.github/deploy_to_netlify.py test-bahamut: needs: - build-bahamut diff --git a/irctest/dashboard/format.py b/irctest/dashboard/format.py new file mode 100644 index 0000000..cd7c6b0 --- /dev/null +++ b/irctest/dashboard/format.py @@ -0,0 +1,322 @@ +import base64 +import dataclasses +import gzip +import hashlib +from pathlib import Path +import re +import sys +from typing import ( + IO, + Callable, + Dict, + Iterable, + Iterator, + List, + Optional, + Tuple, + TypeVar, +) +import xml.dom.minidom +import xml.etree.ElementTree as ET + +from defusedxml.ElementTree import parse as parse_xml + +NETLIFY_CHAR_BLACKLIST = frozenset('":<>|*?\r\n#') +"""Characters not allowed in output filenames""" + + +@dataclasses.dataclass +class CaseResult: + module_name: str + class_name: str + test_name: str + job: str + success: bool + skipped: bool + system_out: Optional[str] + details: Optional[str] = None + type: Optional[str] = None + message: Optional[str] = None + + def output_filename(self): + test_name = self.test_name + if len(test_name) > 50 or set(test_name) & NETLIFY_CHAR_BLACKLIST: + # File name too long or otherwise invalid. This should be good enough: + m = re.match(r"(?P\w+?)\[(?P.+)\]", test_name) + assert m, "File name is too long but has no parameter." + test_name = f'{m.group("function_name")}[{md5sum(m.group("params"))}]' + return f"{self.job}_{self.module_name}.{self.class_name}.{test_name}.txt" + + +TK = TypeVar("TK") +TV = TypeVar("TV") + + +def md5sum(text: str) -> str: + return base64.urlsafe_b64encode(hashlib.md5(text.encode()).digest()).decode() + + +def group_by(list_: Iterable[TV], key: Callable[[TV], TK]) -> Dict[TK, List[TV]]: + groups: Dict[TK, List[TV]] = {} + for value in list_: + groups.setdefault(key(value), []).append(value) + + return groups + + +def iter_job_results(job_file_name: Path, job: ET.ElementTree) -> Iterator[CaseResult]: + (suite,) = job.getroot() + for case in suite: + if "name" not in case.attrib: + continue + + success = True + skipped = False + details = None + system_out = None + extra = {} + for child in case: + if child.tag == "skipped": + success = True + skipped = True + details = None + extra = child.attrib + elif child.tag in ("failure", "error"): + success = False + skipped = False + details = child.text + extra = child.attrib + elif child.tag == "system-out": + assert ( + system_out is None + # for some reason, skipped tests have two system-out; + # and the second one contains test teardown + or child.text.startswith(system_out.rstrip()) + ), ("Duplicate system-out tag", repr(system_out), repr(child.text)) + system_out = child.text + else: + assert False, child + + (module_name, class_name) = case.attrib["classname"].rsplit(".", 1) + m = re.match( + r"(.*/)?pytest[ -]results[ _](?P.*)" + r"[ _][(]?(stable|release|devel|devel_release)[)]?/pytest.xml(.gz)?", + str(job_file_name), + ) + assert m, job_file_name + yield CaseResult( + module_name=module_name, + class_name=class_name, + test_name=case.attrib["name"], + job=m.group("name"), + success=success, + skipped=skipped, + details=details, + system_out=system_out, + **extra, + ) + + +def build_module_html( + jobs: List[str], results: List[CaseResult], module_name: str +) -> ET.Element: + root = ET.Element("html") + head = ET.SubElement(root, "head") + ET.SubElement(head, "title").text = module_name + ET.SubElement(head, "link", rel="stylesheet", type="text/css", href="./style.css") + + body = ET.SubElement(root, "body") + + ET.SubElement(body, "h1").text = module_name + + results_by_class = group_by(results, lambda r: r.class_name) + + table = ET.SubElement(body, "table") + table.set("class", "test-matrix") + + job_row = ET.Element("tr") + ET.SubElement(job_row, "th") # column of case name + for job in jobs: + cell = ET.SubElement(job_row, "th") + ET.SubElement(ET.SubElement(cell, "div"), "span").text = job + cell.set("class", "job-name") + + for (class_name, class_results) in results_by_class.items(): + # Header row: class name + header_row = ET.SubElement(table, "tr") + th = ET.SubElement(header_row, "th", colspan=str(len(jobs) + 1)) + row_anchor = f"{class_name}" + section_header = ET.SubElement( + ET.SubElement(th, "h2"), + "a", + href=f"#{row_anchor}", + id=row_anchor, + ) + section_header.text = class_name + + # Header row: one column for each implementation + table.append(job_row) + + # One row for each test: + results_by_test = group_by(class_results, key=lambda r: r.test_name) + for (test_name, test_results) in results_by_test.items(): + row_anchor = f"{class_name}.{test_name}" + if len(row_anchor) >= 50: + # Too long; give up on generating readable URL + # TODO: only hash test parameter + row_anchor = md5sum(row_anchor) + + row = ET.SubElement(table, "tr", id=row_anchor) + + cell = ET.SubElement(row, "th") + cell.set("class", "test-name") + cell_link = ET.SubElement(cell, "a", href=f"#{row_anchor}") + cell_link.text = test_name + + results_by_job = group_by(test_results, key=lambda r: r.job) + for job_name in jobs: + cell = ET.SubElement(row, "td") + try: + (result,) = results_by_job[job_name] + except KeyError: + cell.set("class", "deselected") + cell.text = "d" + continue + + text: Optional[str] + + if result.skipped: + cell.set("class", "skipped") + if result.type == "pytest.skip": + text = "s" + else: + text = result.type + elif result.success: + cell.set("class", "success") + if result.type: + # dead code? + text = result.type + else: + text = "." + else: + cell.set("class", "failure") + if result.type: + # dead code? + text = result.type + else: + text = "f" + + if result.system_out: + # There is a log file; link to it. + a = ET.SubElement(cell, "a", href=f"./{result.output_filename()}") + a.text = text or "?" + else: + cell.text = text or "?" + + # Hacky: ET expects the namespace to be present in every tag we create instead; + # but it would be excessively verbose. + root.set("xmlns", "http://www.w3.org/1999/xhtml") + + return root + + +def write_html_pages( + output_dir: Path, results: List[CaseResult] +) -> List[Tuple[str, str]]: + """Returns the list of (module_name, file_name).""" + output_dir.mkdir(parents=True, exist_ok=True) + results_by_module = group_by(results, lambda r: r.module_name) + + # used as columns + jobs = list(sorted({r.job for r in results})) + + pages = [] + + for (module_name, module_results) in results_by_module.items(): + root = build_module_html(jobs, module_results, module_name) + file_name = f"{module_name}.xhtml" + write_xml_file(output_dir / file_name, root) + pages.append((module_name, file_name)) + + return pages + + +def write_test_outputs(output_dir: Path, results: List[CaseResult]) -> None: + """Writes stdout files of each test.""" + for result in results: + if result.system_out is None: + continue + output_file = output_dir / result.output_filename() + with output_file.open("wt") as fd: + fd.write(result.system_out) + + +def write_html_index(output_dir: Path, pages: List[Tuple[str, str]]) -> None: + root = ET.Element("html") + head = ET.SubElement(root, "head") + ET.SubElement(head, "title").text = "irctest dashboard" + ET.SubElement(head, "link", rel="stylesheet", type="text/css", href="./style.css") + + body = ET.SubElement(root, "body") + + ET.SubElement(body, "h1").text = "irctest dashboard" + + ul = ET.SubElement(body, "ul") + + for (module_name, file_name) in sorted(pages): + link = ET.SubElement(ET.SubElement(ul, "li"), "a", href=f"./{file_name}") + link.text = module_name + + root.set("xmlns", "http://www.w3.org/1999/xhtml") + + write_xml_file(output_dir / "index.xhtml", root) + + +def write_assets(output_dir: Path) -> None: + css_path = output_dir / "style.css" + source_css_path = Path(__file__).parent / "style.css" + with css_path.open("wt") as fd: + with source_css_path.open() as source_fd: + fd.write(source_fd.read()) + + +def write_xml_file(filename: Path, root: ET.Element) -> None: + # Serialize + s = ET.tostring(root) + + # Prettify + s = xml.dom.minidom.parseString(s).toprettyxml(indent=" ") + + with filename.open("wt") as fd: + fd.write(s) # type: ignore + + +def parse_xml_file(filename: Path) -> ET.ElementTree: + fd: IO + if filename.suffix == ".gz": + with gzip.open(filename, "rb") as fd: # type: ignore + return parse_xml(fd) # type: ignore + else: + with open(filename) as fd: + return parse_xml(fd) # type: ignore + + +def main(output_path: Path, filenames: List[Path]) -> int: + results = [ + result + for filename in filenames + for result in iter_job_results(filename, parse_xml_file(filename)) + ] + + pages = write_html_pages(output_path, results) + + write_html_index(output_path, pages) + write_test_outputs(output_path, results) + write_assets(output_path) + + return 0 + + +if __name__ == "__main__": + (_, output_path, *filenames) = sys.argv + exit(main(Path(output_path), list(map(Path, filenames)))) diff --git a/irctest/dashboard/github_download.py b/irctest/dashboard/github_download.py new file mode 100644 index 0000000..06ebaee --- /dev/null +++ b/irctest/dashboard/github_download.py @@ -0,0 +1,86 @@ +import dataclasses +import gzip +import io +import json +from pathlib import Path +import sys +from typing import Iterator +import urllib.parse +import urllib.request +import zipfile + + +@dataclasses.dataclass +class Artifact: + repo: str + run_id: int + name: str + download_url: str + + @property + def public_download_url(self): + # GitHub API is not available publicly for artifacts, we need to use + # a third-party proxy to access it... + name = urllib.parse.quote(self.name) + return f"https://nightly.link/{repo}/actions/runs/{self.run_id}/{name}.zip" + + +def iter_run_artifacts(repo: str, run_id: int) -> Iterator[Artifact]: + request = urllib.request.Request( + f"https://api.github.com/repos/{repo}/actions/runs/{run_id}/artifacts", + headers={"Accept": "application/vnd.github.v3+json"}, + ) + + response = urllib.request.urlopen(request) + + for artifact in json.load(response)["artifacts"]: + if not artifact["name"].startswith(("pytest_results_", "pytest results ")): + continue + if artifact["expired"]: + continue + yield Artifact( + repo=repo, + run_id=run_id, + name=artifact["name"], + download_url=artifact["archive_download_url"], + ) + + +def download_artifact(output_name: Path, url: str) -> None: + if output_name.exists(): + return + response = urllib.request.urlopen(url) + archive_bytes = response.read() # Can't stream it, it's a ZIP + with zipfile.ZipFile(io.BytesIO(archive_bytes)) as archive: + with archive.open("pytest.xml") as input_fd: + pytest_xml = input_fd.read() + + tmp_output_path = output_name.with_suffix(".tmp") + with gzip.open(tmp_output_path, "wb") as output_fd: + output_fd.write(pytest_xml) + + # Atomically write to the output path, so that we don't write partial files in case + # the download process is interrupted + tmp_output_path.rename(output_name) + + +def main(output_dir: Path, repo: str, run_id: int) -> int: + output_dir.mkdir(parents=True, exist_ok=True) + run_path = output_dir / str(run_id) + run_path.mkdir(exist_ok=True) + + for artifact in iter_run_artifacts(repo, run_id): + artifact_path = run_path / artifact.name / "pytest.xml.gz" + artifact_path.parent.mkdir(exist_ok=True) + try: + download_artifact(artifact_path, artifact.download_url) + except Exception: + download_artifact(artifact_path, artifact.public_download_url) + print("downloaded", artifact.name) + + return 0 + + +if __name__ == "__main__": + (_, output_path, repo, run_id) = sys.argv + exit(main(Path(output_path), repo, int(run_id))) diff --git a/irctest/dashboard/style.css b/irctest/dashboard/style.css new file mode 100644 index 0000000..1609da2 --- /dev/null +++ b/irctest/dashboard/style.css @@ -0,0 +1,60 @@ +@media (prefers-color-scheme: dark) { + body { + background-color: #121212; + color: rgba(255, 255, 255, 0.87); + } + a { + filter: invert(0.85) hue-rotate(180deg); + } +} + +/* Only 1px solid border between cells */ +table.test-matrix { + border-spacing: 0; + border-collapse: collapse; +} +table.test-matrix td { + text-align: center; + border: 1px solid grey; +} + +/* Make link take the whole cell */ +table.test-matrix td a { + display: block; + margin: 0; + padding: 0; + width: 100%; + height: 100%; + color: black; + text-decoration: none; +} + +/* Test matrix colors */ +table.test-matrix .deselected { + background-color: grey; +} +table.test-matrix .success { + background-color: green; +} +table.test-matrix .skipped { + background-color: yellow; +} +table.test-matrix .failure { + background-color: red; +} + +/* Rotate headers, thanks to https://css-tricks.com/rotated-table-column-headers/ */ +th.job-name { + height: 140px; + white-space: nowrap; +} +th.job-name > div { + transform: + translate(28px, 50px) + rotate(315deg); + width: 40px; +} +th.job-name > div > span { + border-bottom: 1px solid grey; + padding-left: 0px; +} diff --git a/make_workflows.py b/make_workflows.py index 1468be0..657ae85 100644 --- a/make_workflows.py +++ b/make_workflows.py @@ -10,7 +10,6 @@ and keep them in sync. import enum import pathlib -import textwrap import yaml @@ -351,7 +350,7 @@ def generate_workflow(config: dict, version_flavor: VersionFlavor): jobs[f"test-{test_id}"] = test_job jobs["publish-test-results"] = { - "name": "Publish Unit Tests Results", + "name": "Publish Dashboard", "needs": sorted({f"test-{test_id}" for test_id in config["tests"]} & set(jobs)), "runs-on": "ubuntu-latest", # the build-and-test job might be skipped, we don't need to run @@ -365,32 +364,31 @@ def generate_workflow(config: dict, version_flavor: VersionFlavor): "with": {"path": "artifacts"}, }, { - "name": "Publish Unit Test Results", - "uses": "actions/github-script@v4", - "if": "github.event_name == 'pull_request'", - "with": { - "result-encoding": "string", - "script": script( - textwrap.dedent( - """\ - let body = ''; - const options = {}; - options.listeners = { - stdout: (data) => { - body += data.toString(); - } - }; - await exec.exec('bash', ['-c', 'shopt -s globstar; python3 report.py artifacts/**/*.xml'], options); - github.issues.createComment({ - issue_number: context.issue.number, - owner: context.repo.owner, - repo: context.repo.repo, - body: body, - }); - return body; - """ - ) - ), + "name": "Install dashboard dependencies", + "run": script( + "python -m pip install --upgrade pip", + "pip install defusedxml", + ), + }, + { + "name": "Generate dashboard", + "run": script( + "shopt -s globstar", + "python3 -m irctest.dashboard.format dashboard/ artifacts/**/*.xml", + "echo '/ /index.xhtml' > dashboard/_redirects", + ), + }, + { + "name": "Install netlify-cli", + "run": "npm i -g netlify-cli", + }, + { + "name": "Deploy to Netlify", + "run": "./.github/deploy_to_netlify.py", + "env": { + "NETLIFY_SITE_ID": "${{ secrets.NETLIFY_SITE_ID }}", + "NETLIFY_AUTH_TOKEN": "${{ secrets.NETLIFY_AUTH_TOKEN }}", + "GITHUB_TOKEN": "${{ secrets.GITHUB_TOKEN }}", }, }, ], diff --git a/mypy.ini b/mypy.ini index 984a93a..c5bf548 100644 --- a/mypy.ini +++ b/mypy.ini @@ -12,6 +12,9 @@ disallow_untyped_defs = False [mypy-irctest.client_tests.*] disallow_untyped_defs = False +[mypy-defusedxml.*] +ignore_missing_imports = True + [mypy-ecdsa] ignore_missing_imports = True diff --git a/pytest.ini b/pytest.ini index 19518e3..2aa6a82 100644 --- a/pytest.ini +++ b/pytest.ini @@ -39,3 +39,6 @@ markers = WHOX python_classes = *TestCase Test* + +# Include stdout in pytest.xml files used by the dashboard. +junit_logging = system-out From a7d3fadd8be76885d17fa81236540cdaaa9b0578 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 10 Apr 2022 11:08:59 +0200 Subject: [PATCH 025/143] Fix crash on scheduled workflows --- .github/deploy_to_netlify.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/deploy_to_netlify.py b/.github/deploy_to_netlify.py index e24d3ee..83518bd 100755 --- a/.github/deploy_to_netlify.py +++ b/.github/deploy_to_netlify.py @@ -2,23 +2,40 @@ import json import os +import pprint import re import subprocess +import sys import urllib.request +event_name = os.environ["GITHUB_EVENT_NAME"] + +is_pull_request = is_push = False +if event_name.startswith("pull_request"): + is_pull_request = True +elif event_name.startswith("push"): + is_push = True +elif event_name.startswith("schedule"): + # Don't publish; not all controllers run on scheduled events + sys.exit(0) +else: + print("Unexpected event name:", event_name) + with open(os.environ["GITHUB_EVENT_PATH"]) as fd: github_event = json.load(fd) +pprint.pprint(github_event) + context_suffix = "" command = ["netlify", "deploy", "--dir=dashboard/"] -if "pull_request" in github_event and "number" in github_event: +if is_pull_request: pr_number = github_event["number"] sha = github_event["after"] # Aliases can't exceed 37 chars command.extend(["--alias", f"pr-{pr_number}-{sha[0:10]}"]) context_suffix = " (pull_request)" -else: +elif is_push: ref = github_event["ref"] m = re.match("refs/heads/(.*)", ref) if m: @@ -95,5 +112,5 @@ def send_pr_comment() -> None: assert response.status == 201, response.read() -if "pull_request" in github_event: +if is_pull_request: send_pr_comment() From ca9ec1733c2c0b1bbc50302bea9e3c116a6afb1e Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 10 Apr 2022 11:14:13 +0200 Subject: [PATCH 026/143] Fix comment --- .github/deploy_to_netlify.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/deploy_to_netlify.py b/.github/deploy_to_netlify.py index 83518bd..2cc3e37 100755 --- a/.github/deploy_to_netlify.py +++ b/.github/deploy_to_netlify.py @@ -16,7 +16,8 @@ if event_name.startswith("pull_request"): elif event_name.startswith("push"): is_push = True elif event_name.startswith("schedule"): - # Don't publish; not all controllers run on scheduled events + # Don't publish; scheduled workflows run against the latest commit of every + # implementation, so they are likely to have failed tests for the wrong reasons sys.exit(0) else: print("Unexpected event name:", event_name) @@ -53,6 +54,7 @@ elif is_push: pass +print("Running", command) proc = subprocess.run(command, capture_output=True) output = proc.stdout.decode() @@ -63,6 +65,8 @@ assert m netlify_site_url = m.group(0) target_url = f"{netlify_site_url}/index.xhtml" +print("Published to", netlify_site_url) + def send_status() -> None: statuses_url = github_event["repository"]["statuses_url"].format(sha=sha) From d24f0b4f1266a97b9f6cf9b7cf711e5e635064a2 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 10 Apr 2022 11:37:35 +0200 Subject: [PATCH 027/143] Add support for Nefarious (#151) --- .github/workflows/test-devel.yml | 38 ++++++++++++++ .github/workflows/test-stable.yml | 38 ++++++++++++++ Makefile | 25 +++++++++- data/nefarious/ircd.pem | 83 +++++++++++++++++++++++++++++++ irctest/controllers/ircu2.py | 1 + irctest/controllers/nefarious.py | 11 ++++ workflows.yml | 28 +++++++++-- 7 files changed, 218 insertions(+), 6 deletions(-) create mode 100644 data/nefarious/ircd.pem create mode 100644 irctest/controllers/nefarious.py diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index b3c82c4..fb936ea 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -380,6 +380,7 @@ jobs: - test-inspircd-anope - test-ircu2 - test-limnoria + - test-nefarious - test-ngircd - test-ngircd-anope - test-ngircd-atheme @@ -728,6 +729,43 @@ jobs: with: name: pytest-results_limnoria_devel path: pytest.xml + test-nefarious: + needs: [] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.7 + uses: actions/setup-python@v2 + with: + python-version: 3.7 + - name: Checkout nefarious + uses: actions/checkout@v2 + with: + path: nefarious + ref: master + repository: evilnet/nefarious2 + - name: Build nefarious + run: | + cd $GITHUB_WORKSPACE/nefarious + ./configure --prefix=$HOME/.local/ --enable-debug + make -j 4 + make install + cp $GITHUB_WORKSPACE/data/nefarious/* $HOME/.local/lib + - name: Install Atheme + run: sudo apt-get install atheme-services + - name: Install irctest dependencies + run: |- + python -m pip install --upgrade pip + pip install pytest pytest-xdist -r requirements.txt + - name: Test with pytest + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make + nefarious + - if: always() + name: Publish results + uses: actions/upload-artifact@v2 + with: + name: pytest-results_nefarious_devel + path: pytest.xml test-ngircd: needs: - build-ngircd diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 02989aa..4067f3b 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -423,6 +423,7 @@ jobs: - test-irc2 - test-ircu2 - test-limnoria + - test-nefarious - test-ngircd - test-ngircd-anope - test-ngircd-atheme @@ -888,6 +889,43 @@ jobs: with: name: pytest-results_limnoria_stable path: pytest.xml + test-nefarious: + needs: [] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.7 + uses: actions/setup-python@v2 + with: + python-version: 3.7 + - name: Checkout nefarious + uses: actions/checkout@v2 + with: + path: nefarious + ref: 985704168ecada12d9e53b46df6087ef9d9fb40b + repository: evilnet/nefarious2 + - name: Build nefarious + run: | + cd $GITHUB_WORKSPACE/nefarious + ./configure --prefix=$HOME/.local/ --enable-debug + make -j 4 + make install + cp $GITHUB_WORKSPACE/data/nefarious/* $HOME/.local/lib + - name: Install Atheme + run: sudo apt-get install atheme-services + - name: Install irctest dependencies + run: |- + python -m pip install --upgrade pip + pip install pytest pytest-xdist -r requirements.txt + - name: Test with pytest + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make + nefarious + - if: always() + name: Publish results + uses: actions/upload-artifact@v2 + with: + name: pytest-results_nefarious_stable + path: pytest.xml test-ngircd: needs: - build-ngircd diff --git a/Makefile b/Makefile index c0d9bc7..a2d7dd2 100644 --- a/Makefile +++ b/Makefile @@ -91,6 +91,20 @@ IRCU2_SELECTORS := \ and not testWhowasCountZero \ $(EXTRA_SELECTORS) +# same justification as ircu2 +# lusers "unregistered" tests fail because Nefarious doesn't seem to distinguish unregistered users from normal ones +NEFARIOUS_SELECTORS := \ + not Ergo \ + and not deprecated \ + and not strict \ + and not buffering \ + and not testQuit \ + and not (lusers and unregistered) \ + and not statusmsg \ + and not (testKeyValidation and empty) \ + and not testEmptyRealname \ + $(EXTRA_SELECTORS) + # same justification as ircu2 SNIRCD_SELECTORS := \ not Ergo \ @@ -196,9 +210,9 @@ UNREALIRCD_SELECTORS := \ and not HelpTestCase \ $(EXTRA_SELECTORS) -.PHONY: all flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon limnoria sopel solanum unrealircd +.PHONY: all flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sopel solanum unrealircd -all: flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon limnoria sopel solanum unrealircd +all: flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sopel solanum unrealircd flakes: find irctest/ -name "*.py" -not -path "irctest/scram/*" -print0 | xargs -0 pyflakes3 @@ -270,6 +284,13 @@ ircu2: -n 10 \ -k '$(IRCU2_SELECTORS)' +nefarious: + $(PYTEST) $(PYTEST_ARGS) \ + --controller=irctest.controllers.nefarious \ + -m 'not services' \ + -n 10 \ + -k '$(NEFARIOUS_SELECTORS)' + snircd: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.snircd \ diff --git a/data/nefarious/ircd.pem b/data/nefarious/ircd.pem new file mode 100644 index 0000000..7f59f3d --- /dev/null +++ b/data/nefarious/ircd.pem @@ -0,0 +1,83 @@ +-----BEGIN PRIVATE KEY----- +MIIJRAIBADANBgkqhkiG9w0BAQEFAASCCS4wggkqAgEAAoICAQDT0URxi7/l7ZGe +tkPv9Yh8h2s9BpbAR4Wq8sakgqETWg/nE/JQM5dPxroVbtZWWQXuJEFsgBKbASLa +/eg5cyJv4Uu5WIZpG1LxdPEEIOSMWjzoAGwoLxbTRGrS7qNXsknB9RwDuq8lPQiK +kiAahg1Cn1vRrQ4cRrG+AkQWpRHJEDoLjCSo8IcAsKAZlw/eGtAcmeNvkr5AujEw +XjIwx2FoDyKaNGRH5Z7gLWvCKBNxQuJuMTzh8guLqdGbE4hH3rqyICbW5DGPaOZL +LErWuJ7kEhLZG2HDW5JaXOr0QfFYAA8pl9/qCuFMdoxRUKRcYBoxoMmz6dlsmipN +7vIj+TT6TemwcAT25pwMJIVS4WC4+BZilNH2eWKD9hZA8Kq7FDPu+1rxOJaLbE/b +vpK8jZeRdqFzE1eBCgPkw8D8V0r7J18d+DsmgOe2kRycaia/t9M4rhqe0FXjX1X1 +lzQ52grxgc28Ejd1fGQXIJmdTh4BqKqTzxup0izS7dgFP1Ezm6Z4O+wklpL5uQF2 +Ex4X6QEj76iCxH+J/01/cvbxMe3iuGXECbO/y1FIrg7FKzZSrQo4aP63lS7Y7aq0 +t2t6kOS83ebhnpgHClgFs8/C3ayzYBBtbK63PYthwO8Rt6WamCIZFF5tA3XoI4Ak +fZcWD18loZai+QznVzbLNINf++rTwwIDAQABAoICAQCs1tT3piZHU2yAyo9zLbJa +kxGxcT/v1CzBSmtG8ATJyrKxRzhxszdj9HABbzjcqrXJFbKA+5yy+OFdOtSUlFtk +Wb21lwPOnmo29sp4KPL1h+itEzMuMwZ4DBry1aFZvPSsnPpoHJwwUbY3hHdHzVzi +oTCGTqT188Wzmxu+MqHppCEJLSj45ZPzvyxU1UwwW0a4H+ZTM7WlEYlzw1lHLlpQ +VBFTLS8q77aNjOKiQptiz0X+zpS0dhJvu3l7BhwtMRS8prmqnffG4r0QWCsVPP8C +cbEJkWtbwswQikN6Xpi1yw6UTQZ8brZa810aOSh07EJTfrU35rjxAndEspbJPd+4 +Zz6fKNnRA7A4fFap2hF4TSP/UF12b4ulQ8FfcMMTFszWko5M6fWFuCeWfNbyXML5 +fmn+NmSOboj7LkDqbpxtyaIVXXb2Y3F6A2fNllm/mxaGrRoEGNaH3o0qBgWRzJJB +TDSvIQtJddzL+iMaqxz4ufXAREJernZmPa3vlkVGLINNQUC9JLrB5eFjLzPiZN2U +8RgQ9YX5tjoJ+DtPWuMFDiCS1ZE20/UBOEYTeqIVuKdK3AjJDMFSjg8fRvsWRqZe +zsHv6tCtIFZFxYRxtrRGTUPQF+1QD6zBjYxZZk1B4n3uYBGVQFM/LnNHUxRnJBx1 +PunD4ICOY97xd2hcPmGiCQKCAQEA8NCXYaHzhv6fg95H/iMuJVcOCKrJ5rVr4poG +SD0KZtS7SLzUYat8WcuoSubh5Eb2hHtrsnLrSOTnwQUO61f4gCRm2sEqHYsOAd7+ +mNe1jfil0UBVqqL9GBcGYJkc5+DHgUlJQaxMV+4YLt8fD0KfZEnHaDAYX3kUdz+p +be//YAKv+JmxWcUdBF60AUWPjbCJT/1pfJeY8nEBFiYtlYKKN24+4OiRdJ2yRGzt +ZtNHaWy5EFF70yVgPX5MGQ7Z2JpejzK+lt+9nG4h1uJ4M2X4YrGVrRCn1W8jwqm/ +bXest3E6wkkLoWDm9EaeYj00DUgMOviPyP4ckyxilG+Fny4JbwKCAQEA4SyUV03X +KEoL5sOD69sLu3TpnIQz73u9an9W/f2f7rsGwmCcR9RdYGV29ltwfBvOm0FnPQio +GliN+3PTWAL6bb8VYo2IR53VKpVHKSQUlzDOD9PGObXw1CT/+0zoMP7FBA4dTJDf +xQ63AQNpSCGdwbxZygPWzLV5O1WxMeXhnQRL1EBvMyJ52od0+HbajDXg5mNiBKNQ +AtVhB9pEu47Blu/KBqWjfh/GeBLPZB7MHmGNBYbBGGskKRLG4sIbwShs9cx8UM0/ +9dxXkX2d8hjnSD/0ZBh54HHUaEvKAKfpz1L8AC0ll7biCAy0CZK23AoZu/KT8zJ+ +qvz3AuJcW3lo7QKCAQEAzfGltNJawPUamBzNttKBUV+s2c6tkkdO92C/xKGnNp/x +dtg+TTTpyKV5zGy9fIsPoecnCFptS06vwAvCYZQ/Kd93stcFXHSiSwlY9H9tfffK +XzTEzoRLLIHsa0omRUufcrqpEqf2NjChr9wS5OsWAx9xkHGpNmUHEqB4FlPsM0C5 +G0LdQCdplGYlTP0fMo5qL+VJhErle1kXE8kcrMMRzyvSTGe4lWGTph79vDUt2kQn +1IPLAJzzPEO5cqiXtzz1Z0N/aOn5b0FkYTAWmeY30LeMiJA46Df+/ihLVKPHKq6E +EMmFT8LeYMPQCbXLwRv/kaMm3D4tU9PejpD9Vk95swKCAQAtULBlxXeIVyaAAVba +L1H0Hroo0n41MtzSwt+568G05JSep5yr4/QKw0CmoY5Im7v/iLEDGmviKXIhaZTd +wHOvhGYEWGFVsFDG6hXRFL7EEoFVtBPPZ2sY9n1BkJ+lxI/XmhORZhJycNypaotU +hddets4HFrCyr86+/ybS2OWHmOa9x13Zl5WYQexrWFfxIaKqGtQOBOPEPjbxwp5U +dI1HF+i7X7hAWJqzbW2pQ31mm9EqjIztoho73diCp/e37q/G46kdBcFadEZ3NCWG +JDbfVmeTgU19usq5Vo9HhINMQvIOAwfuuVJRtmTBDHKaY7n8FfxqU/4j4RbA0Ncv +XYadAoIBAQC7yh4/UZCGhklUhhk/667OfchWvWGriCSaYGmQPdMzxjnIjAvvIUe9 +riOTBSZXYYLmZHsmY/jK7KMGB3AsLTypSa9+ddAWqWn2dvOYyxNiAaSJK/RZfA9A +ocVfvvkhOfNAYIF+A+fyJ2pznsDkBf9tPkhN7kovl+mr/e25qZb1d09377770Pi7 +thzEi+JLrRgYVLrCrPi2j4l7/Va/UaAPz+Dtu2GCT9vXgnhZtpb8R1kTViZFryTv +k+qbNYJzVm61Vit9mVAGe+WuzhlclJnN6LIZGG3zYHIulRAJrH1XDauHZfHzCKgi +FnMesy4thDMH/MhUfRtbylZTq45gtvCA +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIUYHD08+9S32VTD9IEsr2Oe1dH3VEwDQYJKoZIhvcNAQEL +BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM +GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjA0MDQxODE2NTZaFw0yMzA0 +MDQxODE2NTZaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw +HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQDT0URxi7/l7ZGetkPv9Yh8h2s9BpbAR4Wq8sakgqET +Wg/nE/JQM5dPxroVbtZWWQXuJEFsgBKbASLa/eg5cyJv4Uu5WIZpG1LxdPEEIOSM +WjzoAGwoLxbTRGrS7qNXsknB9RwDuq8lPQiKkiAahg1Cn1vRrQ4cRrG+AkQWpRHJ +EDoLjCSo8IcAsKAZlw/eGtAcmeNvkr5AujEwXjIwx2FoDyKaNGRH5Z7gLWvCKBNx +QuJuMTzh8guLqdGbE4hH3rqyICbW5DGPaOZLLErWuJ7kEhLZG2HDW5JaXOr0QfFY +AA8pl9/qCuFMdoxRUKRcYBoxoMmz6dlsmipN7vIj+TT6TemwcAT25pwMJIVS4WC4 ++BZilNH2eWKD9hZA8Kq7FDPu+1rxOJaLbE/bvpK8jZeRdqFzE1eBCgPkw8D8V0r7 +J18d+DsmgOe2kRycaia/t9M4rhqe0FXjX1X1lzQ52grxgc28Ejd1fGQXIJmdTh4B +qKqTzxup0izS7dgFP1Ezm6Z4O+wklpL5uQF2Ex4X6QEj76iCxH+J/01/cvbxMe3i +uGXECbO/y1FIrg7FKzZSrQo4aP63lS7Y7aq0t2t6kOS83ebhnpgHClgFs8/C3ayz +YBBtbK63PYthwO8Rt6WamCIZFF5tA3XoI4AkfZcWD18loZai+QznVzbLNINf++rT +wwIDAQABo1MwUTAdBgNVHQ4EFgQU+9eHi2eqy0f3fDS0GjqkijGDDocwHwYDVR0j +BBgwFoAU+9eHi2eqy0f3fDS0GjqkijGDDocwDwYDVR0TAQH/BAUwAwEB/zANBgkq +hkiG9w0BAQsFAAOCAgEAAJXO3qUc/PW75pI2dt1cKv20VqozkfEf7P0eeVisCDxn +1p3QhVgI2lEe9kzdHp7t42g5xLkUhQEVmBcKm9xbl+k2D1X0+T8px1x6ZiWfbhXL +ptc/qCIXjPCgVN3s+Kasii3hHkZxKGZz/ySmBmfDJZjQZtbZzQWpvvX6SD4s7sjo +gWbZW3qvQ0bFTGdD1IjKYGaxK6aSrNkAIutiAX4RczJ1QSwb9Z2EIen+ABAvOZS9 +xv3LiiidWcuOT7WzXEa4QvOslCEkAF+jj6mGYB7NWtly0kj4AEPvI4IoYTi9dohS +CA0zd1DTfjRwpAnT5P4sj4mpjLyRBumeeVGpCZhUxfKpFjIB2AnlgxrU+LPq5c9R +ZZ9Q5oeLxjRPjpqBeWwgnbjXstQCL9g0U7SsEemsv+zmvG5COhAmG5Wce/65ILlg +450H4bcn1ul0xvxz9hat6tqEZry3HcNE/CGDT+tXuhHpqOXkY1/c78C0QbWjWodR +tCvlXW00a+7TlEhNr4XBNdqtIQfYS9K9yiVVNfZLPEsN/SA3BGXmrr+du1/E4Ria +CkVpmBdJsVu5eMaUj1arsCqI4fwHzljtojJe/pCzZBVkOaSWQEQ+LL4iVnMas68m +qyshtNf4KNiM55OQmyTiFHMTIxCtdEcHaR3mUxR7GrIhc/bxyxUUBtMAuUX0Kjs= +-----END CERTIFICATE----- diff --git a/irctest/controllers/ircu2.py b/irctest/controllers/ircu2.py index 592cfd2..9b0d4bd 100644 --- a/irctest/controllers/ircu2.py +++ b/irctest/controllers/ircu2.py @@ -51,6 +51,7 @@ features {{ class Ircu2Controller(BaseServerController, DirectoryBasedController): + software_name = "Ircu2" supports_sts = False extban_mute_char = None diff --git a/irctest/controllers/nefarious.py b/irctest/controllers/nefarious.py new file mode 100644 index 0000000..379039d --- /dev/null +++ b/irctest/controllers/nefarious.py @@ -0,0 +1,11 @@ +from typing import Type + +from .ircu2 import Ircu2Controller + + +class NefariousController(Ircu2Controller): + software_name = "Nefarious" + + +def get_irctest_controller_class() -> Type[NefariousController]: + return NefariousController diff --git a/workflows.yml b/workflows.yml index 6b19581..aea95da 100644 --- a/workflows.yml +++ b/workflows.yml @@ -204,6 +204,23 @@ software: make -j 4 make install + nefarious: + name: nefarious + repository: evilnet/nefarious2 + refs: + stable: "985704168ecada12d9e53b46df6087ef9d9fb40b" + release: null + devel: "master" + devel_release: null + path: nefarious + separate_build_job: false + build_script: | + cd $GITHUB_WORKSPACE/nefarious + ./configure --prefix=$HOME/.local/ --enable-debug + make -j 4 + make install + cp $GITHUB_WORKSPACE/data/nefarious/* $HOME/.local/lib + ngircd: name: ngircd repository: ngircd/ngircd @@ -358,16 +375,19 @@ tests: plexus4: software: [plexus4, anope] - # doesn't build because it can't find liblex for some reason - #snircd: - # software: [snircd] - irc2: software: [irc2] ircu2: software: [ircu2] + nefarious: + software: [nefarious] + + # doesn't build because it can't find liblex for some reason + #snircd: + # software: [snircd] + unrealircd-5: software: [unrealircd-5] From 93c454c99b402977e637f1d843a34f0dc68632f2 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 10 Apr 2022 11:47:48 +0200 Subject: [PATCH 028/143] Don't use an alias for prod deployment It prevents deployment to the main domain --- .github/deploy_to_netlify.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/deploy_to_netlify.py b/.github/deploy_to_netlify.py index 2cc3e37..5b748f0 100755 --- a/.github/deploy_to_netlify.py +++ b/.github/deploy_to_netlify.py @@ -43,11 +43,10 @@ elif is_push: branch = m.group(1) sha = github_event["head_commit"]["id"] - command.extend(["--alias", f"br-{branch[0:23]}-{sha[0:10]}"]) - if branch in ("main", "master"): command.extend(["--prod"]) else: + command.extend(["--alias", f"br-{branch[0:23]}-{sha[0:10]}"]) context_suffix = " (push)" else: # TODO @@ -59,6 +58,7 @@ proc = subprocess.run(command, capture_output=True) output = proc.stdout.decode() assert proc.returncode == 0, (output, proc.stderr.decode()) +print(output) m = re.search("https://[^ ]*--[^ ]*netlify.app", output) assert m From 107af942e99ceb7156675eddbc6d48601ca46214 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 10 Apr 2022 14:51:14 +0200 Subject: [PATCH 029/143] Add top-level docstrings to all modules Will be used on the dashboard index in a future commit --- irctest/client_tests/cap.py | 2 ++ irctest/client_tests/sasl.py | 5 +++++ irctest/client_tests/tls.py | 2 ++ irctest/self_tests/cases.py | 2 ++ .../{register_verify.py => account_registration.py} | 5 +++++ irctest/server_tests/account_tag.py | 2 +- irctest/server_tests/away.py | 13 +++++++++---- irctest/server_tests/away_notify.py | 2 +- irctest/server_tests/bot_mode.py | 3 +-- irctest/server_tests/bouncer.py | 6 ++++++ irctest/server_tests/buffering.py | 6 ++++-- irctest/server_tests/cap.py | 5 +++++ irctest/server_tests/channel.py | 4 ++++ irctest/server_tests/channel_forward.py | 6 ++++++ irctest/server_tests/channel_rename.py | 4 ++++ irctest/server_tests/chathistory.py | 4 ++++ irctest/server_tests/chmodes/auditorium.py | 6 ++++++ irctest/server_tests/chmodes/ban.py | 9 ++++++++- irctest/server_tests/chmodes/ergo.py | 4 ++++ irctest/server_tests/chmodes/key.py | 7 +++++++ irctest/server_tests/chmodes/moderated.py | 6 ++++++ irctest/server_tests/chmodes/mute_extban.py | 4 ++++ irctest/server_tests/chmodes/secret.py | 7 +++++++ irctest/server_tests/confusables.py | 5 +++++ irctest/server_tests/connection_registration.py | 2 ++ irctest/server_tests/echo_message.py | 2 +- irctest/server_tests/ergo/services.py | 4 ++++ irctest/server_tests/extended_join.py | 2 +- irctest/server_tests/help.py | 2 +- irctest/server_tests/info.py | 5 ++++- irctest/server_tests/invite.py | 7 +++++++ irctest/server_tests/isupport.py | 5 +++++ irctest/server_tests/join.py | 7 +++++++ irctest/server_tests/kick.py | 7 +++++++ irctest/server_tests/labeled_responses.py | 4 ++-- irctest/server_tests/list.py | 9 +++++++++ irctest/server_tests/lusers.py | 8 ++++++++ irctest/server_tests/message_tags.py | 2 +- irctest/server_tests/messages.py | 3 +-- irctest/server_tests/metadata.py | 3 +-- irctest/server_tests/monitor.py | 2 +- irctest/server_tests/multi_prefix.py | 3 +-- irctest/server_tests/multiline.py | 2 +- irctest/server_tests/names.py | 7 +++++++ irctest/server_tests/part.py | 9 +++++++++ irctest/server_tests/pingpong.py | 4 ++++ irctest/server_tests/quit.py | 9 +++++++++ irctest/server_tests/readq.py | 7 +++++-- irctest/server_tests/regressions.py | 2 +- irctest/server_tests/relaymsg.py | 4 ++++ irctest/server_tests/roleplay.py | 4 ++++ irctest/server_tests/statusmsg.py | 7 +++++++ irctest/server_tests/topic.py | 7 +++++++ irctest/server_tests/utf8.py | 7 +++++++ irctest/server_tests/wallops.py | 6 ++++++ irctest/server_tests/who.py | 7 +++++++ irctest/server_tests/whois.py | 6 ++++++ irctest/server_tests/whowas.py | 10 ++++++++++ irctest/server_tests/znc_playback.py | 4 ++++ 59 files changed, 269 insertions(+), 29 deletions(-) rename irctest/server_tests/{register_verify.py => account_registration.py} (98%) diff --git a/irctest/client_tests/cap.py b/irctest/client_tests/cap.py index 5361945..03aa1a1 100644 --- a/irctest/client_tests/cap.py +++ b/irctest/client_tests/cap.py @@ -1,3 +1,5 @@ +"""Format of ``CAP LS`` sent by IRCv3 clients.""" + from irctest import cases from irctest.irc_utils.message_parser import Message diff --git a/irctest/client_tests/sasl.py b/irctest/client_tests/sasl.py index 7cc3781..bf766f9 100644 --- a/irctest/client_tests/sasl.py +++ b/irctest/client_tests/sasl.py @@ -1,3 +1,8 @@ +"""SASL authentication from clients, for all known mechanisms. + +For now, only `SASLv3.1 `_ +is tested, not `SASLv3.2 `_.""" + import base64 import pytest diff --git a/irctest/client_tests/tls.py b/irctest/client_tests/tls.py index e24e421..285bd36 100644 --- a/irctest/client_tests/tls.py +++ b/irctest/client_tests/tls.py @@ -1,3 +1,5 @@ +"""Clients should validate certificates; either with a CA or fingerprints.""" + import socket import ssl diff --git a/irctest/self_tests/cases.py b/irctest/self_tests/cases.py index eda4b5d..63cf659 100644 --- a/irctest/self_tests/cases.py +++ b/irctest/self_tests/cases.py @@ -1,3 +1,5 @@ +"""Internal checks of assertion implementations.""" + from typing import Dict, List, Tuple import pytest diff --git a/irctest/server_tests/register_verify.py b/irctest/server_tests/account_registration.py similarity index 98% rename from irctest/server_tests/register_verify.py rename to irctest/server_tests/account_registration.py index 805d373..ba8a25d 100644 --- a/irctest/server_tests/register_verify.py +++ b/irctest/server_tests/account_registration.py @@ -1,3 +1,8 @@ +""" +`Draft IRCv3 account-registration +`_ +""" + from irctest import cases from irctest.patma import ANYSTR diff --git a/irctest/server_tests/account_tag.py b/irctest/server_tests/account_tag.py index ea909db..f5d2e87 100644 --- a/irctest/server_tests/account_tag.py +++ b/irctest/server_tests/account_tag.py @@ -1,5 +1,5 @@ """ - +`IRCv3 account-tag `_ """ from irctest import cases diff --git a/irctest/server_tests/away.py b/irctest/server_tests/away.py index 7b24a08..a6ea7b4 100644 --- a/irctest/server_tests/away.py +++ b/irctest/server_tests/away.py @@ -1,3 +1,8 @@ +""" +AWAY command (`RFC 2812 `__, +`Modern `__) +""" + from irctest import cases from irctest.numerics import RPL_AWAY, RPL_NOWAWAY, RPL_UNAWAY, RPL_USERHOST from irctest.patma import StrRe @@ -32,7 +37,7 @@ class AwayTestCase(cases.BaseServerTestCase): """ "The server acknowledges the change in away status by returning the `RPL_NOWAWAY` and `RPL_UNAWAY` numerics." - -- https://github.com/ircdocs/modern-irc/pull/100 + -- https://modern.ircdocs.horse/#away-message """ self.connectClient("bar") self.sendLine(1, "AWAY :I'm not here right now") @@ -48,7 +53,7 @@ class AwayTestCase(cases.BaseServerTestCase): """ "Servers SHOULD notify clients when a user they're interacting with is away when relevant" - -- https://github.com/ircdocs/modern-irc/pull/100 + -- https://modern.ircdocs.horse/#away-message " :" -- https://modern.ircdocs.horse/#rplaway-301 @@ -75,7 +80,7 @@ class AwayTestCase(cases.BaseServerTestCase): """ "Servers SHOULD notify clients when a user they're interacting with is away when relevant" - -- https://github.com/ircdocs/modern-irc/pull/100 + -- https://modern.ircdocs.horse/#away-message " :" -- https://modern.ircdocs.horse/#rplaway-301 @@ -113,7 +118,7 @@ class AwayTestCase(cases.BaseServerTestCase): """ "Servers SHOULD notify clients when a user they're interacting with is away when relevant" - -- https://github.com/ircdocs/modern-irc/pull/100 + -- https://modern.ircdocs.horse/#away-message " :" -- https://modern.ircdocs.horse/#rplaway-301 diff --git a/irctest/server_tests/away_notify.py b/irctest/server_tests/away_notify.py index 6d3b8d6..283c007 100644 --- a/irctest/server_tests/away_notify.py +++ b/irctest/server_tests/away_notify.py @@ -1,5 +1,5 @@ """ - +`IRCv3 away-notify `_ """ from irctest import cases diff --git a/irctest/server_tests/bot_mode.py b/irctest/server_tests/bot_mode.py index 161aaf4..224c77e 100644 --- a/irctest/server_tests/bot_mode.py +++ b/irctest/server_tests/bot_mode.py @@ -1,6 +1,5 @@ """ -Draft bot mode specification, as defined in - +`IRCv3 draft bot mode `_ """ from irctest import cases, runner diff --git a/irctest/server_tests/bouncer.py b/irctest/server_tests/bouncer.py index 4daed8d..c1cdc9f 100644 --- a/irctest/server_tests/bouncer.py +++ b/irctest/server_tests/bouncer.py @@ -1,3 +1,9 @@ +""" +`Ergo `_-specific tests of +`multiclient features +`_ +""" + from irctest import cases from irctest.irc_utils.sasl import sasl_plain_blob from irctest.numerics import ERR_NICKNAMEINUSE, RPL_WELCOME diff --git a/irctest/server_tests/buffering.py b/irctest/server_tests/buffering.py index 321e04b..4adf01d 100644 --- a/irctest/server_tests/buffering.py +++ b/irctest/server_tests/buffering.py @@ -1,5 +1,7 @@ -"""Sends packets with various length to check the server reassembles them -correctly. Also checks truncation""" +""" +Sends packets with various length to check the server reassembles them +correctly. Also checks truncation. +""" import socket import time diff --git a/irctest/server_tests/cap.py b/irctest/server_tests/cap.py index b4fd31d..1fd5a76 100644 --- a/irctest/server_tests/cap.py +++ b/irctest/server_tests/cap.py @@ -1,3 +1,8 @@ +""" +`IRCv3 Capability negotiation +`_ +""" + from irctest import cases from irctest.patma import ANYSTR from irctest.runner import CapabilityNotSupported, ImplementationChoice diff --git a/irctest/server_tests/channel.py b/irctest/server_tests/channel.py index 5fead80..61472f7 100644 --- a/irctest/server_tests/channel.py +++ b/irctest/server_tests/channel.py @@ -1,3 +1,7 @@ +""" +Channel casemapping +""" + import pytest from irctest import cases, client_mock, runner diff --git a/irctest/server_tests/channel_forward.py b/irctest/server_tests/channel_forward.py index 9ce7312..11b284d 100644 --- a/irctest/server_tests/channel_forward.py +++ b/irctest/server_tests/channel_forward.py @@ -1,3 +1,9 @@ +""" +`Ergo `_-specific tests of channel forwarding + +TODO: Should be extended to other servers, once a specification is written. +""" + from irctest import cases from irctest.numerics import ERR_CHANOPRIVSNEEDED, ERR_INVALIDMODEPARAM, ERR_LINKCHANNEL diff --git a/irctest/server_tests/channel_rename.py b/irctest/server_tests/channel_rename.py index d22d950..b66fe59 100644 --- a/irctest/server_tests/channel_rename.py +++ b/irctest/server_tests/channel_rename.py @@ -1,3 +1,7 @@ +""" +`Draft IRCv3 channel-rename `_ +""" + from irctest import cases from irctest.numerics import ERR_CHANOPRIVSNEEDED diff --git a/irctest/server_tests/chathistory.py b/irctest/server_tests/chathistory.py index a387e6f..a264ae5 100644 --- a/irctest/server_tests/chathistory.py +++ b/irctest/server_tests/chathistory.py @@ -1,3 +1,7 @@ +""" +`IRCv3 draft chathistory `_ +""" + import secrets import time diff --git a/irctest/server_tests/chmodes/auditorium.py b/irctest/server_tests/chmodes/auditorium.py index 8cd74a3..bd328cb 100644 --- a/irctest/server_tests/chmodes/auditorium.py +++ b/irctest/server_tests/chmodes/auditorium.py @@ -1,3 +1,9 @@ +""" +`Ergo `_-specific tests of auditorium mode + +TODO: Should be extended to other servers, once a specification is written. +""" + import math import time diff --git a/irctest/server_tests/chmodes/ban.py b/irctest/server_tests/chmodes/ban.py index 22cdebc..471e40a 100644 --- a/irctest/server_tests/chmodes/ban.py +++ b/irctest/server_tests/chmodes/ban.py @@ -1,3 +1,10 @@ +""" +Channel ban (`RFC 1459 +`__, +`RFC 2812 `__, +`Modern `__) +""" + from irctest import cases from irctest.numerics import ERR_BANNEDFROMCHAN, RPL_BANLIST, RPL_ENDOFBANLIST from irctest.patma import ANYSTR, StrRe @@ -26,7 +33,7 @@ class BanModeTestCase(cases.BaseServerTestCase): @cases.mark_specifications("Modern") def testBanList(self): - """https://github.com/ircdocs/modern-irc/pull/125""" + """`RPL_BANLIST `""" self.connectClient("chanop") self.joinChannel(1, "#chan") self.getMessages(1) diff --git a/irctest/server_tests/chmodes/ergo.py b/irctest/server_tests/chmodes/ergo.py index eef9d11..ae4d26d 100644 --- a/irctest/server_tests/chmodes/ergo.py +++ b/irctest/server_tests/chmodes/ergo.py @@ -1,3 +1,7 @@ +""" +Various Ergo-specific channel modes +""" + from irctest import cases from irctest.numerics import ERR_CANNOTSENDTOCHAN, ERR_CHANOPRIVSNEEDED diff --git a/irctest/server_tests/chmodes/key.py b/irctest/server_tests/chmodes/key.py index eae1ab3..6878c90 100644 --- a/irctest/server_tests/chmodes/key.py +++ b/irctest/server_tests/chmodes/key.py @@ -1,3 +1,10 @@ +""" +Channel key (`RFC 1459 +`__, +`RFC 2812 `__, +`Modern `__) +""" + import pytest from irctest import cases diff --git a/irctest/server_tests/chmodes/moderated.py b/irctest/server_tests/chmodes/moderated.py index e403dfe..d5b7b26 100644 --- a/irctest/server_tests/chmodes/moderated.py +++ b/irctest/server_tests/chmodes/moderated.py @@ -1,3 +1,9 @@ +""" +Channel moderation mode (`RFC 2812 +`__, +`Modern `__) +""" + from irctest import cases from irctest.numerics import ERR_CANNOTSENDTOCHAN diff --git a/irctest/server_tests/chmodes/mute_extban.py b/irctest/server_tests/chmodes/mute_extban.py index a17b0b4..5c3c6d4 100644 --- a/irctest/server_tests/chmodes/mute_extban.py +++ b/irctest/server_tests/chmodes/mute_extban.py @@ -1,3 +1,7 @@ +""" +Mute extban, currently no specifications or ways to discover it. +""" + from irctest import cases, runner from irctest.numerics import ERR_CANNOTSENDTOCHAN, ERR_CHANOPRIVSNEEDED from irctest.patma import ANYLIST, StrRe diff --git a/irctest/server_tests/chmodes/secret.py b/irctest/server_tests/chmodes/secret.py index 15efaff..30f7759 100644 --- a/irctest/server_tests/chmodes/secret.py +++ b/irctest/server_tests/chmodes/secret.py @@ -1,3 +1,10 @@ +""" +Channel secrecy mode (`RFC 1459 +`__, +`RFC 2812 `__, +`Modern `__) +""" + from irctest import cases from irctest.numerics import RPL_LIST diff --git a/irctest/server_tests/confusables.py b/irctest/server_tests/confusables.py index 223d84b..480ece2 100644 --- a/irctest/server_tests/confusables.py +++ b/irctest/server_tests/confusables.py @@ -1,3 +1,8 @@ +""" +`Ergo `_-specific tests for nick collisions based on Unicode +confusable characters +""" + from irctest import cases from irctest.numerics import ERR_NICKNAMEINUSE, RPL_WELCOME diff --git a/irctest/server_tests/connection_registration.py b/irctest/server_tests/connection_registration.py index 6a1df18..7bf6ffe 100644 --- a/irctest/server_tests/connection_registration.py +++ b/irctest/server_tests/connection_registration.py @@ -1,6 +1,8 @@ """ Tests section 4.1 of RFC 1459. + +TODO: cross-reference Modern and RFC 2812 too """ from irctest import cases diff --git a/irctest/server_tests/echo_message.py b/irctest/server_tests/echo_message.py index f183316..1da3412 100644 --- a/irctest/server_tests/echo_message.py +++ b/irctest/server_tests/echo_message.py @@ -1,5 +1,5 @@ """ - +`IRCv3 echo-message `_ """ import pytest diff --git a/irctest/server_tests/ergo/services.py b/irctest/server_tests/ergo/services.py index eb00ab2..ef877bd 100644 --- a/irctest/server_tests/ergo/services.py +++ b/irctest/server_tests/ergo/services.py @@ -1,3 +1,7 @@ +""" +`Ergo `-specific tests of NickServ. +""" + from irctest import cases from irctest.numerics import RPL_YOUREOPER diff --git a/irctest/server_tests/extended_join.py b/irctest/server_tests/extended_join.py index 7adf811..13dc975 100644 --- a/irctest/server_tests/extended_join.py +++ b/irctest/server_tests/extended_join.py @@ -1,5 +1,5 @@ """ - +`IRCv3 extended-join `_ """ from irctest import cases diff --git a/irctest/server_tests/help.py b/irctest/server_tests/help.py index c5acc65..9058ea3 100644 --- a/irctest/server_tests/help.py +++ b/irctest/server_tests/help.py @@ -1,5 +1,5 @@ """ -The HELP and HELPOP command. +The HELP and HELPOP command (`Modern `__) """ import re diff --git a/irctest/server_tests/info.py b/irctest/server_tests/info.py index db0d9db..8e3ed61 100644 --- a/irctest/server_tests/info.py +++ b/irctest/server_tests/info.py @@ -1,5 +1,8 @@ """ -The INFO command. +The INFO command (`RFC 1459 +`__, +`RFC 2812 `__, +`Modern `__) """ import pytest diff --git a/irctest/server_tests/invite.py b/irctest/server_tests/invite.py index 8c07371..f441849 100644 --- a/irctest/server_tests/invite.py +++ b/irctest/server_tests/invite.py @@ -1,3 +1,10 @@ +""" +The INVITE command (`RFC 1459 +`__, +`RFC 2812 `__, +`Modern `__) +""" + import pytest from irctest import cases diff --git a/irctest/server_tests/isupport.py b/irctest/server_tests/isupport.py index 40f087d..2c3cbaa 100644 --- a/irctest/server_tests/isupport.py +++ b/irctest/server_tests/isupport.py @@ -1,3 +1,8 @@ +""" +RPL_ISUPPORT: `format `__ +and various `tokens `__ +""" + import re from irctest import cases, runner diff --git a/irctest/server_tests/join.py b/irctest/server_tests/join.py index 673d403..e3433e6 100644 --- a/irctest/server_tests/join.py +++ b/irctest/server_tests/join.py @@ -1,3 +1,10 @@ +""" +The JOIN command (`RFC 1459 +`__, +`RFC 2812 `__, +`Modern `__) +""" + from irctest import cases from irctest.irc_utils import ambiguities diff --git a/irctest/server_tests/kick.py b/irctest/server_tests/kick.py index aaafcd5..d2acc72 100644 --- a/irctest/server_tests/kick.py +++ b/irctest/server_tests/kick.py @@ -1,3 +1,10 @@ +""" +The INFO command (`RFC 1459 +`__, +`RFC 2812 `__, +`Modern `__) +""" + import pytest from irctest import cases, client_mock, runner diff --git a/irctest/server_tests/labeled_responses.py b/irctest/server_tests/labeled_responses.py index f1c2374..03f9dfd 100644 --- a/irctest/server_tests/labeled_responses.py +++ b/irctest/server_tests/labeled_responses.py @@ -1,8 +1,8 @@ """ +`IRCv3 labeled-response `_ + This specification is a little hard to test because all labels are optional; so there may be many false positives. - - """ import re diff --git a/irctest/server_tests/list.py b/irctest/server_tests/list.py index 9529cb9..0c57289 100644 --- a/irctest/server_tests/list.py +++ b/irctest/server_tests/list.py @@ -1,3 +1,12 @@ +""" +The LIST command (`RFC 1459 +`__, +`RFC 2812 `__, +`Modern `__) + +TODO: check with Modern +""" + from irctest import cases diff --git a/irctest/server_tests/lusers.py b/irctest/server_tests/lusers.py index 58f3601..7eb59d6 100644 --- a/irctest/server_tests/lusers.py +++ b/irctest/server_tests/lusers.py @@ -1,3 +1,11 @@ +""" +The LUSERS command (`RFC 2812 +`__, +`Modern `__), +which provides statistics on user counts. +""" + + from dataclasses import dataclass import re from typing import Optional diff --git a/irctest/server_tests/message_tags.py b/irctest/server_tests/message_tags.py index 9d7ba14..e19a2d9 100644 --- a/irctest/server_tests/message_tags.py +++ b/irctest/server_tests/message_tags.py @@ -1,5 +1,5 @@ """ -https://ircv3.net/specs/extensions/message-tags.html +`IRCv3 message-tags `_ """ import pytest diff --git a/irctest/server_tests/messages.py b/irctest/server_tests/messages.py index 7f26025..a4e8544 100644 --- a/irctest/server_tests/messages.py +++ b/irctest/server_tests/messages.py @@ -1,6 +1,5 @@ """ -Section 3.2 of RFC 2812 - +The PRIVMSG and NOTICE commands. """ from irctest import cases diff --git a/irctest/server_tests/metadata.py b/irctest/server_tests/metadata.py index 6317c5e..abfde8b 100644 --- a/irctest/server_tests/metadata.py +++ b/irctest/server_tests/metadata.py @@ -1,6 +1,5 @@ """ -Tests METADATA features. - +`Deprecated IRCv3 Metadata `_ """ from irctest import cases diff --git a/irctest/server_tests/monitor.py b/irctest/server_tests/monitor.py index 56e0306..211ee67 100644 --- a/irctest/server_tests/monitor.py +++ b/irctest/server_tests/monitor.py @@ -1,5 +1,5 @@ """ - +`IRCv3 MONITOR `_ """ from irctest import cases diff --git a/irctest/server_tests/multi_prefix.py b/irctest/server_tests/multi_prefix.py index 02e7aa9..1861ac7 100644 --- a/irctest/server_tests/multi_prefix.py +++ b/irctest/server_tests/multi_prefix.py @@ -1,6 +1,5 @@ """ -Tests multi-prefix. - +`IRCv3 multi-prefix `_ """ from irctest import cases diff --git a/irctest/server_tests/multiline.py b/irctest/server_tests/multiline.py index 7e872df..ac4b7d5 100644 --- a/irctest/server_tests/multiline.py +++ b/irctest/server_tests/multiline.py @@ -1,5 +1,5 @@ """ -draft/multiline +`Draft IRCv3 multiline `_ """ from irctest import cases diff --git a/irctest/server_tests/names.py b/irctest/server_tests/names.py index 628b2ac..5a79743 100644 --- a/irctest/server_tests/names.py +++ b/irctest/server_tests/names.py @@ -1,3 +1,10 @@ +""" +The NAMES command (`RFC 1459 +`__, +`RFC 2812 `__, +`Modern `__) +""" + from irctest import cases from irctest.numerics import RPL_ENDOFNAMES from irctest.patma import ANYSTR diff --git a/irctest/server_tests/part.py b/irctest/server_tests/part.py index acc2f86..59a2045 100644 --- a/irctest/server_tests/part.py +++ b/irctest/server_tests/part.py @@ -1,3 +1,12 @@ +""" +The PART command (`RFC 1459 +`__, +`RFC 2812 `__, +`Modern `__) + +TODO: cross-reference Modern +""" + import time from irctest import cases diff --git a/irctest/server_tests/pingpong.py b/irctest/server_tests/pingpong.py index 33dc4aa..0924d6b 100644 --- a/irctest/server_tests/pingpong.py +++ b/irctest/server_tests/pingpong.py @@ -1,3 +1,7 @@ +""" +The PING and PONG commands +""" + from irctest import cases from irctest.numerics import ERR_NEEDMOREPARAMS, ERR_NOORIGIN from irctest.patma import ANYSTR diff --git a/irctest/server_tests/quit.py b/irctest/server_tests/quit.py index 8a7200d..022447f 100644 --- a/irctest/server_tests/quit.py +++ b/irctest/server_tests/quit.py @@ -1,3 +1,12 @@ +""" +The QUITcommand (`RFC 1459 +`__, +`RFC 2812 `__, +`Modern `__) + +TODO: cross-reference RFC 1459 and Modern +""" + import time from irctest import cases diff --git a/irctest/server_tests/readq.py b/irctest/server_tests/readq.py index 4e8182d..05eeeed 100644 --- a/irctest/server_tests/readq.py +++ b/irctest/server_tests/readq.py @@ -1,9 +1,12 @@ +""" +`Ergo `_-specific tests of responses to DoS attacks +using long lines. +""" + from irctest import cases class ReadqTestCase(cases.BaseServerTestCase): - """Test responses to DoS attacks using long lines.""" - @cases.mark_specifications("Ergo") @cases.mark_capabilities("message-tags") def testReadqTags(self): diff --git a/irctest/server_tests/regressions.py b/irctest/server_tests/regressions.py index 0361b3c..15ff21e 100644 --- a/irctest/server_tests/regressions.py +++ b/irctest/server_tests/regressions.py @@ -1,5 +1,5 @@ """ -Regression tests for bugs in oragono. +Regression tests for bugs in `Ergo `_. """ import time diff --git a/irctest/server_tests/relaymsg.py b/irctest/server_tests/relaymsg.py index 07df252..18af038 100644 --- a/irctest/server_tests/relaymsg.py +++ b/irctest/server_tests/relaymsg.py @@ -1,3 +1,7 @@ +""" +RELAYMSG command of `Ergo `_ +""" + from irctest import cases from irctest.irc_utils.junkdrawer import random_name from irctest.patma import ANYSTR diff --git a/irctest/server_tests/roleplay.py b/irctest/server_tests/roleplay.py index 6ccfd68..2387ced 100644 --- a/irctest/server_tests/roleplay.py +++ b/irctest/server_tests/roleplay.py @@ -1,3 +1,7 @@ +""" +Roleplay features of `Ergo `_ +""" + from irctest import cases from irctest.irc_utils.junkdrawer import random_name from irctest.numerics import ERR_CANNOTSENDRP diff --git a/irctest/server_tests/statusmsg.py b/irctest/server_tests/statusmsg.py index 7a07a2b..fb063f8 100644 --- a/irctest/server_tests/statusmsg.py +++ b/irctest/server_tests/statusmsg.py @@ -1,3 +1,10 @@ +""" +STATUSMSG ISUPPORT token and related PRIVMSG (`Modern +`__) + +TODO: cross-reference Modern +""" + from irctest import cases, runner from irctest.numerics import RPL_NAMREPLY diff --git a/irctest/server_tests/topic.py b/irctest/server_tests/topic.py index 4d83604..9489571 100644 --- a/irctest/server_tests/topic.py +++ b/irctest/server_tests/topic.py @@ -1,3 +1,10 @@ +""" +The TOPIC command (`RFC 1459 +`__, +`RFC 2812 `__, +`Modern `__) +""" + from irctest import cases, client_mock, runner from irctest.numerics import ERR_CHANOPRIVSNEEDED, RPL_NOTOPIC, RPL_TOPIC, RPL_TOPICTIME diff --git a/irctest/server_tests/utf8.py b/irctest/server_tests/utf8.py index 46c838b..32ff563 100644 --- a/irctest/server_tests/utf8.py +++ b/irctest/server_tests/utf8.py @@ -1,3 +1,10 @@ +""" +`Ergo `_-specific tests of non-Unicode filtering + +TODO: turn this into a test of `IRCv3 UTF8ONLY +`_ +""" + from irctest import cases from irctest.patma import ANYSTR diff --git a/irctest/server_tests/wallops.py b/irctest/server_tests/wallops.py index 2d5fb69..c1d493b 100644 --- a/irctest/server_tests/wallops.py +++ b/irctest/server_tests/wallops.py @@ -1,3 +1,9 @@ +""" +The WALLOPS command (`RFC 2812 +`__, +`Modern `__) +""" + from irctest import cases, runner from irctest.numerics import ERR_NOPRIVILEGES, ERR_UNKNOWNCOMMAND, RPL_YOUREOPER from irctest.patma import ANYSTR, StrRe diff --git a/irctest/server_tests/who.py b/irctest/server_tests/who.py index c526b0d..e320cb1 100644 --- a/irctest/server_tests/who.py +++ b/irctest/server_tests/who.py @@ -1,3 +1,10 @@ +""" +The WHO command (`Modern `__) +and `IRCv3 WHOX `_ + +TODO: cross-reference RFC 1459 and RFC 2812 +""" + import re import pytest diff --git a/irctest/server_tests/whois.py b/irctest/server_tests/whois.py index 8a0ef3a..a23ee66 100644 --- a/irctest/server_tests/whois.py +++ b/irctest/server_tests/whois.py @@ -1,3 +1,9 @@ +""" +The WHOIS command (`Modern `__) + +TODO: cross-reference RFC 1459 and RFC 2812 +""" + import pytest from irctest import cases diff --git a/irctest/server_tests/whowas.py b/irctest/server_tests/whowas.py index 2550432..dd65b54 100644 --- a/irctest/server_tests/whowas.py +++ b/irctest/server_tests/whowas.py @@ -1,3 +1,13 @@ +""" +The WHOSWAS command (`RFC 1459 +`__, +`RFC 2812 `__, +`Modern `__) + +TODO: cross-reference Modern +""" + + import pytest from irctest import cases, runner diff --git a/irctest/server_tests/znc_playback.py b/irctest/server_tests/znc_playback.py index a5ac61f..ca00efd 100644 --- a/irctest/server_tests/znc_playback.py +++ b/irctest/server_tests/znc_playback.py @@ -1,3 +1,7 @@ +""" +`Ergo `_-specific tests of ZNC-like message playback +""" + import time from irctest import cases From 397509a282740ae2042c3cfaf3fe57e74aae65c7 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 10 Apr 2022 14:52:04 +0200 Subject: [PATCH 030/143] Move CAP tests to the right module --- irctest/server_tests/cap.py | 57 +++++++++++++++++++ .../server_tests/connection_registration.py | 57 ------------------- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/irctest/server_tests/cap.py b/irctest/server_tests/cap.py index 1fd5a76..fac6e68 100644 --- a/irctest/server_tests/cap.py +++ b/irctest/server_tests/cap.py @@ -177,3 +177,60 @@ class CapTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): enabled_caps.discard("cap-notify") # implicitly added by some impls self.assertEqual(enabled_caps, {cap1}) self.assertNotIn("time", cap_list.tags) + + @cases.mark_specifications("IRCv3") + def testIrc301CapLs(self): + """ + Current version: + + "The LS subcommand is used to list the capabilities supported by the server. + The client should send an LS subcommand with no other arguments to solicit + a list of all capabilities." + + "If a client has not indicated support for CAP LS 302 features, + the server MUST NOT send these new features to the client." + -- + + Before the v3.1 / v3.2 merge: + + IRCv3.1: “The LS subcommand is used to list the capabilities + supported by the server. The client should send an LS subcommand with + no other arguments to solicit a list of all capabilities.” + -- + + IRCv3.2: “Servers MUST NOT send messages described by this document if + the client only supports version 3.1.” + -- + """ # noqa + self.addClient() + self.sendLine(1, "CAP LS") + m = self.getRegistrationMessage(1) + self.assertNotEqual( + m.params[2], + "*", + m, + fail_msg="Server replied with multi-line CAP LS to a " + "“CAP LS” (ie. IRCv3.1) request: {msg}", + ) + self.assertFalse( + any("=" in cap for cap in m.params[2].split()), + "Server replied with a name-value capability in " + "CAP LS reply as a response to “CAP LS” (ie. IRCv3.1) " + "request: {}".format(m), + ) + + @cases.mark_specifications("IRCv3") + def testEmptyCapList(self): + """“If no capabilities are active, an empty parameter must be sent.” + -- + """ # noqa + self.addClient() + self.sendLine(1, "CAP LIST") + m = self.getRegistrationMessage(1) + self.assertMessageMatch( + m, + command="CAP", + params=["*", "LIST", ""], + fail_msg="Sending “CAP LIST” as first message got a reply " + "that is not “CAP * LIST :”: {msg}", + ) diff --git a/irctest/server_tests/connection_registration.py b/irctest/server_tests/connection_registration.py index 7bf6ffe..e4a1c0b 100644 --- a/irctest/server_tests/connection_registration.py +++ b/irctest/server_tests/connection_registration.py @@ -185,60 +185,3 @@ class ConnectionRegistrationTestCase(cases.BaseServerTestCase): command=ERR_NEEDMOREPARAMS, params=[StrRe(r"(\*|foo)"), "USER", ANYSTR], ) - - @cases.mark_specifications("IRCv3") - def testIrc301CapLs(self): - """ - Current version: - - "The LS subcommand is used to list the capabilities supported by the server. - The client should send an LS subcommand with no other arguments to solicit - a list of all capabilities." - - "If a client has not indicated support for CAP LS 302 features, - the server MUST NOT send these new features to the client." - -- - - Before the v3.1 / v3.2 merge: - - IRCv3.1: “The LS subcommand is used to list the capabilities - supported by the server. The client should send an LS subcommand with - no other arguments to solicit a list of all capabilities.” - -- - - IRCv3.2: “Servers MUST NOT send messages described by this document if - the client only supports version 3.1.” - -- - """ # noqa - self.addClient() - self.sendLine(1, "CAP LS") - m = self.getRegistrationMessage(1) - self.assertNotEqual( - m.params[2], - "*", - m, - fail_msg="Server replied with multi-line CAP LS to a " - "“CAP LS” (ie. IRCv3.1) request: {msg}", - ) - self.assertFalse( - any("=" in cap for cap in m.params[2].split()), - "Server replied with a name-value capability in " - "CAP LS reply as a response to “CAP LS” (ie. IRCv3.1) " - "request: {}".format(m), - ) - - @cases.mark_specifications("IRCv3") - def testEmptyCapList(self): - """“If no capabilities are active, an empty parameter must be sent.” - -- - """ # noqa - self.addClient() - self.sendLine(1, "CAP LIST") - m = self.getRegistrationMessage(1) - self.assertMessageMatch( - m, - command="CAP", - params=["*", "LIST", ""], - fail_msg="Sending “CAP LIST” as first message got a reply " - "that is not “CAP * LIST :”: {msg}", - ) From a3f0d422485eacc0c86c32876d8b188f4a961936 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 10 Apr 2022 14:52:31 +0200 Subject: [PATCH 031/143] Remove Ergo-specific mark on channel-rename --- irctest/server_tests/channel_rename.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/irctest/server_tests/channel_rename.py b/irctest/server_tests/channel_rename.py index b66fe59..1255d1f 100644 --- a/irctest/server_tests/channel_rename.py +++ b/irctest/server_tests/channel_rename.py @@ -5,24 +5,17 @@ from irctest import cases from irctest.numerics import ERR_CHANOPRIVSNEEDED -MODERN_CAPS = [ - "server-time", - "message-tags", - "batch", - "labeled-response", - "echo-message", - "account-tag", -] RENAME_CAP = "draft/channel-rename" class ChannelRenameTestCase(cases.BaseServerTestCase): """Basic tests for channel-rename.""" - @cases.mark_specifications("Ergo") def testChannelRename(self): - self.connectClient("bar", name="bar", capabilities=MODERN_CAPS + [RENAME_CAP]) - self.connectClient("baz", name="baz", capabilities=MODERN_CAPS) + self.connectClient( + "bar", name="bar", capabilities=[RENAME_CAP], skip_if_cap_nak=True + ) + self.connectClient("baz", name="baz") self.joinChannel("bar", "#bar") self.joinChannel("baz", "#bar") self.getMessages("bar") From 358b6c221326bdcec3863cd18e93c2d901084123 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 10 Apr 2022 14:53:17 +0200 Subject: [PATCH 032/143] dashboard: Show module and class docstrings --- irctest/dashboard/format.py | 45 +++++++++++++++++++++++++------------ 1 file changed, 31 insertions(+), 14 deletions(-) diff --git a/irctest/dashboard/format.py b/irctest/dashboard/format.py index cd7c6b0..5c88eda 100644 --- a/irctest/dashboard/format.py +++ b/irctest/dashboard/format.py @@ -2,6 +2,7 @@ import base64 import dataclasses import gzip import hashlib +import importlib from pathlib import Path import re import sys @@ -16,10 +17,10 @@ from typing import ( Tuple, TypeVar, ) -import xml.dom.minidom import xml.etree.ElementTree as ET from defusedxml.ElementTree import parse as parse_xml +import docutils.core NETLIFY_CHAR_BLACKLIST = frozenset('":<>|*?\r\n#') """Characters not allowed in output filenames""" @@ -117,9 +118,24 @@ def iter_job_results(job_file_name: Path, job: ET.ElementTree) -> Iterator[CaseR ) +def rst_to_element(s: str) -> ET.Element: + html = docutils.core.publish_parts(s, writer_name="xhtml")["html_body"] + htmltree = ET.fromstring(html) + return htmltree + + +def append_docstring(element: ET.Element, obj: object) -> None: + if obj.__doc__ is None: + return + + element.append(rst_to_element(obj.__doc__)) + + def build_module_html( jobs: List[str], results: List[CaseResult], module_name: str ) -> ET.Element: + module = importlib.import_module(module_name) + root = ET.Element("html") head = ET.SubElement(root, "head") ET.SubElement(head, "title").text = module_name @@ -129,6 +145,8 @@ def build_module_html( ET.SubElement(body, "h1").text = module_name + append_docstring(body, module) + results_by_class = group_by(results, lambda r: r.class_name) table = ET.SubElement(body, "table") @@ -153,6 +171,7 @@ def build_module_html( id=row_anchor, ) section_header.text = class_name + append_docstring(th, getattr(module, class_name)) # Header row: one column for each implementation table.append(job_row) @@ -213,10 +232,6 @@ def build_module_html( else: cell.text = text or "?" - # Hacky: ET expects the namespace to be present in every tag we create instead; - # but it would be excessively verbose. - root.set("xmlns", "http://www.w3.org/1999/xhtml") - return root @@ -261,13 +276,14 @@ def write_html_index(output_dir: Path, pages: List[Tuple[str, str]]) -> None: ET.SubElement(body, "h1").text = "irctest dashboard" - ul = ET.SubElement(body, "ul") + dl = ET.SubElement(body, "dl") for (module_name, file_name) in sorted(pages): - link = ET.SubElement(ET.SubElement(ul, "li"), "a", href=f"./{file_name}") - link.text = module_name + module = importlib.import_module(module_name) - root.set("xmlns", "http://www.w3.org/1999/xhtml") + link = ET.SubElement(ET.SubElement(dl, "dt"), "a", href=f"./{file_name}") + link.text = module_name + append_docstring(ET.SubElement(dl, "dd"), module) write_xml_file(output_dir / "index.xhtml", root) @@ -281,14 +297,15 @@ def write_assets(output_dir: Path) -> None: def write_xml_file(filename: Path, root: ET.Element) -> None: + # Hacky: ET expects the namespace to be present in every tag we create instead; + # but it would be excessively verbose. + root.set("xmlns", "http://www.w3.org/1999/xhtml") + # Serialize s = ET.tostring(root) - # Prettify - s = xml.dom.minidom.parseString(s).toprettyxml(indent=" ") - - with filename.open("wt") as fd: - fd.write(s) # type: ignore + with filename.open("wb") as fd: + fd.write(s) def parse_xml_file(filename: Path) -> ET.ElementTree: From e92aee012bbb0764d72d1cd23562750108b5a947 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 10 Apr 2022 15:07:15 +0200 Subject: [PATCH 033/143] Fix CI --- .github/workflows/test-devel.yml | 2 +- .github/workflows/test-devel_release.yml | 2 +- .github/workflows/test-stable.yml | 2 +- irctest/server_tests/channel_rename.py | 1 + make_workflows.py | 2 +- 5 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index fb936ea..f73176b 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -401,7 +401,7 @@ jobs: - name: Install dashboard dependencies run: |- python -m pip install --upgrade pip - pip install defusedxml + pip install defusedxml docutils -r requirements.txt - name: Generate dashboard run: |- shopt -s globstar diff --git a/.github/workflows/test-devel_release.yml b/.github/workflows/test-devel_release.yml index 3e0fc2e..8fa975f 100644 --- a/.github/workflows/test-devel_release.yml +++ b/.github/workflows/test-devel_release.yml @@ -86,7 +86,7 @@ jobs: - name: Install dashboard dependencies run: |- python -m pip install --upgrade pip - pip install defusedxml + pip install defusedxml docutils -r requirements.txt - name: Generate dashboard run: |- shopt -s globstar diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 4067f3b..c4a0468 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -444,7 +444,7 @@ jobs: - name: Install dashboard dependencies run: |- python -m pip install --upgrade pip - pip install defusedxml + pip install defusedxml docutils -r requirements.txt - name: Generate dashboard run: |- shopt -s globstar diff --git a/irctest/server_tests/channel_rename.py b/irctest/server_tests/channel_rename.py index 1255d1f..5bcdbfe 100644 --- a/irctest/server_tests/channel_rename.py +++ b/irctest/server_tests/channel_rename.py @@ -8,6 +8,7 @@ from irctest.numerics import ERR_CHANOPRIVSNEEDED RENAME_CAP = "draft/channel-rename" +@cases.mark_specifications("IRCv3") class ChannelRenameTestCase(cases.BaseServerTestCase): """Basic tests for channel-rename.""" diff --git a/make_workflows.py b/make_workflows.py index 657ae85..62ab72b 100644 --- a/make_workflows.py +++ b/make_workflows.py @@ -367,7 +367,7 @@ def generate_workflow(config: dict, version_flavor: VersionFlavor): "name": "Install dashboard dependencies", "run": script( "python -m pip install --upgrade pip", - "pip install defusedxml", + "pip install defusedxml docutils -r requirements.txt", ), }, { From 09c31f428a5e4b8a73ddd0f5dfe5f594a31f7b3e Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 10 Apr 2022 15:15:51 +0200 Subject: [PATCH 034/143] Format the index as columns when possible To avoid wasting space. --- irctest/dashboard/format.py | 1 + irctest/dashboard/style.css | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/irctest/dashboard/format.py b/irctest/dashboard/format.py index 5c88eda..615a243 100644 --- a/irctest/dashboard/format.py +++ b/irctest/dashboard/format.py @@ -277,6 +277,7 @@ def write_html_index(output_dir: Path, pages: List[Tuple[str, str]]) -> None: ET.SubElement(body, "h1").text = "irctest dashboard" dl = ET.SubElement(body, "dl") + dl.set("class", "module-index") for (module_name, file_name) in sorted(pages): module = importlib.import_module(module_name) diff --git a/irctest/dashboard/style.css b/irctest/dashboard/style.css index 1609da2..ed0b737 100644 --- a/irctest/dashboard/style.css +++ b/irctest/dashboard/style.css @@ -8,6 +8,10 @@ } } +dl.module-index { + column-width: 40em; /* Magic constant for 2 columns on average laptop/desktop */ +} + /* Only 1px solid border between cells */ table.test-matrix { border-spacing: 0; From d90264ca9f81c49c0190ebe45b057e7988e1d0d9 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 12 Apr 2022 18:33:02 +0200 Subject: [PATCH 035/143] dashboard: fix pagination --- irctest/dashboard/github_download.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/irctest/dashboard/github_download.py b/irctest/dashboard/github_download.py index 06ebaee..dd34b70 100644 --- a/irctest/dashboard/github_download.py +++ b/irctest/dashboard/github_download.py @@ -27,14 +27,15 @@ class Artifact: def iter_run_artifacts(repo: str, run_id: int) -> Iterator[Artifact]: request = urllib.request.Request( - f"https://api.github.com/repos/{repo}/actions/runs/{run_id}/artifacts", + f"https://api.github.com/repos/{repo}/actions/runs/{run_id}/artifacts" + "?per_page=100", headers={"Accept": "application/vnd.github.v3+json"}, ) response = urllib.request.urlopen(request) for artifact in json.load(response)["artifacts"]: - if not artifact["name"].startswith(("pytest_results_", "pytest results ")): + if not artifact["name"].startswith(("pytest-results_", "pytest results ")): continue if artifact["expired"]: continue From fc4e31e099de26f393ba1b40277dca9c4ada233b Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 12 Apr 2022 18:33:52 +0200 Subject: [PATCH 036/143] dashboard: Omit irrelevant tests from specific tables --- irctest/dashboard/format.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/irctest/dashboard/format.py b/irctest/dashboard/format.py index 615a243..be211e1 100644 --- a/irctest/dashboard/format.py +++ b/irctest/dashboard/format.py @@ -245,10 +245,39 @@ def write_html_pages( # used as columns jobs = list(sorted({r.job for r in results})) + job_categories = {} + for job in jobs: + is_client = any( + "client_tests" in result.module_name and result.job == job + for result in results + ) + is_server = any( + "server_tests" in result.module_name and result.job == job + for result in results + ) + assert is_client != is_server, (job, is_client, is_server) + if job.endswith(("-atheme", "-anope")): + assert is_server + job_categories[job] = "server-with-services" + elif is_server: + job_categories[job] = "server" # with or without services + else: + assert is_client + job_categories[job] = "client" + pages = [] - for (module_name, module_results) in results_by_module.items(): - root = build_module_html(jobs, module_results, module_name) + for (module_name, module_results) in sorted(results_by_module.items()): + # Filter out client jobs if this is a server test module, and vice versa + module_categories = { + job_categories[result.job] + for result in results + if result.module_name == module_name and not result.skipped + } + + module_jobs = [job for job in jobs if job_categories[job] in module_categories] + + root = build_module_html(module_jobs, module_results, module_name) file_name = f"{module_name}.xhtml" write_xml_file(output_dir / file_name, root) pages.append((module_name, file_name)) From 10b6f8d6da834c9293c90b6e95eaa329f69a92c8 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 12 Apr 2022 18:48:03 +0200 Subject: [PATCH 037/143] Remove useless 'OptionalityHelper'. --- irctest/cases.py | 58 +++++++++-------------- irctest/client_tests/sasl.py | 20 ++++---- irctest/client_tests/tls.py | 2 +- irctest/server_tests/account_tag.py | 6 +-- irctest/server_tests/away_notify.py | 2 +- irctest/server_tests/cap.py | 2 +- irctest/server_tests/extended_join.py | 4 +- irctest/server_tests/labeled_responses.py | 2 +- irctest/server_tests/message_tags.py | 2 +- irctest/server_tests/multiline.py | 2 +- irctest/server_tests/sasl.py | 16 +++---- irctest/server_tests/utf8.py | 2 +- irctest/server_tests/who.py | 6 +-- irctest/server_tests/whois.py | 10 ++-- 14 files changed, 59 insertions(+), 75 deletions(-) diff --git a/irctest/cases.py b/irctest/cases.py index 0a64c1a..e782e11 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -732,50 +732,38 @@ class BaseServerTestCase( raise ChannelJoinException(msg.command, msg.params) -_TSelf = TypeVar("_TSelf", bound="OptionalityHelper") +_TSelf = TypeVar("_TSelf", bound="_IrcTestCase") _TReturn = TypeVar("_TReturn") -class OptionalityHelper(Generic[TController]): - controller: TController - - def checkSaslSupport(self) -> None: - if self.controller.supported_sasl_mechanisms: - return - raise runner.NotImplementedByController("SASL") - - def checkMechanismSupport(self, mechanism: str) -> None: - if mechanism in self.controller.supported_sasl_mechanisms: - return - raise runner.OptionalSaslMechanismNotSupported(mechanism) - - @staticmethod - def skipUnlessHasMechanism( - mech: str, - ) -> Callable[[Callable[..., _TReturn]], Callable[..., _TReturn]]: - # Just a function returning a function that takes functions and - # returns functions, nothing to see here. - # If Python didn't have such an awful syntax for callables, it would be: - # str -> ((TSelf -> TReturn) -> (TSelf -> TReturn)) - def decorator(f: Callable[..., _TReturn]) -> Callable[..., _TReturn]: - @functools.wraps(f) - def newf(self: _TSelf, *args: Any, **kwargs: Any) -> _TReturn: - self.checkMechanismSupport(mech) - return f(self, *args, **kwargs) - - return newf - - return decorator - - @staticmethod - def skipUnlessHasSasl(f: Callable[..., _TReturn]) -> Callable[..., _TReturn]: +def skipUnlessHasMechanism( + mech: str, +) -> Callable[[Callable[..., _TReturn]], Callable[..., _TReturn]]: + # Just a function returning a function that takes functions and + # returns functions, nothing to see here. + # If Python didn't have such an awful syntax for callables, it would be: + # str -> ((TSelf -> TReturn) -> (TSelf -> TReturn)) + def decorator(f: Callable[..., _TReturn]) -> Callable[..., _TReturn]: @functools.wraps(f) def newf(self: _TSelf, *args: Any, **kwargs: Any) -> _TReturn: - self.checkSaslSupport() + if mech not in self.controller.supported_sasl_mechanisms: + raise runner.OptionalSaslMechanismNotSupported(mech) return f(self, *args, **kwargs) return newf + return decorator + + +def skipUnlessHasSasl(f: Callable[..., _TReturn]) -> Callable[..., _TReturn]: + @functools.wraps(f) + def newf(self: _TSelf, *args: Any, **kwargs: Any) -> _TReturn: + if not self.controller.supported_sasl_mechanisms: + raise runner.NotImplementedByController("SASL") + return f(self, *args, **kwargs) + + return newf + def mark_services(cls: TClass) -> TClass: cls.run_services = True diff --git a/irctest/client_tests/sasl.py b/irctest/client_tests/sasl.py index bf766f9..ca771b7 100644 --- a/irctest/client_tests/sasl.py +++ b/irctest/client_tests/sasl.py @@ -39,8 +39,8 @@ class IdentityHash: return self._data -class SaslTestCase(cases.BaseClientTestCase, cases.OptionalityHelper): - @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") +class SaslTestCase(cases.BaseClientTestCase): + @cases.skipUnlessHasMechanism("PLAIN") def testPlain(self): """Test PLAIN authentication with correct username/password.""" auth = authentication.Authentication( @@ -60,7 +60,7 @@ class SaslTestCase(cases.BaseClientTestCase, cases.OptionalityHelper): m = self.negotiateCapabilities(["sasl"], False) self.assertEqual(m, Message({}, None, "CAP", ["END"])) - @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") + @cases.skipUnlessHasMechanism("PLAIN") def testPlainNotAvailable(self): """`sasl=EXTERNAL` is advertized, whereas the client is configured to use PLAIN. @@ -90,7 +90,7 @@ class SaslTestCase(cases.BaseClientTestCase, cases.OptionalityHelper): self.assertMessageMatch(m, command="CAP") @pytest.mark.parametrize("pattern", ["barbaz", "éèà"]) - @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") + @cases.skipUnlessHasMechanism("PLAIN") def testPlainLarge(self, pattern): """Test the client splits large AUTHENTICATE messages whose payload is not a multiple of 400. @@ -119,7 +119,7 @@ class SaslTestCase(cases.BaseClientTestCase, cases.OptionalityHelper): m = self.negotiateCapabilities(["sasl"], False) self.assertEqual(m, Message({}, None, "CAP", ["END"])) - @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") + @cases.skipUnlessHasMechanism("PLAIN") @pytest.mark.parametrize("pattern", ["quux", "éè"]) def testPlainLargeMultiple(self, pattern): """Test the client splits large AUTHENTICATE messages whose payload @@ -150,7 +150,7 @@ class SaslTestCase(cases.BaseClientTestCase, cases.OptionalityHelper): self.assertEqual(m, Message({}, None, "CAP", ["END"])) @pytest.mark.skipif(ecdsa is None, reason="python3-ecdsa is not available") - @cases.OptionalityHelper.skipUnlessHasMechanism("ECDSA-NIST256P-CHALLENGE") + @cases.skipUnlessHasMechanism("ECDSA-NIST256P-CHALLENGE") def testEcdsa(self): """Test ECDSA authentication.""" auth = authentication.Authentication( @@ -184,7 +184,7 @@ class SaslTestCase(cases.BaseClientTestCase, cases.OptionalityHelper): m = self.negotiateCapabilities(["sasl"], False) self.assertEqual(m, Message({}, None, "CAP", ["END"])) - @cases.OptionalityHelper.skipUnlessHasMechanism("SCRAM-SHA-256") + @cases.skipUnlessHasMechanism("SCRAM-SHA-256") def testScram(self): """Test SCRAM-SHA-256 authentication.""" auth = authentication.Authentication( @@ -226,7 +226,7 @@ class SaslTestCase(cases.BaseClientTestCase, cases.OptionalityHelper): self.assertEqual(m.command, "AUTHENTICATE", m) self.assertEqual(m.params, ["+"], m) - @cases.OptionalityHelper.skipUnlessHasMechanism("SCRAM-SHA-256") + @cases.skipUnlessHasMechanism("SCRAM-SHA-256") def testScramBadPassword(self): """Test SCRAM-SHA-256 authentication with a bad password.""" auth = authentication.Authentication( @@ -261,8 +261,8 @@ class SaslTestCase(cases.BaseClientTestCase, cases.OptionalityHelper): authenticator.response(msg) -class Irc302SaslTestCase(cases.BaseClientTestCase, cases.OptionalityHelper): - @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") +class Irc302SaslTestCase(cases.BaseClientTestCase): + @cases.skipUnlessHasMechanism("PLAIN") def testPlainNotAvailable(self): """Test the client does not try to authenticate using a mechanism the server does not advertise. diff --git a/irctest/client_tests/tls.py b/irctest/client_tests/tls.py index 285bd36..7172a96 100644 --- a/irctest/client_tests/tls.py +++ b/irctest/client_tests/tls.py @@ -140,7 +140,7 @@ class TlsTestCase(cases.BaseClientTestCase): self.getMessage() -class StsTestCase(cases.BaseClientTestCase, cases.OptionalityHelper): +class StsTestCase(cases.BaseClientTestCase): def setUp(self): super().setUp() self.insecure_server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) diff --git a/irctest/server_tests/account_tag.py b/irctest/server_tests/account_tag.py index f5d2e87..6a19755 100644 --- a/irctest/server_tests/account_tag.py +++ b/irctest/server_tests/account_tag.py @@ -6,7 +6,7 @@ from irctest import cases @cases.mark_services -class AccountTagTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): +class AccountTagTestCase(cases.BaseServerTestCase): def connectRegisteredClient(self, nick): self.addClient() self.sendLine(2, "CAP LS 302") @@ -40,7 +40,7 @@ class AccountTagTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): self.skipToWelcome(2) @cases.mark_capabilities("account-tag") - @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") + @cases.skipUnlessHasMechanism("PLAIN") def testPrivmsg(self): self.connectClient("foo", capabilities=["account-tag"], skip_if_cap_nak=True) self.getMessages(1) @@ -54,7 +54,7 @@ class AccountTagTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): ) @cases.mark_capabilities("account-tag") - @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") + @cases.skipUnlessHasMechanism("PLAIN") def testInvite(self): self.connectClient("foo", capabilities=["account-tag"], skip_if_cap_nak=True) self.getMessages(1) diff --git a/irctest/server_tests/away_notify.py b/irctest/server_tests/away_notify.py index 283c007..3e9034e 100644 --- a/irctest/server_tests/away_notify.py +++ b/irctest/server_tests/away_notify.py @@ -5,7 +5,7 @@ from irctest import cases -class AwayNotifyTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): +class AwayNotifyTestCase(cases.BaseServerTestCase): @cases.mark_capabilities("away-notify") def testAwayNotify(self): """Basic away-notify test.""" diff --git a/irctest/server_tests/cap.py b/irctest/server_tests/cap.py index fac6e68..9b74447 100644 --- a/irctest/server_tests/cap.py +++ b/irctest/server_tests/cap.py @@ -8,7 +8,7 @@ from irctest.patma import ANYSTR from irctest.runner import CapabilityNotSupported, ImplementationChoice -class CapTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): +class CapTestCase(cases.BaseServerTestCase): @cases.mark_specifications("IRCv3") def testNoReq(self): """Test the server handles gracefully clients which do not send diff --git a/irctest/server_tests/extended_join.py b/irctest/server_tests/extended_join.py index 13dc975..2438f87 100644 --- a/irctest/server_tests/extended_join.py +++ b/irctest/server_tests/extended_join.py @@ -6,7 +6,7 @@ from irctest import cases @cases.mark_services -class MetadataTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): +class MetadataTestCase(cases.BaseServerTestCase): def connectRegisteredClient(self, nick): self.addClient() self.sendLine(2, "CAP LS 302") @@ -50,7 +50,7 @@ class MetadataTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): ) @cases.mark_capabilities("extended-join") - @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") + @cases.skipUnlessHasMechanism("PLAIN") def testLoggedIn(self): self.connectClient("foo", capabilities=["extended-join"], skip_if_cap_nak=True) self.joinChannel(1, "#chan") diff --git a/irctest/server_tests/labeled_responses.py b/irctest/server_tests/labeled_responses.py index 03f9dfd..76aa324 100644 --- a/irctest/server_tests/labeled_responses.py +++ b/irctest/server_tests/labeled_responses.py @@ -14,7 +14,7 @@ from irctest.numerics import ERR_UNKNOWNCOMMAND from irctest.patma import ANYDICT, ANYOPTSTR, NotStrRe, RemainingKeys, StrRe -class LabeledResponsesTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): +class LabeledResponsesTestCase(cases.BaseServerTestCase): @cases.mark_capabilities("echo-message", "batch", "labeled-response") def testLabeledPrivmsgResponsesToMultipleClients(self): self.connectClient( diff --git a/irctest/server_tests/message_tags.py b/irctest/server_tests/message_tags.py index e19a2d9..b5e8071 100644 --- a/irctest/server_tests/message_tags.py +++ b/irctest/server_tests/message_tags.py @@ -10,7 +10,7 @@ from irctest.numerics import ERR_INPUTTOOLONG from irctest.patma import ANYDICT, ANYSTR, StrRe -class MessageTagsTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): +class MessageTagsTestCase(cases.BaseServerTestCase): @pytest.mark.arbitrary_client_tags @cases.mark_capabilities("message-tags") def testBasic(self): diff --git a/irctest/server_tests/multiline.py b/irctest/server_tests/multiline.py index ac4b7d5..8c397ea 100644 --- a/irctest/server_tests/multiline.py +++ b/irctest/server_tests/multiline.py @@ -12,7 +12,7 @@ CONCAT_TAG = "draft/multiline-concat" base_caps = ["message-tags", "batch", "echo-message", "server-time", "labeled-response"] -class MultilineTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): +class MultilineTestCase(cases.BaseServerTestCase): @cases.mark_capabilities("draft/multiline") def testBasic(self): self.connectClient( diff --git a/irctest/server_tests/sasl.py b/irctest/server_tests/sasl.py index f289ca5..d680491 100644 --- a/irctest/server_tests/sasl.py +++ b/irctest/server_tests/sasl.py @@ -12,9 +12,9 @@ class RegistrationTestCase(cases.BaseServerTestCase): @cases.mark_services -class SaslTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): +class SaslTestCase(cases.BaseServerTestCase): @cases.mark_specifications("IRCv3") - @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") + @cases.skipUnlessHasMechanism("PLAIN") def testPlain(self): """PLAIN authentication with correct username/password.""" self.controller.registerUser(self, "foo", "sesame") @@ -54,7 +54,7 @@ class SaslTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): ) @cases.mark_specifications("IRCv3") - @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") + @cases.skipUnlessHasMechanism("PLAIN") def testPlainNonAscii(self): password = "é" * 100 authstring = base64.b64encode( @@ -82,7 +82,7 @@ class SaslTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): ) @cases.mark_specifications("IRCv3") - @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") + @cases.skipUnlessHasMechanism("PLAIN") def testPlainNoAuthzid(self): """“message = [authzid] UTF8NUL authcid UTF8NUL passwd @@ -170,7 +170,7 @@ class SaslTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): ) @cases.mark_specifications("IRCv3") - @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") + @cases.skipUnlessHasMechanism("PLAIN") def testPlainLarge(self): """Test the client splits large AUTHENTICATE messages whose payload is not a multiple of 400. @@ -232,7 +232,7 @@ class SaslTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): # message's length too big for it to be valid. @cases.mark_specifications("IRCv3") - @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") + @cases.skipUnlessHasMechanism("PLAIN") def testPlainLargeEquals400(self): """Test the client splits large AUTHENTICATE messages whose payload is not a multiple of 400. @@ -277,7 +277,7 @@ class SaslTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): # message's length too big for it to be valid. @cases.mark_specifications("IRCv3") - @cases.OptionalityHelper.skipUnlessHasMechanism("SCRAM-SHA-256") + @cases.skipUnlessHasMechanism("SCRAM-SHA-256") def testScramSha256Success(self): self.controller.registerUser(self, "Scramtest", "sesame") @@ -333,7 +333,7 @@ class SaslTestCase(cases.BaseServerTestCase, cases.OptionalityHelper): self.confirmSuccessfulAuth() @cases.mark_specifications("IRCv3") - @cases.OptionalityHelper.skipUnlessHasMechanism("SCRAM-SHA-256") + @cases.skipUnlessHasMechanism("SCRAM-SHA-256") def testScramSha256Failure(self): self.controller.registerUser(self, "Scramtest", "sesame") diff --git a/irctest/server_tests/utf8.py b/irctest/server_tests/utf8.py index 32ff563..ccd8156 100644 --- a/irctest/server_tests/utf8.py +++ b/irctest/server_tests/utf8.py @@ -9,7 +9,7 @@ from irctest import cases from irctest.patma import ANYSTR -class Utf8TestCase(cases.BaseServerTestCase, cases.OptionalityHelper): +class Utf8TestCase(cases.BaseServerTestCase): @cases.mark_specifications("Ergo") def testUtf8Validation(self): self.connectClient( diff --git a/irctest/server_tests/who.py b/irctest/server_tests/who.py index e320cb1..8510e69 100644 --- a/irctest/server_tests/who.py +++ b/irctest/server_tests/who.py @@ -84,7 +84,7 @@ class BaseWhoTestCase: ) -class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase, cases.OptionalityHelper): +class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): @cases.mark_specifications("Modern") def testWhoStar(self): self._init() @@ -422,9 +422,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase, cases.OptionalityHe @cases.mark_services -class WhoServicesTestCase( - BaseWhoTestCase, cases.BaseServerTestCase, cases.OptionalityHelper -): +class WhoServicesTestCase(BaseWhoTestCase, cases.BaseServerTestCase): @cases.mark_specifications("IRCv3") @cases.mark_isupport("WHOX") def testWhoxAccount(self): diff --git a/irctest/server_tests/whois.py b/irctest/server_tests/whois.py index a23ee66..ec432a5 100644 --- a/irctest/server_tests/whois.py +++ b/irctest/server_tests/whois.py @@ -164,7 +164,7 @@ class _WhoisTestMixin(cases.BaseServerTestCase): ) -class WhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase, cases.OptionalityHelper): +class WhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase): @pytest.mark.parametrize( "server", ["", "My.Little.Server", "coolNick"], @@ -210,11 +210,9 @@ class WhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase, cases.Optionality @cases.mark_services -class ServicesWhoisTestCase( - _WhoisTestMixin, cases.BaseServerTestCase, cases.OptionalityHelper -): +class ServicesWhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase): @pytest.mark.parametrize("oper", [False, True], ids=["normal", "oper"]) - @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") + @cases.skipUnlessHasMechanism("PLAIN") @cases.mark_specifications("Modern") def testWhoisNumerics(self, oper): """Tests all numerics are in the exhaustive list defined in the Modern spec, @@ -297,7 +295,7 @@ class ServicesWhoisTestCase( "RPL_WHOISCHANNELS should be sent for a non-invisible nick", ) - @cases.OptionalityHelper.skipUnlessHasMechanism("PLAIN") + @cases.skipUnlessHasMechanism("PLAIN") @cases.mark_specifications("ircdocs") def testWhoisAccount(self): """Test numeric 330, RPL_WHOISACCOUNT. From 2bc68a22085104dc4a96f3a456ee91041d742308 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Tue, 12 Apr 2022 22:36:28 +0200 Subject: [PATCH 038/143] Use xfail instead of deselection for known failures (#155) --- Makefile | 112 +----------------- irctest/cases.py | 27 +++++ irctest/client_tests/sasl.py | 1 + irctest/controllers/anope_services.py | 2 + irctest/controllers/atheme_services.py | 2 + irctest/controllers/irc2.py | 8 +- irctest/controllers/ircu2.py | 2 +- irctest/controllers/plexus4.py | 2 +- irctest/dashboard/format.py | 5 + irctest/dashboard/style.css | 3 + irctest/server_tests/account_tag.py | 3 + irctest/server_tests/bot_mode.py | 8 ++ irctest/server_tests/buffering.py | 10 ++ irctest/server_tests/cap.py | 8 ++ irctest/server_tests/chathistory.py | 31 ++++- irctest/server_tests/chmodes/key.py | 15 +++ .../server_tests/connection_registration.py | 8 ++ irctest/server_tests/help.py | 28 +++++ irctest/server_tests/info.py | 3 + irctest/server_tests/invite.py | 8 ++ irctest/server_tests/kick.py | 4 + irctest/server_tests/list.py | 2 + irctest/server_tests/lusers.py | 20 ++++ irctest/server_tests/messages.py | 12 ++ irctest/server_tests/quit.py | 1 + irctest/server_tests/regressions.py | 9 +- irctest/server_tests/sasl.py | 14 +++ irctest/server_tests/statusmsg.py | 10 ++ irctest/server_tests/wallops.py | 3 + irctest/server_tests/who.py | 21 ++++ irctest/server_tests/whois.py | 3 + irctest/server_tests/whowas.py | 37 ++++++ 32 files changed, 308 insertions(+), 114 deletions(-) diff --git a/Makefile b/Makefile index a2d7dd2..e933806 100644 --- a/Makefile +++ b/Makefile @@ -7,102 +7,47 @@ PYTEST_ARGS ?= # Will be appended at the end of the -k argument to pytest EXTRA_SELECTORS ?= -# testPlainLarge fails because it doesn't handle split AUTHENTICATE (reported on IRC) -ANOPE_SELECTORS := \ - and not testPlainLarge - -# buffering tests cannot pass because of issues with UTF-8 handling: https://github.com/DALnet/bahamut/issues/196 -# mask tests in test_who.py fail because they are not implemented. -# some HelpTestCase::*[HELP] tests fail because Bahamut forwards /HELP to HelpServ (but not /HELPOP) -# testWhowasMultiTarget fails because Bahamut returns the results in query order instead of chronological order BAHAMUT_SELECTORS := \ not Ergo \ and not deprecated \ and not strict \ and not IRCv3 \ - and not buffering \ - and not (testWho and not whois and mask) \ - and not testWhoStar \ - and (not HelpTestCase or HELPOP) \ - and not testWhowasMultiTarget \ $(EXTRA_SELECTORS) -# testQuitErrors is very flaky -# AccountTagTestCase.testInvite fails because https://github.com/solanum-ircd/solanum/issues/166 -# testKickDefaultComment fails because it uses the nick of the kickee rather than the kicker. -# testWhoisNumerics[oper] fails because charybdis uses RPL_WHOISSPECIAL instead of RPL_WHOISOPERATOR -# testWhowasNoSuchNick fails because of a typo (solved in https://github.com/solanum-ircd/solanum/commit/08b7b6bd7e60a760ad47b58cbe8075b45d66166f) CHARYBDIS_SELECTORS := \ not Ergo \ and not deprecated \ and not strict \ - and not testQuitErrors \ - and not testKickDefaultComment \ - and not (AccountTagTestCase and testInvite) \ - and not (testWhoisNumerics and oper) \ - and not testWhowasNoSuchNick \ $(EXTRA_SELECTORS) -# testInfoNosuchserver does not apply to Ergo: Ergo ignores the optional argument ERGO_SELECTORS := \ not deprecated \ - and not testInfoNosuchserver \ $(EXTRA_SELECTORS) -# testInviteUnopped is the only strict test that Hybrid fails HYBRID_SELECTORS := \ not Ergo \ - and not testInviteUnopped \ and not deprecated \ $(EXTRA_SELECTORS) -# testBotPrivateMessage and testBotChannelMessage fail because https://github.com/inspircd/inspircd/pull/1910 is not released yet -# WHOWAS tests fail because https://github.com/inspircd/inspircd/pull/1967 and https://github.com/inspircd/inspircd/pull/1968 are not released yet INSPIRCD_SELECTORS := \ not Ergo \ and not deprecated \ and not strict \ - and not testNoticeNonexistentChannel \ - and not testBotPrivateMessage and not testBotChannelMessage \ - and not whowas \ $(EXTRA_SELECTORS) -# buffering tests fail because ircu2 discards the whole buffer on long lines (TODO: refine how we exclude these tests) -# testQuit and testQuitErrors fail because ircu2 does not send ERROR or QUIT -# lusers "full" tests fail because they depend on Modern behavior, not just RFC2812 -# statusmsg tests fail because STATUSMSG is present in ISUPPORT, but it not actually supported as PRIVMSG target -# testKeyValidation[empty] fails because ircu2 returns ERR_NEEDMOREPARAMS on empty keys: https://github.com/UndernetIRC/ircu2/issues/13 -# testKickDefaultComment fails because it uses the nick of the kickee rather than the kicker. -# testEmptyRealname fails because it uses a default value instead of ERR_NEEDMOREPARAMS. # HelpTestCase fails because it returns NOTICEs instead of numerics -# testWhowasCountZero fails: https://github.com/UndernetIRC/ircu2/pull/19 IRCU2_SELECTORS := \ not Ergo \ and not deprecated \ and not strict \ - and not buffering \ - and not testQuit \ - and not (lusers and full) \ - and not statusmsg \ - and not (testKeyValidation and empty) \ - and not testKickDefaultComment \ - and not testEmptyRealname \ - and not HelpTestCase \ - and not testWhowasCountZero \ $(EXTRA_SELECTORS) # same justification as ircu2 -# lusers "unregistered" tests fail because Nefarious doesn't seem to distinguish unregistered users from normal ones +# lusers "unregistered" tests fail because NEFARIOUS_SELECTORS := \ not Ergo \ and not deprecated \ and not strict \ - and not buffering \ - and not testQuit \ - and not (lusers and unregistered) \ - and not statusmsg \ - and not (testKeyValidation and empty) \ - and not testEmptyRealname \ $(EXTRA_SELECTORS) # same justification as ircu2 @@ -110,24 +55,12 @@ SNIRCD_SELECTORS := \ not Ergo \ and not deprecated \ and not strict \ - and not buffering \ - and not testQuit \ - and not (lusers and full) \ - and not statusmsg \ $(EXTRA_SELECTORS) -# testListEmpty and testListOne fails because irc2 deprecated LIST -# testKickDefaultComment fails because it uses the nick of the kickee rather than the kicker. -# testWallopsPrivileges fails because it ignores the command instead of replying ERR_UNKNOWNCOMMAND -# HelpTestCase fails because it returns NOTICEs instead of numerics IRC2_SELECTORS := \ not Ergo \ and not deprecated \ and not strict \ - and not testListEmpty and not testListOne \ - and not testKickDefaultComment \ - and not testWallopsPrivileges \ - and not HelpTestCase \ $(EXTRA_SELECTORS) MAMMON_SELECTORS := \ @@ -136,28 +69,14 @@ MAMMON_SELECTORS := \ and not strict \ $(EXTRA_SELECTORS) -# testKeyValidation[spaces] and testKeyValidation[empty] fail because ngIRCd does not validate them https://github.com/ngircd/ngircd/issues/290 -# testStarNick: wat -# testEmptyRealname fails because it uses a default value instead of ERR_NEEDMOREPARAMS. -# chathistory tests fail because they need nicks longer than 9 chars -# HelpTestCase::*[HELP] fails because it returns NOTICEs instead of numerics NGIRCD_SELECTORS := \ not Ergo \ and not deprecated \ and not strict \ - and not (testKeyValidation and (spaces or empty)) \ - and not testStarNick \ - and not testEmptyRealname \ - and not chathistory \ - and (not HelpTestCase or HELPOP) \ $(EXTRA_SELECTORS) -# testInviteUnopped is the only strict test that Plexus4 fails -# testInviteInviteOnly fails because Plexus4 allows non-op to invite if (and only if) the channel is not invite-only PLEXUS4_SELECTORS := \ not Ergo \ - and not testInviteUnopped \ - and not testInviteInviteOnly \ and not deprecated \ $(EXTRA_SELECTORS) @@ -168,46 +87,27 @@ LIMNORIA_SELECTORS := \ (foo or not foo) \ $(EXTRA_SELECTORS) -# testQuitErrors is too flaky for CI -# testKickDefaultComment fails because solanum uses the nick of the kickee rather than the kicker. SOLANUM_SELECTORS := \ not Ergo \ and not deprecated \ and not strict \ - and not testQuitErrors \ - and not testKickDefaultComment \ $(EXTRA_SELECTORS) +# Same as Limnoria SOPEL_SELECTORS := \ - not testPlainNotAvailable \ + (foo or not foo) \ $(EXTRA_SELECTORS) -# testNoticeNonexistentChannel fails: https://bugs.unrealircd.org/view.php?id=5949 -# regressions::testTagCap fails: https://bugs.unrealircd.org/view.php?id=5948 -# messages::testLineTooLong fails: https://bugs.unrealircd.org/view.php?id=5947 -# testCapRemovalByClient and testNakWhole fail pending https://github.com/unrealircd/unrealircd/pull/148 # Tests marked with arbitrary_client_tags can't pass because Unreal whitelists which tags it relays # Tests marked with react_tag can't pass because Unreal blocks +draft/react https://github.com/unrealircd/unrealircd/pull/149 # Tests marked with private_chathistory can't pass because Unreal does not implement CHATHISTORY for DMs -# testChathistory[BETWEEN] fails: https://bugs.unrealircd.org/view.php?id=5952 -# testChathistory[AROUND] fails: https://bugs.unrealircd.org/view.php?id=5953 -# testWhoAllOpers fails because Unreal skips results when the mask is too broad -# HELP and HELPOP tests fail because Unreal uses custom numerics https://github.com/unrealircd/unrealircd/pull/184 UNREALIRCD_SELECTORS := \ not Ergo \ and not deprecated \ and not strict \ - and not testNoticeNonexistentChannel \ - and not (regressions.py and testTagCap) \ - and not (messages.py and testLineTooLong) \ - and not (cap.py and (testCapRemovalByClient or testNakWhole)) \ - and not (account_tag.py and testInvite) \ and not arbitrary_client_tags \ and not react_tag \ and not private_chathistory \ - and not (testChathistory and (between or around)) \ - and not testWhoAllOpers \ - and not HelpTestCase \ $(EXTRA_SELECTORS) .PHONY: all flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sopel solanum unrealircd @@ -238,7 +138,7 @@ bahamut-anope: --services-controller=irctest.controllers.anope_services \ -m 'services' \ -n 10 \ - -k '$(BAHAMUT_SELECTORS) $(ANOPE_SELECTORS)' + -k '$(BAHAMUT_SELECTORS)' charybdis: $(PYTEST) $(PYTEST_ARGS) \ @@ -275,7 +175,7 @@ inspircd-anope: --controller=irctest.controllers.inspircd \ --services-controller=irctest.controllers.anope_services \ -m 'services' \ - -k '$(INSPIRCD_SELECTORS) $(ANOPE_SELECTORS)' + -k '$(INSPIRCD_SELECTORS)' ircu2: $(PYTEST) $(PYTEST_ARGS) \ @@ -373,4 +273,4 @@ unrealircd-anope: --controller=irctest.controllers.unrealircd \ --services-controller=irctest.controllers.anope_services \ -m 'services' \ - -k '$(UNREALIRCD_SELECTORS) $(ANOPE_SELECTORS)' + -k '$(UNREALIRCD_SELECTORS)' diff --git a/irctest/cases.py b/irctest/cases.py index e782e11..cc15916 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -765,6 +765,33 @@ def skipUnlessHasSasl(f: Callable[..., _TReturn]) -> Callable[..., _TReturn]: return newf +def xfailIf( + condition: Callable[..., bool], reason: str +) -> Callable[[Callable[..., _TReturn]], Callable[..., _TReturn]]: + # Works about the same as skipUnlessHasMechanism + def decorator(f: Callable[..., _TReturn]) -> Callable[..., _TReturn]: + @functools.wraps(f) + def newf(self: _TSelf, *args: Any, **kwargs: Any) -> _TReturn: + if condition(self): + try: + return f(self, *args, **kwargs) + except Exception: + pytest.xfail(reason) + assert False # make mypy happy + else: + return f(self, *args, **kwargs) + + return newf + + return decorator + + +def xfailIfSoftware( + names: List[str], reason: str +) -> Callable[[Callable[..., _TReturn]], Callable[..., _TReturn]]: + return xfailIf(lambda testcase: testcase.controller.software_name in names, reason) + + def mark_services(cls: TClass) -> TClass: cls.run_services = True return pytest.mark.services(cls) # type: ignore diff --git a/irctest/client_tests/sasl.py b/irctest/client_tests/sasl.py index ca771b7..44f5e76 100644 --- a/irctest/client_tests/sasl.py +++ b/irctest/client_tests/sasl.py @@ -61,6 +61,7 @@ class SaslTestCase(cases.BaseClientTestCase): self.assertEqual(m, Message({}, None, "CAP", ["END"])) @cases.skipUnlessHasMechanism("PLAIN") + @cases.xfailIfSoftware(["Sopel"], "Sopel requests SASL PLAIN even if not available") def testPlainNotAvailable(self): """`sasl=EXTERNAL` is advertized, whereas the client is configured to use PLAIN. diff --git a/irctest/controllers/anope_services.py b/irctest/controllers/anope_services.py index f620dd9..e25e88d 100644 --- a/irctest/controllers/anope_services.py +++ b/irctest/controllers/anope_services.py @@ -73,6 +73,8 @@ module {{ name = "ns_cert" }} class AnopeController(BaseServicesController, DirectoryBasedController): """Collaborator for server controllers that rely on Anope""" + software_name = "Anope" + def run(self, protocol: str, server_hostname: str, server_port: int) -> None: self.create_config() diff --git a/irctest/controllers/atheme_services.py b/irctest/controllers/atheme_services.py index 8994677..485d6d7 100644 --- a/irctest/controllers/atheme_services.py +++ b/irctest/controllers/atheme_services.py @@ -56,6 +56,8 @@ saslserv {{ class AthemeController(BaseServicesController, DirectoryBasedController): """Mixin for server controllers that rely on Atheme""" + software_name = "Atheme" + def run(self, protocol: str, server_hostname: str, server_port: int) -> None: self.create_config() diff --git a/irctest/controllers/irc2.py b/irctest/controllers/irc2.py index 56defc8..0281fb6 100644 --- a/irctest/controllers/irc2.py +++ b/irctest/controllers/irc2.py @@ -29,8 +29,8 @@ O:*:operpassword:operuser:::: """ -class Ircu2Controller(BaseServerController, DirectoryBasedController): - binary_name: str +class Irc2Controller(BaseServerController, DirectoryBasedController): + software_name = "irc2" services_protocol: str supports_sts = False @@ -89,5 +89,5 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController): ) -def get_irctest_controller_class() -> Type[Ircu2Controller]: - return Ircu2Controller +def get_irctest_controller_class() -> Type[Irc2Controller]: + return Irc2Controller diff --git a/irctest/controllers/ircu2.py b/irctest/controllers/ircu2.py index 9b0d4bd..6bf9916 100644 --- a/irctest/controllers/ircu2.py +++ b/irctest/controllers/ircu2.py @@ -51,7 +51,7 @@ features {{ class Ircu2Controller(BaseServerController, DirectoryBasedController): - software_name = "Ircu2" + software_name = "ircu2" supports_sts = False extban_mute_char = None diff --git a/irctest/controllers/plexus4.py b/irctest/controllers/plexus4.py index 395b4e4..a968e56 100644 --- a/irctest/controllers/plexus4.py +++ b/irctest/controllers/plexus4.py @@ -74,7 +74,7 @@ operator {{ class Plexus4Controller(BaseHybridController): - software_name = "Hybrid" + software_name = "Plexus4" binary_name = "ircd" services_protocol = "plexus" diff --git a/irctest/dashboard/format.py b/irctest/dashboard/format.py index be211e1..721dc04 100644 --- a/irctest/dashboard/format.py +++ b/irctest/dashboard/format.py @@ -208,6 +208,9 @@ def build_module_html( cell.set("class", "skipped") if result.type == "pytest.skip": text = "s" + elif result.type == "pytest.xfail": + text = "X" + cell.set("class", "expected-failure") else: text = result.type elif result.success: @@ -231,6 +234,8 @@ def build_module_html( a.text = text or "?" else: cell.text = text or "?" + if result.message: + cell.set("title", result.message) return root diff --git a/irctest/dashboard/style.css b/irctest/dashboard/style.css index ed0b737..628fd8e 100644 --- a/irctest/dashboard/style.css +++ b/irctest/dashboard/style.css @@ -46,6 +46,9 @@ table.test-matrix .skipped { table.test-matrix .failure { background-color: red; } +table.test-matrix .expected-failure { + background-color: orange; +} /* Rotate headers, thanks to https://css-tricks.com/rotated-table-column-headers/ */ th.job-name { diff --git a/irctest/server_tests/account_tag.py b/irctest/server_tests/account_tag.py index 6a19755..45cd942 100644 --- a/irctest/server_tests/account_tag.py +++ b/irctest/server_tests/account_tag.py @@ -55,6 +55,9 @@ class AccountTagTestCase(cases.BaseServerTestCase): @cases.mark_capabilities("account-tag") @cases.skipUnlessHasMechanism("PLAIN") + @cases.xfailIfSoftware( + ["Charybdis"], "https://github.com/solanum-ircd/solanum/issues/166" + ) def testInvite(self): self.connectClient("foo", capabilities=["account-tag"], skip_if_cap_nak=True) self.getMessages(1) diff --git a/irctest/server_tests/bot_mode.py b/irctest/server_tests/bot_mode.py index 224c77e..1b9cf7f 100644 --- a/irctest/server_tests/bot_mode.py +++ b/irctest/server_tests/bot_mode.py @@ -67,6 +67,10 @@ class BotModeTestCase(cases.BaseServerTestCase): message, command=RPL_WHOISBOT, params=["usernick", "botnick", ANYSTR] ) + @cases.xfailIfSoftware( + ["InspIRCd"], + "Uses only vendor tags for now: https://github.com/inspircd/inspircd/pull/1910", + ) def testBotPrivateMessage(self): self._initBot() @@ -84,6 +88,10 @@ class BotModeTestCase(cases.BaseServerTestCase): tags={"draft/bot": None, **ANYDICT}, ) + @cases.xfailIfSoftware( + ["InspIRCd"], + "Uses only vendor tags for now: https://github.com/inspircd/inspircd/pull/1910", + ) def testBotChannelMessage(self): self._initBot() diff --git a/irctest/server_tests/buffering.py b/irctest/server_tests/buffering.py index 4adf01d..53e9f9a 100644 --- a/irctest/server_tests/buffering.py +++ b/irctest/server_tests/buffering.py @@ -32,6 +32,16 @@ def _sendBytePerByte(self, line): class BufferingTestCase(cases.BaseServerTestCase): + @cases.xfailIfSoftware( + ["Bahamut"], + "cannot pass because of issues with UTF-8 handling: " + "https://github.com/DALnet/bahamut/issues/196", + ) + @cases.xfailIfSoftware( + ["ircu2", "Nefarious", "snircd"], + "ircu2 discards the whole buffer on long lines " + "(TODO: refine how we exclude these tests)", + ) @pytest.mark.parametrize( "sender_function,colon", [ diff --git a/irctest/server_tests/cap.py b/irctest/server_tests/cap.py index 9b74447..c079078 100644 --- a/irctest/server_tests/cap.py +++ b/irctest/server_tests/cap.py @@ -78,6 +78,10 @@ class CapTestCase(cases.BaseServerTestCase): ) @cases.mark_specifications("IRCv3") + @cases.xfailIfSoftware( + ["UnrealIRCd"], + "UnrealIRCd sends a trailing space on CAP NAK: https://github.com/unrealircd/unrealircd/pull/148", + ) def testNakWhole(self): """“The capability identifier set must be accepted as a whole, or rejected entirely.” @@ -125,6 +129,10 @@ class CapTestCase(cases.BaseServerTestCase): ) @cases.mark_specifications("IRCv3") + @cases.xfailIfSoftware( + ["UnrealIRCd"], + "UnrealIRCd sends a trailing space on CAP NAK: https://github.com/unrealircd/unrealircd/pull/148", + ) def testCapRemovalByClient(self): """Test CAP LIST and removal of caps via CAP REQ :-tagname.""" cap1 = "echo-message" diff --git a/irctest/server_tests/chathistory.py b/irctest/server_tests/chathistory.py index a264ae5..53b4d16 100644 --- a/irctest/server_tests/chathistory.py +++ b/irctest/server_tests/chathistory.py @@ -2,12 +2,13 @@ `IRCv3 draft chathistory `_ """ +import functools import secrets import time import pytest -from irctest import cases +from irctest import cases, runner from irctest.irc_utils.junkdrawer import random_name from irctest.patma import ANYSTR @@ -42,6 +43,16 @@ def validate_chathistory_batch(msgs): return result +def skip_ngircd(f): + @functools.wraps(f) + def newf(self, *args, **kwargs): + if self.controller.software_name == "ngIRCd": + raise runner.NotImplementedByController("nicks longer 9 characters") + return f(self, *args, **kwargs) + + return newf + + @cases.mark_specifications("IRCv3") @cases.mark_services class ChathistoryTestCase(cases.BaseServerTestCase): @@ -49,6 +60,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase): def config() -> cases.TestCaseControllerConfig: return cases.TestCaseControllerConfig(chathistory=True) + @skip_ngircd def testInvalidTargets(self): bar, pw = random_name("bar"), random_name("pw") self.controller.registerUser(self, bar, pw) @@ -94,6 +106,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase): ) @pytest.mark.private_chathistory + @skip_ngircd def testMessagesToSelf(self): bar, pw = random_name("bar"), random_name("pw") self.controller.registerUser(self, bar, pw) @@ -166,7 +179,19 @@ class ChathistoryTestCase(cases.BaseServerTestCase): self.assertEqual(len(set(msg.time for msg in echo_messages)), num_messages) @pytest.mark.parametrize("subcommand", SUBCOMMANDS) + @skip_ngircd def testChathistory(self, subcommand): + if subcommand == "BETWEEN" and self.controller.software_name == "UnrealIRCd": + pytest.xfail( + "CHATHISTORY BETWEEN does not apply bounds correct " + "https://bugs.unrealircd.org/view.php?id=5952" + ) + if subcommand == "AROUND" and self.controller.software_name == "UnrealIRCd": + pytest.xfail( + "CHATHISTORY AROUND excludes 'central' messages " + "https://bugs.unrealircd.org/view.php?id=5953" + ) + self.connectClient( "bar", capabilities=[ @@ -198,6 +223,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase): self.validate_chathistory(subcommand, echo_messages, 1, chname) @pytest.mark.parametrize("subcommand", SUBCOMMANDS) + @skip_ngircd def testChathistoryEventPlayback(self, subcommand): self.connectClient( "bar", @@ -231,6 +257,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase): @pytest.mark.parametrize("subcommand", SUBCOMMANDS) @pytest.mark.private_chathistory + @skip_ngircd def testChathistoryDMs(self, subcommand): c1 = "foo" + secrets.token_hex(12) c2 = "bar" + secrets.token_hex(12) @@ -553,6 +580,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase): self.assertIn(echo_messages[7], result) @pytest.mark.arbitrary_client_tags + @skip_ngircd def testChathistoryTagmsg(self): c1 = "foo" + secrets.token_hex(12) c2 = "bar" + secrets.token_hex(12) @@ -651,6 +679,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase): @pytest.mark.arbitrary_client_tags @pytest.mark.private_chathistory + @skip_ngircd def testChathistoryDMClientOnlyTags(self): # regression test for Ergo #1411 c1 = "foo" + secrets.token_hex(12) diff --git a/irctest/server_tests/chmodes/key.py b/irctest/server_tests/chmodes/key.py index 6878c90..1f8773a 100644 --- a/irctest/server_tests/chmodes/key.py +++ b/irctest/server_tests/chmodes/key.py @@ -64,6 +64,21 @@ class KeyTestCase(cases.BaseServerTestCase): -- https://modern.ircdocs.horse/#key-channel-mode -- https://github.com/ircdocs/modern-irc/pull/111 """ + if key == "" and self.controller.software_name in ( + "ircu2", + "Nefarious", + "snircd", + ): + pytest.xfail( + "ircu2 returns ERR_NEEDMOREPARAMS on empty keys: " + "https://github.com/UndernetIRC/ircu2/issues/13" + ) + if (key == "" or " " in key) and self.controller.software_name == "ngIRCd": + pytest.xfail( + "ngIRCd does not validate channel keys: " + "https://github.com/ngircd/ngircd/issues/290" + ) + self.connectClient("bar") self.joinChannel(1, "#chan") self.sendLine(1, f"MODE #chan +k :{key}") diff --git a/irctest/server_tests/connection_registration.py b/irctest/server_tests/connection_registration.py index e4a1c0b..d726cfc 100644 --- a/irctest/server_tests/connection_registration.py +++ b/irctest/server_tests/connection_registration.py @@ -84,6 +84,10 @@ class ConnectionRegistrationTestCase(cases.BaseServerTestCase): self.getMessages(1) @cases.mark_specifications("RFC2812") + @cases.xfailIfSoftware(["Charybdis", "Solanum"], "very flaky") + @cases.xfailIfSoftware( + ["ircu2", "Nefarious", "snircd"], "ircu2 does not send ERROR" + ) def testQuitErrors(self): """“A client session is terminated with a quit message. The server acknowledges this by sending an ERROR message to the client.” @@ -164,6 +168,10 @@ class ConnectionRegistrationTestCase(cases.BaseServerTestCase): "neither got 001.", ) + @cases.xfailIfSoftware( + ["ircu2", "Nefarious", "ngIRCd"], + "uses a default value instead of ERR_NEEDMOREPARAMS", + ) def testEmptyRealname(self): """ Syntax: diff --git a/irctest/server_tests/help.py b/irctest/server_tests/help.py index 9058ea3..ff5069e 100644 --- a/irctest/server_tests/help.py +++ b/irctest/server_tests/help.py @@ -2,6 +2,7 @@ The HELP and HELPOP command (`Modern `__) """ +import functools import re import pytest @@ -17,6 +18,30 @@ from irctest.numerics import ( from irctest.patma import ANYSTR, StrRe +def with_xfails(f): + @functools.wraps(f) + def newf(self, command, *args, **kwargs): + if command == "HELP" and self.controller.software_name == "Bahamut": + raise runner.NotImplementedByController( + "fail because Bahamut forwards /HELP to HelpServ (but not /HELPOP)" + ) + + if self.controller.software_name in ("irc2", "ircu2", "ngIRCd"): + raise runner.NotImplementedByController( + "numerics in reply to /HELP and /HELPOP (uses NOTICE instead)" + ) + + if self.controller.software_name == "UnrealIRCd": + raise runner.NotImplementedByController( + "fails because Unreal uses custom numerics " + "https://github.com/unrealircd/unrealircd/pull/184" + ) + + return f(self, command, *args, **kwargs) + + return newf + + class HelpTestCase(cases.BaseServerTestCase): def _assertValidHelp(self, messages, subject): if subject != ANYSTR: @@ -46,6 +71,7 @@ class HelpTestCase(cases.BaseServerTestCase): @pytest.mark.parametrize("command", ["HELP", "HELPOP"]) @cases.mark_specifications("Modern") + @with_xfails def testHelpNoArg(self, command): self.connectClient("nick") self.sendLine(1, f"{command}") @@ -59,6 +85,7 @@ class HelpTestCase(cases.BaseServerTestCase): @pytest.mark.parametrize("command", ["HELP", "HELPOP"]) @cases.mark_specifications("Modern") + @with_xfails def testHelpPrivmsg(self, command): self.connectClient("nick") self.sendLine(1, f"{command} PRIVMSG") @@ -71,6 +98,7 @@ class HelpTestCase(cases.BaseServerTestCase): @pytest.mark.parametrize("command", ["HELP", "HELPOP"]) @cases.mark_specifications("Modern") + @with_xfails def testHelpUnknownSubject(self, command): self.connectClient("nick") self.sendLine(1, f"{command} THISISNOTACOMMAND") diff --git a/irctest/server_tests/info.py b/irctest/server_tests/info.py index 8e3ed61..d1a2613 100644 --- a/irctest/server_tests/info.py +++ b/irctest/server_tests/info.py @@ -87,6 +87,9 @@ class InfoTestCase(cases.BaseServerTestCase): @pytest.mark.parametrize("target", ["invalid.server.example", "invalidserver"]) @cases.mark_specifications("RFC1459", "RFC2812", deprecated=True) + @cases.xfailIfSoftware( + ["Ergo"], "does not apply to Ergo, which ignores the optional argument" + ) def testInfoNosuchserver(self, target): """ diff --git a/irctest/server_tests/invite.py b/irctest/server_tests/invite.py index f441849..2f9c305 100644 --- a/irctest/server_tests/invite.py +++ b/irctest/server_tests/invite.py @@ -200,6 +200,9 @@ class InviteTestCase(cases.BaseServerTestCase): self._testInvite(opped=True, invite_only=invite_only) @cases.mark_specifications("RFC1459", "RFC2812", "Modern", strict=True) + @cases.xfailIfSoftware( + ["Hybrid", "Plexus4"], "the only strict test that Hybrid fails" + ) def testInviteUnopped(self): """Tests invites from unopped users on not-invite-only chans.""" self._testInvite(opped=False, invite_only=False) @@ -237,6 +240,11 @@ class InviteTestCase(cases.BaseServerTestCase): ) @cases.mark_specifications("RFC1459", "RFC2812", "Modern") + @cases.xfailIfSoftware( + ["Plexus4"], + "Plexus4 allows non-op to invite if (and only if) the channel is not " + "invite-only", + ) def testInviteInviteOnly(self): """ "To invite a user to a channel which is invite only (MODE diff --git a/irctest/server_tests/kick.py b/irctest/server_tests/kick.py index d2acc72..afec031 100644 --- a/irctest/server_tests/kick.py +++ b/irctest/server_tests/kick.py @@ -96,6 +96,10 @@ class KickTestCase(cases.BaseServerTestCase): self.assertMessageMatch(m3, command="KICK", params=["#chan", "bar", ANYSTR]) @cases.mark_specifications("RFC2812") + @cases.xfailIfSoftware( + ["Charybdis", "ircu2", "irc2", "Solanum"], + "uses the nick of the kickee rather than the kicker.", + ) def testKickDefaultComment(self): """ "If a "comment" is diff --git a/irctest/server_tests/list.py b/irctest/server_tests/list.py index 0c57289..7a902e3 100644 --- a/irctest/server_tests/list.py +++ b/irctest/server_tests/list.py @@ -12,6 +12,7 @@ from irctest import cases class ListTestCase(cases.BaseServerTestCase): @cases.mark_specifications("RFC1459", "RFC2812") + @cases.xfailIfSoftware(["irc2"], "irc2 deprecated LIST") def testListEmpty(self): """ @@ -40,6 +41,7 @@ class ListTestCase(cases.BaseServerTestCase): ) @cases.mark_specifications("RFC1459", "RFC2812") + @cases.xfailIfSoftware(["irc2"], "irc2 deprecated LIST") def testListOne(self): """When a channel exists, LIST should get it in a reply. diff --git a/irctest/server_tests/lusers.py b/irctest/server_tests/lusers.py index 7eb59d6..786c582 100644 --- a/irctest/server_tests/lusers.py +++ b/irctest/server_tests/lusers.py @@ -153,6 +153,10 @@ class BasicLusersTestCase(LusersTestCase): self.getLusers("bar", True) @cases.mark_specifications("Modern") + @cases.xfailIfSoftware( + ["ircu2", "Nefarious", "snircd"], + "test depends on Modern behavior, not just RFC2812", + ) def testLusersFull(self): self.connectClient("bar", name="bar") lusers = self.getLusers("bar", False) @@ -170,10 +174,22 @@ class BasicLusersTestCase(LusersTestCase): class LusersUnregisteredTestCase(LusersTestCase): @cases.mark_specifications("RFC2812") + @cases.xfailIfSoftware( + ["Nefarious"], + "Nefarious doesn't seem to distinguish unregistered users from normal ones", + ) def testLusersRfc2812(self): self.doLusersTest(True) @cases.mark_specifications("Modern") + @cases.xfailIfSoftware( + ["Nefarious"], + "Nefarious doesn't seem to distinguish unregistered users from normal ones", + ) + @cases.xfailIfSoftware( + ["ircu2", "Nefarious", "snircd"], + "test depends on Modern behavior, not just RFC2812", + ) def testLusersFull(self): self.doLusersTest(False) @@ -237,6 +253,10 @@ class LusersUnregisteredDefaultInvisibleTestCase(LusersUnregisteredTestCase): ) @cases.mark_specifications("Ergo") + @cases.xfailIfSoftware( + ["Nefarious"], + "Nefarious doesn't seem to distinguish unregistered users from normal ones", + ) def testLusers(self): self.doLusersTest(False) lusers = self.getLusers("bar", False) diff --git a/irctest/server_tests/messages.py b/irctest/server_tests/messages.py index a4e8544..b33be17 100644 --- a/irctest/server_tests/messages.py +++ b/irctest/server_tests/messages.py @@ -51,6 +51,15 @@ class NoticeTestCase(cases.BaseServerTestCase): ) @cases.mark_specifications("RFC1459", "RFC2812") + @cases.xfailIfSoftware( + ["InspIRCd"], + "replies with ERR_NOSUCHCHANNEL to NOTICE to non-existent channels", + ) + @cases.xfailIfSoftware( + ["UnrealIRCd"], + "replies with ERR_NOSUCHCHANNEL to NOTICE to non-existent channels: " + "https://bugs.unrealircd.org/view.php?id=5949", + ) def testNoticeNonexistentChannel(self): """ "automatic replies must never be @@ -71,6 +80,9 @@ class NoticeTestCase(cases.BaseServerTestCase): class TagsTestCase(cases.BaseServerTestCase): @cases.mark_capabilities("message-tags") + @cases.xfailIfSoftware( + ["UnrealIRCd"], "https://bugs.unrealircd.org/view.php?id=5947" + ) def testLineTooLong(self): self.connectClient("bar", capabilities=["message-tags"], skip_if_cap_nak=True) self.connectClient( diff --git a/irctest/server_tests/quit.py b/irctest/server_tests/quit.py index 022447f..a62c123 100644 --- a/irctest/server_tests/quit.py +++ b/irctest/server_tests/quit.py @@ -16,6 +16,7 @@ from irctest.patma import StrRe class ChannelQuitTestCase(cases.BaseServerTestCase): @cases.mark_specifications("RFC2812") + @cases.xfailIfSoftware(["ircu2", "Nefarious", "snircd"], "ircu2 does not echo QUIT") def testQuit(self): """“Once a user has joined a channel, he receives information about all commands his server receives affecting the channel. This diff --git a/irctest/server_tests/regressions.py b/irctest/server_tests/regressions.py index 15ff21e..c09fdf5 100644 --- a/irctest/server_tests/regressions.py +++ b/irctest/server_tests/regressions.py @@ -4,7 +4,7 @@ Regression tests for bugs in `Ergo `_. import time -from irctest import cases +from irctest import cases, runner from irctest.numerics import ERR_ERRONEUSNICKNAME, ERR_NICKNAMEINUSE, RPL_WELCOME from irctest.patma import ANYDICT @@ -57,6 +57,12 @@ class RegressionsTestCase(cases.BaseServerTestCase): @cases.mark_capabilities("message-tags", "batch", "echo-message", "server-time") def testTagCap(self): + if self.controller.software_name == "UnrealIRCd": + raise runner.NotImplementedByController( + "Arbitrary +draft/reply values (TODO: adapt this test to use real " + "values so their pass Unreal's validation) " + "https://bugs.unrealircd.org/view.php?id=5948" + ) # regression test for oragono #754 self.connectClient( "alice", @@ -99,6 +105,7 @@ class RegressionsTestCase(cases.BaseServerTestCase): ) @cases.mark_specifications("RFC1459") + @cases.xfailIfSoftware(["ngIRCd"], "wat") def testStarNick(self): self.addClient(1) self.sendLine(1, "NICK *") diff --git a/irctest/server_tests/sasl.py b/irctest/server_tests/sasl.py index d680491..47f53cd 100644 --- a/irctest/server_tests/sasl.py +++ b/irctest/server_tests/sasl.py @@ -171,6 +171,13 @@ class SaslTestCase(cases.BaseServerTestCase): @cases.mark_specifications("IRCv3") @cases.skipUnlessHasMechanism("PLAIN") + @cases.xfailIf( + lambda self: ( + self.controller.services_controller is not None + and self.controller.services_controller.software_name == "Anope" + ), + "Anope does not handle split AUTHENTICATE (reported on IRC)", + ) def testPlainLarge(self): """Test the client splits large AUTHENTICATE messages whose payload is not a multiple of 400. @@ -233,6 +240,13 @@ class SaslTestCase(cases.BaseServerTestCase): @cases.mark_specifications("IRCv3") @cases.skipUnlessHasMechanism("PLAIN") + @cases.xfailIf( + lambda self: ( + self.controller.services_controller is not None + and self.controller.services_controller.software_name == "Anope" + ), + "Anope does not handle split AUTHENTICATE (reported on IRC)", + ) def testPlainLargeEquals400(self): """Test the client splits large AUTHENTICATE messages whose payload is not a multiple of 400. diff --git a/irctest/server_tests/statusmsg.py b/irctest/server_tests/statusmsg.py index fb063f8..1a1910d 100644 --- a/irctest/server_tests/statusmsg.py +++ b/irctest/server_tests/statusmsg.py @@ -17,6 +17,11 @@ class StatusmsgTestCase(cases.BaseServerTestCase): self.assertEqual(self.server_support["STATUSMSG"], "~&@%+") @cases.mark_isupport("STATUSMSG") + @cases.xfailIfSoftware( + ["ircu2", "Nefarious", "snircd"], + "STATUSMSG is present in ISUPPORT, but it not actually supported as PRIVMSG " + "target (only for WALLCOPS/WALLCHOPS/...)", + ) def testStatusmsgFromOp(self): """Test that STATUSMSG are sent to the intended recipients, with the intended prefixes.""" @@ -68,6 +73,11 @@ class StatusmsgTestCase(cases.BaseServerTestCase): self.assertEqual(len(unprivilegedMessages), 0) @cases.mark_isupport("STATUSMSG") + @cases.xfailIfSoftware( + ["ircu2", "Nefarious", "snircd"], + "STATUSMSG is present in ISUPPORT, but it not actually supported as PRIVMSG " + "target (only for WALLCOPS/WALLCHOPS/...)", + ) def testStatusmsgFromRegular(self): """Test that STATUSMSG are sent to the intended recipients, with the intended prefixes.""" diff --git a/irctest/server_tests/wallops.py b/irctest/server_tests/wallops.py index c1d493b..e0d00e0 100644 --- a/irctest/server_tests/wallops.py +++ b/irctest/server_tests/wallops.py @@ -66,6 +66,9 @@ class WallopsTestCase(cases.BaseServerTestCase): ) @cases.mark_specifications("Modern") + @cases.xfailIfSoftware( + ["irc2"], "irc2 ignores the command instead of replying ERR_UNKNOWNCOMMAND" + ) def testWallopsPrivileges(self): """ https://github.com/ircdocs/modern-irc/pull/118 diff --git a/irctest/server_tests/who.py b/irctest/server_tests/who.py index 8510e69..f5e8903 100644 --- a/irctest/server_tests/who.py +++ b/irctest/server_tests/who.py @@ -87,6 +87,9 @@ class BaseWhoTestCase: class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): @cases.mark_specifications("Modern") def testWhoStar(self): + if self.controller.software_name == "Bahamut": + raise runner.NotImplementedByController("WHO mask") + self._init() self.sendLine(2, "WHO *") @@ -115,6 +118,9 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): ) @cases.mark_specifications("Modern") def testWhoNick(self, mask): + if "*" in mask and self.controller.software_name == "Bahamut": + raise runner.NotImplementedByController("WHO mask") + self._init() self.sendLine(2, f"WHO {mask}") @@ -142,6 +148,9 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): ids=["username", "realname-mask", "hostname"], ) def testWhoUsernameRealName(self, mask): + if "*" in mask and self.controller.software_name == "Bahamut": + raise runner.NotImplementedByController("WHO mask") + self._init() self.sendLine(2, f"WHO :{mask}") @@ -192,6 +201,9 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): ) @cases.mark_specifications("Modern") def testWhoNickAway(self, mask): + if "*" in mask and self.controller.software_name == "Bahamut": + raise runner.NotImplementedByController("WHO mask") + self._init() self.sendLine(1, "AWAY :be right back") @@ -218,6 +230,9 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): ) @cases.mark_specifications("Modern") def testWhoNickOper(self, mask): + if "*" in mask and self.controller.software_name == "Bahamut": + raise runner.NotImplementedByController("WHO mask") + self._init() self.sendLine(1, "OPER operuser operpassword") @@ -249,6 +264,9 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): ) @cases.mark_specifications("Modern") def testWhoNickAwayAndOper(self, mask): + if "*" in mask and self.controller.software_name == "Bahamut": + raise runner.NotImplementedByController("WHO mask") + self._init() self.sendLine(1, "OPER operuser operpassword") @@ -280,6 +298,9 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): @pytest.mark.parametrize("mask", ["#chan", "#CHAN"], ids=["exact", "casefolded"]) @cases.mark_specifications("Modern") def testWhoChan(self, mask): + if "*" in mask and self.controller.software_name == "Bahamut": + raise runner.NotImplementedByController("WHO mask") + self._init() self.sendLine(1, "OPER operuser operpassword") diff --git a/irctest/server_tests/whois.py b/irctest/server_tests/whois.py index ec432a5..a22b34f 100644 --- a/irctest/server_tests/whois.py +++ b/irctest/server_tests/whois.py @@ -29,6 +29,9 @@ from irctest.patma import ANYSTR, StrRe class _WhoisTestMixin(cases.BaseServerTestCase): def _testWhoisNumerics(self, authenticate, away, oper): + if oper and self.controller.software_name == "Charybdis": + pytest.xfail("charybdis uses RPL_WHOISSPECIAL instead of RPL_WHOISOPERATOR") + if authenticate: self.connectClient("nick1") self.controller.registerUser(self, "val", "sesame") diff --git a/irctest/server_tests/whowas.py b/irctest/server_tests/whowas.py index dd65b54..a3d9dc0 100644 --- a/irctest/server_tests/whowas.py +++ b/irctest/server_tests/whowas.py @@ -163,6 +163,10 @@ class WhowasTestCase(cases.BaseServerTestCase): ) @cases.mark_specifications("RFC1459", "RFC2812") + @cases.xfailIfSoftware( + ["InspIRCd"], + "Feature not released yet: https://github.com/inspircd/inspircd/pull/1967", + ) def testWhowasMultiple(self): """ "The history is searched backward, returning the most recent entry first." @@ -172,6 +176,10 @@ class WhowasTestCase(cases.BaseServerTestCase): self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2") @cases.mark_specifications("RFC1459", "RFC2812") + @cases.xfailIfSoftware( + ["InspIRCd"], + "Feature not released yet: https://github.com/inspircd/inspircd/pull/1968", + ) def testWhowasCount1(self): """ "If there are multiple entries, up to replies will be returned" @@ -181,6 +189,10 @@ class WhowasTestCase(cases.BaseServerTestCase): self._testWhowasMultiple(second_result=False, whowas_command="WHOWAS nick2 1") @cases.mark_specifications("RFC1459", "RFC2812") + @cases.xfailIfSoftware( + ["InspIRCd"], + "Feature not released yet: https://github.com/inspircd/inspircd/pull/1968", + ) def testWhowasCount2(self): """ "If there are multiple entries, up to replies will be returned" @@ -190,6 +202,10 @@ class WhowasTestCase(cases.BaseServerTestCase): self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 2") @cases.mark_specifications("RFC1459", "RFC2812") + @cases.xfailIfSoftware( + ["InspIRCd"], + "Feature not released yet: https://github.com/inspircd/inspircd/pull/1968", + ) def testWhowasCountNegative(self): """ "If a non-positive number is passed as being , then a full search @@ -200,6 +216,13 @@ class WhowasTestCase(cases.BaseServerTestCase): self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 -1") @cases.mark_specifications("RFC1459", "RFC2812") + @cases.xfailIfSoftware( + ["ircu2"], "Fix not released yet: https://github.com/UndernetIRC/ircu2/pull/19" + ) + @cases.xfailIfSoftware( + ["InspIRCd"], + "Feature not released yet: https://github.com/inspircd/inspircd/pull/1967", + ) def testWhowasCountZero(self): """ "If a non-positive number is passed as being , then a full search @@ -215,6 +238,9 @@ class WhowasTestCase(cases.BaseServerTestCase): "Wildcards are allowed in the parameter." -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 """ + if self.controller.software_name == "Bahamut": + raise runner.NotImplementedByController("WHOWAS mask") + self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS *ck2") @cases.mark_specifications("RFC1459", "RFC2812", deprecated=True) @@ -250,6 +276,12 @@ class WhowasTestCase(cases.BaseServerTestCase): ) @cases.mark_specifications("RFC1459", "RFC2812") + @cases.xfailIfSoftware( + ["Charybdis"], + "fails because of a typo (solved in " + "https://github.com/solanum-ircd/solanum/commit/" + "08b7b6bd7e60a760ad47b58cbe8075b45d66166f)", + ) def testWhowasNoSuchNick(self): """ https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 @@ -285,6 +317,11 @@ class WhowasTestCase(cases.BaseServerTestCase): """ https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 """ + if self.controller.software_name == "Bahamut": + pytest.xfail( + "Bahamut returns entries in query order instead of chronological order" + ) + self.connectClient("nick1") targmax = dict( From 47db85f0262848a7ae68149f06ee7f59014a4951 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 12 Apr 2022 22:53:02 +0200 Subject: [PATCH 039/143] Fix typo --- irctest/server_tests/kick.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irctest/server_tests/kick.py b/irctest/server_tests/kick.py index afec031..7f2149e 100644 --- a/irctest/server_tests/kick.py +++ b/irctest/server_tests/kick.py @@ -1,5 +1,5 @@ """ -The INFO command (`RFC 1459 +The KICK command (`RFC 1459 `__, `RFC 2812 `__, `Modern `__) From 82928bc6fc0921b67f49cc8bb2bf4e87dd881aff Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 12 Apr 2022 22:53:50 +0200 Subject: [PATCH 040/143] Sort results --- irctest/dashboard/format.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/irctest/dashboard/format.py b/irctest/dashboard/format.py index 721dc04..e08873b 100644 --- a/irctest/dashboard/format.py +++ b/irctest/dashboard/format.py @@ -159,7 +159,7 @@ def build_module_html( ET.SubElement(ET.SubElement(cell, "div"), "span").text = job cell.set("class", "job-name") - for (class_name, class_results) in results_by_class.items(): + for (class_name, class_results) in sorted(results_by_class.items()): # Header row: class name header_row = ET.SubElement(table, "tr") th = ET.SubElement(header_row, "th", colspan=str(len(jobs) + 1)) @@ -178,7 +178,7 @@ def build_module_html( # One row for each test: results_by_test = group_by(class_results, key=lambda r: r.test_name) - for (test_name, test_results) in results_by_test.items(): + for (test_name, test_results) in sorted(results_by_test.items()): row_anchor = f"{class_name}.{test_name}" if len(row_anchor) >= 50: # Too long; give up on generating readable URL From 3ab31ca4de3bcc76d2eb6f44b8d5a643c3d21ab8 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Wed, 13 Apr 2022 18:52:12 +0200 Subject: [PATCH 041/143] Add tests for WHOWAS as specified in modern-irc (#142) https://github.com/ircdocs/modern-irc/pull/170 --- irctest/server_tests/whowas.py | 93 +++++++++++++++++++++++++++++++--- 1 file changed, 86 insertions(+), 7 deletions(-) diff --git a/irctest/server_tests/whowas.py b/irctest/server_tests/whowas.py index a3d9dc0..cbe9e3b 100644 --- a/irctest/server_tests/whowas.py +++ b/irctest/server_tests/whowas.py @@ -13,6 +13,7 @@ import pytest from irctest import cases, runner from irctest.exceptions import ConnectionClosed from irctest.numerics import ( + ERR_NEEDMOREPARAMS, ERR_NONICKNAMEGIVEN, ERR_WASNOSUCHNICK, RPL_ENDOFWHOWAS, @@ -88,6 +89,43 @@ class WhowasTestCase(cases.BaseServerTestCase): unexpected_messages, [], fail_msg="Unexpected numeric messages: {got}" ) + @cases.mark_specifications("RFC1459", "RFC2812", "Modern") + def testWhowasEnd(self): + """ + "At the end of all reply batches, there must be RPL_ENDOFWHOWAS" + -- https://datatracker.ietf.org/doc/html/rfc1459#page-50 + -- https://datatracker.ietf.org/doc/html/rfc2812#page-45 + + "Servers MUST reply with either ERR_WASNOSUCHNICK or [...], + both followed with RPL_ENDOFWHOWAS" + -- https://github.com/ircdocs/modern-irc/pull/170 + """ + self.connectClient("nick1") + + self.connectClient("nick2") + self.sendLine(2, "QUIT :bye") + try: + self.getMessages(2) + except ConnectionClosed: + pass + + self.sendLine(1, "WHOWAS nick2") + + messages = [] + for _ in range(10): + messages.extend(self.getMessages(1)) + if RPL_ENDOFWHOWAS in (m.command for m in messages): + break + + last_message = messages.pop() + + self.assertMessageMatch( + last_message, + command=RPL_ENDOFWHOWAS, + params=["nick1", "nick2", ANYSTR], + fail_msg=f"Last message was not RPL_ENDOFWHOWAS ({RPL_ENDOFWHOWAS})", + ) + def _testWhowasMultiple(self, second_result, whowas_command): """ "The history is searched backward, returning the most recent entry first." @@ -162,7 +200,7 @@ class WhowasTestCase(cases.BaseServerTestCase): fail_msg=f"Last message was not RPL_ENDOFWHOWAS ({RPL_ENDOFWHOWAS})", ) - @cases.mark_specifications("RFC1459", "RFC2812") + @cases.mark_specifications("RFC1459", "RFC2812", "Modern") @cases.xfailIfSoftware( ["InspIRCd"], "Feature not released yet: https://github.com/inspircd/inspircd/pull/1967", @@ -172,10 +210,11 @@ class WhowasTestCase(cases.BaseServerTestCase): "The history is searched backward, returning the most recent entry first." -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 + -- https://github.com/ircdocs/modern-irc/pull/170 """ self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2") - @cases.mark_specifications("RFC1459", "RFC2812") + @cases.mark_specifications("RFC1459", "RFC2812", "Modern") @cases.xfailIfSoftware( ["InspIRCd"], "Feature not released yet: https://github.com/inspircd/inspircd/pull/1968", @@ -185,10 +224,11 @@ class WhowasTestCase(cases.BaseServerTestCase): "If there are multiple entries, up to replies will be returned" -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 + -- https://github.com/ircdocs/modern-irc/pull/170 """ self._testWhowasMultiple(second_result=False, whowas_command="WHOWAS nick2 1") - @cases.mark_specifications("RFC1459", "RFC2812") + @cases.mark_specifications("RFC1459", "RFC2812", "Modern") @cases.xfailIfSoftware( ["InspIRCd"], "Feature not released yet: https://github.com/inspircd/inspircd/pull/1968", @@ -198,10 +238,11 @@ class WhowasTestCase(cases.BaseServerTestCase): "If there are multiple entries, up to replies will be returned" -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 + -- https://github.com/ircdocs/modern-irc/pull/170 """ self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 2") - @cases.mark_specifications("RFC1459", "RFC2812") + @cases.mark_specifications("RFC1459", "RFC2812", "Modern") @cases.xfailIfSoftware( ["InspIRCd"], "Feature not released yet: https://github.com/inspircd/inspircd/pull/1968", @@ -212,10 +253,11 @@ class WhowasTestCase(cases.BaseServerTestCase): is done." -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 + -- https://github.com/ircdocs/modern-irc/pull/170 """ self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 -1") - @cases.mark_specifications("RFC1459", "RFC2812") + @cases.mark_specifications("RFC1459", "RFC2812", "Modern") @cases.xfailIfSoftware( ["ircu2"], "Fix not released yet: https://github.com/UndernetIRC/ircu2/pull/19" ) @@ -229,6 +271,7 @@ class WhowasTestCase(cases.BaseServerTestCase): is done." -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 + -- https://github.com/ircdocs/modern-irc/pull/170 """ self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 0") @@ -237,6 +280,7 @@ class WhowasTestCase(cases.BaseServerTestCase): """ "Wildcards are allowed in the parameter." -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 + -- https://github.com/ircdocs/modern-irc/pull/170 """ if self.controller.software_name == "Bahamut": raise runner.NotImplementedByController("WHOWAS mask") @@ -244,7 +288,7 @@ class WhowasTestCase(cases.BaseServerTestCase): self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS *ck2") @cases.mark_specifications("RFC1459", "RFC2812", deprecated=True) - def testWhowasNoParam(self): + def testWhowasNoParamRfc(self): """ https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 @@ -275,7 +319,35 @@ class WhowasTestCase(cases.BaseServerTestCase): params=["nick1", "nick2", ANYSTR], ) - @cases.mark_specifications("RFC1459", "RFC2812") + @cases.mark_specifications("Modern") + def testWhowasNoParamModern(self): + """ + "If the `` argument is missing, they SHOULD send a single reply, using + either ERR_NONICKNAMEGIVEN or ERR_NEEDMOREPARAMS" + -- https://github.com/ircdocs/modern-irc/pull/170 + """ + # But no one seems to follow this. Most implementations use ERR_NEEDMOREPARAMS + # instead of ERR_NONICKNAMEGIVEN; and I couldn't find any that returns + # RPL_ENDOFWHOWAS either way. + self.connectClient("nick1") + + self.sendLine(1, "WHOWAS") + + m = self.getMessage(1) + if m.command == ERR_NONICKNAMEGIVEN: + self.assertMessageMatch( + m, + command=ERR_NONICKNAMEGIVEN, + params=["nick1", ANYSTR], + ) + else: + self.assertMessageMatch( + m, + command=ERR_NEEDMOREPARAMS, + params=["nick1", "WHOWAS", ANYSTR], + ) + + @cases.mark_specifications("RFC1459", "RFC2812", "Modern") @cases.xfailIfSoftware( ["Charybdis"], "fails because of a typo (solved in " @@ -286,6 +358,7 @@ class WhowasTestCase(cases.BaseServerTestCase): """ https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 + -- https://github.com/ircdocs/modern-irc/pull/170 and: @@ -293,6 +366,12 @@ class WhowasTestCase(cases.BaseServerTestCase): (even if there was only one reply and it was an error)." -- https://datatracker.ietf.org/doc/html/rfc1459#page-50 -- https://datatracker.ietf.org/doc/html/rfc2812#page-45 + + and: + + "Servers MUST reply with either ERR_WASNOSUCHNICK or [...], + both followed with RPL_ENDOFWHOWAS" + -- https://github.com/ircdocs/modern-irc/pull/170 """ self.connectClient("nick1") From 6539ed881a8893ca752d65962f9dbeba88b082c4 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Wed, 13 Apr 2022 18:54:42 +0200 Subject: [PATCH 042/143] Add tests for NAMES (#145) --- irctest/server_tests/names.py | 208 +++++++++++++++++++++++++++++++++- 1 file changed, 204 insertions(+), 4 deletions(-) diff --git a/irctest/server_tests/names.py b/irctest/server_tests/names.py index 5a79743..801fcac 100644 --- a/irctest/server_tests/names.py +++ b/irctest/server_tests/names.py @@ -5,12 +5,114 @@ The NAMES command (`RFC 1459 `Modern `__) """ -from irctest import cases -from irctest.numerics import RPL_ENDOFNAMES -from irctest.patma import ANYSTR - +from irctest import cases, runner +from irctest.numerics import RPL_ENDOFNAMES, RPL_NAMREPLY +from irctest.patma import ANYSTR, StrRe class NamesTestCase(cases.BaseServerTestCase): + def _testNames(self, symbol): + self.connectClient("nick1") + self.sendLine(1, "JOIN #chan") + self.getMessages(1) + self.connectClient("nick2") + self.sendLine(2, "JOIN #chan") + self.getMessages(2) + self.getMessages(1) + + self.sendLine(1, "NAMES #chan") + + # TODO: It is technically allowed to have one line for each; + # but noone does that. + self.assertMessageMatch( + self.getMessage(1), + command=RPL_NAMREPLY, + params=[ + "nick1", + *(["="] if symbol else []), + "#chan", + StrRe("(nick2 @nick1|@nick1 nick2)"), + ], + ) + + self.assertMessageMatch( + self.getMessage(1), + command=RPL_ENDOFNAMES, + params=["nick1", "#chan", ANYSTR], + ) + + @cases.mark_specifications("RFC1459", deprecated=True) + def testNames1459(self): + """ + https://modern.ircdocs.horse/#names-message + https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5 + https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5 + """ + self._testNames(symbol=False) + + @cases.mark_specifications("RFC1459", "RFC2812", "Modern") + def testNames2812(self): + """ + https://modern.ircdocs.horse/#names-message + https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5 + https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5 + """ + self._testNames(symbol=True) + + def _testNamesMultipleChannels(self, symbol): + self.connectClient("nick1") + + targmax = dict( + item.split(":", 1) + for item in self.server_support.get("TARGMAX", "").split(",") + if item + ) + if targmax.get("NAMES", "1") == "1": + raise runner.NotImplementedByController("Multi-target NAMES") + + self.sendLine(1, "JOIN #chan1") + self.sendLine(1, "JOIN #chan2") + self.getMessages(1) + + self.sendLine(1, "NAMES #chan1,#chan2") + + # TODO: order is unspecified + self.assertMessageMatch( + self.getMessage(1), + command=RPL_NAMREPLY, + params=["nick1", *(["="] if symbol else []), "#chan1", "@nick1"], + ) + self.assertMessageMatch( + self.getMessage(1), + command=RPL_NAMREPLY, + params=["nick1", *(["="] if symbol else []), "#chan2", "@nick1"], + ) + + self.assertMessageMatch( + self.getMessage(1), + command=RPL_ENDOFNAMES, + params=["nick1", "#chan1,#chan2", ANYSTR], + ) + + @cases.mark_isupport("TARGMAX") + @cases.mark_specifications("RFC1459", deprecated=True) + def testNamesMultipleChannels1459(self): + """ + https://modern.ircdocs.horse/#names-message + https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5 + https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5 + """ + self._testNamesMultipleChannels(symbol=False) + + @cases.mark_isupport("TARGMAX") + @cases.mark_specifications("RFC1459", "RFC2812", "Modern") + def testNamesMultipleChannels2812(self): + """ + https://modern.ircdocs.horse/#names-message + https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5 + https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5 + """ + self._testNamesMultipleChannels(symbol=True) + @cases.mark_specifications("RFC1459", "RFC2812", "Modern") def testNamesInvalidChannel(self): """ @@ -54,3 +156,101 @@ class NamesTestCase(cases.BaseServerTestCase): command=RPL_ENDOFNAMES, params=["foo", "#nonexisting", ANYSTR], ) + + def _testNamesNoArgumentPublic(self, symbol): + self.connectClient("nick1") + self.getMessages(1) + self.sendLine(1, "JOIN #chan1") + self.connectClient("nick2") + self.sendLine(2, "JOIN #chan2") + self.sendLine(2, "MODE #chan2 -sp") + self.getMessages(1) + self.getMessages(2) + + self.sendLine(1, "NAMES") + + # TODO: order is unspecified + self.assertMessageMatch( + self.getMessage(1), + command=RPL_NAMREPLY, + params=["nick1", *(["="] if symbol else []), "#chan1", "@nick1"], + ) + self.assertMessageMatch( + self.getMessage(1), + command=RPL_NAMREPLY, + params=["nick1", *(["="] if symbol else []), "#chan2", "@nick2"], + ) + + self.assertMessageMatch( + self.getMessage(1), + command=RPL_ENDOFNAMES, + params=["nick1", ANYSTR, ANYSTR], + ) + + @cases.mark_specifications("RFC1459", deprecated=True) + def testNamesNoArgumentPublic1459(self): + """ + "If no parameter is given, a list of all channels and their + occupants is returned." + -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5 + -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5 + """ + self._testNamesNoArgumentPublic(symbol=False) + + @cases.mark_specifications("RFC2812", deprecated=True) + def testNamesNoArgumentPublic2812(self): + """ + "If no parameter is given, a list of all channels and their + occupants is returned." + -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5 + -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5 + """ + self._testNamesNoArgumentPublic(symbol=True) + + def _testNamesNoArgumentPrivate(self, symbol): + self.connectClient("nick1") + self.getMessages(1) + self.sendLine(1, "JOIN #chan1") + self.connectClient("nick2") + self.sendLine(2, "JOIN #chan2") + self.sendLine(2, "MODE #chan2 +sp") + self.getMessages(1) + self.getMessages(2) + + self.sendLine(1, "NAMES") + + self.assertMessageMatch( + self.getMessage(1), + command=RPL_NAMREPLY, + params=["nick1", *(["="] if symbol else []), "#chan1", "@nick1"], + ) + + self.assertMessageMatch( + self.getMessage(1), + command=RPL_ENDOFNAMES, + params=["nick1", ANYSTR, ANYSTR], + ) + + @cases.mark_specifications("RFC1459", deprecated=True) + def testNamesNoArgumentPrivate1459(self): + """ + "If no parameter is given, a list of all channels and their + occupants is returned. At the end of this list, a list of users who + are visible but either not on any channel or not on a visible channel + are listed as being on `channel' "*"." + -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5 + -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5 + """ + self._testNamesNoArgumentPrivate(symbol=False) + + @cases.mark_specifications("RFC2812", deprecated=True) + def testNamesNoArgumentPrivate2812(self): + """ + "If no parameter is given, a list of all channels and their + occupants is returned. At the end of this list, a list of users who + are visible but either not on any channel or not on a visible channel + are listed as being on `channel' "*"." + -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.2.5 + -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.2.5 + """ + self._testNamesNoArgumentPrivate(symbol=True) From 363b62cc80923d307a2095d0eb52ce5b7bdc0514 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Wed, 13 Apr 2022 18:56:29 +0200 Subject: [PATCH 043/143] Add tests for LINKS (#147) --- irctest/controllers/inspircd.py | 2 +- irctest/controllers/irc2.py | 2 +- irctest/controllers/ngircd.py | 2 +- irctest/controllers/unrealircd.py | 2 +- irctest/server_tests/links.py | 136 ++++++++++++++++++++++++++++++ 5 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 irctest/server_tests/links.py diff --git a/irctest/controllers/inspircd.py b/irctest/controllers/inspircd.py index 1cd40ba..402a10b 100644 --- a/irctest/controllers/inspircd.py +++ b/irctest/controllers/inspircd.py @@ -83,7 +83,7 @@ TEMPLATE_CONFIG = """ # Misc: - + """ TEMPLATE_SSL_CONFIG = """ diff --git a/irctest/controllers/irc2.py b/irctest/controllers/irc2.py index 0281fb6..8006ea6 100644 --- a/irctest/controllers/irc2.py +++ b/irctest/controllers/irc2.py @@ -10,7 +10,7 @@ from irctest.basecontrollers import ( TEMPLATE_CONFIG = """ # M:::::: -M:My.Little.Server:{hostname}:Somewhere:{port}:0042: +M:My.Little.Server:{hostname}:test server:{port}:0042: # A:::::: A:Organization, IRC dept.:Daemon :Client Server::IRCnet: diff --git a/irctest/controllers/ngircd.py b/irctest/controllers/ngircd.py index 17b3540..e296899 100644 --- a/irctest/controllers/ngircd.py +++ b/irctest/controllers/ngircd.py @@ -12,7 +12,7 @@ from irctest.irc_utils.junkdrawer import find_hostname_and_port TEMPLATE_CONFIG = """ [Global] Name = My.Little.Server - Info = ExampleNET Server + Info = test server Bind = {hostname} Ports = {port} AdminInfo1 = Bob Smith diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py index af397c1..529ed80 100644 --- a/irctest/controllers/unrealircd.py +++ b/irctest/controllers/unrealircd.py @@ -22,7 +22,7 @@ include "help/help.conf"; me {{ name "My.Little.Server"; - info "ExampleNET Server"; + info "test server"; sid "001"; }} admin {{ diff --git a/irctest/server_tests/links.py b/irctest/server_tests/links.py new file mode 100644 index 0000000..62c746f --- /dev/null +++ b/irctest/server_tests/links.py @@ -0,0 +1,136 @@ +from irctest import cases, runner +from irctest.numerics import ERR_UNKNOWNCOMMAND, RPL_ENDOFLINKS, RPL_LINKS +from irctest.patma import ANYSTR, StrRe + + +class LinksTestCase(cases.BaseServerTestCase): + @cases.mark_specifications("RFC1459", "RFC2812", "Modern") + def testLinksSingleServer(self): + """ + Only testing the parameter-less case. + + https://datatracker.ietf.org/doc/html/rfc1459#section-4.3.3 + https://datatracker.ietf.org/doc/html/rfc2812#section-3.4.5 + https://github.com/ircdocs/modern-irc/pull/175 + + " + 364 RPL_LINKS + " : " + 365 RPL_ENDOFLINKS + " :End of /LINKS list" + + - In replying to the LINKS message, a server must send + replies back using the RPL_LINKS numeric and mark the + end of the list using an RPL_ENDOFLINKS reply. + " + -- https://datatracker.ietf.org/doc/html/rfc1459#page-51 + -- https://datatracker.ietf.org/doc/html/rfc2812#page-48 + + RPL_LINKS: " * : " + RPL_ENDOFLINKS: " * :End of /LINKS list" + -- https://github.com/ircdocs/modern-irc/pull/175/files + """ + self.connectClient("nick") + self.sendLine(1, "LINKS") + messages = self.getMessages(1) + if messages[0].command == ERR_UNKNOWNCOMMAND: + raise runner.NotImplementedByController("LINKS") + + # Ignore '/LINKS has been disabled' from ircu2 + messages = [m for m in messages if m.command != "NOTICE"] + + self.assertMessageMatch( + messages.pop(-1), + command=RPL_ENDOFLINKS, + params=["nick", "*", ANYSTR], + ) + + if not messages: + # This server probably redacts links + return + + self.assertMessageMatch( + messages[0], + command=RPL_LINKS, + params=[ + "nick", + "My.Little.Server", + "My.Little.Server", + StrRe("0 (0042 )?test server"), + ], + ) + + +@cases.mark_services +class ServicesLinksTestCase(cases.BaseServerTestCase): + # On every IRCd but Ergo, services are linked. + # Ergo does not implement LINKS at all, so this test is skipped. + @cases.mark_specifications("RFC1459", "RFC2812", "Modern") + def testLinksWithServices(self): + """ + Only testing the parameter-less case. + + https://datatracker.ietf.org/doc/html/rfc1459#section-4.3.3 + https://datatracker.ietf.org/doc/html/rfc2812#section-3.4.5 + + " + 364 RPL_LINKS + " : " + 365 RPL_ENDOFLINKS + " :End of /LINKS list" + + - In replying to the LINKS message, a server must send + replies back using the RPL_LINKS numeric and mark the + end of the list using an RPL_ENDOFLINKS reply. + " + -- https://datatracker.ietf.org/doc/html/rfc1459#page-51 + -- https://datatracker.ietf.org/doc/html/rfc2812#page-48 + + RPL_LINKS: " * : " + RPL_ENDOFLINKS: " * :End of /LINKS list" + -- https://github.com/ircdocs/modern-irc/pull/175/files + """ + self.connectClient("nick") + self.sendLine(1, "LINKS") + messages = self.getMessages(1) + + if messages[0].command == ERR_UNKNOWNCOMMAND: + raise runner.NotImplementedByController("LINKS") + + # Ignore '/LINKS has been disabled' from ircu2 + messages = [m for m in messages if m.command != "NOTICE"] + + self.assertMessageMatch( + messages.pop(-1), + command=RPL_ENDOFLINKS, + params=["nick", "*", ANYSTR], + ) + + if not messages: + # This server redacts links + return + + messages.sort(key=lambda m: m.params[-1]) + + self.assertMessageMatch( + messages.pop(0), + command=RPL_LINKS, + params=[ + "nick", + "My.Little.Server", + "My.Little.Server", + StrRe("0 (0042 )?test server"), + ], + ) + self.assertMessageMatch( + messages.pop(0), + command=RPL_LINKS, + params=[ + "nick", + "services.example.org", + "My.Little.Server", + StrRe("1 .+"), # SID instead of description for Anope... + ], + ) + + self.assertEqual(messages, []) From 83867dad3267603281244166444ec329c15d27d9 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Wed, 13 Apr 2022 18:59:34 +0200 Subject: [PATCH 044/143] testWrongPassword: Add stricter check of the reply's command (#144) --- irctest/server_tests/connection_registration.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/irctest/server_tests/connection_registration.py b/irctest/server_tests/connection_registration.py index d726cfc..7399cb9 100644 --- a/irctest/server_tests/connection_registration.py +++ b/irctest/server_tests/connection_registration.py @@ -7,7 +7,7 @@ TODO: cross-reference Modern and RFC 2812 too from irctest import cases from irctest.client_mock import ConnectionClosed -from irctest.numerics import ERR_NEEDMOREPARAMS +from irctest.numerics import ERR_NEEDMOREPARAMS, ERR_PASSWDMISMATCH from irctest.patma import ANYSTR, StrRe @@ -38,8 +38,14 @@ class PasswordedConnectionRegistrationTestCase(cases.BaseServerTestCase): m.command, "001", msg="Got 001 after NICK+USER but missing PASS" ) - @cases.mark_specifications("RFC1459", "RFC2812") + @cases.mark_specifications("Modern") def testWrongPassword(self): + """ + "If the password supplied does not match the password expected by the server, + then the server SHOULD send ERR_PASSWDMISMATCH and MUST close the connection + with ERROR." + -- https://github.com/ircdocs/modern-irc/pull/172 + """ self.addClient() self.sendLine(1, "PASS {}".format(self.password + "garbage")) self.sendLine(1, "NICK foo") @@ -48,6 +54,13 @@ class PasswordedConnectionRegistrationTestCase(cases.BaseServerTestCase): self.assertNotEqual( m.command, "001", msg="Got 001 after NICK+USER but incorrect PASS" ) + self.assertIn(m.command, {ERR_PASSWDMISMATCH, "ERROR"}) + + if m.command == "ERR_PASSWDMISMATCH": + m = self.getRegistrationMessage(1) + self.assertEqual( + m.command, "ERROR", msg="ERR_PASSWDMISMATCH not followed by ERROR." + ) @cases.mark_specifications("RFC1459", "RFC2812", strict=True) def testPassAfterNickuser(self): From 1e01cb32865c914c34ffbea24291c7575fa7ca41 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Wed, 13 Apr 2022 19:57:16 +0200 Subject: [PATCH 045/143] Fix CI (#157) Broken by recent merges --- .github/workflows/test-devel.yml | 4 ++-- .github/workflows/test-stable.yml | 4 ++-- irctest/server_tests/list.py | 14 ++++++++++---- irctest/server_tests/names.py | 1 + 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 0ba6d98..6d8ab8f 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -751,8 +751,8 @@ jobs: make -j 4 make install cp $GITHUB_WORKSPACE/data/nefarious/* $HOME/.local/lib - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 5936fe6..e9d42cc 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -911,8 +911,8 @@ jobs: make -j 4 make install cp $GITHUB_WORKSPACE/data/nefarious/* $HOME/.local/lib - - name: Install Atheme - run: sudo apt-get install atheme-services + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies run: |- python -m pip install --upgrade pip diff --git a/irctest/server_tests/list.py b/irctest/server_tests/list.py index 2649076..56e300f 100644 --- a/irctest/server_tests/list.py +++ b/irctest/server_tests/list.py @@ -7,9 +7,6 @@ The LIST command (`RFC 1459 import time - -from irctest import cases - from irctest import cases, runner from irctest.numerics import RPL_LIST, RPL_LISTEND, RPL_LISTSTART @@ -300,7 +297,12 @@ class FaketimeListTestCase(_BasedListTestCase): self.sendLine(2, "LIST C>10") self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) - elif self.controller.software_name in ("Solanum", "Charybdis", "InspIRCd"): + elif self.controller.software_name in ( + "Solanum", + "Charybdis", + "InspIRCd", + "Nefarious", + ): self.sendLine(2, "LIST C>2") self.assertEqual(self._parseChanList(2), {"#chan1"}) @@ -323,6 +325,9 @@ class FaketimeListTestCase(_BasedListTestCase): @cases.mark_isupport("ELIST") @cases.mark_specifications("Modern") + @cases.xfailIfSoftware( + ["UnrealIRCd"], "UnrealIRCd advertises ELIST=T but does not implement it" + ) def testListTopicTime(self): """ "T: Searching based on topic time, via the "Tval" @@ -387,6 +392,7 @@ class FaketimeListTestCase(_BasedListTestCase): "InspIRCd", "Plexus4", "Hybrid", + "Nefarious", ): self.sendLine(1, "LIST T>2") self.assertEqual(self._parseChanList(1), {"#chan1"}) diff --git a/irctest/server_tests/names.py b/irctest/server_tests/names.py index 801fcac..3285be2 100644 --- a/irctest/server_tests/names.py +++ b/irctest/server_tests/names.py @@ -9,6 +9,7 @@ from irctest import cases, runner from irctest.numerics import RPL_ENDOFNAMES, RPL_NAMREPLY from irctest.patma import ANYSTR, StrRe + class NamesTestCase(cases.BaseServerTestCase): def _testNames(self, symbol): self.connectClient("nick1") From 8e2670df54438b270737652e6df5665a232e7ab0 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Wed, 13 Apr 2022 20:19:07 +0200 Subject: [PATCH 046/143] unreal: Prevent download of geoIP database on first startup (#156) --- .github/workflows/test-devel.yml | 4 ++++ .github/workflows/test-stable.yml | 4 ++++ workflows.yml | 2 ++ 3 files changed, 10 insertions(+) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 6d8ab8f..445df17 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -315,6 +315,8 @@ jobs: CFLAGS="-O0 -march=x86-64" CXXFLAGS="$CFLAGS" ./Config -quick make -j 4 make install + # Prevent download of geoIP database on first startup + sed -i 's/loadmodule "geoip_classic";//' ~/.local/unrealircd/conf/modules.default.conf - name: Make artefact tarball run: cd ~; tar -czf artefacts-unrealircd.tar.gz .local/ go/ - name: Upload build artefacts @@ -359,6 +361,8 @@ jobs: CFLAGS="-O0 -march=x86-64" CXXFLAGS="$CFLAGS" ./Config -quick make -j 4 make install + # Prevent download of geoIP database on first startup + sed -i 's/loadmodule "geoip_classic";//' ~/.local/unrealircd/conf/modules.default.conf - name: Make artefact tarball run: cd ~; tar -czf artefacts-unrealircd-5.tar.gz .local/ go/ - name: Upload build artefacts diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index e9d42cc..52e4088 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -355,6 +355,8 @@ jobs: CFLAGS="-O0 -march=x86-64" CXXFLAGS="$CFLAGS" ./Config -quick make -j 4 make install + # Prevent download of geoIP database on first startup + sed -i 's/loadmodule "geoip_classic";//' ~/.local/unrealircd/conf/modules.default.conf - name: Make artefact tarball run: cd ~; tar -czf artefacts-unrealircd.tar.gz .local/ go/ - name: Upload build artefacts @@ -399,6 +401,8 @@ jobs: CFLAGS="-O0 -march=x86-64" CXXFLAGS="$CFLAGS" ./Config -quick make -j 4 make install + # Prevent download of geoIP database on first startup + sed -i 's/loadmodule "geoip_classic";//' ~/.local/unrealircd/conf/modules.default.conf - name: Make artefact tarball run: cd ~; tar -czf artefacts-unrealircd-5.tar.gz .local/ go/ - name: Upload build artefacts diff --git a/workflows.yml b/workflows.yml index aea95da..811593c 100644 --- a/workflows.yml +++ b/workflows.yml @@ -284,6 +284,8 @@ software: CFLAGS="-O0 -march=x86-64" CXXFLAGS="$CFLAGS" ./Config -quick make -j 4 make install + # Prevent download of geoIP database on first startup + sed -i 's/loadmodule "geoip_classic";//' ~/.local/unrealircd/conf/modules.default.conf unrealircd-5: name: UnrealIRCd 5 From 778510e021d4f4124ef41ad909057804b258dc87 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Wed, 13 Apr 2022 20:25:45 +0200 Subject: [PATCH 047/143] Bump Unreal to 6.0.3 and remove ELIST workarounds (#158) Workarounds that are only still needed for Unreal 5 and and Hybrid/Plexus --- .github/workflows/test-stable.yml | 2 +- irctest/controllers/unrealircd.py | 1 + irctest/server_tests/list.py | 121 +++++++++++------------------- workflows.yml | 4 +- 4 files changed, 48 insertions(+), 80 deletions(-) diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 52e4088..74e5d7f 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -342,7 +342,7 @@ jobs: uses: actions/checkout@v2 with: path: unrealircd - ref: daa0c11f285c7123ba9fa2966dee2d1a17729f1e + ref: cedd23ae9cdd5985ce16e9869cbdb808479c3fc4 repository: unrealircd/unrealircd - name: Build UnrealIRCd 6 run: | diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py index a24bcc0..12dfdbe 100644 --- a/irctest/controllers/unrealircd.py +++ b/irctest/controllers/unrealircd.py @@ -139,6 +139,7 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): supports_sts = False extban_mute_char = "quiet" if installed_version() >= 6 else "q" + software_version = installed_version() def create_config(self) -> None: super().create_config() diff --git a/irctest/server_tests/list.py b/irctest/server_tests/list.py index 56e300f..53d148d 100644 --- a/irctest/server_tests/list.py +++ b/irctest/server_tests/list.py @@ -236,6 +236,17 @@ class FaketimeListTestCase(_BasedListTestCase): @cases.mark_isupport("ELIST") @cases.mark_specifications("Modern") + @cases.xfailIfSoftware( + ["Plexus4", "Hybrid"], + "Hybrid and Plexus4 filter on ELIST=C with the opposite meaning", + ) + @cases.xfailIf( + lambda self: bool( + self.controller.software_name == "UnrealIRCd" + and self.controller.software_version == 5 + ), + "UnrealIRCd <6.0.3 filters on ELIST=C with the opposite meaning", + ) def testListCreationTime(self): """ " C: Searching based on channel creation time, via the "Cval" @@ -282,51 +293,32 @@ class FaketimeListTestCase(_BasedListTestCase): self._sleep_minutes(1) - if self.controller.software_name in ("UnrealIRCd", "Plexus4", "Hybrid"): - self.sendLine(2, "LIST C<2") - self.assertEqual(self._parseChanList(2), {"#chan1"}) + self.sendLine(2, "LIST C>2") + self.assertEqual(self._parseChanList(2), {"#chan1"}) - self.sendLine(2, "LIST C>2") - self.assertEqual(self._parseChanList(2), {"#chan2"}) + self.sendLine(2, "LIST C<2") + self.assertEqual(self._parseChanList(2), {"#chan2"}) - self.sendLine(2, "LIST C>0") - self.assertEqual(self._parseChanList(2), set()) - - self.sendLine(2, "LIST C<0") - self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) - - self.sendLine(2, "LIST C>10") - self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) - elif self.controller.software_name in ( - "Solanum", - "Charybdis", - "InspIRCd", - "Nefarious", - ): - self.sendLine(2, "LIST C>2") - self.assertEqual(self._parseChanList(2), {"#chan1"}) - - self.sendLine(2, "LIST C<2") - self.assertEqual(self._parseChanList(2), {"#chan2"}) - - self.sendLine(2, "LIST C<0") - if self.controller.software_name == "InspIRCd": - self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) - else: - self.assertEqual(self._parseChanList(2), set()) - - self.sendLine(2, "LIST C>0") - self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) - - self.sendLine(2, "LIST C<10") + self.sendLine(2, "LIST C<0") + if self.controller.software_name == "InspIRCd": self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) else: - assert False, f"{self.controller.software_name} not supported" + self.assertEqual(self._parseChanList(2), set()) + + self.sendLine(2, "LIST C>0") + self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) + + self.sendLine(2, "LIST C<10") + self.assertEqual(self._parseChanList(2), {"#chan1", "#chan2"}) @cases.mark_isupport("ELIST") @cases.mark_specifications("Modern") - @cases.xfailIfSoftware( - ["UnrealIRCd"], "UnrealIRCd advertises ELIST=T but does not implement it" + @cases.xfailIf( + lambda self: bool( + self.controller.software_name == "UnrealIRCd" + and self.controller.software_version == 5 + ), + "UnrealIRCd <6.0.3 advertises ELIST=T but does not implement it", ) def testListTopicTime(self): """ @@ -371,46 +363,21 @@ class FaketimeListTestCase(_BasedListTestCase): self._sleep_minutes(1) - if self.controller.software_name in ("UnrealIRCd",): - self.sendLine(1, "LIST T<2") - self.assertEqual(self._parseChanList(1), {"#chan1"}) + self.sendLine(1, "LIST T>2") + self.assertEqual(self._parseChanList(1), {"#chan1"}) - self.sendLine(1, "LIST T>2") - self.assertEqual(self._parseChanList(1), {"#chan2"}) + self.sendLine(1, "LIST T<2") + self.assertEqual(self._parseChanList(1), {"#chan2"}) - self.sendLine(1, "LIST T>0") - self.assertEqual(self._parseChanList(1), set()) - - self.sendLine(1, "LIST T<0") - self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"}) - - self.sendLine(1, "LIST T>10") - self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"}) - elif self.controller.software_name in ( - "Solanum", - "Charybdis", - "InspIRCd", - "Plexus4", - "Hybrid", - "Nefarious", - ): - self.sendLine(1, "LIST T>2") - self.assertEqual(self._parseChanList(1), {"#chan1"}) - - self.sendLine(1, "LIST T<2") - self.assertEqual(self._parseChanList(1), {"#chan2"}) - - self.sendLine(1, "LIST T<0") - if self.controller.software_name == "InspIRCd": - # Insp internally represents "LIST T>0" like "LIST" - self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"}) - else: - self.assertEqual(self._parseChanList(1), set()) - - self.sendLine(1, "LIST T>0") - self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"}) - - self.sendLine(1, "LIST T<10") + self.sendLine(1, "LIST T<0") + if self.controller.software_name == "InspIRCd": + # Insp internally represents "LIST T>0" like "LIST" self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"}) else: - assert False, f"{self.controller.software_name} not supported" + self.assertEqual(self._parseChanList(1), set()) + + self.sendLine(1, "LIST T>0") + self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"}) + + self.sendLine(1, "LIST T<10") + self.assertEqual(self._parseChanList(1), {"#chan1", "#chan2"}) diff --git a/workflows.yml b/workflows.yml index 811593c..88c29d6 100644 --- a/workflows.yml +++ b/workflows.yml @@ -267,8 +267,8 @@ software: name: UnrealIRCd 6 repository: unrealircd/unrealircd refs: - stable: daa0c11f285c7123ba9fa2966dee2d1a17729f1e # 6.0.2 + a few commits - release: 29fd2e772a6b4b9107daa4e3c237df454b055810 # 6.0.2 + stable: cedd23ae9cdd5985ce16e9869cbdb808479c3fc4 # 6.0.3 + release: cedd23ae9cdd5985ce16e9869cbdb808479c3fc4 # 6.0.3 devel: unreal60_dev devel_release: null path: unrealircd From 2cd5fc1dca80e65023607ac94811ee2801318277 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Thu, 14 Apr 2022 19:56:06 +0200 Subject: [PATCH 048/143] dashboard: Add a page for each implementation (#159) --- irctest/dashboard/format.py | 82 +++++++++++++++++++++++++++++++++---- irctest/dashboard/style.css | 6 +-- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/irctest/dashboard/format.py b/irctest/dashboard/format.py index e08873b..df9c3ed 100644 --- a/irctest/dashboard/format.py +++ b/irctest/dashboard/format.py @@ -131,6 +131,24 @@ def append_docstring(element: ET.Element, obj: object) -> None: element.append(rst_to_element(obj.__doc__)) +def build_job_html(job: str, results: List[CaseResult]) -> ET.Element: + jobs = sorted({result.job for result in results}) + root = ET.Element("html") + head = ET.SubElement(root, "head") + ET.SubElement(head, "title").text = job + ET.SubElement(head, "link", rel="stylesheet", type="text/css", href="./style.css") + + body = ET.SubElement(root, "body") + + ET.SubElement(body, "h1").text = job + + table = build_test_table(jobs, results) + table.set("class", "job-results test-matrix") + body.append(table) + + return root + + def build_module_html( jobs: List[str], results: List[CaseResult], module_name: str ) -> ET.Element: @@ -147,10 +165,19 @@ def build_module_html( append_docstring(body, module) - results_by_class = group_by(results, lambda r: r.class_name) + table = build_test_table(jobs, results) + table.set("class", "module-results test-matrix") + body.append(table) - table = ET.SubElement(body, "table") - table.set("class", "test-matrix") + return root + + +def build_test_table(jobs: List[str], results: List[CaseResult]) -> ET.Element: + results_by_module_and_class = group_by( + results, lambda r: (r.module_name, r.class_name) + ) + + table = ET.Element("table") job_row = ET.Element("tr") ET.SubElement(job_row, "th") # column of case name @@ -159,7 +186,11 @@ def build_module_html( ET.SubElement(ET.SubElement(cell, "div"), "span").text = job cell.set("class", "job-name") - for (class_name, class_results) in sorted(results_by_class.items()): + for ((module_name, class_name), class_results) in sorted( + results_by_module_and_class.items() + ): + module = importlib.import_module(module_name) + # Header row: class name header_row = ET.SubElement(table, "tr") th = ET.SubElement(header_row, "th", colspan=str(len(jobs) + 1)) @@ -237,12 +268,12 @@ def build_module_html( if result.message: cell.set("title", result.message) - return root + return table def write_html_pages( output_dir: Path, results: List[CaseResult] -) -> List[Tuple[str, str]]: +) -> List[Tuple[str, str, str]]: """Returns the list of (module_name, file_name).""" output_dir.mkdir(parents=True, exist_ok=True) results_by_module = group_by(results, lambda r: r.module_name) @@ -285,7 +316,19 @@ def write_html_pages( root = build_module_html(module_jobs, module_results, module_name) file_name = f"{module_name}.xhtml" write_xml_file(output_dir / file_name, root) - pages.append((module_name, file_name)) + pages.append(("module", module_name, file_name)) + + for category in ("server", "client"): + for job in [job for job in job_categories if job_categories[job] == category]: + job_results = [ + result + for result in results + if result.job == job or result.job.startswith(job + "-") + ] + root = build_job_html(job, job_results) + file_name = f"{job}.xhtml" + write_xml_file(output_dir / file_name, root) + pages.append(("job", job, file_name)) return pages @@ -300,7 +343,7 @@ def write_test_outputs(output_dir: Path, results: List[CaseResult]) -> None: fd.write(result.system_out) -def write_html_index(output_dir: Path, pages: List[Tuple[str, str]]) -> None: +def write_html_index(output_dir: Path, pages: List[Tuple[str, str, str]]) -> None: root = ET.Element("html") head = ET.SubElement(root, "head") ET.SubElement(head, "title").text = "irctest dashboard" @@ -310,16 +353,37 @@ def write_html_index(output_dir: Path, pages: List[Tuple[str, str]]) -> None: ET.SubElement(body, "h1").text = "irctest dashboard" + module_pages = [] + job_pages = [] + for (page_type, title, file_name) in sorted(pages): + if page_type == "module": + module_pages.append((title, file_name)) + elif page_type == "job": + job_pages.append((title, file_name)) + else: + assert False, page_type + + ET.SubElement(body, "h2").text = "Tests by command/specification" + dl = ET.SubElement(body, "dl") dl.set("class", "module-index") - for (module_name, file_name) in sorted(pages): + for (module_name, file_name) in sorted(module_pages): module = importlib.import_module(module_name) link = ET.SubElement(ET.SubElement(dl, "dt"), "a", href=f"./{file_name}") link.text = module_name append_docstring(ET.SubElement(dl, "dd"), module) + ET.SubElement(body, "h2").text = "Tests by implementation" + + ul = ET.SubElement(body, "ul") + ul.set("class", "job-index") + + for (job, file_name) in sorted(job_pages): + link = ET.SubElement(ET.SubElement(ul, "li"), "a", href=f"./{file_name}") + link.text = job + write_xml_file(output_dir / "index.xhtml", root) diff --git a/irctest/dashboard/style.css b/irctest/dashboard/style.css index 628fd8e..9a9c982 100644 --- a/irctest/dashboard/style.css +++ b/irctest/dashboard/style.css @@ -51,17 +51,17 @@ table.test-matrix .expected-failure { } /* Rotate headers, thanks to https://css-tricks.com/rotated-table-column-headers/ */ -th.job-name { +table.module-results th.job-name { height: 140px; white-space: nowrap; } -th.job-name > div { +table.module-results th.job-name > div { transform: translate(28px, 50px) rotate(315deg); width: 40px; } -th.job-name > div > span { +table.module-results th.job-name > div > span { border-bottom: 1px solid grey; padding-left: 0px; } From 9bc331483aebcc7a615be2e6f21959830450e22e Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Thu, 14 Apr 2022 20:21:49 +0200 Subject: [PATCH 049/143] deploy_to_netlify.py: Fix crash on the first commit of a PR (#160) --- .github/deploy_to_netlify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/deploy_to_netlify.py b/.github/deploy_to_netlify.py index 5b748f0..1f77187 100755 --- a/.github/deploy_to_netlify.py +++ b/.github/deploy_to_netlify.py @@ -32,7 +32,7 @@ context_suffix = "" command = ["netlify", "deploy", "--dir=dashboard/"] if is_pull_request: pr_number = github_event["number"] - sha = github_event["after"] + sha = github_event.get("after") or github_event["pull_request"]["head"]["sha"] # Aliases can't exceed 37 chars command.extend(["--alias", f"pr-{pr_number}-{sha[0:10]}"]) context_suffix = " (pull_request)" From 5122c04826b6715650446aa6008ecfc6f7cf9048 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Thu, 14 Apr 2022 21:28:12 +0200 Subject: [PATCH 050/143] Add tests for the two invite lists (#149) * Add tests for the two invite lists * Add workaround for Hybrid * Skip testInviteList on ircu2 * Fix merge --- irctest/controllers/inspircd.py | 1 + irctest/server_tests/invite.py | 84 ++++++++++++++++++++++++++++++++- irctest/specifications.py | 1 + pytest.ini | 1 + 4 files changed, 86 insertions(+), 1 deletion(-) diff --git a/irctest/controllers/inspircd.py b/irctest/controllers/inspircd.py index 2f71c1a..ca14f7e 100644 --- a/irctest/controllers/inspircd.py +++ b/irctest/controllers/inspircd.py @@ -62,6 +62,7 @@ TEMPLATE_CONFIG = """ # Protocol: + diff --git a/irctest/server_tests/invite.py b/irctest/server_tests/invite.py index 2f9c305..68bef09 100644 --- a/irctest/server_tests/invite.py +++ b/irctest/server_tests/invite.py @@ -7,11 +7,12 @@ The INVITE command (`RFC 1459 import pytest -from irctest import cases +from irctest import cases, runner from irctest.numerics import ( ERR_BANNEDFROMCHAN, ERR_CHANOPRIVSNEEDED, ERR_INVITEONLYCHAN, + ERR_NEEDMOREPARAMS, ERR_NOSUCHNICK, ERR_NOTONCHANNEL, ERR_USERONCHANNEL, @@ -372,6 +373,87 @@ class InviteTestCase(cases.BaseServerTestCase): params=["foo", "bar", "#chan", ANYSTR], ) + @cases.mark_specifications("RFC2812", "Modern") + @cases.xfailIfSoftware( + ["ircu2"], + "Uses 346/347 instead of 336/337 to reply to INVITE " + "https://github.com/UndernetIRC/ircu2/pull/20", + ) + def testInviteList(self): + self.connectClient("foo") + self.connectClient("bar") + self.getMessages(1) + self.getMessages(2) + + self.sendLine(1, "JOIN #chan") + self.getMessages(1) + + self.sendLine(1, "INVITE bar #chan") + self.getMessages(1) + self.getMessages(2) + + self.sendLine(2, "INVITE") + m = self.getMessage(2) + if m.command == ERR_NEEDMOREPARAMS: + raise runner.NotImplementedByController("INVITE with no parameter") + if m.command != "337": + # Hybrid always sends an empty list; so skip this. + self.assertMessageMatch( + m, + command="336", + params=["bar", "#chan"], + ) + m = self.getMessage(2) + self.assertMessageMatch( + m, + command="337", + params=["bar", ANYSTR], + ) + + @cases.mark_isupport("INVEX") + @cases.mark_specifications("Modern") + def testInvexList(self): + self.connectClient("foo") + self.getMessages(1) + + if "INVEX" in self.server_support: + invex = self.server_support.get("INVEX") or "I" + else: + raise runner.NotImplementedByController("INVEX") + + self.sendLine(1, "JOIN #chan") + self.getMessages(1) + + self.sendLine(1, f"MODE #chan +{invex} bar!*@*") + self.getMessages(1) + + self.sendLine(1, f"MODE #chan +{invex}") + m = self.getMessage(1) + if len(m.params) == 3: + # Old format + self.assertMessageMatch( + m, + command="346", + params=["foo", "#chan", "bar!*@*"], + ) + else: + self.assertMessageMatch( + m, + command="346", + params=[ + "foo", + "#chan", + "bar!*@*", + StrRe("foo(!.*@.*)?"), + StrRe("[0-9]+"), + ], + ) + self.assertMessageMatch( + self.getMessage(1), + command="347", + params=["foo", "#chan", ANYSTR], + ) + @cases.mark_specifications("Ergo") def testInviteExemptsFromBan(self): # regression test for ergochat/ergo#1876; diff --git a/irctest/specifications.py b/irctest/specifications.py index 16257fa..99e0646 100644 --- a/irctest/specifications.py +++ b/irctest/specifications.py @@ -51,6 +51,7 @@ class Capabilities(enum.Enum): class IsupportTokens(enum.Enum): BOT = "BOT" ELIST = "ELIST" + INVEX = "INVEX" PREFIX = "PREFIX" MONITOR = "MONITOR" STATUSMSG = "STATUSMSG" diff --git a/pytest.ini b/pytest.ini index 1bebd29..9b5d071 100644 --- a/pytest.ini +++ b/pytest.ini @@ -33,6 +33,7 @@ markers = # isupport tokens BOT ELIST + INVEX MONITOR PREFIX STATUSMSG From 45dd42e682c58ed98113cd9cf9527ace6274ae46 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Fri, 15 Apr 2022 16:01:36 +0200 Subject: [PATCH 051/143] Replace incorrect uses of NotImplementedByController exception (#161) --- irctest/cases.py | 10 -------- irctest/server_tests/channel.py | 4 +-- irctest/server_tests/chathistory.py | 2 +- irctest/server_tests/echo_message.py | 38 +++++++++------------------- irctest/server_tests/help.py | 6 ++--- irctest/server_tests/invite.py | 4 +-- irctest/server_tests/isupport.py | 4 +-- irctest/server_tests/kick.py | 2 +- irctest/server_tests/links.py | 4 +-- irctest/server_tests/monitor.py | 5 ++-- irctest/server_tests/names.py | 2 +- irctest/server_tests/regressions.py | 2 +- irctest/server_tests/wallops.py | 4 +-- irctest/server_tests/who.py | 14 +++++----- irctest/server_tests/whowas.py | 4 +-- 15 files changed, 40 insertions(+), 65 deletions(-) diff --git a/irctest/cases.py b/irctest/cases.py index 9a7056d..18b4a38 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -762,16 +762,6 @@ def skipUnlessHasMechanism( return decorator -def skipUnlessHasSasl(f: Callable[..., _TReturn]) -> Callable[..., _TReturn]: - @functools.wraps(f) - def newf(self: _TSelf, *args: Any, **kwargs: Any) -> _TReturn: - if not self.controller.supported_sasl_mechanisms: - raise runner.NotImplementedByController("SASL") - return f(self, *args, **kwargs) - - return newf - - def xfailIf( condition: Callable[..., bool], reason: str ) -> Callable[[Callable[..., _TReturn]], Callable[..., _TReturn]]: diff --git a/irctest/server_tests/channel.py b/irctest/server_tests/channel.py index 61472f7..d2bc763 100644 --- a/irctest/server_tests/channel.py +++ b/irctest/server_tests/channel.py @@ -22,7 +22,7 @@ class ChannelCaseSensitivityTestCase(cases.BaseServerTestCase): self.connectClient("foo") self.connectClient("bar") if self.server_support["CASEMAPPING"] != casemapping: - raise runner.NotImplementedByController( + raise runner.ImplementationChoice( "Casemapping {} not implemented".format(casemapping) ) self.joinClient(1, name1) @@ -47,7 +47,7 @@ class ChannelCaseSensitivityTestCase(cases.BaseServerTestCase): self.connectClient("foo") self.connectClient("bar") if self.server_support["CASEMAPPING"] != casemapping: - raise runner.NotImplementedByController( + raise runner.ImplementationChoice( "Casemapping {} not implemented".format(casemapping) ) self.joinClient(1, name1) diff --git a/irctest/server_tests/chathistory.py b/irctest/server_tests/chathistory.py index 53b4d16..28a201a 100644 --- a/irctest/server_tests/chathistory.py +++ b/irctest/server_tests/chathistory.py @@ -47,7 +47,7 @@ def skip_ngircd(f): @functools.wraps(f) def newf(self, *args, **kwargs): if self.controller.software_name == "ngIRCd": - raise runner.NotImplementedByController("nicks longer 9 characters") + raise runner.OptionalExtensionNotSupported("nicks longer 9 characters") return f(self, *args, **kwargs) return newf diff --git a/irctest/server_tests/echo_message.py b/irctest/server_tests/echo_message.py index 1da3412..063028a 100644 --- a/irctest/server_tests/echo_message.py +++ b/irctest/server_tests/echo_message.py @@ -5,7 +5,6 @@ import pytest from irctest import cases -from irctest.basecontrollers import NotImplementedByController from irctest.irc_utils.junkdrawer import random_name from irctest.patma import ANYDICT @@ -23,31 +22,18 @@ class EchoMessageTestCase(cases.BaseServerTestCase): @cases.mark_capabilities("echo-message") def testEchoMessage(self, command, solo, server_time): """""" - self.addClient() - self.sendLine(1, "CAP LS 302") - capabilities = self.getCapLs(1) - if "echo-message" not in capabilities: - raise NotImplementedByController("echo-message") - if server_time and "server-time" not in capabilities: - raise NotImplementedByController("server-time") - - # TODO: check also without this - self.sendLine( - 1, - "CAP REQ :echo-message{}".format(" server-time" if server_time else ""), - ) - self.getRegistrationMessage(1) - # TODO: Remove this one the trailing space issue is fixed in Charybdis - # and Mammon: - # self.assertMessageMatch(m, command='CAP', - # params=['*', 'ACK', 'echo-message'] + - # (['server-time'] if server_time else []), - # fail_msg='Did not ACK advertised capabilities: {msg}') - self.sendLine(1, "USER f * * :foo") - self.sendLine(1, "NICK baz") - self.sendLine(1, "CAP END") - self.skipToWelcome(1) - self.getMessages(1) + if server_time: + self.connectClient( + "baz", + capabilities=["echo-message", "server-time"], + skip_if_cap_nak=True, + ) + else: + self.connectClient( + "baz", + capabilities=["echo-message", "server-time"], + skip_if_cap_nak=True, + ) self.sendLine(1, "JOIN #chan") diff --git a/irctest/server_tests/help.py b/irctest/server_tests/help.py index ff5069e..32475fa 100644 --- a/irctest/server_tests/help.py +++ b/irctest/server_tests/help.py @@ -22,17 +22,17 @@ def with_xfails(f): @functools.wraps(f) def newf(self, command, *args, **kwargs): if command == "HELP" and self.controller.software_name == "Bahamut": - raise runner.NotImplementedByController( + raise runner.ImplementationChoice( "fail because Bahamut forwards /HELP to HelpServ (but not /HELPOP)" ) if self.controller.software_name in ("irc2", "ircu2", "ngIRCd"): - raise runner.NotImplementedByController( + raise runner.ImplementationChoice( "numerics in reply to /HELP and /HELPOP (uses NOTICE instead)" ) if self.controller.software_name == "UnrealIRCd": - raise runner.NotImplementedByController( + raise runner.ImplementationChoice( "fails because Unreal uses custom numerics " "https://github.com/unrealircd/unrealircd/pull/184" ) diff --git a/irctest/server_tests/invite.py b/irctest/server_tests/invite.py index 68bef09..bb6e6df 100644 --- a/irctest/server_tests/invite.py +++ b/irctest/server_tests/invite.py @@ -395,7 +395,7 @@ class InviteTestCase(cases.BaseServerTestCase): self.sendLine(2, "INVITE") m = self.getMessage(2) if m.command == ERR_NEEDMOREPARAMS: - raise runner.NotImplementedByController("INVITE with no parameter") + raise runner.OptionalExtensionNotSupported("INVITE with no parameter") if m.command != "337": # Hybrid always sends an empty list; so skip this. self.assertMessageMatch( @@ -419,7 +419,7 @@ class InviteTestCase(cases.BaseServerTestCase): if "INVEX" in self.server_support: invex = self.server_support.get("INVEX") or "I" else: - raise runner.NotImplementedByController("INVEX") + raise runner.IsupportTokenNotSupported("INVEX") self.sendLine(1, "JOIN #chan") self.getMessages(1) diff --git a/irctest/server_tests/isupport.py b/irctest/server_tests/isupport.py index 2c3cbaa..ae0ac18 100644 --- a/irctest/server_tests/isupport.py +++ b/irctest/server_tests/isupport.py @@ -16,7 +16,7 @@ class IsupportTestCase(cases.BaseServerTestCase): self.connectClient("foo") if "PREFIX" not in self.server_support: - raise runner.NotImplementedByController("PREFIX") + raise runner.IsupportTokenNotSupported("PREFIX") if self.server_support["PREFIX"] == "": # "The value is OPTIONAL and when it is not specified indicates that no @@ -80,7 +80,7 @@ class IsupportTestCase(cases.BaseServerTestCase): self.connectClient("foo") if "TARGMAX" not in self.server_support: - raise runner.NotImplementedByController("TARGMAX") + raise runner.IsupportTokenNotSupported("TARGMAX") parts = self.server_support["TARGMAX"].split(",") for part in parts: diff --git a/irctest/server_tests/kick.py b/irctest/server_tests/kick.py index 7f2149e..06fda59 100644 --- a/irctest/server_tests/kick.py +++ b/irctest/server_tests/kick.py @@ -236,7 +236,7 @@ class KickTestCase(cases.BaseServerTestCase): if item ) if targmax.get("KICK", "1") == "1": - raise runner.NotImplementedByController("Multi-target KICK") + raise runner.OptionalExtensionNotSupported("Multi-target KICK") # TODO: check foo is an operator diff --git a/irctest/server_tests/links.py b/irctest/server_tests/links.py index 62c746f..4a77554 100644 --- a/irctest/server_tests/links.py +++ b/irctest/server_tests/links.py @@ -34,7 +34,7 @@ class LinksTestCase(cases.BaseServerTestCase): self.sendLine(1, "LINKS") messages = self.getMessages(1) if messages[0].command == ERR_UNKNOWNCOMMAND: - raise runner.NotImplementedByController("LINKS") + raise runner.OptionalCommandNotSupported("LINKS") # Ignore '/LINKS has been disabled' from ircu2 messages = [m for m in messages if m.command != "NOTICE"] @@ -95,7 +95,7 @@ class ServicesLinksTestCase(cases.BaseServerTestCase): messages = self.getMessages(1) if messages[0].command == ERR_UNKNOWNCOMMAND: - raise runner.NotImplementedByController("LINKS") + raise runner.OptionalCommandNotSupported("LINKS") # Ignore '/LINKS has been disabled' from ircu2 messages = [m for m in messages if m.command != "NOTICE"] diff --git a/irctest/server_tests/monitor.py b/irctest/server_tests/monitor.py index 211ee67..4f2f552 100644 --- a/irctest/server_tests/monitor.py +++ b/irctest/server_tests/monitor.py @@ -2,8 +2,7 @@ `IRCv3 MONITOR `_ """ -from irctest import cases -from irctest.basecontrollers import NotImplementedByController +from irctest import cases, runner from irctest.client_mock import NoMessageException from irctest.numerics import ( RPL_ENDOFMONLIST, @@ -17,7 +16,7 @@ from irctest.patma import ANYSTR, StrRe class MonitorTestCase(cases.BaseServerTestCase): def check_server_support(self): if "MONITOR" not in self.server_support: - raise NotImplementedByController("MONITOR") + raise runner.IsupportTokenNotSupported("MONITOR") def assertMononline(self, client, nick, m=None): if not m: diff --git a/irctest/server_tests/names.py b/irctest/server_tests/names.py index 3285be2..597bf8b 100644 --- a/irctest/server_tests/names.py +++ b/irctest/server_tests/names.py @@ -68,7 +68,7 @@ class NamesTestCase(cases.BaseServerTestCase): if item ) if targmax.get("NAMES", "1") == "1": - raise runner.NotImplementedByController("Multi-target NAMES") + raise runner.OptionalExtensionNotSupported("Multi-target NAMES") self.sendLine(1, "JOIN #chan1") self.sendLine(1, "JOIN #chan2") diff --git a/irctest/server_tests/regressions.py b/irctest/server_tests/regressions.py index c09fdf5..bf9145c 100644 --- a/irctest/server_tests/regressions.py +++ b/irctest/server_tests/regressions.py @@ -58,7 +58,7 @@ class RegressionsTestCase(cases.BaseServerTestCase): @cases.mark_capabilities("message-tags", "batch", "echo-message", "server-time") def testTagCap(self): if self.controller.software_name == "UnrealIRCd": - raise runner.NotImplementedByController( + raise runner.ImplementationChoice( "Arbitrary +draft/reply values (TODO: adapt this test to use real " "values so their pass Unreal's validation) " "https://bugs.unrealircd.org/view.php?id=5948" diff --git a/irctest/server_tests/wallops.py b/irctest/server_tests/wallops.py index e0d00e0..d23dc58 100644 --- a/irctest/server_tests/wallops.py +++ b/irctest/server_tests/wallops.py @@ -44,7 +44,7 @@ class WallopsTestCase(cases.BaseServerTestCase): messages = self.getMessages(1) if ERR_UNKNOWNCOMMAND in (message.command for message in messages): - raise runner.NotImplementedByController("WALLOPS") + raise runner.OptionalCommandNotSupported("WALLOPS") for message in messages: self.assertMessageMatch( message, @@ -77,7 +77,7 @@ class WallopsTestCase(cases.BaseServerTestCase): self.sendLine(1, "WALLOPS :hi everyone") message = self.getMessage(1) if message.command == ERR_UNKNOWNCOMMAND: - raise runner.NotImplementedByController("WALLOPS") + raise runner.OptionalCommandNotSupported("WALLOPS") self.assertMessageMatch( message, command=ERR_NOPRIVILEGES, params=["nick1", ANYSTR] ) diff --git a/irctest/server_tests/who.py b/irctest/server_tests/who.py index f5e8903..10430d8 100644 --- a/irctest/server_tests/who.py +++ b/irctest/server_tests/who.py @@ -88,7 +88,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): @cases.mark_specifications("Modern") def testWhoStar(self): if self.controller.software_name == "Bahamut": - raise runner.NotImplementedByController("WHO mask") + raise runner.OptionalExtensionNotSupported("WHO mask") self._init() @@ -119,7 +119,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): @cases.mark_specifications("Modern") def testWhoNick(self, mask): if "*" in mask and self.controller.software_name == "Bahamut": - raise runner.NotImplementedByController("WHO mask") + raise runner.OptionalExtensionNotSupported("WHO mask") self._init() @@ -149,7 +149,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): ) def testWhoUsernameRealName(self, mask): if "*" in mask and self.controller.software_name == "Bahamut": - raise runner.NotImplementedByController("WHO mask") + raise runner.OptionalExtensionNotSupported("WHO mask") self._init() @@ -202,7 +202,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): @cases.mark_specifications("Modern") def testWhoNickAway(self, mask): if "*" in mask and self.controller.software_name == "Bahamut": - raise runner.NotImplementedByController("WHO mask") + raise runner.OptionalExtensionNotSupported("WHO mask") self._init() @@ -231,7 +231,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): @cases.mark_specifications("Modern") def testWhoNickOper(self, mask): if "*" in mask and self.controller.software_name == "Bahamut": - raise runner.NotImplementedByController("WHO mask") + raise runner.OptionalExtensionNotSupported("WHO mask") self._init() @@ -265,7 +265,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): @cases.mark_specifications("Modern") def testWhoNickAwayAndOper(self, mask): if "*" in mask and self.controller.software_name == "Bahamut": - raise runner.NotImplementedByController("WHO mask") + raise runner.OptionalExtensionNotSupported("WHO mask") self._init() @@ -299,7 +299,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): @cases.mark_specifications("Modern") def testWhoChan(self, mask): if "*" in mask and self.controller.software_name == "Bahamut": - raise runner.NotImplementedByController("WHO mask") + raise runner.OptionalExtensionNotSupported("WHO mask") self._init() diff --git a/irctest/server_tests/whowas.py b/irctest/server_tests/whowas.py index cbe9e3b..79da7a4 100644 --- a/irctest/server_tests/whowas.py +++ b/irctest/server_tests/whowas.py @@ -283,7 +283,7 @@ class WhowasTestCase(cases.BaseServerTestCase): -- https://github.com/ircdocs/modern-irc/pull/170 """ if self.controller.software_name == "Bahamut": - raise runner.NotImplementedByController("WHOWAS mask") + raise runner.OptionalExtensionNotSupported("WHOWAS mask") self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS *ck2") @@ -409,7 +409,7 @@ class WhowasTestCase(cases.BaseServerTestCase): if item ) if targmax.get("WHOWAS", "1") == "1": - raise runner.NotImplementedByController("Multi-target WHOWAS") + raise runner.OptionalExtensionNotSupported("Multi-target WHOWAS") self.connectClient("nick2", ident="ident2") self.sendLine(2, "QUIT :bye") From a923353ec43cdb3e66ed39660fa567f07ed9ea16 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 16 Apr 2022 08:12:27 +0200 Subject: [PATCH 052/143] Add test for ban exception mode (+e) (#162) --- irctest/controllers/inspircd.py | 1 + irctest/server_tests/chmodes/ban.py | 64 ++++++++++++++++++++- irctest/server_tests/chmodes/mute_extban.py | 2 +- 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/irctest/controllers/inspircd.py b/irctest/controllers/inspircd.py index ca14f7e..75e2a69 100644 --- a/irctest/controllers/inspircd.py +++ b/irctest/controllers/inspircd.py @@ -60,6 +60,7 @@ TEMPLATE_CONFIG = """ target="services.example.org"> # Protocol: + diff --git a/irctest/server_tests/chmodes/ban.py b/irctest/server_tests/chmodes/ban.py index 471e40a..d0f4829 100644 --- a/irctest/server_tests/chmodes/ban.py +++ b/irctest/server_tests/chmodes/ban.py @@ -3,9 +3,10 @@ Channel ban (`RFC 1459 `__, `RFC 2812 `__, `Modern `__) +and ban exception (`Modern `__) """ -from irctest import cases +from irctest import cases, runner from irctest.numerics import ERR_BANNEDFROMCHAN, RPL_BANLIST, RPL_ENDOFBANLIST from irctest.patma import ANYSTR, StrRe @@ -33,7 +34,7 @@ class BanModeTestCase(cases.BaseServerTestCase): @cases.mark_specifications("Modern") def testBanList(self): - """`RPL_BANLIST `""" + """`RPL_BANLIST `_""" self.connectClient("chanop") self.joinChannel(1, "#chan") self.getMessages(1) @@ -77,6 +78,65 @@ class BanModeTestCase(cases.BaseServerTestCase): ], ) + @cases.mark_specifications("Modern") + def testBanException(self): + """`Exception mode `_ and checked against + `ISUPPORT CHANMODES `_""" + self.connectClient("chanop", name="chanop") + + if "EXCEPTS" in self.server_support: + mode = self.server_support["EXCEPTS"] or "e" + if "CHANMODES" in self.server_support: + self.assertIn( + mode, + self.server_support["CHANMODES"], + fail_msg="ISUPPORT EXCEPTS is present, but '{item}' is missing " + "from 'CHANMODES={list}'", + ) + self.assertIn( + mode, + self.server_support["CHANMODES"].split(",")[0], + fail_msg="ISUPPORT EXCEPTS is present, but '{item}' is not " + "in group A", + ) + else: + mode = "e" + if "CHANMODES" in self.server_support: + if "e" not in self.server_support["CHANMODES"]: + raise runner.OptionalExtensionNotSupported( + "Ban exception (or mode letter is not +e)" + ) + self.assertIn( + mode, + self.server_support["CHANMODES"].split(",")[0], + fail_msg="Mode +e (assumed to be ban exception) is present, " + "but 'e' is not in group A", + ) + else: + raise runner.OptionalExtensionNotSupported("ISUPPORT CHANMODES") + + self.sendLine("chanop", "JOIN #chan") + self.getMessages("chanop") + self.sendLine("chanop", "MODE #chan +b ba*!*@*") + self.getMessages("chanop") + + # banned client cannot join + self.connectClient("Bar", name="bar") + self.sendLine("bar", "JOIN #chan") + self.assertMessageMatch(self.getMessage("bar"), command=ERR_BANNEDFROMCHAN) + + # chanop sets exception + self.sendLine("chanop", "MODE #chan +e *ar!*@*") + self.assertMessageMatch(self.getMessage("chanop"), command="MODE") + + # client can now join + self.sendLine("bar", "JOIN #chan") + self.assertMessageMatch(self.getMessage("bar"), command="JOIN") + + # TODO: Add testBanExceptionList, once the numerics are specified in Modern + @cases.mark_specifications("Ergo") def testCaseInsensitive(self): """Some clients allow unsetting modes if their argument matches diff --git a/irctest/server_tests/chmodes/mute_extban.py b/irctest/server_tests/chmodes/mute_extban.py index 5c3c6d4..cd8e131 100644 --- a/irctest/server_tests/chmodes/mute_extban.py +++ b/irctest/server_tests/chmodes/mute_extban.py @@ -198,7 +198,7 @@ class MuteExtbanTestCase(cases.BaseServerTestCase): self.getMessages(client) # +e grants an exemption to +b - self.sendLine("chanop", f"MODE #chan +e {prefix}{self.char()}:*!~evan@*") + self.sendLine("chanop", f"MODE #chan +e {prefix}{self.char()}:*!*evan@*") replies = {msg.command for msg in self.getMessages("chanop")} self.assertIn("MODE", replies) self.assertNotIn(ERR_CHANOPRIVSNEEDED, replies) From a15025a2763aef43f85ff415b0c3a568c389e050 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 16 Apr 2022 12:15:56 +0200 Subject: [PATCH 053/143] Add tests for JOIN with some invalid channels in the target param (#163) --- irctest/cases.py | 6 ++ irctest/numerics.py | 1 + irctest/server_tests/join.py | 127 +++++++++++++++++++++++++++++++-- irctest/server_tests/kick.py | 7 +- irctest/server_tests/names.py | 7 +- irctest/server_tests/whowas.py | 7 +- 6 files changed, 133 insertions(+), 22 deletions(-) diff --git a/irctest/cases.py b/irctest/cases.py index 18b4a38..7b37815 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -709,6 +709,12 @@ class BaseServerTestCase( self.server_support[param] = None welcome.append(m) + self.targmax: Dict[str, Optional[str]] = dict( + item.split(":", 1) # type: ignore + for item in (self.server_support.get("TARGMAX") or "").split(",") + if item + ) + return welcome def joinClient(self, client: TClientName, channel: str) -> None: diff --git a/irctest/numerics.py b/irctest/numerics.py index 8aafb08..8dc9736 100644 --- a/irctest/numerics.py +++ b/irctest/numerics.py @@ -142,6 +142,7 @@ ERR_USERONCHANNEL = "443" ERR_NOLOGIN = "444" ERR_SUMMONDISABLED = "445" ERR_USERSDISABLED = "446" +ERR_FORBIDDENCHANNEL = "448" ERR_NOTREGISTERED = "451" ERR_NEEDMOREPARAMS = "461" ERR_ALREADYREGISTRED = "462" diff --git a/irctest/server_tests/join.py b/irctest/server_tests/join.py index e3433e6..833f9c1 100644 --- a/irctest/server_tests/join.py +++ b/irctest/server_tests/join.py @@ -5,8 +5,26 @@ The JOIN command (`RFC 1459 `Modern `__) """ -from irctest import cases +from irctest import cases, runner from irctest.irc_utils import ambiguities +from irctest.numerics import ( + ERR_BADCHANMASK, + ERR_FORBIDDENCHANNEL, + ERR_NOSUCHCHANNEL, + RPL_ENDOFNAMES, + RPL_NAMREPLY, +) +from irctest.patma import ANYSTR, StrRe + +ERR_BADCHANNAME = "479" # Hybrid only, and conflicts with others + + +JOIN_ERROR_NUMERICS = { + ERR_BADCHANMASK, + ERR_NOSUCHCHANNEL, + ERR_FORBIDDENCHANNEL, + ERR_BADCHANNAME, +} class JoinTestCase(cases.BaseServerTestCase): @@ -26,13 +44,22 @@ class JoinTestCase(cases.BaseServerTestCase): self.connectClient("foo") self.sendLine(1, "JOIN #chan") received_commands = {m.command for m in self.getMessages(1)} - expected_commands = {"353", "366"} # RPL_NAMREPLY # RPL_ENDOFNAMES - self.assertTrue( - expected_commands.issubset(received_commands), + expected_commands = {RPL_NAMREPLY, RPL_ENDOFNAMES, "JOIN"} + acceptable_commands = expected_commands | {"MODE"} + self.assertLessEqual( # set inclusion + expected_commands, + received_commands, "Server sent {} commands, but at least {} were expected.".format( received_commands, expected_commands ), ) + self.assertLessEqual( # ditto + received_commands, + acceptable_commands, + "Server sent {} commands, but only {} were expected.".format( + received_commands, acceptable_commands + ), + ) @cases.mark_specifications("RFC2812") def testJoinNamreply(self): @@ -117,3 +144,95 @@ class JoinTestCase(cases.BaseServerTestCase): '"foo" with an optional "+" or "@" prefix, but got: ' "{msg}", ) + + def testJoinPartiallyInvalid(self): + """TODO: specify this in Modern""" + self.connectClient("foo") + if int(self.targmax.get("JOIN") or "4") < 2: + raise runner.OptionalExtensionNotSupported("multi-channel JOIN") + + self.sendLine(1, "JOIN #valid,inv@lid") + messages = self.getMessages(1) + received_commands = {m.command for m in messages} + expected_commands = {RPL_NAMREPLY, RPL_ENDOFNAMES, "JOIN"} + acceptable_commands = expected_commands | JOIN_ERROR_NUMERICS | {"MODE"} + self.assertLessEqual( + expected_commands, + received_commands, + "Server sent {} commands, but at least {} were expected.".format( + received_commands, expected_commands + ), + ) + self.assertLessEqual( + received_commands, + acceptable_commands, + "Server sent {} commands, but only {} were expected.".format( + received_commands, acceptable_commands + ), + ) + + nb_errors = 0 + for m in messages: + if m.command in JOIN_ERROR_NUMERICS: + nb_errors += 1 + self.assertMessageMatch(m, params=["foo", "inv@lid", ANYSTR]) + + self.assertEqual( + nb_errors, + 1, + fail_msg="Expected 1 error when joining channels '#valid' and 'inv@lid', " + "got {got}", + ) + + @cases.mark_capabilities("batch", "labeled-response") + def testJoinPartiallyInvalidLabeledResponse(self): + """TODO: specify this in Modern""" + self.connectClient( + "foo", capabilities=["batch", "labeled-response"], skip_if_cap_nak=True + ) + if int(self.targmax.get("JOIN") or "4") < 2: + raise runner.OptionalExtensionNotSupported("multi-channel JOIN") + + self.sendLine(1, "@label=label1 JOIN #valid,inv@lid") + messages = self.getMessages(1) + + first_msg = messages.pop(0) + last_msg = messages.pop(-1) + self.assertMessageMatch( + first_msg, command="BATCH", params=[StrRe(r"\+.*"), "labeled-response"] + ) + batch_id = first_msg.params[0][1:] + self.assertMessageMatch(last_msg, command="BATCH", params=["-" + batch_id]) + + received_commands = {m.command for m in messages} + expected_commands = {RPL_NAMREPLY, RPL_ENDOFNAMES, "JOIN"} + acceptable_commands = expected_commands | JOIN_ERROR_NUMERICS | {"MODE"} + self.assertLessEqual( + expected_commands, + received_commands, + "Server sent {} commands, but at least {} were expected.".format( + received_commands, expected_commands + ), + ) + self.assertLessEqual( + received_commands, + acceptable_commands, + "Server sent {} commands, but only {} were expected.".format( + received_commands, acceptable_commands + ), + ) + + nb_errors = 0 + for m in messages: + self.assertIn("batch", m.tags) + self.assertEqual(m.tags["batch"], batch_id) + if m.command in JOIN_ERROR_NUMERICS: + nb_errors += 1 + self.assertMessageMatch(m, params=["foo", "inv@lid", ANYSTR]) + + self.assertEqual( + nb_errors, + 1, + fail_msg="Expected 1 error when joining channels '#valid' and 'inv@lid', " + "got {got}", + ) diff --git a/irctest/server_tests/kick.py b/irctest/server_tests/kick.py index 06fda59..ac511de 100644 --- a/irctest/server_tests/kick.py +++ b/irctest/server_tests/kick.py @@ -230,12 +230,7 @@ class KickTestCase(cases.BaseServerTestCase): self.connectClient("qux") self.joinChannel(4, "#chan") - targmax = dict( - item.split(":", 1) - for item in self.server_support.get("TARGMAX", "").split(",") - if item - ) - if targmax.get("KICK", "1") == "1": + if self.targmax.get("KICK", "1") == "1": raise runner.OptionalExtensionNotSupported("Multi-target KICK") # TODO: check foo is an operator diff --git a/irctest/server_tests/names.py b/irctest/server_tests/names.py index 597bf8b..f45731a 100644 --- a/irctest/server_tests/names.py +++ b/irctest/server_tests/names.py @@ -62,12 +62,7 @@ class NamesTestCase(cases.BaseServerTestCase): def _testNamesMultipleChannels(self, symbol): self.connectClient("nick1") - targmax = dict( - item.split(":", 1) - for item in self.server_support.get("TARGMAX", "").split(",") - if item - ) - if targmax.get("NAMES", "1") == "1": + if self.targmax.get("NAMES", "1") == "1": raise runner.OptionalExtensionNotSupported("Multi-target NAMES") self.sendLine(1, "JOIN #chan1") diff --git a/irctest/server_tests/whowas.py b/irctest/server_tests/whowas.py index 79da7a4..76b250c 100644 --- a/irctest/server_tests/whowas.py +++ b/irctest/server_tests/whowas.py @@ -403,12 +403,7 @@ class WhowasTestCase(cases.BaseServerTestCase): self.connectClient("nick1") - targmax = dict( - item.split(":", 1) - for item in self.server_support.get("TARGMAX", "").split(",") - if item - ) - if targmax.get("WHOWAS", "1") == "1": + if self.targmax.get("WHOWAS", "1") == "1": raise runner.OptionalExtensionNotSupported("Multi-target WHOWAS") self.connectClient("nick2", ident="ident2") From c0af9bc0a834fb56112a6056067b379ad30f146a Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 25 Apr 2022 23:05:32 -0400 Subject: [PATCH 054/143] add a regression test for ergochat/ergo#1928 LIST on a nonexistent channel does not get an error response. --- irctest/server_tests/list.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/irctest/server_tests/list.py b/irctest/server_tests/list.py index 53d148d..18d94a4 100644 --- a/irctest/server_tests/list.py +++ b/irctest/server_tests/list.py @@ -105,6 +105,26 @@ class ListTestCase(_BasedListTestCase): "or 323 (RPL_LISTEND), or but: {msg}", ) + @cases.mark_specifications("RFC1459", "RFC2812") + @cases.xfailIfSoftware( + ["Charybdis", "Solanum"], + "Charybdis and Solanum insert ERR_NOSUCHNICK reply in LIST", + ) + def testListNonexistent(self): + """LIST on a nonexistent channel does not send an error + response. + + + """ + self.connectClient("bar") + self.sendLine(1, "LIST #nonexistent") + responses = {msg.command for msg in self.getMessages(1)} + # successful response MUST include RPL_LISTEND: + self.assertIn(RPL_LISTEND, responses) + # and MUST NOT include RPL_LIST (since there is no matching channel) + # or any error numerics: + self.assertLessEqual(responses, {RPL_LISTSTART, RPL_LISTEND}) + @cases.mark_isupport("ELIST") @cases.mark_specifications("Modern") def testListMask(self): From c4d86aef4e0703b41a0f3e410a9fea14d76e0816 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Tue, 26 Apr 2022 02:12:52 -0400 Subject: [PATCH 055/143] bump black to fix click dependency issue https://github.com/psf/black/ #2964 --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 00ff787..8d20213 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ exclude: ^irctest/scram repos: - repo: https://github.com/psf/black - rev: 20.8b1 + rev: 22.3.0 hooks: - id: black language_version: python3 From 011bdff7e4ec6d2627bc7d25eb4ddd9aca0c2607 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 26 Apr 2022 22:22:36 +0200 Subject: [PATCH 056/143] Fix ELIST detection --- irctest/server_tests/list.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/irctest/server_tests/list.py b/irctest/server_tests/list.py index 18d94a4..2063acb 100644 --- a/irctest/server_tests/list.py +++ b/irctest/server_tests/list.py @@ -206,8 +206,8 @@ class ListTestCase(_BasedListTestCase): """ self.connectClient("foo") - if "M" not in self.server_support.get("ELIST", ""): - raise runner.OptionalExtensionNotSupported("ELIST=M") + if "U" not in self.server_support.get("ELIST", ""): + raise runner.OptionalExtensionNotSupported("ELIST=U") self.sendLine(1, "JOIN #chan1") self.getMessages(1) From ca35069487257bae48252a1973b16d1a7a568509 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 26 Apr 2022 22:29:22 +0200 Subject: [PATCH 057/143] Replace remote download of irc2 with a git clone To avoid flakiness and hitting the irc.org servers too hard --- .github/workflows/test-stable.yml | 33 ++++++++++------------ workflows.yml | 46 +++++++++++++++---------------- 2 files changed, 37 insertions(+), 42 deletions(-) diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 74e5d7f..85fa56c 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -784,36 +784,31 @@ jobs: uses: actions/setup-python@v2 with: python-version: 3.7 - - name: Get source code - run: curl http://ftp.irc.org/ftp/irc/server/irc2.11.2p3.tgz | tar -zx - - name: Configure - run: 'cd $GITHUB_WORKSPACE/irc2.11.2p3 - + - name: Checkout irc2 + uses: actions/checkout@v2 + with: + path: irc2.11.2p3 + ref: 59649f24c3a5c27bad5648b48774f27475bccfd3 + repository: irc-archive/irc2-mirror + - name: Build irc2 + run: | + # Configure + cd $GITHUB_WORKSPACE/irc2.11.2p3 ./configure --prefix=$HOME/.local/ - cd x86* - echo "#define CMDLINE_CONFIG/" >> config.h - echo "#define DEFAULT_SPLIT_USERS 0" >> config.h - echo "#define DEFAULT_SPLIT_SERVERS 0" >> config.h - #echo "#undef LIST_ALIS_NOTE" >> config.h - # TODO: find a better way to make it not fork... + echo "#define fork() (0)" >> config.h - echo "#define fork() (0)" >> config.h' - - name: Compile and install - run: 'cd $GITHUB_WORKSPACE/irc2.11.2p3/x86* - + # Compile and install + cd $GITHUB_WORKSPACE/irc2.11.2p3/x86* make -j 4 all - make install - mkdir -p $HOME/.local/bin - - cp $HOME/.local/sbin/ircd $HOME/.local/bin/ircd' + cp $HOME/.local/sbin/ircd $HOME/.local/bin/ircd - name: Install system dependencies run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies diff --git a/workflows.yml b/workflows.yml index 88c29d6..b626a32 100644 --- a/workflows.yml +++ b/workflows.yml @@ -159,32 +159,32 @@ software: irc2: name: irc2 separate_build_job: false - install_steps: - stable: - - name: Get source code - run: |- - curl http://ftp.irc.org/ftp/irc/server/irc2.11.2p3.tgz | tar -zx - - name: Configure - run: |- - cd $GITHUB_WORKSPACE/irc2.11.2p3 - ./configure --prefix=$HOME/.local/ - cd x86* - echo "#define CMDLINE_CONFIG/" >> config.h - echo "#define DEFAULT_SPLIT_USERS 0" >> config.h - echo "#define DEFAULT_SPLIT_SERVERS 0" >> config.h - #echo "#undef LIST_ALIS_NOTE" >> config.h - # TODO: find a better way to make it not fork... - echo "#define fork() (0)" >> config.h - - name: Compile and install - run: |- - cd $GITHUB_WORKSPACE/irc2.11.2p3/x86* - make -j 4 all - make install - mkdir -p $HOME/.local/bin - cp $HOME/.local/sbin/ircd $HOME/.local/bin/ircd + repository : irc-archive/irc2-mirror + path: irc2.11.2p3 + cache: true + refs: + stable: 59649f24c3a5c27bad5648b48774f27475bccfd3 # irc2.11.2p3 release: null devel: null devel_release: null + build_script: | + # Configure + cd $GITHUB_WORKSPACE/irc2.11.2p3 + ./configure --prefix=$HOME/.local/ + cd x86* + echo "#define CMDLINE_CONFIG/" >> config.h + echo "#define DEFAULT_SPLIT_USERS 0" >> config.h + echo "#define DEFAULT_SPLIT_SERVERS 0" >> config.h + #echo "#undef LIST_ALIS_NOTE" >> config.h + # TODO: find a better way to make it not fork... + echo "#define fork() (0)" >> config.h + + # Compile and install + cd $GITHUB_WORKSPACE/irc2.11.2p3/x86* + make -j 4 all + make install + mkdir -p $HOME/.local/bin + cp $HOME/.local/sbin/ircd $HOME/.local/bin/ircd ircu2: name: ircu2 From 8c73ac2b757fc9564c58c04d36b5cc583ebabc3f Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Thu, 28 Apr 2022 20:12:18 +0200 Subject: [PATCH 058/143] patma: Add support for operators in keys Will be used to match either '@bot' or '@draft/bot'. --- irctest/patma.py | 14 ++++++++------ irctest/self_tests/cases.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/irctest/patma.py b/irctest/patma.py index 30ce278..b02c15e 100644 --- a/irctest/patma.py +++ b/irctest/patma.py @@ -155,13 +155,15 @@ def match_dict( for (expected_key, expected_value) in expected.items(): if isinstance(expected_key, RemainingKeys): remaining_keys_wildcard = (expected_key.key, expected_value) - elif isinstance(expected_key, Operator): - raise NotImplementedError(f"Unsupported operator: {expected_key}") else: - if expected_key not in got: - return False - got_value = got.pop(expected_key) - if not match_string(got_value, expected_value): + for key in got: + if match_string(key, expected_key) and match_string( + got[key], expected_value + ): + got.pop(key) + break + else: + # Found no (key, value) pair matching the request return False if remaining_keys_wildcard: diff --git a/irctest/self_tests/cases.py b/irctest/self_tests/cases.py index 63cf659..6f8920b 100644 --- a/irctest/self_tests/cases.py +++ b/irctest/self_tests/cases.py @@ -179,6 +179,39 @@ MESSAGE_SPECS: List[Tuple[Dict, List[str], List[str], List[str]]] = [ "expected tags to match {'tag1': 'bar', RemainingKeys(ANYSTR): ANYOPTSTR}, got {}", ] ), + ( + # the specification: + dict( + tags={StrRe("tag[12]"): "bar", **ANYDICT}, + command="PRIVMSG", + params=["#chan", "hello"], + ), + # matches: + [ + "@tag1=bar PRIVMSG #chan :hello", + "@tag1=bar;tag2= PRIVMSG #chan :hello", + "@tag1=bar :foo!baz@qux PRIVMSG #chan :hello", + "@tag2=bar PRIVMSG #chan :hello", + "@tag1=bar;tag2= PRIVMSG #chan :hello", + "@tag1=;tag2=bar PRIVMSG #chan :hello", + ], + # and does not match: + [ + "PRIVMG #chan :hello", + "@tag1=value1 PRIVMSG #chan :hello", + "PRIVMSG #chan hello2", + "PRIVMSG #chan2 hello", + ":foo!baz@qux PRIVMSG #chan hello", + ], + # and they each error with: + [ + "expected command to be PRIVMSG, got PRIVMG", + "expected tags to match {StrRe(r'tag[12]'): 'bar', RemainingKeys(ANYSTR): ANYOPTSTR}, got {'tag1': 'value1'}", + "expected params to match ['#chan', 'hello'], got ['#chan', 'hello2']", + "expected params to match ['#chan', 'hello'], got ['#chan2', 'hello']", + "expected tags to match {StrRe(r'tag[12]'): 'bar', RemainingKeys(ANYSTR): ANYOPTSTR}, got {}", + ] + ), ( # the specification: dict( From 641bea5f0a9350d0b3c633066aa451275d05a781 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Thu, 28 Apr 2022 20:38:49 +0200 Subject: [PATCH 059/143] bot_mode: Make draft/ prefix optional (#167) The spec is ratified. --- irctest/server_tests/bot_mode.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/irctest/server_tests/bot_mode.py b/irctest/server_tests/bot_mode.py index 1b9cf7f..0c550ba 100644 --- a/irctest/server_tests/bot_mode.py +++ b/irctest/server_tests/bot_mode.py @@ -1,5 +1,5 @@ """ -`IRCv3 draft bot mode `_ +`IRCv3 bot mode `_ """ from irctest import cases, runner @@ -85,7 +85,7 @@ class BotModeTestCase(cases.BaseServerTestCase): self.getMessage("user"), command="PRIVMSG", params=["usernick", "beep boop"], - tags={"draft/bot": None, **ANYDICT}, + tags={StrRe("(draft/)?bot"): None, **ANYDICT}, ) @cases.xfailIfSoftware( @@ -111,7 +111,7 @@ class BotModeTestCase(cases.BaseServerTestCase): self.getMessage("user"), command="PRIVMSG", params=["#chan", "beep boop"], - tags={"draft/bot": None, **ANYDICT}, + tags={StrRe("(draft/)?bot"): None, **ANYDICT}, ) def testBotWhox(self): From 627f0b6415cd34c4e123acffe7490632e5ae4bf5 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 1 May 2022 11:31:50 +0200 Subject: [PATCH 061/143] Try fixing flakyness of Plexus4 and others --- irctest/cases.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/irctest/cases.py b/irctest/cases.py index 7b37815..2186304 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -69,6 +69,30 @@ TController = TypeVar("TController", bound=basecontrollers._BaseController) T = TypeVar("T") +def retry(f: TCallable) -> TCallable: + """Retry the function if it raises ConnectionClosed; as a workaround for flaky + connection, such as:: + + 1: connects to server. + 1 -> S: NICK foo + 1 -> S: USER username * * :Realname + S -> 1: :My.Little.Server NOTICE * :*** Found your hostname (cached) + S -> 1: :My.Little.Server NOTICE * :*** Checking Ident + S -> 1: :My.Little.Server NOTICE * :*** No Ident response + S -> 1: ERROR :Closing Link: cpu-pool.com (Use a different port) + """ + + @functools.wraps(f) + def newf(*args, **kwargs): # type: ignore + try: + return f(*args, **kwargs) + except ConnectionClosed: + time.sleep(1) + return f(*args, **kwargs) + + return newf # type: ignore + + class ChannelJoinException(Exception): def __init__(self, code: str, params: List[str]): super().__init__(f"Failed to join channel ({code}): {params}") @@ -661,6 +685,7 @@ class BaseServerTestCase( m = self.getRegistrationMessage(client) self.assertIn(m.command, ["900", "903"], str(m)) + @retry def connectClient( self, nick: str, From 83017483bae2a981bc6f0f345c352dc1adede338 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Fri, 13 May 2022 13:49:40 -0400 Subject: [PATCH 062/143] test +R user mode as implemented in Ergo (#168) --- irctest/server_tests/umodes/registeredonly.py | 124 ++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 irctest/server_tests/umodes/registeredonly.py diff --git a/irctest/server_tests/umodes/registeredonly.py b/irctest/server_tests/umodes/registeredonly.py new file mode 100644 index 0000000..1b63d2c --- /dev/null +++ b/irctest/server_tests/umodes/registeredonly.py @@ -0,0 +1,124 @@ +""" +Test the registered-only DM user mode (commonly +R). +""" + +from irctest import cases +from irctest.numerics import ERR_NEEDREGGEDNICK + + +@cases.mark_services +class RegisteredOnlyUmodeTestCase(cases.BaseServerTestCase): + @cases.mark_specifications("Ergo") + def testRegisteredOnlyUserMode(self): + """Test the +R registered-only mode.""" + self.controller.registerUser(self, "evan", "sesame") + self.controller.registerUser(self, "carmen", "pink") + + self.connectClient( + "evan", + name="evan", + account="evan", + password="sesame", + capabilities=["sasl"], + ) + self.connectClient("shivaram", name="shivaram") + self.sendLine("evan", "MODE evan +R") + self.assertMessageMatch( + self.getMessage("evan"), command="MODE", params=["evan", "+R"] + ) + + # this DM should be blocked by +R registered-only + self.getMessages("shivaram") + self.sendLine("shivaram", "PRIVMSG evan :hey there") + self.assertMessageMatch( + self.getMessage("shivaram"), + command=ERR_NEEDREGGEDNICK, + ) + self.assertEqual(self.getMessages("evan"), []) + + self.connectClient( + "carmen", + name="carmen", + account="carmen", + password="pink", + capabilities=["sasl"], + ) + self.getMessages("evan") + self.sendLine("carmen", "PRIVMSG evan :hey there") + self.assertEqual(self.getMessages("carmen"), []) + # this message should go through fine: + self.assertMessageMatch( + self.getMessage("evan"), + command="PRIVMSG", + params=["evan", "hey there"], + ) + + @cases.mark_specifications("Ergo") + def testRegisteredOnlyUserModeAcceptCommand(self): + """Test that the ACCEPT command can authorize another user + to send the accept-er direct messages, overriding the + +R registered-only mode.""" + self.controller.registerUser(self, "evan", "sesame") + self.connectClient( + "evan", + name="evan", + account="evan", + password="sesame", + capabilities=["sasl"], + ) + self.connectClient("shivaram", name="shivaram") + self.sendLine("evan", "MODE evan +R") + self.assertMessageMatch( + self.getMessage("evan"), command="MODE", params=["evan", "+R"] + ) + self.sendLine("evan", "ACCEPT shivaram") + self.getMessages("evan") + + self.sendLine("shivaram", "PRIVMSG evan :hey there") + self.assertEqual(self.getMessages("shivaram"), []) + self.assertMessageMatch( + self.getMessage("evan"), + command="PRIVMSG", + params=["evan", "hey there"], + ) + + self.sendLine("evan", "ACCEPT -shivaram") + self.getMessages("evan") + self.sendLine("shivaram", "PRIVMSG evan :how's it going") + self.assertMessageMatch( + self.getMessage("shivaram"), + command=ERR_NEEDREGGEDNICK, + ) + self.assertEqual(self.getMessages("evan"), []) + + @cases.mark_specifications("Ergo") + def testRegisteredOnlyUserModeAutoAcceptOnDM(self): + """Test that sending someone a DM automatically authorizes them to + reply, overriding the +R registered-only mode.""" + self.controller.registerUser(self, "evan", "sesame") + self.connectClient( + "evan", + name="evan", + account="evan", + password="sesame", + capabilities=["sasl"], + ) + self.connectClient("shivaram", name="shivaram") + self.sendLine("evan", "MODE evan +R") + self.assertMessageMatch( + self.getMessage("evan"), command="MODE", params=["evan", "+R"] + ) + self.sendLine("evan", "PRIVMSG shivaram :hey there") + self.getMessages("evan") + self.assertMessageMatch( + self.getMessage("shivaram"), + command="PRIVMSG", + params=["shivaram", "hey there"], + ) + self.sendLine("shivaram", "PRIVMSG evan :how's it going") + self.assertEqual(self.getMessages("shivaram"), []) + self.assertMessageMatch( + self.getMessage("evan"), + command="PRIVMSG", + params=["evan", "how's it going"], + ) From 0f100a5c80e7f6c03cb287036ae3b0fc63454fe3 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Fri, 13 May 2022 22:11:32 +0200 Subject: [PATCH 063/143] Work around Unreal >=6.0.4 sending RPL_WHOISSPECIAL by default https://github.com/unrealircd/unrealircd/commit/085490d78094c4768af98911efe1111879ae06db --- irctest/controllers/unrealircd.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py index 12dfdbe..13bb7dc 100644 --- a/irctest/controllers/unrealircd.py +++ b/irctest/controllers/unrealircd.py @@ -100,6 +100,12 @@ set {{ }} }} modes-on-join "+H 100:1d"; // Enables CHATHISTORY + + // Remove RPL_WHOISSPECIAL used to advertise security groups + whois-details {{ + security-groups {{ everyone none; self none; oper none; }} + }} + }} tld {{ From 683f7c0a15a6c32396ed2513543072623796c9dc Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Fri, 13 May 2022 22:29:59 +0200 Subject: [PATCH 064/143] Fix support of Unreal 5 --- irctest/controllers/unrealircd.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py index 13bb7dc..edf9cfb 100644 --- a/irctest/controllers/unrealircd.py +++ b/irctest/controllers/unrealircd.py @@ -101,10 +101,7 @@ set {{ }} modes-on-join "+H 100:1d"; // Enables CHATHISTORY - // Remove RPL_WHOISSPECIAL used to advertise security groups - whois-details {{ - security-groups {{ everyone none; self none; oper none; }} - }} + {set_extras} }} @@ -193,8 +190,20 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): loadmodule "cloak_md5"; """ ) + set_extras = textwrap.indent( + textwrap.dedent( + """ + // Remove RPL_WHOISSPECIAL used to advertise security groups + whois-details { + security-groups { everyone none; self none; oper none; } + } + """ + ), + " ", + ) else: extras = "" + set_extras = "" with self.open_file("empty.txt") as fd: fd.write("\n") @@ -215,6 +224,7 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): pem_path=self.pem_path, empty_file=os.path.join(self.directory, "empty.txt"), extras=extras, + set_extras=set_extras, ) ) From 058fab85b0967ed0f7011d1b161a5ad0a702b441 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 29 May 2022 00:49:21 -0700 Subject: [PATCH 065/143] test incorrect channel keys (#169) --- irctest/server_tests/chmodes/key.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/irctest/server_tests/chmodes/key.py b/irctest/server_tests/chmodes/key.py index 1f8773a..fd4c008 100644 --- a/irctest/server_tests/chmodes/key.py +++ b/irctest/server_tests/chmodes/key.py @@ -27,10 +27,16 @@ class KeyTestCase(cases.BaseServerTestCase): self.connectClient("qux") self.getMessages(2) + # JOIN with a missing key MUST receive ERR_BADCHANNELKEY: self.sendLine(2, "JOIN #chan") - reply = self.getMessages(2) - self.assertNotIn("JOIN", {msg.command for msg in reply}) - self.assertIn(ERR_BADCHANNELKEY, {msg.command for msg in reply}) + reply_cmds = {msg.command for msg in self.getMessages(2)} + self.assertNotIn("JOIN", reply_cmds) + self.assertIn(ERR_BADCHANNELKEY, reply_cmds) + # similarly for JOIN with an incorrect key: + self.sendLine(2, "JOIN #chan bees") + reply_cmds = {msg.command for msg in self.getMessages(2)} + self.assertNotIn("JOIN", reply_cmds) + self.assertIn(ERR_BADCHANNELKEY, reply_cmds) self.sendLine(2, "JOIN #chan beer") reply = self.getMessages(2) From 53710779f0a6401302d1be1a1f8500fdc598250d Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 11 Jun 2022 02:08:58 +0200 Subject: [PATCH 066/143] Prevent tests from blocking for too long Bahamut frequently gets stuck, and waiting 6h is a waste of time. --- .github/workflows/test-devel.yml | 20 ++++++++++++++++++++ .github/workflows/test-devel_release.yml | 3 +++ .github/workflows/test-stable.yml | 23 +++++++++++++++++++++++ make_workflows.py | 1 + 4 files changed, 47 insertions(+) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 445df17..57dceb7 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -445,6 +445,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make bahamut + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -483,6 +484,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make bahamut-anope + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -515,6 +517,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make bahamut-atheme + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -554,6 +557,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/go/sbin:~/go/bin:$PATH make ergo + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -592,6 +596,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make hybrid + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -624,6 +629,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH make inspircd + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -662,6 +668,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH make inspircd-anope + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -700,6 +707,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make ircu2 + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -727,6 +735,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make limnoria + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -764,6 +773,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make nefarious + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -796,6 +806,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH make ngircd + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -834,6 +845,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH make ngircd-anope + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -866,6 +878,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH make ngircd-atheme + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -904,6 +917,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make plexus4 + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -936,6 +950,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make solanum + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -962,6 +977,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make sopel + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -994,6 +1010,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH make unrealircd + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -1026,6 +1043,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH make unrealircd-5 + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -1064,6 +1082,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH make unrealircd-anope + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -1096,6 +1115,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH make unrealircd-atheme + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 diff --git a/.github/workflows/test-devel_release.yml b/.github/workflows/test-devel_release.yml index d639825..84170e4 100644 --- a/.github/workflows/test-devel_release.yml +++ b/.github/workflows/test-devel_release.yml @@ -126,6 +126,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH make inspircd + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -164,6 +165,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH make inspircd-anope + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -196,6 +198,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH make inspircd-atheme + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 85fa56c..44996e5 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -488,6 +488,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make bahamut + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -526,6 +527,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make bahamut-anope + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -558,6 +560,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make bahamut-atheme + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -590,6 +593,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make charybdis + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -629,6 +633,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/go/sbin:~/go/bin:$PATH make ergo + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -667,6 +672,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make hybrid + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -699,6 +705,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH make inspircd + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -737,6 +744,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH make inspircd-anope + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -769,6 +777,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH make inspircd-atheme + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -818,6 +827,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make irc2 + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -856,6 +866,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make ircu2 + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -882,6 +893,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make limnoria + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -919,6 +931,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make nefarious + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -951,6 +964,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH make ngircd + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -989,6 +1003,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH make ngircd-anope + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -1021,6 +1036,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH make ngircd-atheme + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -1059,6 +1075,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make plexus4 + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -1091,6 +1108,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make solanum + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -1117,6 +1135,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make sopel + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -1149,6 +1168,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH make unrealircd + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -1181,6 +1201,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH make unrealircd-5 + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -1219,6 +1240,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH make unrealircd-anope + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 @@ -1251,6 +1273,7 @@ jobs: - name: Test with pytest run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH make unrealircd-atheme + timeout-minutes: 30 - if: always() name: Publish results uses: actions/upload-artifact@v2 diff --git a/make_workflows.py b/make_workflows.py index 6d95675..32e8584 100644 --- a/make_workflows.py +++ b/make_workflows.py @@ -225,6 +225,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs): }, { "name": "Test with pytest", + "timeout-minutes": 30, "run": ( f"PYTEST_ARGS='--junit-xml pytest.xml' " f"PATH=$HOME/.local/bin:$PATH " From 81dac6f5821f6cdb6d3bdc10bd56132c97e2fb15 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 18 Jun 2022 13:14:51 +0200 Subject: [PATCH 067/143] bahamut: lower mainloop delay, and reduce parallelism to make tests less flaky --- .github/workflows/test-devel.yml | 1 + .github/workflows/test-stable.yml | 1 + Makefile | 5 ++--- patches/bahamut_mainloop.patch | 15 +++++++++++++++ workflows.yml | 1 + 5 files changed, 20 insertions(+), 3 deletions(-) create mode 100644 patches/bahamut_mainloop.patch diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 57dceb7..ea75cce 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -67,6 +67,7 @@ jobs: run: | cd $GITHUB_WORKSPACE/Bahamut/ patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch + patch src/s_bsd.c < $GITHUB_WORKSPACE/patches/bahamut_mainloop.patch echo "#undef THROTTLE_ENABLE" >> include/config.h libtoolize --force aclocal diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 44996e5..5a414e1 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -67,6 +67,7 @@ jobs: run: | cd $GITHUB_WORKSPACE/Bahamut/ patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch + patch src/s_bsd.c < $GITHUB_WORKSPACE/patches/bahamut_mainloop.patch echo "#undef THROTTLE_ENABLE" >> include/config.h libtoolize --force aclocal diff --git a/Makefile b/Makefile index 0c82a97..201d2da 100644 --- a/Makefile +++ b/Makefile @@ -122,7 +122,8 @@ bahamut: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.bahamut \ -m 'not services' \ - -n 10 \ + -n 4 \ + -vv -s \ -k '$(BAHAMUT_SELECTORS)' bahamut-atheme: @@ -130,7 +131,6 @@ bahamut-atheme: --controller=irctest.controllers.bahamut \ --services-controller=irctest.controllers.atheme_services \ -m 'services' \ - -n 10 \ -k '$(BAHAMUT_SELECTORS)' bahamut-anope: @@ -138,7 +138,6 @@ bahamut-anope: --controller=irctest.controllers.bahamut \ --services-controller=irctest.controllers.anope_services \ -m 'services' \ - -n 10 \ -k '$(BAHAMUT_SELECTORS)' charybdis: diff --git a/patches/bahamut_mainloop.patch b/patches/bahamut_mainloop.patch new file mode 100644 index 0000000..0e0f992 --- /dev/null +++ b/patches/bahamut_mainloop.patch @@ -0,0 +1,15 @@ +Lower Bahamut's delay between processing incoming commands + +diff --git a/src/s_bsd.c b/src/s_bsd.c +index fcc1d02..951fd8c 100644 +--- a/src/s_bsd.c ++++ b/src/s_bsd.c +@@ -1458,7 +1458,7 @@ int do_client_queue(aClient *cptr) + int dolen = 0, done; + + while (SBufLength(&cptr->recvQ) && !NoNewLine(cptr) && +- ((cptr->status < STAT_UNKNOWN) || (cptr->since - timeofday < 10) || ++ ((cptr->status < STAT_UNKNOWN) || (cptr->since - timeofday < 20) || + IsNegoServer(cptr))) + { + /* If it's become registered as a server, just parse the whole block */ diff --git a/workflows.yml b/workflows.yml index b626a32..97ee359 100644 --- a/workflows.yml +++ b/workflows.yml @@ -105,6 +105,7 @@ software: build_script: | cd $GITHUB_WORKSPACE/Bahamut/ patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch + patch src/s_bsd.c < $GITHUB_WORKSPACE/patches/bahamut_mainloop.patch echo "#undef THROTTLE_ENABLE" >> include/config.h libtoolize --force aclocal From 8a4f254a2165c347515d491abe5bde946c866bc6 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 18 Jun 2022 22:01:36 +0200 Subject: [PATCH 068/143] Reduce parallelism on other servers as well --- Makefile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index 201d2da..6216218 100644 --- a/Makefile +++ b/Makefile @@ -181,28 +181,28 @@ ircu2: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.ircu2 \ -m 'not services and not IRCv3' \ - -n 10 \ + -n 4 \ -k '$(IRCU2_SELECTORS)' nefarious: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.nefarious \ -m 'not services' \ - -n 10 \ + -n 4 \ -k '$(NEFARIOUS_SELECTORS)' snircd: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.snircd \ -m 'not services and not IRCv3' \ - -n 10 \ + -n 4 \ -k '$(SNIRCD_SELECTORS)' irc2: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.irc2 \ -m 'not services and not IRCv3' \ - -n 10 \ + -n 4 \ -k '$(IRC2_SELECTORS)' limnoria: @@ -225,7 +225,7 @@ ngircd: $(PYTEST) $(PYTEST_ARGS) \ --controller irctest.controllers.ngircd \ -m 'not services' \ - -n 10 \ + -n 4 \ -k "$(NGIRCD_SELECTORS)" ngircd-anope: From e205cc1531a4bf16e10fccd345db808672d910bd Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 19 Jun 2022 11:59:03 +0200 Subject: [PATCH 069/143] bahamut: pre-initialize entropy to avoid freezing on GH Actions --- irctest/controllers/bahamut.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/irctest/controllers/bahamut.py b/irctest/controllers/bahamut.py index 6a23aaa..315c73d 100644 --- a/irctest/controllers/bahamut.py +++ b/irctest/controllers/bahamut.py @@ -80,6 +80,19 @@ oper {{ """ +def initialize_entropy(directory: str) -> None: + # https://github.com/DALnet/bahamut/blob/7fc039d403f66a954225c5dc4ad1fe683aedd794/include/dh.h#L35-L38 + nb_rand_bytes = 512 // 8 + # https://github.com/DALnet/bahamut/blob/7fc039d403f66a954225c5dc4ad1fe683aedd794/src/dh.c#L186 + entropy_file_size = nb_rand_bytes * 4 + + # Not actually random; but we don't care. + entropy = b"\x00" * entropy_file_size + + with open(os.path.join(directory, ".ircd.entropy"), "wb") as fd: + fd.write(entropy) + + class BahamutController(BaseServerController, DirectoryBasedController): software_name = "Bahamut" supported_sasl_mechanisms: Set[str] = set() @@ -121,6 +134,11 @@ class BahamutController(BaseServerController, DirectoryBasedController): assert self.directory + # Bahamut reads some bytes from /dev/urandom on startup, which causes + # GitHub Actions to sometimes freeze and timeout. + # This initializes the entropy file so Bahamut does not need to do it itself. + initialize_entropy(self.directory) + # they are hardcoded... thankfully Bahamut reads them from the CWD. shutil.copy(self.pem_path, os.path.join(self.directory, "ircd.crt")) shutil.copy(self.key_path, os.path.join(self.directory, "ircd.key")) From 601f49a9ef24f0c2ff6017f39a972ccea747a609 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 26 Jun 2022 12:37:55 +0200 Subject: [PATCH 070/143] Fix infinite loop when server is slow (eg. Bahamut) --- irctest/basecontrollers.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index e8680b3..4c7c7f9 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -222,6 +222,7 @@ class BaseServerController(_BaseController): raise NotImplementedByController("account registration") def wait_for_port(self) -> None: + started_at = time.time() while not self.port_open: self.check_is_alive() time.sleep(self._port_wait_interval) @@ -244,11 +245,16 @@ class BaseServerController(_BaseController): # ircu2 cuts the connection without a message if registration # is not complete. pass + except socket.timeout: + # irc2 just keeps it open + pass c.close() self.port_open = True - except Exception: - continue + except ConnectionRefusedError: + if time.time() - started_at >= 60: + # waited for 60 seconds, giving up + raise def wait_for_services(self) -> None: assert self.services_controller From 6b6017b40c23d9ed28fb96075f27250280c2b7f4 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 26 Jun 2022 16:37:47 +0200 Subject: [PATCH 071/143] testStarNick: Replace unreliable workaround for irc2 --- irctest/server_tests/regressions.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/irctest/server_tests/regressions.py b/irctest/server_tests/regressions.py index bf9145c..1f11ff5 100644 --- a/irctest/server_tests/regressions.py +++ b/irctest/server_tests/regressions.py @@ -2,10 +2,13 @@ Regression tests for bugs in `Ergo `_. """ -import time - from irctest import cases, runner -from irctest.numerics import ERR_ERRONEUSNICKNAME, ERR_NICKNAMEINUSE, RPL_WELCOME +from irctest.numerics import ( + ERR_ERRONEUSNICKNAME, + ERR_NICKNAMEINUSE, + RPL_HELLO, + RPL_WELCOME, +) from irctest.patma import ANYDICT @@ -111,8 +114,7 @@ class RegressionsTestCase(cases.BaseServerTestCase): self.sendLine(1, "NICK *") self.sendLine(1, "USER u s e r") replies = {"NOTICE"} - time.sleep(2) # give time to slow servers, like irc2 to reply - while replies == {"NOTICE"}: + while replies <= {"NOTICE", RPL_HELLO}: replies = set(msg.command for msg in self.getMessages(1, synchronize=False)) self.assertIn(ERR_ERRONEUSNICKNAME, replies) self.assertNotIn(RPL_WELCOME, replies) From f1c9218fbb23fed9ecd6dfe49304818753d028c2 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Thu, 4 Aug 2022 21:24:48 +0200 Subject: [PATCH 072/143] Bump Go version for Ergo --- .github/workflows/test-devel.yml | 2 +- .github/workflows/test-stable.yml | 2 +- workflows.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index ea75cce..175167e 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -542,7 +542,7 @@ jobs: repository: ergochat/ergo - uses: actions/setup-go@v2 with: - go-version: ^1.18.0 + go-version: ^1.19.0 - run: go version - name: Build Ergo run: | diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 5a414e1..b9a279b 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -618,7 +618,7 @@ jobs: repository: ergochat/ergo - uses: actions/setup-go@v2 with: - go-version: ^1.18.0 + go-version: ^1.19.0 - run: go version - name: Build Ergo run: | diff --git a/workflows.yml b/workflows.yml index 97ee359..27f43e7 100644 --- a/workflows.yml +++ b/workflows.yml @@ -131,7 +131,7 @@ software: pre_deps: - uses: actions/setup-go@v2 with: - go-version: '^1.18.0' + go-version: '^1.19.0' - run: go version separate_build_job: false build_script: | From 6290825c64b492a2b1192979a8adcda9136e1948 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 20 Aug 2022 18:05:47 +0200 Subject: [PATCH 073/143] README: Remove reference to setup.py --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 18edbc5..605c553 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,6 @@ cd ~ git clone https://github.com/ProgVal/irctest.git cd irctest pip3 install --user -r requirements.txt -python3 setup.py install --user ``` Add `~/.local/bin/` (and/or `~/go/bin/` for Ergo) From dbdadec6770a47e8a70578b8b7d8b61879bd9cb7 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Fri, 26 Aug 2022 10:01:41 -0700 Subject: [PATCH 074/143] test that WHO ignores +i for bare nicknames (#171) --- irctest/server_tests/who.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/irctest/server_tests/who.py b/irctest/server_tests/who.py index 10430d8..4956bd5 100644 --- a/irctest/server_tests/who.py +++ b/irctest/server_tests/who.py @@ -503,3 +503,34 @@ class WhoServicesTestCase(BaseWhoTestCase, cases.BaseServerTestCase): command=RPL_ENDOFWHO, params=["otherNick", InsensitiveStr("coolNick"), ANYSTR], ) + + +class WhoInvisibleTestCase(cases.BaseServerTestCase): + @cases.mark_specifications("Modern") + def testWhoInvisible(self): + if self.controller.software_name == "Bahamut": + raise runner.OptionalExtensionNotSupported("WHO mask") + + self.connectClient("evan", name="evan") + self.sendLine("evan", "MODE evan +i") + self.getMessages("evan") + + self.connectClient("shivaram", name="shivaram") + self.getMessages("shivaram") + self.sendLine("shivaram", "WHO eva*") + reply_cmds = {msg.command for msg in self.getMessages("shivaram")} + self.assertEqual(reply_cmds, {RPL_ENDOFWHO}) + + # invisibility should not be respected for plain nicknames, only for masks: + self.sendLine("shivaram", "WHO evan") + replies = self.getMessages("shivaram") + reply_cmds = {msg.command for msg in replies} + self.assertEqual(reply_cmds, {RPL_WHOREPLY, RPL_ENDOFWHO}) + + # invisibility should not be respected if the users share a channel + self.joinChannel("evan", "#test") + self.joinChannel("shivaram", "#test") + self.sendLine("shivaram", "WHO eva*") + replies = self.getMessages("shivaram") + reply_cmds = {msg.command for msg in replies} + self.assertEqual(reply_cmds, {RPL_WHOREPLY, RPL_ENDOFWHO}) From 507f5b74261c109be39bb357ce2e38f44254ecd6 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 10 Sep 2022 11:38:29 +0200 Subject: [PATCH 075/143] Use pathlib to work with temporary config dirs --- irctest/basecontrollers.py | 22 +++++++++++----------- irctest/controllers/anope_services.py | 11 ++++------- irctest/controllers/atheme_services.py | 5 ++--- irctest/controllers/bahamut.py | 12 ++++++------ irctest/controllers/base_hybrid.py | 5 ++--- irctest/controllers/ergo.py | 10 ++++------ irctest/controllers/inspircd.py | 3 +-- irctest/controllers/irc2.py | 5 ++--- irctest/controllers/ircu2.py | 5 ++--- irctest/controllers/limnoria.py | 5 +---- irctest/controllers/mammon.py | 3 +-- irctest/controllers/ngircd.py | 5 ++--- irctest/controllers/snircd.py | 5 ++--- irctest/controllers/sopel.py | 10 +++++----- irctest/controllers/unrealircd.py | 12 ++++++------ 15 files changed, 51 insertions(+), 67 deletions(-) diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index 4c7c7f9..c2744a3 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -2,6 +2,7 @@ from __future__ import annotations import dataclasses import os +from pathlib import Path import shutil import socket import subprocess @@ -87,7 +88,7 @@ class DirectoryBasedController(_BaseController): """Helper for controllers whose software configuration is based on an arbitrary directory.""" - directory: Optional[str] + directory: Optional[Path] def __init__(self, test_config: TestCaseControllerConfig): super().__init__(test_config) @@ -110,22 +111,21 @@ class DirectoryBasedController(_BaseController): """Open a file in the configuration directory.""" assert self.directory if os.sep in name: - dir_ = os.path.join(self.directory, os.path.dirname(name)) - if not os.path.isdir(dir_): - os.makedirs(dir_) - assert os.path.isdir(dir_) - return open(os.path.join(self.directory, name), mode) + dir_ = self.directory / os.path.dirname(name) + dir_.mkdir(parents=True, exist_ok=True) + assert dir_.is_dir() + return (self.directory / name).open(mode) def create_config(self) -> None: if not self.directory: - self.directory = tempfile.mkdtemp() + self.directory = Path(tempfile.mkdtemp()) def gen_ssl(self) -> None: assert self.directory - self.csr_path = os.path.join(self.directory, "ssl.csr") - self.key_path = os.path.join(self.directory, "ssl.key") - self.pem_path = os.path.join(self.directory, "ssl.pem") - self.dh_path = os.path.join(self.directory, "dh.pem") + self.csr_path = self.directory / "ssl.csr" + self.key_path = self.directory / "ssl.key" + self.pem_path = self.directory / "ssl.pem" + self.dh_path = self.directory / "dh.pem" subprocess.check_output( [ self.openssl_bin, diff --git a/irctest/controllers/anope_services.py b/irctest/controllers/anope_services.py index e25e88d..bf577bf 100644 --- a/irctest/controllers/anope_services.py +++ b/irctest/controllers/anope_services.py @@ -1,4 +1,4 @@ -import os +from pathlib import Path import shutil import subprocess from typing import Type @@ -101,14 +101,11 @@ class AnopeController(BaseServicesController, DirectoryBasedController): pass assert self.directory + services_path = shutil.which("services") + assert services_path # Config and code need to be in the same directory, *obviously* - os.symlink( - os.path.join( - os.path.dirname(shutil.which("services")), "..", "lib" # type: ignore - ), - os.path.join(self.directory, "lib"), - ) + (self.directory / "lib").symlink_to(Path(services_path).parent.parent / "lib") self.proc = subprocess.Popen( [ diff --git a/irctest/controllers/atheme_services.py b/irctest/controllers/atheme_services.py index 485d6d7..aa9ec22 100644 --- a/irctest/controllers/atheme_services.py +++ b/irctest/controllers/atheme_services.py @@ -1,4 +1,3 @@ -import os import subprocess from typing import Optional, Type @@ -81,11 +80,11 @@ class AthemeController(BaseServicesController, DirectoryBasedController): "atheme-services", "-n", # don't fork "-c", - os.path.join(self.directory, "services.conf"), + self.directory / "services.conf", "-l", f"/tmp/services-{server_port}.log", "-p", - os.path.join(self.directory, "services.pid"), + self.directory / "services.pid", "-D", self.directory, ], diff --git a/irctest/controllers/bahamut.py b/irctest/controllers/bahamut.py index 315c73d..2f187ab 100644 --- a/irctest/controllers/bahamut.py +++ b/irctest/controllers/bahamut.py @@ -1,4 +1,4 @@ -import os +from pathlib import Path import shutil import subprocess from typing import Optional, Set, Type @@ -80,7 +80,7 @@ oper {{ """ -def initialize_entropy(directory: str) -> None: +def initialize_entropy(directory: Path) -> None: # https://github.com/DALnet/bahamut/blob/7fc039d403f66a954225c5dc4ad1fe683aedd794/include/dh.h#L35-L38 nb_rand_bytes = 512 // 8 # https://github.com/DALnet/bahamut/blob/7fc039d403f66a954225c5dc4ad1fe683aedd794/src/dh.c#L186 @@ -89,7 +89,7 @@ def initialize_entropy(directory: str) -> None: # Not actually random; but we don't care. entropy = b"\x00" * entropy_file_size - with open(os.path.join(directory, ".ircd.entropy"), "wb") as fd: + with (directory / ".ircd.entropy").open("wb") as fd: fd.write(entropy) @@ -140,8 +140,8 @@ class BahamutController(BaseServerController, DirectoryBasedController): initialize_entropy(self.directory) # they are hardcoded... thankfully Bahamut reads them from the CWD. - shutil.copy(self.pem_path, os.path.join(self.directory, "ircd.crt")) - shutil.copy(self.key_path, os.path.join(self.directory, "ircd.key")) + shutil.copy(self.pem_path, self.directory / "ircd.crt") + shutil.copy(self.key_path, self.directory / "ircd.key") with self.open_file("server.conf") as fd: fd.write( @@ -168,7 +168,7 @@ class BahamutController(BaseServerController, DirectoryBasedController): "ircd", "-t", # don't fork "-f", - os.path.join(self.directory, "server.conf"), + self.directory / "server.conf", ], ) diff --git a/irctest/controllers/base_hybrid.py b/irctest/controllers/base_hybrid.py index e2c0418..79897ed 100644 --- a/irctest/controllers/base_hybrid.py +++ b/irctest/controllers/base_hybrid.py @@ -1,4 +1,3 @@ -import os import shutil import subprocess from typing import Optional, Set @@ -88,9 +87,9 @@ class BaseHybridController(BaseServerController, DirectoryBasedController): self.binary_name, "-foreground", "-configfile", - os.path.join(self.directory, "server.conf"), + self.directory / "server.conf", "-pidfile", - os.path.join(self.directory, "server.pid"), + self.directory / "server.pid", ], # stderr=subprocess.DEVNULL, ) diff --git a/irctest/controllers/ergo.py b/irctest/controllers/ergo.py index 3878e7b..6157712 100644 --- a/irctest/controllers/ergo.py +++ b/irctest/controllers/ergo.py @@ -185,21 +185,19 @@ class ErgoController(BaseServerController, DirectoryBasedController): bind_address = "127.0.0.1:%s" % (port,) listener_conf = None # plaintext if ssl: - self.key_path = os.path.join(self.directory, "ssl.key") - self.pem_path = os.path.join(self.directory, "ssl.pem") + self.key_path = self.directory / "ssl.key" + self.pem_path = self.directory / "ssl.pem" listener_conf = {"tls": {"cert": self.pem_path, "key": self.key_path}} config["server"]["listeners"][bind_address] = listener_conf # type: ignore - config["datastore"]["path"] = os.path.join( # type: ignore - self.directory, "ircd.db" - ) + config["datastore"]["path"] = str(self.directory / "ircd.db") # type: ignore if password is not None: config["server"]["password"] = hash_password(password) # type: ignore assert self.proc is None - self._config_path = os.path.join(self.directory, "server.yml") + self._config_path = self.directory / "server.yml" self._config = config self._write_config() subprocess.call(["ergo", "initdb", "--conf", self._config_path, "--quiet"]) diff --git a/irctest/controllers/inspircd.py b/irctest/controllers/inspircd.py index 75e2a69..a2db87c 100644 --- a/irctest/controllers/inspircd.py +++ b/irctest/controllers/inspircd.py @@ -1,4 +1,3 @@ -import os import shutil import subprocess from typing import Optional, Set, Type @@ -164,7 +163,7 @@ class InspircdController(BaseServerController, DirectoryBasedController): "inspircd", "--nofork", "--config", - os.path.join(self.directory, "server.conf"), + self.directory / "server.conf", ], stdout=subprocess.DEVNULL, ) diff --git a/irctest/controllers/irc2.py b/irctest/controllers/irc2.py index 69a5fb0..26cd37c 100644 --- a/irctest/controllers/irc2.py +++ b/irctest/controllers/irc2.py @@ -1,4 +1,3 @@ -import os import shutil import subprocess from typing import Optional, Set, Type @@ -68,7 +67,7 @@ class Irc2Controller(BaseServerController, DirectoryBasedController): self.create_config() password_field = password if password else "" assert self.directory - pidfile = os.path.join(self.directory, "ircd.pid") + pidfile = self.directory / "ircd.pid" with self.open_file("server.conf") as fd: fd.write( TEMPLATE_CONFIG.format( @@ -93,7 +92,7 @@ class Irc2Controller(BaseServerController, DirectoryBasedController): "-p", "on", "-f", - os.path.join(self.directory, "server.conf"), + self.directory / "server.conf", ], # stderr=subprocess.DEVNULL, ) diff --git a/irctest/controllers/ircu2.py b/irctest/controllers/ircu2.py index 2be36f7..e574a9c 100644 --- a/irctest/controllers/ircu2.py +++ b/irctest/controllers/ircu2.py @@ -1,4 +1,3 @@ -import os import shutil import subprocess from typing import Optional, Set, Type @@ -87,7 +86,7 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController): self.create_config() password_field = 'password = "{}";'.format(password) if password else "" assert self.directory - pidfile = os.path.join(self.directory, "ircd.pid") + pidfile = self.directory / "ircd.pid" with self.open_file("server.conf") as fd: fd.write( TEMPLATE_CONFIG.format( @@ -110,7 +109,7 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController): "ircd", "-n", # don't detach "-f", - os.path.join(self.directory, "server.conf"), + self.directory / "server.conf", "-x", "DEBUG", ], diff --git a/irctest/controllers/limnoria.py b/irctest/controllers/limnoria.py index 5b38e21..b56c921 100644 --- a/irctest/controllers/limnoria.py +++ b/irctest/controllers/limnoria.py @@ -1,4 +1,3 @@ -import os import subprocess from typing import Optional, Type @@ -85,9 +84,7 @@ class LimnoriaController(BaseClientController, DirectoryBasedController): ) ) assert self.directory - self.proc = subprocess.Popen( - ["supybot", os.path.join(self.directory, "bot.conf")] - ) + self.proc = subprocess.Popen(["supybot", self.directory / "bot.conf"]) def get_irctest_controller_class() -> Type[LimnoriaController]: diff --git a/irctest/controllers/mammon.py b/irctest/controllers/mammon.py index 591347e..04f59f7 100644 --- a/irctest/controllers/mammon.py +++ b/irctest/controllers/mammon.py @@ -1,4 +1,3 @@ -import os import shutil import subprocess from typing import Optional, Set, Type @@ -128,7 +127,7 @@ class MammonController(BaseServerController, DirectoryBasedController): "mammond", "--nofork", # '--debug', "--config", - os.path.join(self.directory, "server.yml"), + self.directory / "server.yml", ] ) diff --git a/irctest/controllers/ngircd.py b/irctest/controllers/ngircd.py index 50577dd..be42b16 100644 --- a/irctest/controllers/ngircd.py +++ b/irctest/controllers/ngircd.py @@ -1,4 +1,3 @@ -import os import shutil import subprocess from typing import Optional, Set, Type @@ -94,7 +93,7 @@ class NgircdController(BaseServerController, DirectoryBasedController): password_field=password_field, key_path=self.key_path, pem_path=self.pem_path, - empty_file=os.path.join(self.directory, "empty.txt"), + empty_file=self.directory / "empty.txt", ) ) @@ -110,7 +109,7 @@ class NgircdController(BaseServerController, DirectoryBasedController): "ngircd", "--nodaemon", "--config", - os.path.join(self.directory, "server.conf"), + self.directory / "server.conf", ], # stdout=subprocess.DEVNULL, ) diff --git a/irctest/controllers/snircd.py b/irctest/controllers/snircd.py index 2edb85c..f49a347 100644 --- a/irctest/controllers/snircd.py +++ b/irctest/controllers/snircd.py @@ -1,4 +1,3 @@ -import os import shutil import subprocess from typing import Optional, Set, Type @@ -86,7 +85,7 @@ class SnircdController(BaseServerController, DirectoryBasedController): self.create_config() password_field = 'password = "{}";'.format(password) if password else "" assert self.directory - pidfile = os.path.join(self.directory, "ircd.pid") + pidfile = self.directory / "ircd.pid" with self.open_file("server.conf") as fd: fd.write( TEMPLATE_CONFIG.format( @@ -109,7 +108,7 @@ class SnircdController(BaseServerController, DirectoryBasedController): "ircd", "-n", # don't detach "-f", - os.path.join(self.directory, "server.conf"), + self.directory / "server.conf", "-x", "DEBUG", ], diff --git a/irctest/controllers/sopel.py b/irctest/controllers/sopel.py index 7304315..ce5aa3b 100644 --- a/irctest/controllers/sopel.py +++ b/irctest/controllers/sopel.py @@ -1,4 +1,4 @@ -import os +from pathlib import Path import subprocess import tempfile from typing import Optional, TextIO, Type, cast @@ -38,14 +38,14 @@ class SopelController(BaseClientController): super().kill() if self.filename: try: - os.unlink(os.path.join(os.path.expanduser("~/.sopel/"), self.filename)) + (Path("~/.sopel/").expanduser() / self.filename).unlink() except OSError: #  File does not exist pass def open_file(self, filename: str, mode: str = "a") -> TextIO: - dir_path = os.path.expanduser("~/.sopel/") - os.makedirs(dir_path, exist_ok=True) - return cast(TextIO, open(os.path.join(dir_path, filename), mode)) + dir_path = Path("~/.sopel/").expanduser() + dir_path.mkdir(parents=True, exist_ok=True) + return cast(TextIO, (dir_path / filename).open(mode)) def create_config(self) -> None: with self.open_file(self.filename): diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py index edf9cfb..51275f2 100644 --- a/irctest/controllers/unrealircd.py +++ b/irctest/controllers/unrealircd.py @@ -5,7 +5,7 @@ import shutil import signal import subprocess import textwrap -from typing import Optional, Set, Type +from typing import List, Optional, Set, Type, Union from irctest.basecontrollers import ( BaseServerController, @@ -222,20 +222,20 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): password_field=password_field, key_path=self.key_path, pem_path=self.pem_path, - empty_file=os.path.join(self.directory, "empty.txt"), + empty_file=self.directory / "empty.txt", extras=extras, set_extras=set_extras, ) ) - proot_cmd = [] + proot_cmd: List[Union[str, pathlib.Path]] = [] self.using_proot = False if shutil.which("proot"): unrealircd_path = shutil.which("unrealircd") if unrealircd_path: unrealircd_prefix = pathlib.Path(unrealircd_path).parents[1] - tmpdir = os.path.join(self.directory, "tmp") - os.mkdir(tmpdir) + tmpdir = self.directory / "tmp" + tmpdir.mkdir() # Unreal cleans its tmp/ directory after each run, which prevents # multiple processes from running at the same time. # Using PRoot, we can isolate them, with a tmp/ directory for each @@ -258,7 +258,7 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): "-t", "-F", # BOOT_NOFORK "-f", - os.path.join(self.directory, "unrealircd.conf"), + self.directory / "unrealircd.conf", ], # stdout=subprocess.DEVNULL, ) From c1442c43010cf515087a95ded1caed4baedd5b1d Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 10 Sep 2022 13:46:17 +0200 Subject: [PATCH 076/143] unrealircd: Use lock around startup/shutdown instead of proot to ensure no unrealircd instance is starting up while another clears $PREFIX/tmp/ While proot allows full parallelism and is less error-prone, it takes a long time to start; and segfaults on my Armbian system. --- irctest/controllers/unrealircd.py | 168 ++++++++++++++++-------------- 1 file changed, 89 insertions(+), 79 deletions(-) diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py index 51275f2..9adfef3 100644 --- a/irctest/controllers/unrealircd.py +++ b/irctest/controllers/unrealircd.py @@ -1,11 +1,11 @@ +import contextlib +import fcntl import functools -import os -import pathlib +from pathlib import Path import shutil -import signal import subprocess import textwrap -from typing import List, Optional, Set, Type, Union +from typing import Callable, ContextManager, Iterator, Optional, Set, Type from irctest.basecontrollers import ( BaseServerController, @@ -125,6 +125,35 @@ oper "operuser" {{ """ +def _filelock(path: Path) -> Callable[[], ContextManager]: + """Alternative to :cls:`multiprocessing.Lock` that works with pytest-xdist""" + + @contextlib.contextmanager + def f() -> Iterator[None]: + with open(path, "a") as fd: + fcntl.flock(fd, fcntl.LOCK_EX) + yield + + return f + + +_UNREALIRCD_BIN = shutil.which("unrealircd") +if _UNREALIRCD_BIN: + _UNREALIRCD_PREFIX = Path(_UNREALIRCD_BIN).parent.parent + + # Try to keep that lock file specific to this Unrealircd instance + _LOCK_PATH = _UNREALIRCD_PREFIX / "irctest-unrealircd-startstop.lock" +else: + # unrealircd not found; we are probably going to crash later anyway... + _LOCK_PATH = Path("/tmp/irctest-unrealircd-startstop.lock") + +_STARTSTOP_LOCK = _filelock(_LOCK_PATH) +""" +Unreal cleans its tmp/ directory after each run, which prevents +multiple processes from starting/stopping at the same time. +""" + + @functools.lru_cache() def installed_version() -> int: output = subprocess.check_output(["unrealircd", "-v"], universal_newlines=True) @@ -170,18 +199,6 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): self.port = port self.hostname = hostname self.create_config() - (unused_hostname, unused_port) = find_hostname_and_port() - (services_hostname, services_port) = find_hostname_and_port() - - password_field = 'password "{}";'.format(password) if password else "" - - self.gen_ssl() - if ssl: - (tls_hostname, tls_port) = (hostname, port) - (hostname, port) = (unused_hostname, unused_port) - else: - # Unreal refuses to start without TLS enabled - (tls_hostname, tls_port) = (unused_hostname, unused_port) if installed_version() >= 6: extras = textwrap.dedent( @@ -208,63 +225,60 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): with self.open_file("empty.txt") as fd: fd.write("\n") - assert self.directory + password_field = 'password "{}";'.format(password) if password else "" - with self.open_file("unrealircd.conf") as fd: - fd.write( - TEMPLATE_CONFIG.format( - hostname=hostname, - port=port, - services_hostname=services_hostname, - services_port=services_port, - tls_hostname=tls_hostname, - tls_port=tls_port, - password_field=password_field, - key_path=self.key_path, - pem_path=self.pem_path, - empty_file=self.directory / "empty.txt", - extras=extras, - set_extras=set_extras, + with _STARTSTOP_LOCK(): + (services_hostname, services_port) = find_hostname_and_port() + (unused_hostname, unused_port) = find_hostname_and_port() + + self.gen_ssl() + if ssl: + (tls_hostname, tls_port) = (hostname, port) + (hostname, port) = (unused_hostname, unused_port) + else: + # Unreal refuses to start without TLS enabled + (tls_hostname, tls_port) = (unused_hostname, unused_port) + + assert self.directory + + with self.open_file("unrealircd.conf") as fd: + fd.write( + TEMPLATE_CONFIG.format( + hostname=hostname, + port=port, + services_hostname=services_hostname, + services_port=services_port, + tls_hostname=tls_hostname, + tls_port=tls_port, + password_field=password_field, + key_path=self.key_path, + pem_path=self.pem_path, + empty_file=self.directory / "empty.txt", + extras=extras, + set_extras=set_extras, + ) ) + + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + + self.proc = subprocess.Popen( + [ + *faketime_cmd, + "unrealircd", + "-t", + "-F", # BOOT_NOFORK + "-f", + self.directory / "unrealircd.conf", + ], + # stdout=subprocess.DEVNULL, ) - - proot_cmd: List[Union[str, pathlib.Path]] = [] - self.using_proot = False - if shutil.which("proot"): - unrealircd_path = shutil.which("unrealircd") - if unrealircd_path: - unrealircd_prefix = pathlib.Path(unrealircd_path).parents[1] - tmpdir = self.directory / "tmp" - tmpdir.mkdir() - # Unreal cleans its tmp/ directory after each run, which prevents - # multiple processes from running at the same time. - # Using PRoot, we can isolate them, with a tmp/ directory for each - # process, so they don't interfere with each other, allowing use of - # the -n option (of pytest-xdist) to speed-up tests - proot_cmd = ["proot", "-b", f"{tmpdir}:{unrealircd_prefix}/tmp"] - self.using_proot = True - - if faketime and shutil.which("faketime"): - faketime_cmd = ["faketime", "-f", faketime] - self.faketime_enabled = True - else: - faketime_cmd = [] - - self.proc = subprocess.Popen( - [ - *proot_cmd, - *faketime_cmd, - "unrealircd", - "-t", - "-F", # BOOT_NOFORK - "-f", - self.directory / "unrealircd.conf", - ], - # stdout=subprocess.DEVNULL, - ) + self.wait_for_port() if run_services: - self.wait_for_port() self.services_controller = self.services_controller_class( self.test_config, self ) @@ -274,17 +288,13 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): server_port=services_port, ) - def kill(self) -> None: - if self.using_proot: - # Kill grandchild process, instead of killing proot, which takes more - # time (and does not seem to always work) - assert self.proc is not None - output = subprocess.check_output( - ["ps", "-opid", "--no-headers", "--ppid", str(self.proc.pid)] - ) - (grandchild_pid,) = [int(line) for line in output.decode().split()] - os.kill(grandchild_pid, signal.SIGKILL) - super().kill() + def kill_proc(self) -> None: + assert self.proc + + with _STARTSTOP_LOCK(): + self.proc.kill() + self.proc.wait(5) # wait for it to actually die + self.proc = None def get_irctest_controller_class() -> Type[UnrealircdController]: From cae3aec338b2e582f2ec6b53d820e841660b01f6 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 10 Sep 2022 14:53:20 +0200 Subject: [PATCH 077/143] workflows: Remove special-casing of Anope --- .github/workflows/test-devel.yml | 14 ++++--- .github/workflows/test-devel_release.yml | 14 ++++--- .github/workflows/test-stable.yml | 14 ++++--- make_workflows.py | 48 +----------------------- workflows.yml | 22 +++++++++++ 5 files changed, 50 insertions(+), 62 deletions(-) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 175167e..2fad08f 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -5,18 +5,22 @@ jobs: build-anope: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - name: Create directories run: cd ~/; mkdir -p .local/ go/ - - name: Cache Anope + - name: Cache dependencies uses: actions/cache@v2 with: - key: 3-${{ runner.os }}-anope-2.0.9 + key: 3-${{ runner.os }}-anope-devel path: '~/.cache - ${{ github.workspace }}/anope + ${ github.workspace }/anope ' + - uses: actions/checkout@v2 + - name: Set up Python 3.7 + uses: actions/setup-python@v2 + with: + python-version: 3.7 - name: Checkout Anope uses: actions/checkout@v2 with: @@ -24,7 +28,7 @@ jobs: ref: 2.0.9 repository: anope/anope - name: Build Anope - run: |- + run: | cd $GITHUB_WORKSPACE/anope/ cp $GITHUB_WORKSPACE/data/anope/* . CFLAGS=-O0 ./Config -quick diff --git a/.github/workflows/test-devel_release.yml b/.github/workflows/test-devel_release.yml index 84170e4..eb1691f 100644 --- a/.github/workflows/test-devel_release.yml +++ b/.github/workflows/test-devel_release.yml @@ -5,18 +5,22 @@ jobs: build-anope: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - name: Create directories run: cd ~/; mkdir -p .local/ go/ - - name: Cache Anope + - name: Cache dependencies uses: actions/cache@v2 with: - key: 3-${{ runner.os }}-anope-2.0.9 + key: 3-${{ runner.os }}-anope-devel_release path: '~/.cache - ${{ github.workspace }}/anope + ${ github.workspace }/anope ' + - uses: actions/checkout@v2 + - name: Set up Python 3.7 + uses: actions/setup-python@v2 + with: + python-version: 3.7 - name: Checkout Anope uses: actions/checkout@v2 with: @@ -24,7 +28,7 @@ jobs: ref: 2.0.9 repository: anope/anope - name: Build Anope - run: |- + run: | cd $GITHUB_WORKSPACE/anope/ cp $GITHUB_WORKSPACE/data/anope/* . CFLAGS=-O0 ./Config -quick diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index b9a279b..4f33501 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -5,18 +5,22 @@ jobs: build-anope: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - name: Create directories run: cd ~/; mkdir -p .local/ go/ - - name: Cache Anope + - name: Cache dependencies uses: actions/cache@v2 with: - key: 3-${{ runner.os }}-anope-2.0.9 + key: 3-${{ runner.os }}-anope-stable path: '~/.cache - ${{ github.workspace }}/anope + ${ github.workspace }/anope ' + - uses: actions/checkout@v2 + - name: Set up Python 3.7 + uses: actions/setup-python@v2 + with: + python-version: 3.7 - name: Checkout Anope uses: actions/checkout@v2 with: @@ -24,7 +28,7 @@ jobs: ref: 2.0.9 repository: anope/anope - name: Build Anope - run: |- + run: | cd $GITHUB_WORKSPACE/anope/ cp $GITHUB_WORKSPACE/data/anope/* . CFLAGS=-O0 ./Config -quick diff --git a/make_workflows.py b/make_workflows.py index 32e8584..3644eeb 100644 --- a/make_workflows.py +++ b/make_workflows.py @@ -144,11 +144,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs): downloads = [] install_steps = [] for software_id in test_config.get("software", []): - if software_id == "anope": - # TODO: don't hardcode anope here - software_config = {"separate_build_job": True} - else: - software_config = config["software"][software_id] + software_config = config["software"][software_id] env += test_config.get("env", {}).get(version_flavor.value, "") + " " if "prefix" in software_config: @@ -245,47 +241,6 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs): } -def get_build_job_anope(): - return { - "runs-on": "ubuntu-latest", - "steps": [ - {"uses": "actions/checkout@v2"}, - { - "name": "Create directories", - "run": "cd ~/; mkdir -p .local/ go/", - }, - { - "name": "Cache Anope", - "uses": "actions/cache@v2", - "with": { - "path": "~/.cache\n${{ github.workspace }}/anope\n", - "key": "3-${{ runner.os }}-anope-2.0.9", - }, - }, - { - "name": "Checkout Anope", - "uses": "actions/checkout@v2", - "with": { - "repository": "anope/anope", - "ref": "2.0.9", - "path": "anope", - }, - }, - { - "name": "Build Anope", - "run": script( - "cd $GITHUB_WORKSPACE/anope/", - "cp $GITHUB_WORKSPACE/data/anope/* .", - "CFLAGS=-O0 ./Config -quick", - "make -C build -j 4", - "make -C build install", - ), - }, - *upload_steps("anope"), - ], - } - - def upload_steps(software_id): """Make a tarball (to preserve permissions) and upload""" return [ @@ -326,7 +281,6 @@ def generate_workflow(config: dict, version_flavor: VersionFlavor): } jobs = {} - jobs["build-anope"] = get_build_job_anope() for software_id in config["software"]: software_config = config["software"][software_id] diff --git a/workflows.yml b/workflows.yml index 27f43e7..80ccc1d 100644 --- a/workflows.yml +++ b/workflows.yml @@ -301,6 +301,28 @@ software: separate_build_job: true build_script: *unrealircd_build_script + + ############################# + # Services: + anope: + name: Anope + repository: anope/anope + separate_build_job: true + path: anope + refs: + stable: "2.0.9" + release: "2.0.9" + devel: "2.0.9" + devel_release: "2.0.9" + build_script: | + cd $GITHUB_WORKSPACE/anope/ + cp $GITHUB_WORKSPACE/data/anope/* . + CFLAGS=-O0 ./Config -quick + make -C build -j 4 + make -C build install + + + ############################# # Clients: From 9d4212504b63cf61b056b902716584f67d80835b Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 11 Sep 2022 17:18:10 +0200 Subject: [PATCH 078/143] Add tests for TIME. (#127) --- irctest/server_tests/time.py | 48 ++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 irctest/server_tests/time.py diff --git a/irctest/server_tests/time.py b/irctest/server_tests/time.py new file mode 100644 index 0000000..4481c33 --- /dev/null +++ b/irctest/server_tests/time.py @@ -0,0 +1,48 @@ +import math +import time + +from irctest import cases +from irctest.numerics import RPL_TIME +from irctest.patma import ANYSTR, StrRe + + +class TimeTestCase(cases.BaseServerTestCase): + def testTime(self): + self.connectClient("user") + + time_before = math.floor(time.time()) + self.sendLine(1, "TIME") + + msg = self.getMessage(1) + + time_after = math.ceil(time.time()) + + if len(msg.params) == 5: + # ircu2, snircd + self.assertMessageMatch( + msg, + command=RPL_TIME, + params=["user", "My.Little.Server", StrRe("[0-9]+"), "0", ANYSTR], + ) + self.assertIn( + int(msg.params[2]), + range(time_before, time_after + 1), + "Timestamp not in expected range", + ) + elif len(msg.params) == 4: + # bahamut + self.assertMessageMatch( + msg, + command=RPL_TIME, + params=["user", "My.Little.Server", StrRe("[0-9]+"), ANYSTR], + ) + self.assertIn( + int(msg.params[2]), + range(time_before, time_after + 1), + "Timestamp not in expected range", + ) + else: + # Common case + self.assertMessageMatch( + msg, command=RPL_TIME, params=["user", "My.Little.Server", ANYSTR] + ) From 40385c112ba279ec580f6673598242784345c8c3 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 18 Sep 2022 10:27:48 -0700 Subject: [PATCH 079/143] add a test for AWAY :\r\n (#175) --- irctest/server_tests/away.py | 38 +++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/irctest/server_tests/away.py b/irctest/server_tests/away.py index a6ea7b4..d8cd059 100644 --- a/irctest/server_tests/away.py +++ b/irctest/server_tests/away.py @@ -4,7 +4,13 @@ AWAY command (`RFC 2812 Date: Sat, 22 Oct 2022 12:33:20 +0200 Subject: [PATCH 080/143] Bump flake8 version Fixes support for importlib_metadata 5.0.0, https://github.com/PyCQA/flake8/issues/1701 --- .pre-commit-config.yaml | 2 +- irctest/controllers/sopel.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8d20213..1539ef3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -13,7 +13,7 @@ repos: - id: isort - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.3 + rev: 5.0.4 hooks: - id: flake8 diff --git a/irctest/controllers/sopel.py b/irctest/controllers/sopel.py index ce5aa3b..076cf51 100644 --- a/irctest/controllers/sopel.py +++ b/irctest/controllers/sopel.py @@ -39,7 +39,7 @@ class SopelController(BaseClientController): if self.filename: try: (Path("~/.sopel/").expanduser() / self.filename).unlink() - except OSError: #  File does not exist + except OSError: # File does not exist pass def open_file(self, filename: str, mode: str = "a") -> TextIO: From 65d7e0e506612bf4fce41e06878565ec522d9098 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 22 Oct 2022 15:21:19 +0200 Subject: [PATCH 081/143] whowas: Update quotes and links to Modern spec In particular, this takes https://github.com/ircdocs/modern-irc/pull/196 into account. --- irctest/server_tests/whowas.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/irctest/server_tests/whowas.py b/irctest/server_tests/whowas.py index 76b250c..f193595 100644 --- a/irctest/server_tests/whowas.py +++ b/irctest/server_tests/whowas.py @@ -98,7 +98,7 @@ class WhowasTestCase(cases.BaseServerTestCase): "Servers MUST reply with either ERR_WASNOSUCHNICK or [...], both followed with RPL_ENDOFWHOWAS" - -- https://github.com/ircdocs/modern-irc/pull/170 + -- https://modern.ircdocs.horse/#whowas-message """ self.connectClient("nick1") @@ -210,7 +210,7 @@ class WhowasTestCase(cases.BaseServerTestCase): "The history is searched backward, returning the most recent entry first." -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 - -- https://github.com/ircdocs/modern-irc/pull/170 + -- https://modern.ircdocs.horse/#whowas-message """ self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2") @@ -224,7 +224,7 @@ class WhowasTestCase(cases.BaseServerTestCase): "If there are multiple entries, up to replies will be returned" -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 - -- https://github.com/ircdocs/modern-irc/pull/170 + -- https://modern.ircdocs.horse/#whowas-message """ self._testWhowasMultiple(second_result=False, whowas_command="WHOWAS nick2 1") @@ -238,7 +238,7 @@ class WhowasTestCase(cases.BaseServerTestCase): "If there are multiple entries, up to replies will be returned" -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 - -- https://github.com/ircdocs/modern-irc/pull/170 + -- https://modern.ircdocs.horse/#whowas-message """ self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 2") @@ -253,7 +253,10 @@ class WhowasTestCase(cases.BaseServerTestCase): is done." -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 - -- https://github.com/ircdocs/modern-irc/pull/170 + + "If given, SHOULD be a positive number. Otherwise, a full search + "is done. + -- https://modern.ircdocs.horse/#whowas-message """ self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 -1") @@ -271,7 +274,10 @@ class WhowasTestCase(cases.BaseServerTestCase): is done." -- https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 - -- https://github.com/ircdocs/modern-irc/pull/170 + + "If given, SHOULD be a positive number. Otherwise, a full search + "is done. + -- https://modern.ircdocs.horse/#whowas-message """ self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 0") @@ -280,7 +286,7 @@ class WhowasTestCase(cases.BaseServerTestCase): """ "Wildcards are allowed in the parameter." -- https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 - -- https://github.com/ircdocs/modern-irc/pull/170 + -- https://modern.ircdocs.horse/#whowas-message """ if self.controller.software_name == "Bahamut": raise runner.OptionalExtensionNotSupported("WHOWAS mask") @@ -324,7 +330,7 @@ class WhowasTestCase(cases.BaseServerTestCase): """ "If the `` argument is missing, they SHOULD send a single reply, using either ERR_NONICKNAMEGIVEN or ERR_NEEDMOREPARAMS" - -- https://github.com/ircdocs/modern-irc/pull/170 + -- https://modern.ircdocs.horse/#whowas-message """ # But no one seems to follow this. Most implementations use ERR_NEEDMOREPARAMS # instead of ERR_NONICKNAMEGIVEN; and I couldn't find any that returns @@ -358,7 +364,7 @@ class WhowasTestCase(cases.BaseServerTestCase): """ https://datatracker.ietf.org/doc/html/rfc1459#section-4.5.3 https://datatracker.ietf.org/doc/html/rfc2812#section-3.6.3 - -- https://github.com/ircdocs/modern-irc/pull/170 + -- https://modern.ircdocs.horse/#whowas-message and: @@ -371,7 +377,7 @@ class WhowasTestCase(cases.BaseServerTestCase): "Servers MUST reply with either ERR_WASNOSUCHNICK or [...], both followed with RPL_ENDOFWHOWAS" - -- https://github.com/ircdocs/modern-irc/pull/170 + -- https://modern.ircdocs.horse/#whowas-message """ self.connectClient("nick1") From d0645ab1a87bd33bafe8c38d9f732526ee616623 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 12 Nov 2022 11:21:02 +0100 Subject: [PATCH 082/143] dashboard: Use qualified class names in multi-module views --- irctest/dashboard/format.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/irctest/dashboard/format.py b/irctest/dashboard/format.py index df9c3ed..e5e16f6 100644 --- a/irctest/dashboard/format.py +++ b/irctest/dashboard/format.py @@ -173,6 +173,7 @@ def build_module_html( def build_test_table(jobs: List[str], results: List[CaseResult]) -> ET.Element: + multiple_modules = len({r.module_name for r in results}) > 1 results_by_module_and_class = group_by( results, lambda r: (r.module_name, r.class_name) ) @@ -189,19 +190,29 @@ def build_test_table(jobs: List[str], results: List[CaseResult]) -> ET.Element: for ((module_name, class_name), class_results) in sorted( results_by_module_and_class.items() ): + if multiple_modules: + # if the page shows classes from various modules, use the fully-qualified + # name in order to disambiguate and be clearer (eg. show + # "irctest.server_tests.extended_join.MetadataTestCase" instead of just + # "MetadataTestCase" which looks like it's about IRCv3's METADATA spec. + qualified_class_name = f"{module_name}.{class_name}" + else: + # otherwise, it's not needed, so let's not display it + qualified_class_name = class_name + module = importlib.import_module(module_name) # Header row: class name header_row = ET.SubElement(table, "tr") th = ET.SubElement(header_row, "th", colspan=str(len(jobs) + 1)) - row_anchor = f"{class_name}" + row_anchor = f"{qualified_class_name}" section_header = ET.SubElement( ET.SubElement(th, "h2"), "a", href=f"#{row_anchor}", id=row_anchor, ) - section_header.text = class_name + section_header.text = qualified_class_name append_docstring(th, getattr(module, class_name)) # Header row: one column for each implementation @@ -210,7 +221,7 @@ def build_test_table(jobs: List[str], results: List[CaseResult]) -> ET.Element: # One row for each test: results_by_test = group_by(class_results, key=lambda r: r.test_name) for (test_name, test_results) in sorted(results_by_test.items()): - row_anchor = f"{class_name}.{test_name}" + row_anchor = f"{qualified_class_name}.{test_name}" if len(row_anchor) >= 50: # Too long; give up on generating readable URL # TODO: only hash test parameter From fd0b050686b78be471d610fac98d99fec26f3965 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Mon, 14 Nov 2022 22:58:30 +0100 Subject: [PATCH 083/143] Add support for Dlk-Services (#176) --- .github/workflows/test-devel.yml | 47 ++++++ .github/workflows/test-stable.yml | 47 ++++++ Makefile | 7 + irctest/basecontrollers.py | 12 +- irctest/controllers/dlk_services.py | 245 ++++++++++++++++++++++++++++ irctest/dashboard/format.py | 2 +- irctest/server_tests/sasl.py | 8 + irctest/server_tests/who.py | 2 +- irctest/server_tests/whois.py | 5 +- make_workflows.py | 2 +- workflows.yml | 22 +++ 11 files changed, 390 insertions(+), 9 deletions(-) create mode 100644 irctest/controllers/dlk_services.py diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 2fad08f..0b74c78 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -400,6 +400,7 @@ jobs: - test-unrealircd-5 - test-unrealircd-anope - test-unrealircd-atheme + - test-unrealircd-dlk runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -1127,6 +1128,52 @@ jobs: with: name: pytest-results_unrealircd-atheme_devel path: pytest.xml + test-unrealircd-dlk: + needs: + - build-unrealircd + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.7 + uses: actions/setup-python@v2 + with: + python-version: 3.7 + - name: Download build artefacts + uses: actions/download-artifact@v2 + with: + name: installed-unrealircd + path: '~' + - name: Unpack artefacts + run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; + - name: Checkout Dlk + uses: actions/checkout@v2 + with: + path: Dlk-Services + ref: main + repository: DalekIRC/Dalek-Services + - name: Build Dlk + run: | + pip install pifpaf + wget -q https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar + wget -q https://wordpress.org/latest.zip -O wordpress-latest.zip + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime + - name: Install irctest dependencies + run: |- + python -m pip install --upgrade pip + pip install pytest pytest-xdist -r requirements.txt + - name: Test with pytest + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH + IRCTEST_DLK_PATH="${{ github.workspace }}/Dlk-Services" IRCTEST_WP_CLI_PATH="${{ + github.workspace }}/wp-cli.phar" IRCTEST_WP_ZIP_PATH="${{ github.workspace + }}/wordpress-latest.zip" make unrealircd-dlk + timeout-minutes: 30 + - if: always() + name: Publish results + uses: actions/upload-artifact@v2 + with: + name: pytest-results_unrealircd-dlk_devel + path: pytest.xml name: irctest with devel versions 'on': schedule: diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 4f33501..9c453fb 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -443,6 +443,7 @@ jobs: - test-unrealircd-5 - test-unrealircd-anope - test-unrealircd-atheme + - test-unrealircd-dlk runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -1285,6 +1286,52 @@ jobs: with: name: pytest-results_unrealircd-atheme_stable path: pytest.xml + test-unrealircd-dlk: + needs: + - build-unrealircd + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.7 + uses: actions/setup-python@v2 + with: + python-version: 3.7 + - name: Download build artefacts + uses: actions/download-artifact@v2 + with: + name: installed-unrealircd + path: '~' + - name: Unpack artefacts + run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; + - name: Checkout Dlk + uses: actions/checkout@v2 + with: + path: Dlk-Services + ref: effd18652fc1c847d1959089d9cca9ff9837a8c0 + repository: DalekIRC/Dalek-Services + - name: Build Dlk + run: | + pip install pifpaf + wget -q https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar + wget -q https://wordpress.org/latest.zip -O wordpress-latest.zip + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime + - name: Install irctest dependencies + run: |- + python -m pip install --upgrade pip + pip install pytest pytest-xdist -r requirements.txt + - name: Test with pytest + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH + IRCTEST_DLK_PATH="${{ github.workspace }}/Dlk-Services" IRCTEST_WP_CLI_PATH="${{ + github.workspace }}/wp-cli.phar" IRCTEST_WP_ZIP_PATH="${{ github.workspace + }}/wordpress-latest.zip" make unrealircd-dlk + timeout-minutes: 30 + - if: always() + name: Publish results + uses: actions/upload-artifact@v2 + with: + name: pytest-results_unrealircd-dlk_stable + path: pytest.xml name: irctest with stable versions 'on': pull_request: null diff --git a/Makefile b/Makefile index 6216218..f56a249 100644 --- a/Makefile +++ b/Makefile @@ -274,3 +274,10 @@ unrealircd-anope: --services-controller=irctest.controllers.anope_services \ -m 'services' \ -k '$(UNREALIRCD_SELECTORS)' + +unrealircd-dlk: + pifpaf run mysql -- $(PYTEST) $(PYTEST_ARGS) \ + --controller=irctest.controllers.unrealircd \ + --services-controller=irctest.controllers.dlk_services \ + -m 'services' \ + -k '$(UNREALIRCD_SELECTORS)' diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index c2744a3..864b905 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -301,10 +301,11 @@ class BaseServicesController(_BaseController): c.sendLine("PONG :" + msg.params[0]) c.getMessages() - timeout = time.time() + 5 + timeout = time.time() + 3 while True: - c.sendLine(f"PRIVMSG {self.server_controller.nickserv} :HELP") - msgs = self.getNickServResponse(c) + c.sendLine(f"PRIVMSG {self.server_controller.nickserv} :help") + + msgs = self.getNickServResponse(c, timeout=1) for msg in msgs: if msg.command == "401": # NickServ not available yet @@ -330,11 +331,12 @@ class BaseServicesController(_BaseController): c.disconnect() self.services_up = True - def getNickServResponse(self, client: Any) -> List[Message]: + def getNickServResponse(self, client: Any, timeout: int = 0) -> List[Message]: """Wrapper aroung getMessages() that waits longer, because NickServ is queried asynchronously.""" msgs: List[Message] = [] - while not msgs: + start_time = time.time() + while not msgs and (not timeout or start_time + timeout > time.time()): time.sleep(0.05) msgs = client.getMessages() return msgs diff --git a/irctest/controllers/dlk_services.py b/irctest/controllers/dlk_services.py new file mode 100644 index 0000000..9934354 --- /dev/null +++ b/irctest/controllers/dlk_services.py @@ -0,0 +1,245 @@ +import os +from pathlib import Path +import secrets +import subprocess +from typing import Optional, Type + +import irctest +from irctest.basecontrollers import BaseServicesController, DirectoryBasedController +import irctest.cases +import irctest.runner + +TEMPLATE_DLK_CONFIG = """\ +info {{ + SID "00A"; + network-name "testnetwork"; + services-name "services.example.org"; + admin-email "admin@example.org"; +}} + +link {{ + hostname "{server_hostname}"; + port "{server_port}"; + password "password"; +}} + +log {{ + debug "yes"; +}} + +sql {{ + port "3306"; + username "pifpaf"; + password "pifpaf"; + database "pifpaf"; + sockfile "{mysql_socket}"; + prefix "{dlk_prefix}"; +}} + +wordpress {{ + prefix "{wp_prefix}"; +}} + +""" + +TEMPLATE_DLK_WP_CONFIG = """ + "{wp_prefix}", + + + "default_avatar" => "https://valware.uk/wp-content/plugins/ultimate-member/assets/img/default_avatar.jpg", + "forumschan" => "#DLK-Support", + +]; +""" + +TEMPLATE_WP_CONFIG = """ +define( 'DB_NAME', 'pifpaf' ); +define( 'DB_USER', 'pifpaf' ); +define( 'DB_PASSWORD', 'pifpaf' ); +define( 'DB_HOST', 'localhost:{mysql_socket}' ); +define( 'DB_CHARSET', 'utf8' ); +define( 'DB_COLLATE', '' ); + +define( 'AUTH_KEY', 'put your unique phrase here' ); +define( 'SECURE_AUTH_KEY', 'put your unique phrase here' ); +define( 'LOGGED_IN_KEY', 'put your unique phrase here' ); +define( 'NONCE_KEY', 'put your unique phrase here' ); +define( 'AUTH_SALT', 'put your unique phrase here' ); +define( 'SECURE_AUTH_SALT', 'put your unique phrase here' ); +define( 'LOGGED_IN_SALT', 'put your unique phrase here' ); +define( 'NONCE_SALT', 'put your unique phrase here' ); + +$table_prefix = '{wp_prefix}'; + +define( 'WP_DEBUG', false ); + +if (!defined('ABSPATH')) {{ + define( 'ABSPATH', '{wp_path}' ); +}} + +/* That's all, stop editing! Happy publishing. */ + +/** Absolute path to the WordPress directory. */ + + +/** Sets up WordPress vars and included files. */ +require_once ABSPATH . 'wp-settings.php'; +""" + + +class DlkController(BaseServicesController, DirectoryBasedController): + """Mixin for server controllers that rely on DLK""" + + software_name = "Dlk-Services" + + def run_sql(self, sql: str) -> None: + mysql_socket = os.environ["PIFPAF_MYSQL_SOCKET"] + subprocess.run( + ["mysql", "-S", mysql_socket, "pifpaf"], + input=sql.encode(), + check=True, + ) + + def run(self, protocol: str, server_hostname: str, server_port: int) -> None: + self.create_config() + + if protocol == "unreal4": + protocol = "unreal5" + assert protocol in ("unreal5",), protocol + + mysql_socket = os.environ["PIFPAF_MYSQL_SOCKET"] + + assert self.directory + + try: + self.wp_cli_path = Path(os.environ["IRCTEST_WP_CLI_PATH"]) + if not self.wp_cli_path.is_file(): + raise KeyError() + except KeyError: + raise RuntimeError( + "$IRCTEST_WP_CLI_PATH must be set to a WP-CLI executable (eg. " + "downloaded from )" + ) from None + + try: + self.dlk_path = Path(os.environ["IRCTEST_DLK_PATH"]) + if not self.dlk_path.is_dir(): + raise KeyError() + except KeyError: + raise RuntimeError("$IRCTEST_DLK_PATH is not set") from None + self.dlk_path = self.dlk_path.resolve() + + # Unpack a fresh Wordpress install in the temporary directory. + # In theory we could have a common Wordpress install and only wp-config.php + # in the temporary directory; but wp-cli assumes wp-config.php must be + # in a Wordpress directory, and fails in various places if it isn't. + # Rather than symlinking everything to make it work, let's just copy + # the whole code, it's not that big. + try: + wp_zip_path = Path(os.environ["IRCTEST_WP_ZIP_PATH"]) + if not wp_zip_path.is_file(): + raise KeyError() + except KeyError: + raise RuntimeError( + "$IRCTEST_WP_ZIP_PATH must be set to a Wordpress source zipball " + "(eg. downloaded from )" + ) from None + subprocess.run( + ["unzip", wp_zip_path, "-d", self.directory], stdout=subprocess.DEVNULL + ) + self.wp_path = self.directory / "wordpress" + + rand_hex = secrets.token_hex(6) + self.wp_prefix = f"wp{rand_hex}_" + self.dlk_prefix = f"dlk{rand_hex}_" + template_vars = dict( + protocol=protocol, + server_hostname=server_hostname, + server_port=server_port, + mysql_socket=mysql_socket, + wp_path=self.wp_path, + wp_prefix=self.wp_prefix, + dlk_prefix=self.dlk_prefix, + ) + + # Configure Wordpress + wp_config_path = self.directory / "wp-config.php" + with open(wp_config_path, "w") as fd: + fd.write(TEMPLATE_WP_CONFIG.format(**template_vars)) + + subprocess.run( + [ + "php", + self.wp_cli_path, + "core", + "install", + "--url=http://localhost/", + "--title=irctest site", + "--admin_user=adminuser", + "--admin_email=adminuser@example.org", + f"--path={self.wp_path}", + ], + check=True, + ) + + # Configure Dlk + dlk_log_dir = self.directory / "logs" + dlk_conf_dir = self.directory / "conf" + dlk_conf_path = dlk_conf_dir / "dalek.conf" + os.mkdir(dlk_conf_dir) + with open(dlk_conf_path, "w") as fd: + fd.write(TEMPLATE_DLK_CONFIG.format(**template_vars)) + dlk_wp_config_path = dlk_conf_dir / "wordpress.conf" + with open(dlk_wp_config_path, "w") as fd: + fd.write(TEMPLATE_DLK_WP_CONFIG.format(**template_vars)) + (dlk_conf_dir / "modules.conf").symlink_to(self.dlk_path / "conf/modules.conf") + + self.proc = subprocess.Popen( + [ + "php", + "src/dalek", + ], + cwd=self.dlk_path, + env={ + **os.environ, + "DALEK_CONF_DIR": str(dlk_conf_dir), + "DALEK_LOG_DIR": str(dlk_log_dir), + }, + ) + + def terminate(self) -> None: + super().terminate() + + def kill(self) -> None: + super().kill() + + def registerUser( + self, + case: irctest.cases.BaseServerTestCase, + username: str, + password: Optional[str] = None, + ) -> None: + assert password + subprocess.run( + [ + "php", + self.wp_cli_path, + "user", + "create", + username, + f"{username}@example.org", + f"--user_pass={password}", + f"--path={self.wp_path}", + ], + check=True, + ) + + +def get_irctest_controller_class() -> Type[DlkController]: + return DlkController diff --git a/irctest/dashboard/format.py b/irctest/dashboard/format.py index e5e16f6..e267f99 100644 --- a/irctest/dashboard/format.py +++ b/irctest/dashboard/format.py @@ -303,7 +303,7 @@ def write_html_pages( for result in results ) assert is_client != is_server, (job, is_client, is_server) - if job.endswith(("-atheme", "-anope")): + if job.endswith(("-atheme", "-anope", "-dlk")): assert is_server job_categories[job] = "server-with-services" elif is_server: diff --git a/irctest/server_tests/sasl.py b/irctest/server_tests/sasl.py index 47f53cd..600f959 100644 --- a/irctest/server_tests/sasl.py +++ b/irctest/server_tests/sasl.py @@ -178,6 +178,14 @@ class SaslTestCase(cases.BaseServerTestCase): ), "Anope does not handle split AUTHENTICATE (reported on IRC)", ) + @cases.xfailIf( + lambda self: ( + self.controller.services_controller is not None + and self.controller.services_controller.software_name == "Dlk-Services" + ), + "Dlk does not handle split AUTHENTICATE " + "https://github.com/DalekIRC/Dalek-Services/issues/28", + ) def testPlainLarge(self): """Test the client splits large AUTHENTICATE messages whose payload is not a multiple of 400. diff --git a/irctest/server_tests/who.py b/irctest/server_tests/who.py index 4956bd5..b3892d6 100644 --- a/irctest/server_tests/who.py +++ b/irctest/server_tests/who.py @@ -37,8 +37,8 @@ class BaseWhoTestCase: self.sendLine(1, f"USER {self.username} 0 * :{self.realname}") if auth: self.sendLine(1, "CAP END") - self.getRegistrationMessage(1) self.skipToWelcome(1) + self.getMessages(1) self.sendLine(1, "JOIN #chan") self.getMessages(1) diff --git a/irctest/server_tests/whois.py b/irctest/server_tests/whois.py index a22b34f..b2a7d29 100644 --- a/irctest/server_tests/whois.py +++ b/irctest/server_tests/whois.py @@ -71,7 +71,10 @@ class _WhoisTestMixin(cases.BaseServerTestCase): last_message, command=RPL_ENDOFWHOIS, params=["nick1", "nick2", ANYSTR], - fail_msg=f"Last message was not RPL_ENDOFWHOIS ({RPL_ENDOFWHOIS})", + fail_msg=( + f"Expected RPL_ENDOFWHOIS ({RPL_ENDOFWHOIS}) as last message, " + f"got {{msg}}" + ), ) unexpected_messages = [] diff --git a/make_workflows.py b/make_workflows.py index 3644eeb..c389881 100644 --- a/make_workflows.py +++ b/make_workflows.py @@ -146,7 +146,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs): for software_id in test_config.get("software", []): software_config = config["software"][software_id] - env += test_config.get("env", {}).get(version_flavor.value, "") + " " + env += software_config.get("env", "") + " " if "prefix" in software_config: env += ( f"PATH={software_config['prefix']}/sbin" diff --git a/workflows.yml b/workflows.yml index 80ccc1d..4f63757 100644 --- a/workflows.yml +++ b/workflows.yml @@ -304,6 +304,7 @@ software: ############################# # Services: + anope: name: Anope repository: anope/anope @@ -321,6 +322,24 @@ software: make -C build -j 4 make -C build install + dlk: + name: Dlk + repository: DalekIRC/Dalek-Services + separate_build_job: false + path: Dlk-Services + refs: + stable: &dlk_stable "effd18652fc1c847d1959089d9cca9ff9837a8c0" + release: *dlk_stable + devel: "main" + devel_release: *dlk_stable + build_script: | + pip install pifpaf + wget -q https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar + wget -q https://wordpress.org/latest.zip -O wordpress-latest.zip + env: >- + IRCTEST_DLK_PATH="${{ github.workspace }}/Dlk-Services" + IRCTEST_WP_CLI_PATH="${{ github.workspace }}/wp-cli.phar" + IRCTEST_WP_ZIP_PATH="${{ github.workspace }}/wordpress-latest.zip" ############################# @@ -425,6 +444,9 @@ tests: unrealircd-anope: software: [unrealircd, anope] + unrealircd-dlk: + software: [unrealircd, dlk] + limnoria: software: [limnoria] From 29e4c2bbdbb2fa7218e5d903c7a1b6dba3700b91 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Fri, 18 Nov 2022 18:57:51 +0100 Subject: [PATCH 084/143] Hardcode DH parameters openssl version in ubuntu 22.04 forbids moduli smaller than 512, which would take longer to generate. --- irctest/basecontrollers.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index 864b905..8ada1cb 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -7,6 +7,7 @@ import shutil import socket import subprocess import tempfile +import textwrap import time from typing import IO, Any, Callable, Dict, List, Optional, Set, Tuple, Type @@ -156,10 +157,18 @@ class DirectoryBasedController(_BaseController): ], stderr=subprocess.DEVNULL, ) - subprocess.check_output( - [self.openssl_bin, "dhparam", "-out", self.dh_path, "128"], - stderr=subprocess.DEVNULL, - ) + with self.dh_path.open("w") as fd: + fd.write( + textwrap.dedent( + """ + -----BEGIN DH PARAMETERS----- + MIGHAoGBAJICSyQAiLj1fw8b5xELcnpqBQ+wvOyKgim4IetWOgZnRQFkTgOeoRZD + HksACRFJL/EqHxDKcy/2Ghwr2axhNxSJ+UOBmraP3WfodV/fCDPnZ+XnI9fjHsIr + rjisPMqomjXeiTB1UeAHvLUmCK4yx6lpAJsCYwJjsqkycUfHiy1bAgEC + -----END DH PARAMETERS----- + """ + ) + ) class BaseClientController(_BaseController): From 35d342a478f8ddc7d6b9ba7b2e55f769c60478d1 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 20 Nov 2022 23:33:20 +0100 Subject: [PATCH 085/143] account_registration: Add missing 'services' mark --- irctest/server_tests/account_registration.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/irctest/server_tests/account_registration.py b/irctest/server_tests/account_registration.py index ba8a25d..6283e8c 100644 --- a/irctest/server_tests/account_registration.py +++ b/irctest/server_tests/account_registration.py @@ -9,6 +9,7 @@ from irctest.patma import ANYSTR REGISTER_CAP_NAME = "draft/account-registration" +@cases.mark_services @cases.mark_specifications("IRCv3") class RegisterBeforeConnectTestCase(cases.BaseServerTestCase): @staticmethod @@ -33,6 +34,7 @@ class RegisterBeforeConnectTestCase(cases.BaseServerTestCase): self.assertMessageMatch(register_response, params=["SUCCESS", ANYSTR, ANYSTR]) +@cases.mark_services @cases.mark_specifications("IRCv3") class RegisterBeforeConnectDisallowedTestCase(cases.BaseServerTestCase): @staticmethod @@ -60,6 +62,7 @@ class RegisterBeforeConnectDisallowedTestCase(cases.BaseServerTestCase): ) +@cases.mark_services @cases.mark_specifications("IRCv3") class RegisterEmailVerifiedTestCase(cases.BaseServerTestCase): @staticmethod @@ -110,6 +113,7 @@ class RegisterEmailVerifiedTestCase(cases.BaseServerTestCase): ) +@cases.mark_services @cases.mark_specifications("IRCv3", "Ergo") class RegisterNoLandGrabsTestCase(cases.BaseServerTestCase): @staticmethod From 544ca4b7ed3ada1bb810f53751d1a4141d383c6b Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 3 Dec 2022 08:57:04 +0100 Subject: [PATCH 086/143] Update flake8 URL The Gitlab.com repo was removed today --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1539ef3..35b0470 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,7 +12,7 @@ repos: hooks: - id: isort - - repo: https://gitlab.com/pycqa/flake8 + - repo: https://github.com/PyCQA/flake8 rev: 5.0.4 hooks: - id: flake8 From 5fe4d4cfd8ac90d8af40150f11974918772548e5 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 6 Dec 2022 20:59:11 +0100 Subject: [PATCH 087/143] github: Force ubuntu-20.04 Bahamut does not support ubuntu-22.04 --- .github/workflows/test-devel.yml | 62 ++++++++++----------- .github/workflows/test-devel_release.yml | 12 ++-- .github/workflows/test-stable.yml | 70 ++++++++++++------------ make_workflows.py | 6 +- 4 files changed, 75 insertions(+), 75 deletions(-) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 0b74c78..44eda53 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -3,7 +3,7 @@ jobs: build-anope: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -43,7 +43,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-bahamut: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -92,7 +92,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-hybrid: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -131,7 +131,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-inspircd: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -162,7 +162,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-ngircd: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -203,7 +203,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-plexus4: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -245,7 +245,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-solanum: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -285,7 +285,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-unrealircd: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -331,7 +331,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-unrealircd-5: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -401,7 +401,7 @@ jobs: - test-unrealircd-anope - test-unrealircd-atheme - test-unrealircd-dlk - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Download Artifacts @@ -428,7 +428,7 @@ jobs: test-bahamut: needs: - build-bahamut - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -462,7 +462,7 @@ jobs: needs: - build-bahamut - build-anope - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -500,7 +500,7 @@ jobs: test-bahamut-atheme: needs: - build-bahamut - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -532,7 +532,7 @@ jobs: path: pytest.xml test-ergo: needs: [] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -574,7 +574,7 @@ jobs: needs: - build-hybrid - build-anope - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -612,7 +612,7 @@ jobs: test-inspircd: needs: - build-inspircd - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -646,7 +646,7 @@ jobs: needs: - build-inspircd - build-anope - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -683,7 +683,7 @@ jobs: path: pytest.xml test-ircu2: needs: [] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -722,7 +722,7 @@ jobs: path: pytest.xml test-limnoria: needs: [] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -750,7 +750,7 @@ jobs: path: pytest.xml test-nefarious: needs: [] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -789,7 +789,7 @@ jobs: test-ngircd: needs: - build-ngircd - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -823,7 +823,7 @@ jobs: needs: - build-ngircd - build-anope - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -861,7 +861,7 @@ jobs: test-ngircd-atheme: needs: - build-ngircd - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -895,7 +895,7 @@ jobs: needs: - build-plexus4 - build-anope - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -933,7 +933,7 @@ jobs: test-solanum: needs: - build-solanum - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -965,7 +965,7 @@ jobs: path: pytest.xml test-sopel: needs: [] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -993,7 +993,7 @@ jobs: test-unrealircd: needs: - build-unrealircd - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -1026,7 +1026,7 @@ jobs: test-unrealircd-5: needs: - build-unrealircd-5 - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -1060,7 +1060,7 @@ jobs: needs: - build-unrealircd - build-anope - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -1098,7 +1098,7 @@ jobs: test-unrealircd-atheme: needs: - build-unrealircd - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -1131,7 +1131,7 @@ jobs: test-unrealircd-dlk: needs: - build-unrealircd - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 diff --git a/.github/workflows/test-devel_release.yml b/.github/workflows/test-devel_release.yml index eb1691f..03ffeef 100644 --- a/.github/workflows/test-devel_release.yml +++ b/.github/workflows/test-devel_release.yml @@ -3,7 +3,7 @@ jobs: build-anope: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -43,7 +43,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-inspircd: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -80,7 +80,7 @@ jobs: - test-inspircd - test-inspircd-anope - test-inspircd-atheme - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Download Artifacts @@ -107,7 +107,7 @@ jobs: test-inspircd: needs: - build-inspircd - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -141,7 +141,7 @@ jobs: needs: - build-inspircd - build-anope - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -179,7 +179,7 @@ jobs: test-inspircd-atheme: needs: - build-inspircd - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 9c453fb..f4f7778 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -3,7 +3,7 @@ jobs: build-anope: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -43,7 +43,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-bahamut: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -92,7 +92,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-charybdis: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -132,7 +132,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-hybrid: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -171,7 +171,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-inspircd: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -202,7 +202,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-ngircd: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -243,7 +243,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-plexus4: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -285,7 +285,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-solanum: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -325,7 +325,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-unrealircd: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -371,7 +371,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-unrealircd-5: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -444,7 +444,7 @@ jobs: - test-unrealircd-anope - test-unrealircd-atheme - test-unrealircd-dlk - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Download Artifacts @@ -471,7 +471,7 @@ jobs: test-bahamut: needs: - build-bahamut - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -505,7 +505,7 @@ jobs: needs: - build-bahamut - build-anope - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -543,7 +543,7 @@ jobs: test-bahamut-atheme: needs: - build-bahamut - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -576,7 +576,7 @@ jobs: test-charybdis: needs: - build-charybdis - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -608,7 +608,7 @@ jobs: path: pytest.xml test-ergo: needs: [] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -650,7 +650,7 @@ jobs: needs: - build-hybrid - build-anope - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -688,7 +688,7 @@ jobs: test-inspircd: needs: - build-inspircd - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -722,7 +722,7 @@ jobs: needs: - build-inspircd - build-anope - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -760,7 +760,7 @@ jobs: test-inspircd-atheme: needs: - build-inspircd - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -792,7 +792,7 @@ jobs: path: pytest.xml test-irc2: needs: [] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -842,7 +842,7 @@ jobs: path: pytest.xml test-ircu2: needs: [] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -881,7 +881,7 @@ jobs: path: pytest.xml test-limnoria: needs: [] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -908,7 +908,7 @@ jobs: path: pytest.xml test-nefarious: needs: [] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -947,7 +947,7 @@ jobs: test-ngircd: needs: - build-ngircd - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -981,7 +981,7 @@ jobs: needs: - build-ngircd - build-anope - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -1019,7 +1019,7 @@ jobs: test-ngircd-atheme: needs: - build-ngircd - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -1053,7 +1053,7 @@ jobs: needs: - build-plexus4 - build-anope - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -1091,7 +1091,7 @@ jobs: test-solanum: needs: - build-solanum - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -1123,7 +1123,7 @@ jobs: path: pytest.xml test-sopel: needs: [] - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -1151,7 +1151,7 @@ jobs: test-unrealircd: needs: - build-unrealircd - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -1184,7 +1184,7 @@ jobs: test-unrealircd-5: needs: - build-unrealircd-5 - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -1218,7 +1218,7 @@ jobs: needs: - build-unrealircd - build-anope - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -1256,7 +1256,7 @@ jobs: test-unrealircd-atheme: needs: - build-unrealircd - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 @@ -1289,7 +1289,7 @@ jobs: test-unrealircd-dlk: needs: - build-unrealircd - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 - name: Set up Python 3.7 diff --git a/make_workflows.py b/make_workflows.py index c389881..d8b6766 100644 --- a/make_workflows.py +++ b/make_workflows.py @@ -116,7 +116,7 @@ def get_build_job(*, software_config, software_id, version_flavor): return None return { - "runs-on": "ubuntu-latest", + "runs-on": "ubuntu-20.04", "steps": [ { "name": "Create directories", @@ -191,7 +191,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs): unpack = [] return { - "runs-on": "ubuntu-latest", + "runs-on": "ubuntu-20.04", "needs": needs, "steps": [ {"uses": "actions/checkout@v2"}, @@ -307,7 +307,7 @@ def generate_workflow(config: dict, version_flavor: VersionFlavor): jobs["publish-test-results"] = { "name": "Publish Dashboard", "needs": sorted({f"test-{test_id}" for test_id in config["tests"]} & set(jobs)), - "runs-on": "ubuntu-latest", + "runs-on": "ubuntu-20.04", # the build-and-test job might be skipped, we don't need to run # this job then "if": "success() || failure()", From 6181dd07ad6a36f987b77b67a1d628a785876fe2 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Fri, 16 Dec 2022 19:09:09 +0100 Subject: [PATCH 088/143] Skip failure on RPL_WHOISSPECIAL with Dlk-Services --- irctest/server_tests/whois.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/irctest/server_tests/whois.py b/irctest/server_tests/whois.py index b2a7d29..2853553 100644 --- a/irctest/server_tests/whois.py +++ b/irctest/server_tests/whois.py @@ -99,6 +99,12 @@ class _WhoisTestMixin(cases.BaseServerTestCase): ], ) elif m.command == RPL_WHOISSPECIAL: + services_controller = self.controller.services_controller + if ( + services_controller is not None + and services_controller.software_name == "Dlk-Services" + ): + continue # Technically allowed, but it's a bad style to use this without # explicit configuration by the operators. assert False, "RPL_WHOISSPECIAL in use with default configuration" From b7e8a7a5f557635064893f597a639d6206d53077 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Sun, 22 Jan 2023 04:45:25 -0800 Subject: [PATCH 089/143] direct message tests (#184) * Test privmsg to non-existent user * Test privmsg to user * fix synchronization issue * apply black Co-authored-by: ma-anwar --- irctest/server_tests/messages.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/irctest/server_tests/messages.py b/irctest/server_tests/messages.py index b33be17..c815e94 100644 --- a/irctest/server_tests/messages.py +++ b/irctest/server_tests/messages.py @@ -32,6 +32,26 @@ class PrivmsgTestCase(cases.BaseServerTestCase): # ERR_NOSUCHNICK, ERR_NOSUCHCHANNEL, or ERR_CANNOTSENDTOCHAN self.assertIn(msg.command, ("401", "403", "404")) + @cases.mark_specifications("RFC1459", "RFC2812") + def testPrivmsgToUser(self): + """""" + self.connectClient("foo") + self.connectClient("bar") + self.sendLine(1, "PRIVMSG bar :hey there!") + self.getMessages(1) + pms = [msg for msg in self.getMessages(2) if msg.command == "PRIVMSG"] + self.assertEqual(len(pms), 1) + self.assertMessageMatch(pms[0], command="PRIVMSG", params=["bar", "hey there!"]) + + @cases.mark_specifications("RFC1459", "RFC2812") + def testPrivmsgNonexistentUser(self): + """https://tools.ietf.org/html/rfc2812#section-3.3.1""" + self.connectClient("foo") + self.sendLine(1, "PRIVMSG bar :hey there!") + msg = self.getMessage(1) + # ERR_NOSUCHNICK + self.assertIn(msg.command, ("401")) + class NoticeTestCase(cases.BaseServerTestCase): @cases.mark_specifications("RFC1459", "RFC2812") From 00562ff82d2e15484246a595b97438fa91a06ed8 Mon Sep 17 00:00:00 2001 From: Mitchell Riley Date: Sat, 28 Jan 2023 04:12:32 -0500 Subject: [PATCH 090/143] Run utf8 tests on servers which advertise UTF8ONLY (#185) --- irctest/server_tests/utf8.py | 41 +++++++++++++++++++++--------------- irctest/specifications.py | 1 + pytest.ini | 1 + 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/irctest/server_tests/utf8.py b/irctest/server_tests/utf8.py index ccd8156..14cb1dc 100644 --- a/irctest/server_tests/utf8.py +++ b/irctest/server_tests/utf8.py @@ -1,36 +1,21 @@ """ `Ergo `_-specific tests of non-Unicode filtering -TODO: turn this into a test of `IRCv3 UTF8ONLY `_ """ -from irctest import cases +from irctest import cases, runner from irctest.patma import ANYSTR class Utf8TestCase(cases.BaseServerTestCase): @cases.mark_specifications("Ergo") - def testUtf8Validation(self): + def testNonUtf8Filtering(self): self.connectClient( "bar", capabilities=["batch", "echo-message", "labeled-response"], ) self.joinChannel(1, "#qux") - self.sendLine(1, "PRIVMSG #qux hi") - ms = self.getMessages(1) - self.assertMessageMatch( - [m for m in ms if m.command == "PRIVMSG"][0], params=["#qux", "hi"] - ) - - self.sendLine(1, b"PRIVMSG #qux hi\xaa") - self.assertMessageMatch( - self.getMessage(1), - command="FAIL", - params=["PRIVMSG", "INVALID_UTF8", ANYSTR], - tags={}, - ) - self.sendLine(1, b"@label=xyz PRIVMSG #qux hi\xaa") self.assertMessageMatch( self.getMessage(1), @@ -38,3 +23,25 @@ class Utf8TestCase(cases.BaseServerTestCase): params=["PRIVMSG", "INVALID_UTF8", ANYSTR], tags={"label": "xyz"}, ) + + @cases.mark_isupport("UTF8ONLY") + def testUtf8Validation(self): + self.connectClient("foo") + self.connectClient("bar") + + if "UTF8ONLY" not in self.server_support: + raise runner.IsupportTokenNotSupported("UTF8ONLY") + + self.sendLine(1, "PRIVMSG bar hi") + ms = self.getMessages(2) + self.assertMessageMatch( + [m for m in ms if m.command == "PRIVMSG"][0], params=["bar", "hi"] + ) + + self.sendLine(1, b"PRIVMSG bar hi\xaa") + + m = self.getMessage(1) + assert m.command in ("FAIL", "WARN", "ERROR") + + if m.command in ("FAIL", "WARN"): + self.assertMessageMatch(m, params=["PRIVMSG", "INVALID_UTF8", ANYSTR]) diff --git a/irctest/specifications.py b/irctest/specifications.py index 99e0646..35510c0 100644 --- a/irctest/specifications.py +++ b/irctest/specifications.py @@ -56,6 +56,7 @@ class IsupportTokens(enum.Enum): MONITOR = "MONITOR" STATUSMSG = "STATUSMSG" TARGMAX = "TARGMAX" + UTF8ONLY = "UTF8ONLY" WHOX = "WHOX" @classmethod diff --git a/pytest.ini b/pytest.ini index 9b5d071..138bd43 100644 --- a/pytest.ini +++ b/pytest.ini @@ -38,6 +38,7 @@ markers = PREFIX STATUSMSG TARGMAX + UTF8ONLY WHOX python_classes = *TestCase Test* From 6815dd238b8afd8ad73712d50d3e93cf997c26db Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 11 Feb 2023 22:26:23 +0100 Subject: [PATCH 091/143] Fix race condition on Ergo --- irctest/server_tests/utf8.py | 1 + 1 file changed, 1 insertion(+) diff --git a/irctest/server_tests/utf8.py b/irctest/server_tests/utf8.py index 14cb1dc..bb5e77d 100644 --- a/irctest/server_tests/utf8.py +++ b/irctest/server_tests/utf8.py @@ -33,6 +33,7 @@ class Utf8TestCase(cases.BaseServerTestCase): raise runner.IsupportTokenNotSupported("UTF8ONLY") self.sendLine(1, "PRIVMSG bar hi") + self.getMessages(1) # synchronize ms = self.getMessages(2) self.assertMessageMatch( [m for m in ms if m.command == "PRIVMSG"][0], params=["bar", "hi"] From 8530c85adc2ea530c76000043036e7efed4806f4 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Wed, 15 Feb 2023 19:11:51 +0100 Subject: [PATCH 092/143] sopel: remove use of deprecated argument it's removed in https://github.com/sopel-irc/sopel/commit/aceedf583791ad5eb468b0556d1a8b2bd0d39589 --- irctest/controllers/sopel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irctest/controllers/sopel.py b/irctest/controllers/sopel.py index 076cf51..b6d2443 100644 --- a/irctest/controllers/sopel.py +++ b/irctest/controllers/sopel.py @@ -73,7 +73,7 @@ class SopelController(BaseClientController): auth_method="auth_method = sasl" if auth else "", ) ) - self.proc = subprocess.Popen(["sopel", "--quiet", "-c", self.filename]) + self.proc = subprocess.Popen(["sopel", "-c", self.filename]) def get_irctest_controller_class() -> Type[SopelController]: From 1ea3e1c15c4183d72ecf8a7b0db8983f2d6ec09d Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Wed, 1 Mar 2023 20:07:58 +0100 Subject: [PATCH 093/143] Fix insp4 support after 'helpop' config file was renamed (#187) https://github.com/inspircd/inspircd/commit/c2e954903ad40581043c49dc0552ae7dd87e8ab0 --- irctest/controllers/inspircd.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/irctest/controllers/inspircd.py b/irctest/controllers/inspircd.py index a2db87c..b0b1821 100644 --- a/irctest/controllers/inspircd.py +++ b/irctest/controllers/inspircd.py @@ -1,3 +1,4 @@ +import functools import shutil import subprocess from typing import Optional, Set, Type @@ -80,8 +81,8 @@ TEMPLATE_CONFIG = """ # HELP/HELPOP # for the HELP alias - - + + # Misc: @@ -94,6 +95,17 @@ TEMPLATE_SSL_CONFIG = """ """ +@functools.lru_cache() +def installed_version() -> int: + output = subprocess.check_output(["inspircd", "--version"], universal_newlines=True) + if output.startswith("InspIRCd-3"): + return 3 + if output.startswith("InspIRCd-4"): + return 4 + else: + assert False, f"unexpected version: {output}" + + class InspircdController(BaseServerController, DirectoryBasedController): software_name = "InspIRCd" supported_sasl_mechanisms = {"PLAIN"} @@ -138,6 +150,13 @@ class InspircdController(BaseServerController, DirectoryBasedController): else: ssl_config = "" + if installed_version() == 3: + help_module_name = "helpop" + elif installed_version() == 4: + help_module_name = "help" + else: + assert False, f"unexpected version: {installed_version()}" + with self.open_file("server.conf") as fd: fd.write( TEMPLATE_CONFIG.format( @@ -147,6 +166,7 @@ class InspircdController(BaseServerController, DirectoryBasedController): services_port=services_port, password_field=password_field, ssl_config=ssl_config, + help_module_name=help_module_name, ) ) assert self.directory From 5364f963ae5035fe7ab4f6cc9d124b51769407c5 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 4 Mar 2023 10:11:51 +0100 Subject: [PATCH 094/143] Add tests for draft/extended-monitor (#180) --- irctest/server_tests/monitor.py | 263 ++++++++++++++++++++++++++++++-- irctest/specifications.py | 3 + pytest.ini | 3 + 3 files changed, 260 insertions(+), 9 deletions(-) diff --git a/irctest/server_tests/monitor.py b/irctest/server_tests/monitor.py index 4f2f552..9305d37 100644 --- a/irctest/server_tests/monitor.py +++ b/irctest/server_tests/monitor.py @@ -1,7 +1,10 @@ """ `IRCv3 MONITOR `_ +and `IRCv3 extended-monitor` `_ """ +import pytest + from irctest import cases, runner from irctest.client_mock import NoMessageException from irctest.numerics import ( @@ -13,7 +16,7 @@ from irctest.numerics import ( from irctest.patma import ANYSTR, StrRe -class MonitorTestCase(cases.BaseServerTestCase): +class _BaseMonitorTestCase(cases.BaseServerTestCase): def check_server_support(self): if "MONITOR" not in self.server_support: raise runner.IsupportTokenNotSupported("MONITOR") @@ -42,6 +45,8 @@ class MonitorTestCase(cases.BaseServerTestCase): extra_format=(nick,), ) + +class MonitorTestCase(_BaseMonitorTestCase): @cases.mark_specifications("IRCv3") @cases.mark_isupport("MONITOR") def testMonitorOneDisconnected(self): @@ -295,10 +300,11 @@ class MonitorTestCase(cases.BaseServerTestCase): self.sendLine(2, "NICK qux") self.getMessages(2) mononline = self.getMessages(1)[0] - self.assertEqual(mononline.command, RPL_MONONLINE) - self.assertEqual(len(mononline.params), 2, mononline.params) - self.assertIn(mononline.params[0], ("bar", "*")) - self.assertEqual(mononline.params[1].split("!")[0], "qux") + self.assertMessageMatch( + mononline, + command=RPL_MONONLINE, + params=[StrRe(r"(bar|\*)"), StrRe("qux(!.*)?")], + ) # no numerics for a case change self.sendLine(2, "NICK QUX") @@ -309,7 +315,246 @@ class MonitorTestCase(cases.BaseServerTestCase): self.getMessages(2) monoffline = self.getMessages(1)[0] # should get RPL_MONOFFLINE with the current unfolded nick - self.assertEqual(monoffline.command, RPL_MONOFFLINE) - self.assertEqual(len(monoffline.params), 2, monoffline.params) - self.assertIn(monoffline.params[0], ("bar", "*")) - self.assertEqual(monoffline.params[1].split("!")[0], "QUX") + self.assertMessageMatch( + monoffline, + command=RPL_MONOFFLINE, + params=[StrRe(r"(bar|\*)"), "QUX"], + ) + + +class _BaseExtendedMonitorTestCase(_BaseMonitorTestCase): + def _setupExtendedMonitor(self, monitor_before_connect, watcher_caps, watched_caps): + """Tests https://ircv3.net/specs/extensions/extended-monitor.html""" + self.connectClient( + "foo", + capabilities=["draft/extended-monitor", *watcher_caps], + skip_if_cap_nak=True, + ) + + if monitor_before_connect: + self.sendLine(1, "MONITOR + bar") + self.getMessages(1) + self.connectClient("bar", capabilities=watched_caps, skip_if_cap_nak=True) + self.getMessages(2) + else: + self.connectClient("bar", capabilities=watched_caps, skip_if_cap_nak=True) + self.getMessages(2) + self.sendLine(1, "MONITOR + bar") + + self.assertMononline(1, "bar") + self.assertEqual(self.getMessages(1), []) + + +class ExtendedMonitorTestCase(_BaseExtendedMonitorTestCase): + @cases.mark_specifications("IRCv3") + @cases.mark_capabilities("extended-monitor", "away-notify") + @pytest.mark.parametrize( + "monitor_before_connect,cap", + [ + pytest.param( + monitor_before_connect, + cap, + id=("monitor_before_connect" if monitor_before_connect else "") + + "-" + + ("with-cap" if cap else ""), + ) + for monitor_before_connect in [True, False] + for cap in [True, False] + ], + ) + def testExtendedMonitorAway(self, monitor_before_connect, cap): + """Tests https://ircv3.net/specs/extensions/extended-monitor.html + with https://ircv3.net/specs/extensions/away-notify + """ + if cap: + self._setupExtendedMonitor( + monitor_before_connect, ["away-notify"], ["away-notify"] + ) + else: + self._setupExtendedMonitor(monitor_before_connect, ["away-notify"], []) + + self.sendLine(2, "AWAY :afk") + self.getMessages(2) + self.assertMessageMatch( + self.getMessage(1), nick="bar", command="AWAY", params=["afk"] + ) + self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages") + + self.sendLine(2, "AWAY") + self.getMessages(2) + self.assertMessageMatch( + self.getMessage(1), nick="bar", command="AWAY", params=[] + ) + self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages") + + @cases.mark_specifications("IRCv3") + @cases.mark_capabilities("extended-monitor", "away-notify") + @pytest.mark.parametrize( + "monitor_before_connect,cap", + [ + pytest.param( + monitor_before_connect, + cap, + id=("monitor_before_connect" if monitor_before_connect else "") + + "-" + + ("with-cap" if cap else ""), + ) + for monitor_before_connect in [True, False] + for cap in [True, False] + ], + ) + def testExtendedMonitorAwayNoCap(self, monitor_before_connect, cap): + """Tests https://ircv3.net/specs/extensions/extended-monitor.html + does nothing when ``away-notify`` is not enabled by the watcher + """ + if cap: + self._setupExtendedMonitor(monitor_before_connect, [], ["away-notify"]) + else: + self._setupExtendedMonitor(monitor_before_connect, [], []) + + self.sendLine(2, "AWAY :afk") + self.getMessages(2) + self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages") + + self.sendLine(2, "AWAY") + self.getMessages(2) + self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages") + + @cases.mark_specifications("IRCv3") + @cases.mark_capabilities("extended-monitor", "setname") + @pytest.mark.parametrize("monitor_before_connect", [True, False]) + def testExtendedMonitorSetName(self, monitor_before_connect): + """Tests https://ircv3.net/specs/extensions/extended-monitor.html + with https://ircv3.net/specs/extensions/setname + """ + self._setupExtendedMonitor(monitor_before_connect, ["setname"], ["setname"]) + + self.sendLine(2, "SETNAME :new name") + self.getMessages(2) + self.assertMessageMatch( + self.getMessage(1), nick="bar", command="SETNAME", params=["new name"] + ) + self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages") + + @cases.mark_specifications("IRCv3") + @cases.mark_capabilities("extended-monitor", "setname") + @pytest.mark.parametrize("monitor_before_connect", [True, False]) + def testExtendedMonitorSetNameNoCap(self, monitor_before_connect): + """Tests https://ircv3.net/specs/extensions/extended-monitor.html + does nothing when ``setname`` is not enabled by the watcher + """ + self._setupExtendedMonitor(monitor_before_connect, [], ["setname"]) + + self.sendLine(2, "SETNAME :new name") + self.getMessages(2) + self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages") + + +@cases.mark_services +class AuthenticatedExtendedMonitorTestCase(_BaseExtendedMonitorTestCase): + @cases.mark_specifications("IRCv3") + @cases.mark_capabilities("extended-monitor", "account-notify") + @pytest.mark.parametrize( + "monitor_before_connect,cap", + [ + pytest.param( + monitor_before_connect, + cap, + id=("monitor_before_connect" if monitor_before_connect else "") + + "-" + + ("with-cap" if cap else ""), + ) + for monitor_before_connect in [True, False] + for cap in [True, False] + ], + ) + def testExtendedMonitorAccountNotify(self, monitor_before_connect, cap): + """Tests https://ircv3.net/specs/extensions/extended-monitor.html + does nothing when ``account-notify`` is not enabled by the watcher + """ + self.controller.registerUser(self, "jilles", "sesame") + + if cap: + self._setupExtendedMonitor( + monitor_before_connect, + ["account-notify"], + ["account-notify", "sasl", "cap-notify"], + ) + else: + self._setupExtendedMonitor( + monitor_before_connect, ["account-notify"], ["sasl", "cap-notify"] + ) + + self.sendLine(2, "AUTHENTICATE PLAIN") + m = self.getRegistrationMessage(2) + self.assertMessageMatch( + m, + command="AUTHENTICATE", + params=["+"], + fail_msg="Sent “AUTHENTICATE PLAIN”, server should have " + "replied with “AUTHENTICATE +”, but instead sent: {msg}", + ) + self.sendLine(2, "AUTHENTICATE amlsbGVzAGppbGxlcwBzZXNhbWU=") + m = self.getRegistrationMessage(2) + self.assertMessageMatch( + m, + command="900", + fail_msg="Did not send 900 after correct SASL authentication.", + ) + self.getMessages(2) + + self.assertMessageMatch( + self.getMessage(1), nick="bar", command="ACCOUNT", params=["jilles"] + ) + self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages") + + @cases.mark_specifications("IRCv3") + @cases.mark_capabilities("extended-monitor", "account-notify") + @pytest.mark.parametrize( + "monitor_before_connect,cap", + [ + pytest.param( + monitor_before_connect, + cap, + id=("monitor_before_connect" if monitor_before_connect else "") + + "-" + + ("with-cap" if cap else ""), + ) + for monitor_before_connect in [True, False] + for cap in [True, False] + ], + ) + def testExtendedMonitorAccountNotifyNoCap(self, monitor_before_connect, cap): + """Tests https://ircv3.net/specs/extensions/extended-monitor.html + does nothing when ``account-notify`` is not enabled by the watcher + """ + self.controller.registerUser(self, "jilles", "sesame") + + if cap: + self._setupExtendedMonitor( + monitor_before_connect, [], ["account-notify", "sasl", "cap-notify"] + ) + else: + self._setupExtendedMonitor( + monitor_before_connect, [], ["sasl", "cap-notify"] + ) + + self.sendLine(2, "AUTHENTICATE PLAIN") + m = self.getRegistrationMessage(2) + self.assertMessageMatch( + m, + command="AUTHENTICATE", + params=["+"], + fail_msg="Sent “AUTHENTICATE PLAIN”, server should have " + "replied with “AUTHENTICATE +”, but instead sent: {msg}", + ) + self.sendLine(2, "AUTHENTICATE amlsbGVzAGppbGxlcwBzZXNhbWU=") + m = self.getRegistrationMessage(2) + self.assertMessageMatch( + m, + command="900", + fail_msg="Did not send 900 after correct SASL authentication.", + ) + self.getMessages(2) + + self.assertEqual(self.getMessages(1), [], "watcher got unexpected messages") diff --git a/irctest/specifications.py b/irctest/specifications.py index 35510c0..9c4617b 100644 --- a/irctest/specifications.py +++ b/irctest/specifications.py @@ -27,16 +27,19 @@ class Specifications(enum.Enum): @enum.unique class Capabilities(enum.Enum): + ACCOUNT_NOTIFY = "account-notify" ACCOUNT_TAG = "account-tag" AWAY_NOTIFY = "away-notify" BATCH = "batch" ECHO_MESSAGE = "echo-message" EXTENDED_JOIN = "extended-join" + EXTENDED_MONITOR = "extended-monitor" LABELED_RESPONSE = "labeled-response" MESSAGE_TAGS = "message-tags" MULTILINE = "draft/multiline" MULTI_PREFIX = "multi-prefix" SERVER_TIME = "server-time" + SETNAME = "setname" STS = "sts" @classmethod diff --git a/pytest.ini b/pytest.ini index 138bd43..375f2bb 100644 --- a/pytest.ini +++ b/pytest.ini @@ -18,16 +18,19 @@ markers = private_chathistory # capabilities + account-notify account-tag away-notify batch echo-message extended-join + extended-monitor labeled-response message-tags draft/multiline multi-prefix server-time + setname sts # isupport tokens From 136a7923c09d3cea3ab13f5dfe042d011a5b9615 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 4 Mar 2023 10:51:40 +0100 Subject: [PATCH 095/143] Bump linter versions (#188) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The isort we had has some weird poetry issue, I figured I might as well bump the other linters at the same time ``` [INFO] Installing environment for https://github.com/PyCQA/isort. [INFO] Once installed this environment will be reused. [INFO] This may take a few minutes... An unexpected error has occurred: CalledProcessError: command: ('/home/runner/.cache/pre-commit/repo0m3eczdf/py_env-python3.7/bin/python', '-mpip', 'install', '.') return code: 1 stdout: Processing /home/runner/.cache/pre-commit/repo0m3eczdf Installing build dependencies: started Installing build dependencies: finished with status 'done' Getting requirements to build wheel: started Getting requirements to build wheel: finished with status 'done' Preparing metadata (pyproject.toml): started Preparing metadata (pyproject.toml): finished with status 'error' stderr: error: subprocess-exited-with-error × Preparing metadata (pyproject.toml) did not run successfully. │ exit code: 1 ╰─> [14 lines of output] Traceback (most recent call last): File "/home/runner/.cache/pre-commit/repo0m3eczdf/py_env-python3.7/lib/python3.7/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 353, in main() File "/home/runner/.cache/pre-commit/repo0m3eczdf/py_env-python3.7/lib/python3.7/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 335, in main json_out['return_val'] = hook(**hook_input['kwargs']) File "/home/runner/.cache/pre-commit/repo0m3eczdf/py_env-python3.7/lib/python3.7/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py", line 149, in prepare_metadata_for_build_wheel return hook(metadata_directory, config_settings) File "/tmp/pip-build-env-beaf5dxh/overlay/lib/python3.7/site-packages/poetry/core/masonry/api.py", line 40, in prepare_metadata_for_build_wheel poetry = Factory().create_poetry(Path(".").resolve(), with_groups=False) File "/tmp/pip-build-env-beaf5dxh/overlay/lib/python3.7/site-packages/poetry/core/factory.py", line 57, in create_poetry raise RuntimeError("The Poetry configuration is invalid:\n" + message) RuntimeError: The Poetry configuration is invalid: - [extras.pipfile_deprecated_finder.2] 'pip-shims<=0.3.4' does not match '^[a-zA-Z-_.0-9]+$' [end of output] note: This error originates from a subprocess, and is likely not a problem with pip. error: metadata-generation-failed × Encountered error while generating package metadata. ╰─> See above for output. note: This is an issue with the package mentioned above, not pip. hint: See above for details. ``` --- .pre-commit-config.yaml | 7 ++++--- irctest/cases.py | 20 +++++++++++--------- irctest/dashboard/format.py | 16 ++++++++-------- irctest/dashboard/github_download.py | 2 +- irctest/patma.py | 4 ++-- make_workflows.py | 1 - mypy.ini | 3 +++ report.py | 2 +- 8 files changed, 30 insertions(+), 25 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 35b0470..ad82e7e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,13 +2,13 @@ exclude: ^irctest/scram repos: - repo: https://github.com/psf/black - rev: 22.3.0 + rev: 23.1.0 hooks: - id: black language_version: python3 - repo: https://github.com/PyCQA/isort - rev: 5.5.2 + rev: 5.11.5 hooks: - id: isort @@ -18,6 +18,7 @@ repos: - id: flake8 - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.812 + rev: v1.0.1 hooks: - id: mypy + additional_dependencies: [types-PyYAML, types-docutils] diff --git a/irctest/cases.py b/irctest/cases.py index 2186304..9a3ff38 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -173,7 +173,7 @@ class _IrcTestCase(Generic[TController]): ) -> Optional[str]: """Returns an error message if the message doesn't match the given arguments, or None if it matches.""" - for (key, value) in kwargs.items(): + for key, value in kwargs.items(): if getattr(msg, key) != value: fail_msg = ( fail_msg or "expected {param} to be {expects}, got {got}: {msg}" @@ -351,8 +351,8 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]): nick: Optional[str] = None user: Optional[List[str]] = None server: socket.socket - protocol_version = Optional[str] - acked_capabilities = Optional[Set[str]] + protocol_version: Optional[str] + acked_capabilities: Optional[Set[str]] __new__ = object.__new__ # pytest won't collect Generic[] subclasses otherwise @@ -448,7 +448,9 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]): print("{:.3f} S: {}".format(time.time(), line.strip())) def readCapLs( - self, auth: Optional[Authentication] = None, tls_config: tls.TlsConfig = None + self, + auth: Optional[Authentication] = None, + tls_config: Optional[tls.TlsConfig] = None, ) -> None: (hostname, port) = self.server.getsockname() self.controller.run( @@ -458,9 +460,9 @@ class BaseClientTestCase(_IrcTestCase[basecontrollers.BaseClientController]): m = self.getMessage() self.assertEqual(m.command, "CAP", "First message is not CAP LS.") if m.params == ["LS"]: - self.protocol_version = 301 + self.protocol_version = "301" elif m.params == ["LS", "302"]: - self.protocol_version = 302 + self.protocol_version = "302" elif m.params == ["END"]: self.protocol_version = None else: @@ -689,7 +691,7 @@ class BaseServerTestCase( def connectClient( self, nick: str, - name: TClientName = None, + name: Optional[TClientName] = None, capabilities: Optional[List[str]] = None, skip_if_cap_nak: bool = False, show_io: Optional[bool] = None, @@ -734,8 +736,8 @@ class BaseServerTestCase( self.server_support[param] = None welcome.append(m) - self.targmax: Dict[str, Optional[str]] = dict( - item.split(":", 1) # type: ignore + self.targmax: Dict[str, Optional[str]] = dict( # type: ignore[assignment] + item.split(":", 1) for item in (self.server_support.get("TARGMAX") or "").split(",") if item ) diff --git a/irctest/dashboard/format.py b/irctest/dashboard/format.py index e267f99..51f946c 100644 --- a/irctest/dashboard/format.py +++ b/irctest/dashboard/format.py @@ -39,7 +39,7 @@ class CaseResult: type: Optional[str] = None message: Optional[str] = None - def output_filename(self): + def output_filename(self) -> str: test_name = self.test_name if len(test_name) > 50 or set(test_name) & NETLIFY_CHAR_BLACKLIST: # File name too long or otherwise invalid. This should be good enough: @@ -75,7 +75,7 @@ def iter_job_results(job_file_name: Path, job: ET.ElementTree) -> Iterator[CaseR skipped = False details = None system_out = None - extra = {} + extra: Dict[str, str] = {} for child in case: if child.tag == "skipped": success = True @@ -187,7 +187,7 @@ def build_test_table(jobs: List[str], results: List[CaseResult]) -> ET.Element: ET.SubElement(ET.SubElement(cell, "div"), "span").text = job cell.set("class", "job-name") - for ((module_name, class_name), class_results) in sorted( + for (module_name, class_name), class_results in sorted( results_by_module_and_class.items() ): if multiple_modules: @@ -220,7 +220,7 @@ def build_test_table(jobs: List[str], results: List[CaseResult]) -> ET.Element: # One row for each test: results_by_test = group_by(class_results, key=lambda r: r.test_name) - for (test_name, test_results) in sorted(results_by_test.items()): + for test_name, test_results in sorted(results_by_test.items()): row_anchor = f"{qualified_class_name}.{test_name}" if len(row_anchor) >= 50: # Too long; give up on generating readable URL @@ -314,7 +314,7 @@ def write_html_pages( pages = [] - for (module_name, module_results) in sorted(results_by_module.items()): + for module_name, module_results in sorted(results_by_module.items()): # Filter out client jobs if this is a server test module, and vice versa module_categories = { job_categories[result.job] @@ -366,7 +366,7 @@ def write_html_index(output_dir: Path, pages: List[Tuple[str, str, str]]) -> Non module_pages = [] job_pages = [] - for (page_type, title, file_name) in sorted(pages): + for page_type, title, file_name in sorted(pages): if page_type == "module": module_pages.append((title, file_name)) elif page_type == "job": @@ -379,7 +379,7 @@ def write_html_index(output_dir: Path, pages: List[Tuple[str, str, str]]) -> Non dl = ET.SubElement(body, "dl") dl.set("class", "module-index") - for (module_name, file_name) in sorted(module_pages): + for module_name, file_name in sorted(module_pages): module = importlib.import_module(module_name) link = ET.SubElement(ET.SubElement(dl, "dt"), "a", href=f"./{file_name}") @@ -391,7 +391,7 @@ def write_html_index(output_dir: Path, pages: List[Tuple[str, str, str]]) -> Non ul = ET.SubElement(body, "ul") ul.set("class", "job-index") - for (job, file_name) in sorted(job_pages): + for job, file_name in sorted(job_pages): link = ET.SubElement(ET.SubElement(ul, "li"), "a", href=f"./{file_name}") link.text = job diff --git a/irctest/dashboard/github_download.py b/irctest/dashboard/github_download.py index dd34b70..65620f2 100644 --- a/irctest/dashboard/github_download.py +++ b/irctest/dashboard/github_download.py @@ -18,7 +18,7 @@ class Artifact: download_url: str @property - def public_download_url(self): + def public_download_url(self) -> str: # GitHub API is not available publicly for artifacts, we need to use # a third-party proxy to access it... name = urllib.parse.quote(self.name) diff --git a/irctest/patma.py b/irctest/patma.py index b02c15e..42fa4ae 100644 --- a/irctest/patma.py +++ b/irctest/patma.py @@ -152,7 +152,7 @@ def match_dict( # Set to not-None if we find a Keys() operator in the dict keys remaining_keys_wildcard = None - for (expected_key, expected_value) in expected.items(): + for expected_key, expected_value in expected.items(): if isinstance(expected_key, RemainingKeys): remaining_keys_wildcard = (expected_key.key, expected_value) else: @@ -168,7 +168,7 @@ def match_dict( if remaining_keys_wildcard: (expected_key, expected_value) = remaining_keys_wildcard - for (key, value) in got.items(): + for key, value in got.items(): if not match_string(key, expected_key): return False if not match_string(value, expected_value): diff --git a/make_workflows.py b/make_workflows.py index d8b6766..00c49e0 100644 --- a/make_workflows.py +++ b/make_workflows.py @@ -263,7 +263,6 @@ def upload_steps(software_id): def generate_workflow(config: dict, version_flavor: VersionFlavor): - on: dict if version_flavor == VersionFlavor.STABLE: on = {"push": None, "pull_request": None} diff --git a/mypy.ini b/mypy.ini index c5bf548..840ecdc 100644 --- a/mypy.ini +++ b/mypy.ini @@ -12,6 +12,9 @@ disallow_untyped_defs = False [mypy-irctest.client_tests.*] disallow_untyped_defs = False +[mypy-irctest.self_tests.*] +disallow_untyped_defs = False + [mypy-defusedxml.*] ignore_missing_imports = True diff --git a/report.py b/report.py index 744fb7c..0c626a3 100644 --- a/report.py +++ b/report.py @@ -42,7 +42,7 @@ def partial_compaction(d): # tests separate compacted_d = {} successes = [] - for (k, v) in d.items(): + for k, v in d.items(): if isinstance(v, CompactedResult) and v.success and v.nb_skipped == 0: successes.append((k, v)) else: From 418b52603364b0f4f277e9cddf04d969a7e97a3d Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Tue, 4 Apr 2023 22:01:20 +0200 Subject: [PATCH 096/143] Prevent random port collisions between controllers (#191) This happens from time to time on the CI and is pretty annoying --- irctest/basecontrollers.py | 56 +++++++++++++++++++++++++++--- irctest/controllers/bahamut.py | 5 ++- irctest/controllers/base_hybrid.py | 3 +- irctest/controllers/inspircd.py | 3 +- irctest/controllers/ngircd.py | 3 +- irctest/controllers/unrealircd.py | 5 ++- 6 files changed, 59 insertions(+), 16 deletions(-) diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index 8ada1cb..b64cdaf 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -1,6 +1,7 @@ from __future__ import annotations import dataclasses +import multiprocessing import os from pathlib import Path import shutil @@ -9,7 +10,18 @@ import subprocess import tempfile import textwrap import time -from typing import IO, Any, Callable, Dict, List, Optional, Set, Tuple, Type +from typing import ( + IO, + Any, + Callable, + Dict, + List, + MutableMapping, + Optional, + Set, + Tuple, + Type, +) import irctest @@ -58,9 +70,43 @@ class _BaseController: supported_sasl_mechanisms: Set[str] proc: Optional[subprocess.Popen] + _used_ports: Set[Tuple[str, int]] + """``(hostname, port))`` used by this controller.""" + # the following need to be shared between processes in case we are running in + # parallel (with pytest-xdist) + # The dicts are used as a set of (hostname, port), because _manager.set() doesn't + # exist. + _manager = multiprocessing.Manager() + _port_lock = _manager.Lock() + """Lock for access to ``_all_used_ports`` and ``_available_ports``.""" + _all_used_ports: MutableMapping[Tuple[str, int], None] = _manager.dict() + """``(hostname, port)`` used by all controllers.""" + _available_ports: MutableMapping[Tuple[str, int], None] = _manager.dict() + """``(hostname, port)`` available to any controller.""" + def __init__(self, test_config: TestCaseControllerConfig): self.test_config = test_config self.proc = None + self._used_ports = set() + + def get_hostname_and_port(self) -> Tuple[str, int]: + with self._port_lock: + try: + # try to get a known available port + ((hostname, port), _) = self._available_ports.popitem() + except KeyError: + # if there aren't any, iterate while we get a fresh one. + while True: + (hostname, port) = find_hostname_and_port() + if (hostname, port) not in self._all_used_ports: + # double-checking in self._used_ports to prevent collisions + # between controllers starting at the same time. + break + + # Make this port unavailable to other processes + self._all_used_ports[(hostname, port)] = None + + return (hostname, port) def check_is_alive(self) -> None: assert self.proc @@ -84,6 +130,11 @@ class _BaseController: if self.proc: self.kill_proc() + # move this controller's ports from _all_used_ports to _available_ports + for hostname, port in self._used_ports: + del self._all_used_ports[(hostname, port)] + self._available_ports[(hostname, port)] = None + class DirectoryBasedController(_BaseController): """Helper for controllers whose software configuration is based on an @@ -202,9 +253,6 @@ class BaseServerController(_BaseController): super().__init__(*args, **kwargs) self.faketime_enabled = False - def get_hostname_and_port(self) -> Tuple[str, int]: - return find_hostname_and_port() - def run( self, hostname: str, diff --git a/irctest/controllers/bahamut.py b/irctest/controllers/bahamut.py index 2f187ab..5ca57ee 100644 --- a/irctest/controllers/bahamut.py +++ b/irctest/controllers/bahamut.py @@ -8,7 +8,6 @@ from irctest.basecontrollers import ( DirectoryBasedController, NotImplementedByController, ) -from irctest.irc_utils.junkdrawer import find_hostname_and_port TEMPLATE_CONFIG = """ global {{ @@ -125,8 +124,8 @@ class BahamutController(BaseServerController, DirectoryBasedController): self.port = port self.hostname = hostname self.create_config() - (unused_hostname, unused_port) = find_hostname_and_port() - (services_hostname, services_port) = find_hostname_and_port() + (unused_hostname, unused_port) = self.get_hostname_and_port() + (services_hostname, services_port) = self.get_hostname_and_port() password_field = "passwd {};".format(password) if password else "" diff --git a/irctest/controllers/base_hybrid.py b/irctest/controllers/base_hybrid.py index 79897ed..32922e7 100644 --- a/irctest/controllers/base_hybrid.py +++ b/irctest/controllers/base_hybrid.py @@ -7,7 +7,6 @@ from irctest.basecontrollers import ( DirectoryBasedController, NotImplementedByController, ) -from irctest.irc_utils.junkdrawer import find_hostname_and_port TEMPLATE_SSL_CONFIG = """ ssl_private_key = "{key_path}"; @@ -53,7 +52,7 @@ class BaseHybridController(BaseServerController, DirectoryBasedController): self.port = port self.hostname = hostname self.create_config() - (services_hostname, services_port) = find_hostname_and_port() + (services_hostname, services_port) = self.get_hostname_and_port() password_field = 'password = "{}";'.format(password) if password else "" if ssl: self.gen_ssl() diff --git a/irctest/controllers/inspircd.py b/irctest/controllers/inspircd.py index b0b1821..3afb41f 100644 --- a/irctest/controllers/inspircd.py +++ b/irctest/controllers/inspircd.py @@ -8,7 +8,6 @@ from irctest.basecontrollers import ( DirectoryBasedController, NotImplementedByController, ) -from irctest.irc_utils.junkdrawer import find_hostname_and_port TEMPLATE_CONFIG = """ # Clients: @@ -138,7 +137,7 @@ class InspircdController(BaseServerController, DirectoryBasedController): self.port = port self.hostname = hostname self.create_config() - (services_hostname, services_port) = find_hostname_and_port() + (services_hostname, services_port) = self.get_hostname_and_port() password_field = 'password="{}"'.format(password) if password else "" diff --git a/irctest/controllers/ngircd.py b/irctest/controllers/ngircd.py index be42b16..c158e47 100644 --- a/irctest/controllers/ngircd.py +++ b/irctest/controllers/ngircd.py @@ -7,7 +7,6 @@ from irctest.basecontrollers import ( DirectoryBasedController, NotImplementedByController, ) -from irctest.irc_utils.junkdrawer import find_hostname_and_port TEMPLATE_CONFIG = """ [Global] @@ -66,7 +65,7 @@ class NgircdController(BaseServerController, DirectoryBasedController): self.port = port self.hostname = hostname self.create_config() - (unused_hostname, unused_port) = find_hostname_and_port() + (unused_hostname, unused_port) = self.get_hostname_and_port() password_field = "Password = {}".format(password) if password else "" diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py index 9adfef3..f906d3d 100644 --- a/irctest/controllers/unrealircd.py +++ b/irctest/controllers/unrealircd.py @@ -12,7 +12,6 @@ from irctest.basecontrollers import ( DirectoryBasedController, NotImplementedByController, ) -from irctest.irc_utils.junkdrawer import find_hostname_and_port TEMPLATE_CONFIG = """ include "modules.default.conf"; @@ -228,8 +227,8 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): password_field = 'password "{}";'.format(password) if password else "" with _STARTSTOP_LOCK(): - (services_hostname, services_port) = find_hostname_and_port() - (unused_hostname, unused_port) = find_hostname_and_port() + (services_hostname, services_port) = self.get_hostname_and_port() + (unused_hostname, unused_port) = self.get_hostname_and_port() self.gen_ssl() if ssl: From aed6478a2c3b2156cb5d85958a3de1bad15dfef3 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Wed, 5 Apr 2023 08:24:34 +0200 Subject: [PATCH 097/143] Bump UnrealIRCd to v6.0.7 (#192) --- .github/workflows/test-stable.yml | 2 +- irctest/controllers/unrealircd.py | 36 +++++++++++++++++++------------ irctest/server_tests/messages.py | 9 ++++++-- workflows.yml | 4 ++-- 4 files changed, 32 insertions(+), 19 deletions(-) diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index f4f7778..9ac8406 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -347,7 +347,7 @@ jobs: uses: actions/checkout@v2 with: path: unrealircd - ref: cedd23ae9cdd5985ce16e9869cbdb808479c3fc4 + ref: da3c1c654481a33035b9c703957e1c25d0158259 repository: unrealircd/unrealircd - name: Build UnrealIRCd 6 run: | diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py index f906d3d..b361796 100644 --- a/irctest/controllers/unrealircd.py +++ b/irctest/controllers/unrealircd.py @@ -100,7 +100,7 @@ set {{ }} modes-on-join "+H 100:1d"; // Enables CHATHISTORY - {set_extras} + {set_v6only} }} @@ -123,6 +123,24 @@ oper "operuser" {{ }} """ +SET_V6ONLY = """ +// Remove RPL_WHOISSPECIAL used to advertise security groups +whois-details { + security-groups { everyone none; self none; oper none; } +} + +plaintext-policy { + server warn; // https://www.unrealircd.org/docs/FAQ#server-requires-tls + oper warn; // https://www.unrealircd.org/docs/FAQ#oper-requires-tls +} + +anti-flood { + everyone { + connect-flood 255:10; + } +} +""" + def _filelock(path: Path) -> Callable[[], ContextManager]: """Alternative to :cls:`multiprocessing.Lock` that works with pytest-xdist""" @@ -206,20 +224,10 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): loadmodule "cloak_md5"; """ ) - set_extras = textwrap.indent( - textwrap.dedent( - """ - // Remove RPL_WHOISSPECIAL used to advertise security groups - whois-details { - security-groups { everyone none; self none; oper none; } - } - """ - ), - " ", - ) + set_v6only = SET_V6ONLY else: extras = "" - set_extras = "" + set_v6only = "" with self.open_file("empty.txt") as fd: fd.write("\n") @@ -253,8 +261,8 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): key_path=self.key_path, pem_path=self.pem_path, empty_file=self.directory / "empty.txt", + set_v6only=set_v6only, extras=extras, - set_extras=set_extras, ) ) diff --git a/irctest/server_tests/messages.py b/irctest/server_tests/messages.py index c815e94..8282ee2 100644 --- a/irctest/server_tests/messages.py +++ b/irctest/server_tests/messages.py @@ -100,8 +100,13 @@ class NoticeTestCase(cases.BaseServerTestCase): class TagsTestCase(cases.BaseServerTestCase): @cases.mark_capabilities("message-tags") - @cases.xfailIfSoftware( - ["UnrealIRCd"], "https://bugs.unrealircd.org/view.php?id=5947" + @cases.xfailIf( + lambda self: bool( + self.controller.software_name == "UnrealIRCd" + and self.controller.software_version == 5 + ), + "UnrealIRCd <6.0.7 dropped messages with excessively large tags: " + "https://bugs.unrealircd.org/view.php?id=5947", ) def testLineTooLong(self): self.connectClient("bar", capabilities=["message-tags"], skip_if_cap_nak=True) diff --git a/workflows.yml b/workflows.yml index 4f63757..9b66fc3 100644 --- a/workflows.yml +++ b/workflows.yml @@ -268,8 +268,8 @@ software: name: UnrealIRCd 6 repository: unrealircd/unrealircd refs: - stable: cedd23ae9cdd5985ce16e9869cbdb808479c3fc4 # 6.0.3 - release: cedd23ae9cdd5985ce16e9869cbdb808479c3fc4 # 6.0.3 + stable: da3c1c654481a33035b9c703957e1c25d0158259 # 6.0.7 + release: da3c1c654481a33035b9c703957e1c25d0158259 # 6.0.7 devel: unreal60_dev devel_release: null path: unrealircd From ddb37d6c3fcc10fedb4de173c32e666bdecc3692 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 15 Apr 2023 23:04:24 +0200 Subject: [PATCH 098/143] Use real metadata keys (#194) --- irctest/basecontrollers.py | 2 -- irctest/cases.py | 4 ---- irctest/controllers/bahamut.py | 13 +---------- irctest/controllers/base_hybrid.py | 14 ++--------- irctest/controllers/ergo.py | 16 ++----------- irctest/controllers/external_server.py | 5 +--- irctest/controllers/inspircd.py | 15 ++---------- irctest/controllers/irc2.py | 8 +------ irctest/controllers/ircu2.py | 8 +------ irctest/controllers/mammon.py | 11 +++------ irctest/controllers/ngircd.py | 13 +---------- irctest/controllers/snircd.py | 8 +------ irctest/controllers/unrealircd.py | 15 ++---------- irctest/server_tests/metadata.py | 32 +++++++++++++------------- 14 files changed, 33 insertions(+), 131 deletions(-) diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index b64cdaf..02b9a34 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -261,8 +261,6 @@ class BaseServerController(_BaseController): password: Optional[str], ssl: bool, run_services: bool, - valid_metadata_keys: Optional[Set[str]], - invalid_metadata_keys: Optional[Set[str]], faketime: Optional[str], ) -> None: raise NotImplementedError() diff --git a/irctest/cases.py b/irctest/cases.py index 9a3ff38..4ddf07f 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -529,8 +529,6 @@ class BaseServerTestCase( password: Optional[str] = None ssl = False - valid_metadata_keys: Set[str] = set() - invalid_metadata_keys: Set[str] = set() server_support: Optional[Dict[str, Optional[str]]] run_services = False @@ -550,8 +548,6 @@ class BaseServerTestCase( self.hostname, self.port, password=self.password, - valid_metadata_keys=self.valid_metadata_keys, - invalid_metadata_keys=self.invalid_metadata_keys, ssl=self.ssl, run_services=self.run_services, faketime=self.faketime, diff --git a/irctest/controllers/bahamut.py b/irctest/controllers/bahamut.py index 5ca57ee..06a3661 100644 --- a/irctest/controllers/bahamut.py +++ b/irctest/controllers/bahamut.py @@ -3,11 +3,7 @@ import shutil import subprocess from typing import Optional, Set, Type -from irctest.basecontrollers import ( - BaseServerController, - DirectoryBasedController, - NotImplementedByController, -) +from irctest.basecontrollers import BaseServerController, DirectoryBasedController TEMPLATE_CONFIG = """ global {{ @@ -111,15 +107,8 @@ class BahamutController(BaseServerController, DirectoryBasedController): password: Optional[str], ssl: bool, run_services: bool, - valid_metadata_keys: Optional[Set[str]] = None, - invalid_metadata_keys: Optional[Set[str]] = None, - restricted_metadata_keys: Optional[Set[str]] = None, faketime: Optional[str], ) -> None: - if valid_metadata_keys or invalid_metadata_keys: - raise NotImplementedByController( - "Defining valid and invalid METADATA keys." - ) assert self.proc is None self.port = port self.hostname = hostname diff --git a/irctest/controllers/base_hybrid.py b/irctest/controllers/base_hybrid.py index 32922e7..38d7073 100644 --- a/irctest/controllers/base_hybrid.py +++ b/irctest/controllers/base_hybrid.py @@ -1,12 +1,8 @@ import shutil import subprocess -from typing import Optional, Set +from typing import Optional -from irctest.basecontrollers import ( - BaseServerController, - DirectoryBasedController, - NotImplementedByController, -) +from irctest.basecontrollers import BaseServerController, DirectoryBasedController TEMPLATE_SSL_CONFIG = """ ssl_private_key = "{key_path}"; @@ -40,14 +36,8 @@ class BaseHybridController(BaseServerController, DirectoryBasedController): password: Optional[str], ssl: bool, run_services: bool, - valid_metadata_keys: Optional[Set[str]] = None, - invalid_metadata_keys: Optional[Set[str]] = None, faketime: Optional[str], ) -> None: - if valid_metadata_keys or invalid_metadata_keys: - raise NotImplementedByController( - "Defining valid and invalid METADATA keys." - ) assert self.proc is None self.port = port self.hostname = hostname diff --git a/irctest/controllers/ergo.py b/irctest/controllers/ergo.py index 6157712..15d2ff0 100644 --- a/irctest/controllers/ergo.py +++ b/irctest/controllers/ergo.py @@ -3,13 +3,9 @@ import json import os import shutil import subprocess -from typing import Any, Dict, Optional, Set, Type, Union +from typing import Any, Dict, Optional, Type, Union -from irctest.basecontrollers import ( - BaseServerController, - DirectoryBasedController, - NotImplementedByController, -) +from irctest.basecontrollers import BaseServerController, DirectoryBasedController from irctest.cases import BaseServerTestCase BASE_CONFIG = { @@ -153,17 +149,9 @@ class ErgoController(BaseServerController, DirectoryBasedController): password: Optional[str], ssl: bool, run_services: bool, - valid_metadata_keys: Optional[Set[str]] = None, - invalid_metadata_keys: Optional[Set[str]] = None, - restricted_metadata_keys: Optional[Set[str]] = None, faketime: Optional[str], config: Optional[Any] = None, ) -> None: - if valid_metadata_keys or invalid_metadata_keys: - raise NotImplementedByController( - "Defining valid and invalid METADATA keys." - ) - self.create_config() if config is None: config = copy.deepcopy(BASE_CONFIG) diff --git a/irctest/controllers/external_server.py b/irctest/controllers/external_server.py index e8c822a..021e3bd 100644 --- a/irctest/controllers/external_server.py +++ b/irctest/controllers/external_server.py @@ -1,5 +1,5 @@ import os -from typing import Optional, Set, Tuple, Type +from typing import Optional, Tuple, Type from irctest.basecontrollers import BaseServerController @@ -39,9 +39,6 @@ class ExternalServerController(BaseServerController): password: Optional[str], ssl: bool, run_services: bool, - valid_metadata_keys: Optional[Set[str]] = None, - invalid_metadata_keys: Optional[Set[str]] = None, - restricted_metadata_keys: Optional[Set[str]] = None, faketime: Optional[str], ) -> None: pass diff --git a/irctest/controllers/inspircd.py b/irctest/controllers/inspircd.py index 3afb41f..b932ec2 100644 --- a/irctest/controllers/inspircd.py +++ b/irctest/controllers/inspircd.py @@ -1,13 +1,9 @@ import functools import shutil import subprocess -from typing import Optional, Set, Type +from typing import Optional, Type -from irctest.basecontrollers import ( - BaseServerController, - DirectoryBasedController, - NotImplementedByController, -) +from irctest.basecontrollers import BaseServerController, DirectoryBasedController TEMPLATE_CONFIG = """ # Clients: @@ -124,15 +120,8 @@ class InspircdController(BaseServerController, DirectoryBasedController): password: Optional[str], ssl: bool, run_services: bool, - valid_metadata_keys: Optional[Set[str]] = None, - invalid_metadata_keys: Optional[Set[str]] = None, - restricted_metadata_keys: Optional[Set[str]] = None, faketime: Optional[str] = None, ) -> None: - if valid_metadata_keys or invalid_metadata_keys: - raise NotImplementedByController( - "Defining valid and invalid METADATA keys." - ) assert self.proc is None self.port = port self.hostname = hostname diff --git a/irctest/controllers/irc2.py b/irctest/controllers/irc2.py index 26cd37c..ac3cde2 100644 --- a/irctest/controllers/irc2.py +++ b/irctest/controllers/irc2.py @@ -1,6 +1,6 @@ import shutil import subprocess -from typing import Optional, Set, Type +from typing import Optional, Type from irctest.basecontrollers import ( BaseServerController, @@ -49,14 +49,8 @@ class Irc2Controller(BaseServerController, DirectoryBasedController): password: Optional[str], ssl: bool, run_services: bool, - valid_metadata_keys: Optional[Set[str]] = None, - invalid_metadata_keys: Optional[Set[str]] = None, faketime: Optional[str], ) -> None: - if valid_metadata_keys or invalid_metadata_keys: - raise NotImplementedByController( - "Defining valid and invalid METADATA keys." - ) if ssl: raise NotImplementedByController("TLS") if run_services: diff --git a/irctest/controllers/ircu2.py b/irctest/controllers/ircu2.py index e574a9c..15124c7 100644 --- a/irctest/controllers/ircu2.py +++ b/irctest/controllers/ircu2.py @@ -1,6 +1,6 @@ import shutil import subprocess -from typing import Optional, Set, Type +from typing import Optional, Type from irctest.basecontrollers import ( BaseServerController, @@ -68,14 +68,8 @@ class Ircu2Controller(BaseServerController, DirectoryBasedController): password: Optional[str], ssl: bool, run_services: bool, - valid_metadata_keys: Optional[Set[str]] = None, - invalid_metadata_keys: Optional[Set[str]] = None, faketime: Optional[str], ) -> None: - if valid_metadata_keys or invalid_metadata_keys: - raise NotImplementedByController( - "Defining valid and invalid METADATA keys." - ) if ssl: raise NotImplementedByController("TLS") if run_services: diff --git a/irctest/controllers/mammon.py b/irctest/controllers/mammon.py index 04f59f7..8c22776 100644 --- a/irctest/controllers/mammon.py +++ b/irctest/controllers/mammon.py @@ -33,10 +33,10 @@ extensions: - mammon.ext.ircv3.sasl - mammon.ext.misc.nopost metadata: - restricted_keys: -{restricted_keys} + restricted_keys: [] whitelist: -{authorized_keys} + - display-name + - avatar monitor: limit: 20 motd: @@ -89,9 +89,6 @@ class MammonController(BaseServerController, DirectoryBasedController): password: Optional[str], ssl: bool, run_services: bool, - valid_metadata_keys: Optional[Set[str]] = None, - invalid_metadata_keys: Optional[Set[str]] = None, - restricted_metadata_keys: Optional[Set[str]] = None, faketime: Optional[str], ) -> None: if password is not None: @@ -107,8 +104,6 @@ class MammonController(BaseServerController, DirectoryBasedController): directory=self.directory, hostname=hostname, port=port, - authorized_keys=make_list(valid_metadata_keys or set()), - restricted_keys=make_list(restricted_metadata_keys or set()), ) ) # with self.open_file('server.yml', 'r') as fd: diff --git a/irctest/controllers/ngircd.py b/irctest/controllers/ngircd.py index c158e47..0bcd0ca 100644 --- a/irctest/controllers/ngircd.py +++ b/irctest/controllers/ngircd.py @@ -2,11 +2,7 @@ import shutil import subprocess from typing import Optional, Set, Type -from irctest.basecontrollers import ( - BaseServerController, - DirectoryBasedController, - NotImplementedByController, -) +from irctest.basecontrollers import BaseServerController, DirectoryBasedController TEMPLATE_CONFIG = """ [Global] @@ -52,15 +48,8 @@ class NgircdController(BaseServerController, DirectoryBasedController): password: Optional[str], ssl: bool, run_services: bool, - valid_metadata_keys: Optional[Set[str]] = None, - invalid_metadata_keys: Optional[Set[str]] = None, - restricted_metadata_keys: Optional[Set[str]] = None, faketime: Optional[str], ) -> None: - if valid_metadata_keys or invalid_metadata_keys: - raise NotImplementedByController( - "Defining valid and invalid METADATA keys." - ) assert self.proc is None self.port = port self.hostname = hostname diff --git a/irctest/controllers/snircd.py b/irctest/controllers/snircd.py index f49a347..1a8b2fa 100644 --- a/irctest/controllers/snircd.py +++ b/irctest/controllers/snircd.py @@ -1,6 +1,6 @@ import shutil import subprocess -from typing import Optional, Set, Type +from typing import Optional, Type from irctest.basecontrollers import ( BaseServerController, @@ -67,14 +67,8 @@ class SnircdController(BaseServerController, DirectoryBasedController): password: Optional[str], ssl: bool, run_services: bool, - valid_metadata_keys: Optional[Set[str]] = None, - invalid_metadata_keys: Optional[Set[str]] = None, faketime: Optional[str], ) -> None: - if valid_metadata_keys or invalid_metadata_keys: - raise NotImplementedByController( - "Defining valid and invalid METADATA keys." - ) if ssl: raise NotImplementedByController("TLS") if run_services: diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py index b361796..53da8ba 100644 --- a/irctest/controllers/unrealircd.py +++ b/irctest/controllers/unrealircd.py @@ -5,13 +5,9 @@ from pathlib import Path import shutil import subprocess import textwrap -from typing import Callable, ContextManager, Iterator, Optional, Set, Type +from typing import Callable, ContextManager, Iterator, Optional, Type -from irctest.basecontrollers import ( - BaseServerController, - DirectoryBasedController, - NotImplementedByController, -) +from irctest.basecontrollers import BaseServerController, DirectoryBasedController TEMPLATE_CONFIG = """ include "modules.default.conf"; @@ -203,15 +199,8 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): password: Optional[str], ssl: bool, run_services: bool, - valid_metadata_keys: Optional[Set[str]] = None, - invalid_metadata_keys: Optional[Set[str]] = None, - restricted_metadata_keys: Optional[Set[str]] = None, faketime: Optional[str], ) -> None: - if valid_metadata_keys or invalid_metadata_keys: - raise NotImplementedByController( - "Defining valid and invalid METADATA keys." - ) assert self.proc is None self.port = port self.hostname = hostname diff --git a/irctest/server_tests/metadata.py b/irctest/server_tests/metadata.py index abfde8b..555b6b4 100644 --- a/irctest/server_tests/metadata.py +++ b/irctest/server_tests/metadata.py @@ -6,8 +6,8 @@ from irctest import cases class MetadataTestCase(cases.BaseServerTestCase): - valid_metadata_keys = {"valid_key1", "valid_key2"} - invalid_metadata_keys = {"invalid_key1", "invalid_key2"} + valid_metadata_keys = {"display-name", "avatar"} + invalid_metadata_keys = {"indisplay-name", "inavatar"} @cases.mark_specifications("IRCv3", deprecated=True) def testInIsupport(self): @@ -36,7 +36,7 @@ class MetadataTestCase(cases.BaseServerTestCase): def testGetOneUnsetValid(self): """""" self.connectClient("foo") - self.sendLine(1, "METADATA * GET valid_key1") + self.sendLine(1, "METADATA * GET display-name") m = self.getMessage(1) self.assertMessageMatch( m, @@ -52,7 +52,7 @@ class MetadataTestCase(cases.BaseServerTestCase): -- """ self.connectClient("foo") - self.sendLine(1, "METADATA * GET valid_key1 valid_key2") + self.sendLine(1, "METADATA * GET display-name avatar") m = self.getMessage(1) self.assertMessageMatch( m, @@ -62,10 +62,10 @@ class MetadataTestCase(cases.BaseServerTestCase): ) self.assertEqual( m.params[1], - "valid_key1", + "display-name", m, - fail_msg="Response to “METADATA * GET valid_key1 valid_key2” " - "did not respond to valid_key1 first: {msg}", + fail_msg="Response to “METADATA * GET display-name avatar” " + "did not respond to display-name first: {msg}", ) m = self.getMessage(1) self.assertMessageMatch( @@ -76,10 +76,10 @@ class MetadataTestCase(cases.BaseServerTestCase): ) self.assertEqual( m.params[1], - "valid_key2", + "avatar", m, - fail_msg="Response to “METADATA * GET valid_key1 valid_key2” " - "did not respond to valid_key2 as second response: {msg}", + fail_msg="Response to “METADATA * GET display-name avatar” " + "did not respond to avatar as second response: {msg}", ) @cases.mark_specifications("IRCv3", deprecated=True) @@ -135,7 +135,7 @@ class MetadataTestCase(cases.BaseServerTestCase): ) self.assertEqual( m.params[1], - "valid_key1", + "display-name", m, fail_msg="Second param of 761 after setting “{expects}” to " "“{}” is not “{expects}”: {msg}.", @@ -190,7 +190,7 @@ class MetadataTestCase(cases.BaseServerTestCase): def testSetGetValid(self): """""" self.connectClient("foo") - self.assertSetGetValue("*", "valid_key1", "myvalue") + self.assertSetGetValue("*", "display-name", "myvalue") @cases.mark_specifications("IRCv3", deprecated=True) def testSetGetZeroCharInValue(self): @@ -198,7 +198,7 @@ class MetadataTestCase(cases.BaseServerTestCase): -- """ self.connectClient("foo") - self.assertSetGetValue("*", "valid_key1", "zero->\0<-zero", "zero->\\0<-zero") + self.assertSetGetValue("*", "display-name", "zero->\0<-zero", "zero->\\0<-zero") @cases.mark_specifications("IRCv3", deprecated=True) def testSetGetHeartInValue(self): @@ -209,7 +209,7 @@ class MetadataTestCase(cases.BaseServerTestCase): self.connectClient("foo") self.assertSetGetValue( "*", - "valid_key1", + "display-name", "->{}<-".format(heart), "zero->{}<-zero".format(heart.encode()), ) @@ -223,7 +223,7 @@ class MetadataTestCase(cases.BaseServerTestCase): # Sending directly because it is not valid UTF-8 so Python would # not like it self.clients[1].conn.sendall( - b"METADATA * SET valid_key1 " b":invalid UTF-8 ->\xc3<-\r\n" + b"METADATA * SET display-name " b":invalid UTF-8 ->\xc3<-\r\n" ) commands = {m.command for m in self.getMessages(1)} self.assertNotIn( @@ -233,7 +233,7 @@ class MetadataTestCase(cases.BaseServerTestCase): "UTF-8 was answered with 761 (RPL_KEYVALUE)", ) self.clients[1].conn.sendall( - b"METADATA * SET valid_key1 " b":invalid UTF-8: \xc3\r\n" + b"METADATA * SET display-name " b":invalid UTF-8: \xc3\r\n" ) commands = {m.command for m in self.getMessages(1)} self.assertNotIn( From 11dc5b046e85f22d3d1beabf21b65e22a385a68b Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 16 Apr 2023 09:19:05 +0200 Subject: [PATCH 099/143] unrealircd: Move SSL and port generation out of the critical section (#196) --- irctest/controllers/unrealircd.py | 64 +++++++++++++++---------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py index 53da8ba..8d7e643 100644 --- a/irctest/controllers/unrealircd.py +++ b/irctest/controllers/unrealircd.py @@ -223,44 +223,44 @@ class UnrealircdController(BaseServerController, DirectoryBasedController): password_field = 'password "{}";'.format(password) if password else "" - with _STARTSTOP_LOCK(): - (services_hostname, services_port) = self.get_hostname_and_port() - (unused_hostname, unused_port) = self.get_hostname_and_port() + (services_hostname, services_port) = self.get_hostname_and_port() + (unused_hostname, unused_port) = self.get_hostname_and_port() - self.gen_ssl() - if ssl: - (tls_hostname, tls_port) = (hostname, port) - (hostname, port) = (unused_hostname, unused_port) - else: - # Unreal refuses to start without TLS enabled - (tls_hostname, tls_port) = (unused_hostname, unused_port) + self.gen_ssl() + if ssl: + (tls_hostname, tls_port) = (hostname, port) + (hostname, port) = (unused_hostname, unused_port) + else: + # Unreal refuses to start without TLS enabled + (tls_hostname, tls_port) = (unused_hostname, unused_port) - assert self.directory + assert self.directory - with self.open_file("unrealircd.conf") as fd: - fd.write( - TEMPLATE_CONFIG.format( - hostname=hostname, - port=port, - services_hostname=services_hostname, - services_port=services_port, - tls_hostname=tls_hostname, - tls_port=tls_port, - password_field=password_field, - key_path=self.key_path, - pem_path=self.pem_path, - empty_file=self.directory / "empty.txt", - set_v6only=set_v6only, - extras=extras, - ) + with self.open_file("unrealircd.conf") as fd: + fd.write( + TEMPLATE_CONFIG.format( + hostname=hostname, + port=port, + services_hostname=services_hostname, + services_port=services_port, + tls_hostname=tls_hostname, + tls_port=tls_port, + password_field=password_field, + key_path=self.key_path, + pem_path=self.pem_path, + empty_file=self.directory / "empty.txt", + set_v6only=set_v6only, + extras=extras, ) + ) - if faketime and shutil.which("faketime"): - faketime_cmd = ["faketime", "-f", faketime] - self.faketime_enabled = True - else: - faketime_cmd = [] + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + with _STARTSTOP_LOCK(): self.proc = subprocess.Popen( [ *faketime_cmd, From 6edf4e27f1ba2c021cda6fd33978922e572250ca Mon Sep 17 00:00:00 2001 From: alice <92898519+alicetries@users.noreply.github.com> Date: Mon, 17 Apr 2023 17:45:50 +0100 Subject: [PATCH 100/143] Remove xfail in WHOWAS as linked PRs have been merged (#197) * Bump inspircd stable version. * Remove xfail in WHOWAS as linked PRs have been merged --- .github/workflows/test-stable.yml | 2 +- irctest/server_tests/whowas.py | 20 -------------------- workflows.yml | 2 +- 3 files changed, 2 insertions(+), 22 deletions(-) diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 9ac8406..df7b955 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -184,7 +184,7 @@ jobs: uses: actions/checkout@v2 with: path: inspircd - ref: v3.12.0 + ref: v3.15.0 repository: inspircd/inspircd - name: Build InspIRCd run: | diff --git a/irctest/server_tests/whowas.py b/irctest/server_tests/whowas.py index f193595..5b5d926 100644 --- a/irctest/server_tests/whowas.py +++ b/irctest/server_tests/whowas.py @@ -201,10 +201,6 @@ class WhowasTestCase(cases.BaseServerTestCase): ) @cases.mark_specifications("RFC1459", "RFC2812", "Modern") - @cases.xfailIfSoftware( - ["InspIRCd"], - "Feature not released yet: https://github.com/inspircd/inspircd/pull/1967", - ) def testWhowasMultiple(self): """ "The history is searched backward, returning the most recent entry first." @@ -215,10 +211,6 @@ class WhowasTestCase(cases.BaseServerTestCase): self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2") @cases.mark_specifications("RFC1459", "RFC2812", "Modern") - @cases.xfailIfSoftware( - ["InspIRCd"], - "Feature not released yet: https://github.com/inspircd/inspircd/pull/1968", - ) def testWhowasCount1(self): """ "If there are multiple entries, up to replies will be returned" @@ -229,10 +221,6 @@ class WhowasTestCase(cases.BaseServerTestCase): self._testWhowasMultiple(second_result=False, whowas_command="WHOWAS nick2 1") @cases.mark_specifications("RFC1459", "RFC2812", "Modern") - @cases.xfailIfSoftware( - ["InspIRCd"], - "Feature not released yet: https://github.com/inspircd/inspircd/pull/1968", - ) def testWhowasCount2(self): """ "If there are multiple entries, up to replies will be returned" @@ -243,10 +231,6 @@ class WhowasTestCase(cases.BaseServerTestCase): self._testWhowasMultiple(second_result=True, whowas_command="WHOWAS nick2 2") @cases.mark_specifications("RFC1459", "RFC2812", "Modern") - @cases.xfailIfSoftware( - ["InspIRCd"], - "Feature not released yet: https://github.com/inspircd/inspircd/pull/1968", - ) def testWhowasCountNegative(self): """ "If a non-positive number is passed as being , then a full search @@ -264,10 +248,6 @@ class WhowasTestCase(cases.BaseServerTestCase): @cases.xfailIfSoftware( ["ircu2"], "Fix not released yet: https://github.com/UndernetIRC/ircu2/pull/19" ) - @cases.xfailIfSoftware( - ["InspIRCd"], - "Feature not released yet: https://github.com/inspircd/inspircd/pull/1967", - ) def testWhowasCountZero(self): """ "If a non-positive number is passed as being , then a full search diff --git a/workflows.yml b/workflows.yml index 9b66fc3..ee2b358 100644 --- a/workflows.yml +++ b/workflows.yml @@ -143,7 +143,7 @@ software: name: InspIRCd repository: inspircd/inspircd refs: &inspircd_refs - stable: v3.12.0 + stable: v3.15.0 release: null devel: master devel_release: insp3 From 3b7f81e22c317eae885f5a942767ba4f6a30414e Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Tue, 18 Apr 2023 23:52:21 -0700 Subject: [PATCH 101/143] strip whitespace from Ergo hashed password output (#198) Removes the need for some special-casing in `ergo genpasswd` --- irctest/controllers/ergo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irctest/controllers/ergo.py b/irctest/controllers/ergo.py index 15d2ff0..5655e82 100644 --- a/irctest/controllers/ergo.py +++ b/irctest/controllers/ergo.py @@ -126,7 +126,7 @@ def hash_password(password: Union[str, bytes]) -> str: ["ergo", "genpasswd"], stdin=subprocess.PIPE, stdout=subprocess.PIPE ) out, _ = p.communicate(input_) - return out.decode("utf-8") + return out.decode("utf-8").strip() class ErgoController(BaseServerController, DirectoryBasedController): From 05e9b3746efdeb0000a12b594c7f628c065aec8a Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 20 May 2023 13:32:42 +0200 Subject: [PATCH 102/143] ci: Bump versions of actions we use (#199) So Github stops complaining about the deprecated Nodejs version --- .github/workflows/test-devel.yml | 268 ++++++++++---------- .github/workflows/test-devel_release.yml | 48 ++-- .github/workflows/test-stable.yml | 302 +++++++++++------------ make_workflows.py | 22 +- 4 files changed, 320 insertions(+), 320 deletions(-) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 44eda53..47e23d8 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -8,7 +8,7 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: key: 3-${{ runner.os }}-anope-devel path: '~/.cache @@ -16,13 +16,13 @@ jobs: ${ github.workspace }/anope ' - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout Anope - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: anope ref: 2.0.9 @@ -37,7 +37,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-anope.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-anope path: ~/artefacts-*.tar.gz @@ -48,7 +48,7 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: key: 3-${{ runner.os }}-bahamut-devel path: '~/.cache @@ -56,13 +56,13 @@ jobs: ${ github.workspace }/Bahamut ' - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout Bahamut - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: Bahamut ref: master @@ -86,7 +86,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-bahamut.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-bahamut path: ~/artefacts-*.tar.gz @@ -97,7 +97,7 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: key: 3-${{ runner.os }}-hybrid-devel path: '~/.cache @@ -105,13 +105,13 @@ jobs: ${ github.workspace }/ircd-hybrid ' - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout Hybrid - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: ircd-hybrid ref: 8.2.x @@ -125,7 +125,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-hybrid.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-hybrid path: ~/artefacts-*.tar.gz @@ -135,13 +135,13 @@ jobs: steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout InspIRCd - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: inspircd ref: master @@ -156,7 +156,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-inspircd.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-inspircd path: ~/artefacts-*.tar.gz @@ -167,7 +167,7 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: key: 3-${{ runner.os }}-ngircd-devel path: '~/.cache @@ -175,13 +175,13 @@ jobs: ${ github.workspace }/ngircd ' - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout ngircd - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: ngircd ref: master @@ -197,7 +197,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-ngircd.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-ngircd path: ~/artefacts-*.tar.gz @@ -208,7 +208,7 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: key: 3-${{ runner.os }}-plexus4-devel path: '~/.cache @@ -216,9 +216,9 @@ jobs: ${ github.workspace }/placeholder ' - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: clone @@ -239,7 +239,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-plexus4.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-plexus4 path: ~/artefacts-*.tar.gz @@ -250,7 +250,7 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: key: 3-${{ runner.os }}-solanum-devel path: '~/.cache @@ -258,13 +258,13 @@ jobs: ${ github.workspace }/solanum ' - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout Solanum - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: solanum ref: main @@ -279,7 +279,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-solanum.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-solanum path: ~/artefacts-*.tar.gz @@ -290,7 +290,7 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: key: 3-${{ runner.os }}-unrealircd-devel path: '~/.cache @@ -298,13 +298,13 @@ jobs: ${ github.workspace }/unrealircd ' - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout UnrealIRCd 6 - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: unrealircd ref: unreal60_dev @@ -325,7 +325,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-unrealircd.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-unrealircd path: ~/artefacts-*.tar.gz @@ -336,7 +336,7 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: key: 3-${{ runner.os }}-unrealircd-5-devel path: '~/.cache @@ -344,13 +344,13 @@ jobs: ${ github.workspace }/unrealircd ' - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout UnrealIRCd 5 - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: unrealircd ref: unreal52 @@ -371,7 +371,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-unrealircd-5.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-unrealircd-5 path: ~/artefacts-*.tar.gz @@ -403,9 +403,9 @@ jobs: - test-unrealircd-dlk runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Download Artifacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: path: artifacts - name: Install dashboard dependencies @@ -430,13 +430,13 @@ jobs: - build-bahamut runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-bahamut path: '~' @@ -454,7 +454,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_bahamut_devel path: pytest.xml @@ -464,18 +464,18 @@ jobs: - build-anope runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-bahamut path: '~' - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-anope path: '~' @@ -493,7 +493,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_bahamut-anope_devel path: pytest.xml @@ -502,13 +502,13 @@ jobs: - build-bahamut runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-bahamut path: '~' @@ -526,7 +526,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_bahamut-atheme_devel path: pytest.xml @@ -534,13 +534,13 @@ jobs: needs: [] runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout Ergo - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: ergo ref: master @@ -566,7 +566,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_ergo_devel path: pytest.xml @@ -576,18 +576,18 @@ jobs: - build-anope runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-hybrid path: '~' - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-anope path: '~' @@ -605,7 +605,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_hybrid_devel path: pytest.xml @@ -614,13 +614,13 @@ jobs: - build-inspircd runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-inspircd path: '~' @@ -638,7 +638,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_inspircd_devel path: pytest.xml @@ -648,18 +648,18 @@ jobs: - build-anope runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-inspircd path: '~' - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-anope path: '~' @@ -677,7 +677,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_inspircd-anope_devel path: pytest.xml @@ -685,13 +685,13 @@ jobs: needs: [] runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout ircu2 - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: ircu2 ref: u2_10_12_branch @@ -716,7 +716,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_ircu2_devel path: pytest.xml @@ -724,9 +724,9 @@ jobs: needs: [] runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Install dependencies @@ -744,7 +744,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_limnoria_devel path: pytest.xml @@ -752,13 +752,13 @@ jobs: needs: [] runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout nefarious - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: nefarious ref: master @@ -782,7 +782,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_nefarious_devel path: pytest.xml @@ -791,13 +791,13 @@ jobs: - build-ngircd runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-ngircd path: '~' @@ -815,7 +815,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_ngircd_devel path: pytest.xml @@ -825,18 +825,18 @@ jobs: - build-anope runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-ngircd path: '~' - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-anope path: '~' @@ -854,7 +854,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_ngircd-anope_devel path: pytest.xml @@ -863,13 +863,13 @@ jobs: - build-ngircd runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-ngircd path: '~' @@ -887,7 +887,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_ngircd-atheme_devel path: pytest.xml @@ -897,18 +897,18 @@ jobs: - build-anope runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-plexus4 path: '~' - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-anope path: '~' @@ -926,7 +926,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_plexus4_devel path: pytest.xml @@ -935,13 +935,13 @@ jobs: - build-solanum runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-solanum path: '~' @@ -959,7 +959,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_solanum_devel path: pytest.xml @@ -967,9 +967,9 @@ jobs: needs: [] runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Install dependencies @@ -986,7 +986,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_sopel_devel path: pytest.xml @@ -995,13 +995,13 @@ jobs: - build-unrealircd runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-unrealircd path: '~' @@ -1019,7 +1019,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_unrealircd_devel path: pytest.xml @@ -1028,13 +1028,13 @@ jobs: - build-unrealircd-5 runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-unrealircd-5 path: '~' @@ -1052,7 +1052,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_unrealircd-5_devel path: pytest.xml @@ -1062,18 +1062,18 @@ jobs: - build-anope runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-unrealircd path: '~' - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-anope path: '~' @@ -1091,7 +1091,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_unrealircd-anope_devel path: pytest.xml @@ -1100,13 +1100,13 @@ jobs: - build-unrealircd runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-unrealircd path: '~' @@ -1124,7 +1124,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_unrealircd-atheme_devel path: pytest.xml @@ -1133,20 +1133,20 @@ jobs: - build-unrealircd runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-unrealircd path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - name: Checkout Dlk - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: Dlk-Services ref: main @@ -1170,7 +1170,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_unrealircd-dlk_devel path: pytest.xml diff --git a/.github/workflows/test-devel_release.yml b/.github/workflows/test-devel_release.yml index 03ffeef..2bdd1b5 100644 --- a/.github/workflows/test-devel_release.yml +++ b/.github/workflows/test-devel_release.yml @@ -8,7 +8,7 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: key: 3-${{ runner.os }}-anope-devel_release path: '~/.cache @@ -16,13 +16,13 @@ jobs: ${ github.workspace }/anope ' - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout Anope - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: anope ref: 2.0.9 @@ -37,7 +37,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-anope.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-anope path: ~/artefacts-*.tar.gz @@ -47,13 +47,13 @@ jobs: steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout InspIRCd - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: inspircd ref: insp3 @@ -68,7 +68,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-inspircd.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-inspircd path: ~/artefacts-*.tar.gz @@ -82,9 +82,9 @@ jobs: - test-inspircd-atheme runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Download Artifacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: path: artifacts - name: Install dashboard dependencies @@ -109,13 +109,13 @@ jobs: - build-inspircd runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-inspircd path: '~' @@ -133,7 +133,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_inspircd_devel_release path: pytest.xml @@ -143,18 +143,18 @@ jobs: - build-anope runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-inspircd path: '~' - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-anope path: '~' @@ -172,7 +172,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_inspircd-anope_devel_release path: pytest.xml @@ -181,13 +181,13 @@ jobs: - build-inspircd runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-inspircd path: '~' @@ -205,7 +205,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_inspircd-atheme_devel_release path: pytest.xml diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index df7b955..3c6d556 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -8,7 +8,7 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: key: 3-${{ runner.os }}-anope-stable path: '~/.cache @@ -16,13 +16,13 @@ jobs: ${ github.workspace }/anope ' - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout Anope - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: anope ref: 2.0.9 @@ -37,7 +37,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-anope.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-anope path: ~/artefacts-*.tar.gz @@ -48,7 +48,7 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: key: 3-${{ runner.os }}-bahamut-stable path: '~/.cache @@ -56,13 +56,13 @@ jobs: ${ github.workspace }/Bahamut ' - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout Bahamut - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: Bahamut ref: v2.2.1 @@ -86,7 +86,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-bahamut.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-bahamut path: ~/artefacts-*.tar.gz @@ -97,7 +97,7 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: key: 3-${{ runner.os }}-charybdis-stable path: '~/.cache @@ -105,13 +105,13 @@ jobs: ${ github.workspace }/charybdis ' - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout Charybdis - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: charybdis ref: charybdis-4.1.2 @@ -126,7 +126,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-charybdis.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-charybdis path: ~/artefacts-*.tar.gz @@ -137,7 +137,7 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: key: 3-${{ runner.os }}-hybrid-stable path: '~/.cache @@ -145,13 +145,13 @@ jobs: ${ github.workspace }/ircd-hybrid ' - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout Hybrid - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: ircd-hybrid ref: 8.2.39 @@ -165,7 +165,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-hybrid.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-hybrid path: ~/artefacts-*.tar.gz @@ -175,13 +175,13 @@ jobs: steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout InspIRCd - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: inspircd ref: v3.15.0 @@ -196,7 +196,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-inspircd.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-inspircd path: ~/artefacts-*.tar.gz @@ -207,7 +207,7 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: key: 3-${{ runner.os }}-ngircd-stable path: '~/.cache @@ -215,13 +215,13 @@ jobs: ${ github.workspace }/ngircd ' - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout ngircd - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: ngircd ref: rel-26.1 @@ -237,7 +237,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-ngircd.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-ngircd path: ~/artefacts-*.tar.gz @@ -248,7 +248,7 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: key: 3-${{ runner.os }}-plexus4-stable path: '~/.cache @@ -256,9 +256,9 @@ jobs: ${ github.workspace }/placeholder ' - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: clone @@ -279,7 +279,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-plexus4.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-plexus4 path: ~/artefacts-*.tar.gz @@ -290,7 +290,7 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: key: 3-${{ runner.os }}-solanum-stable path: '~/.cache @@ -298,13 +298,13 @@ jobs: ${ github.workspace }/solanum ' - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout Solanum - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: solanum ref: 492d560ee13e71dc35403fd676e58c2d5bdcf2a9 @@ -319,7 +319,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-solanum.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-solanum path: ~/artefacts-*.tar.gz @@ -330,7 +330,7 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: key: 3-${{ runner.os }}-unrealircd-stable path: '~/.cache @@ -338,13 +338,13 @@ jobs: ${ github.workspace }/unrealircd ' - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout UnrealIRCd 6 - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: unrealircd ref: da3c1c654481a33035b9c703957e1c25d0158259 @@ -365,7 +365,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-unrealircd.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-unrealircd path: ~/artefacts-*.tar.gz @@ -376,7 +376,7 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - name: Cache dependencies - uses: actions/cache@v2 + uses: actions/cache@v3 with: key: 3-${{ runner.os }}-unrealircd-5-stable path: '~/.cache @@ -384,13 +384,13 @@ jobs: ${ github.workspace }/unrealircd ' - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout UnrealIRCd 5 - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: unrealircd ref: 6604856973f713a494f83d38992d7d61ce6b9db4 @@ -411,7 +411,7 @@ jobs: - name: Make artefact tarball run: cd ~; tar -czf artefacts-unrealircd-5.tar.gz .local/ go/ - name: Upload build artefacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: installed-unrealircd-5 path: ~/artefacts-*.tar.gz @@ -446,9 +446,9 @@ jobs: - test-unrealircd-dlk runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Download Artifacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: path: artifacts - name: Install dashboard dependencies @@ -473,13 +473,13 @@ jobs: - build-bahamut runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-bahamut path: '~' @@ -497,7 +497,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_bahamut_stable path: pytest.xml @@ -507,18 +507,18 @@ jobs: - build-anope runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-bahamut path: '~' - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-anope path: '~' @@ -536,7 +536,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_bahamut-anope_stable path: pytest.xml @@ -545,13 +545,13 @@ jobs: - build-bahamut runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-bahamut path: '~' @@ -569,7 +569,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_bahamut-atheme_stable path: pytest.xml @@ -578,13 +578,13 @@ jobs: - build-charybdis runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-charybdis path: '~' @@ -602,7 +602,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_charybdis_stable path: pytest.xml @@ -610,13 +610,13 @@ jobs: needs: [] runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout Ergo - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: ergo ref: irctest_stable @@ -642,7 +642,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_ergo_stable path: pytest.xml @@ -652,18 +652,18 @@ jobs: - build-anope runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-hybrid path: '~' - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-anope path: '~' @@ -681,7 +681,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_hybrid_stable path: pytest.xml @@ -690,13 +690,13 @@ jobs: - build-inspircd runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-inspircd path: '~' @@ -714,7 +714,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_inspircd_stable path: pytest.xml @@ -724,18 +724,18 @@ jobs: - build-anope runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-inspircd path: '~' - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-anope path: '~' @@ -753,7 +753,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_inspircd-anope_stable path: pytest.xml @@ -762,13 +762,13 @@ jobs: - build-inspircd runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-inspircd path: '~' @@ -786,7 +786,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_inspircd-atheme_stable path: pytest.xml @@ -794,13 +794,13 @@ jobs: needs: [] runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout irc2 - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: irc2.11.2p3 ref: 59649f24c3a5c27bad5648b48774f27475bccfd3 @@ -836,7 +836,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_irc2_stable path: pytest.xml @@ -844,13 +844,13 @@ jobs: needs: [] runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout ircu2 - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: ircu2 ref: u2.10.12.19 @@ -875,7 +875,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_ircu2_stable path: pytest.xml @@ -883,9 +883,9 @@ jobs: needs: [] runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Install dependencies @@ -902,7 +902,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_limnoria_stable path: pytest.xml @@ -910,13 +910,13 @@ jobs: needs: [] runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Checkout nefarious - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: nefarious ref: 985704168ecada12d9e53b46df6087ef9d9fb40b @@ -940,7 +940,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_nefarious_stable path: pytest.xml @@ -949,13 +949,13 @@ jobs: - build-ngircd runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-ngircd path: '~' @@ -973,7 +973,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_ngircd_stable path: pytest.xml @@ -983,18 +983,18 @@ jobs: - build-anope runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-ngircd path: '~' - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-anope path: '~' @@ -1012,7 +1012,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_ngircd-anope_stable path: pytest.xml @@ -1021,13 +1021,13 @@ jobs: - build-ngircd runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-ngircd path: '~' @@ -1045,7 +1045,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_ngircd-atheme_stable path: pytest.xml @@ -1055,18 +1055,18 @@ jobs: - build-anope runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-plexus4 path: '~' - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-anope path: '~' @@ -1084,7 +1084,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_plexus4_stable path: pytest.xml @@ -1093,13 +1093,13 @@ jobs: - build-solanum runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-solanum path: '~' @@ -1117,7 +1117,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_solanum_stable path: pytest.xml @@ -1125,9 +1125,9 @@ jobs: needs: [] runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Install dependencies @@ -1144,7 +1144,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_sopel_stable path: pytest.xml @@ -1153,13 +1153,13 @@ jobs: - build-unrealircd runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-unrealircd path: '~' @@ -1177,7 +1177,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_unrealircd_stable path: pytest.xml @@ -1186,13 +1186,13 @@ jobs: - build-unrealircd-5 runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-unrealircd-5 path: '~' @@ -1210,7 +1210,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_unrealircd-5_stable path: pytest.xml @@ -1220,18 +1220,18 @@ jobs: - build-anope runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-unrealircd path: '~' - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-anope path: '~' @@ -1249,7 +1249,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_unrealircd-anope_stable path: pytest.xml @@ -1258,13 +1258,13 @@ jobs: - build-unrealircd runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-unrealircd path: '~' @@ -1282,7 +1282,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_unrealircd-atheme_stable path: pytest.xml @@ -1291,20 +1291,20 @@ jobs: - build-unrealircd runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Set up Python 3.7 - uses: actions/setup-python@v2 + uses: actions/setup-python@v4 with: python-version: 3.7 - name: Download build artefacts - uses: actions/download-artifact@v2 + uses: actions/download-artifact@v3 with: name: installed-unrealircd path: '~' - name: Unpack artefacts run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - name: Checkout Dlk - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: path: Dlk-Services ref: effd18652fc1c847d1959089d9cca9ff9837a8c0 @@ -1328,7 +1328,7 @@ jobs: timeout-minutes: 30 - if: always() name: Publish results - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: pytest-results_unrealircd-dlk_stable path: pytest.xml diff --git a/make_workflows.py b/make_workflows.py index 00c49e0..5c9f783 100644 --- a/make_workflows.py +++ b/make_workflows.py @@ -65,7 +65,7 @@ def get_install_steps(*, software_config, software_id, version_flavor): install_steps = [ { "name": f"Checkout {name}", - "uses": "actions/checkout@v2", + "uses": "actions/checkout@v3", "with": { "repository": software_config["repository"], "ref": ref, @@ -94,7 +94,7 @@ def get_build_job(*, software_config, software_id, version_flavor): cache = [ { "name": "Cache dependencies", - "uses": "actions/cache@v2", + "uses": "actions/cache@v3", "with": { "path": f"~/.cache\n${{ github.workspace }}/{path}\n", "key": "3-${{ runner.os }}-" @@ -123,10 +123,10 @@ def get_build_job(*, software_config, software_id, version_flavor): "run": "cd ~/; mkdir -p .local/ go/", }, *cache, - {"uses": "actions/checkout@v2"}, + {"uses": "actions/checkout@v3"}, { "name": "Set up Python 3.7", - "uses": "actions/setup-python@v2", + "uses": "actions/setup-python@v4", "with": {"python-version": 3.7}, }, *install_steps, @@ -159,7 +159,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs): downloads.append( { "name": "Download build artefacts", - "uses": "actions/download-artifact@v2", + "uses": "actions/download-artifact@v3", "with": {"name": f"installed-{software_id}", "path": "~"}, } ) @@ -194,10 +194,10 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs): "runs-on": "ubuntu-20.04", "needs": needs, "steps": [ - {"uses": "actions/checkout@v2"}, + {"uses": "actions/checkout@v3"}, { "name": "Set up Python 3.7", - "uses": "actions/setup-python@v2", + "uses": "actions/setup-python@v4", "with": {"python-version": 3.7}, }, *downloads, @@ -231,7 +231,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs): { "name": "Publish results", "if": "always()", - "uses": "actions/upload-artifact@v2", + "uses": "actions/upload-artifact@v3", "with": { "name": f"pytest-results_{test_id}_{version_flavor.value}", "path": "pytest.xml", @@ -250,7 +250,7 @@ def upload_steps(software_id): }, { "name": "Upload build artefacts", - "uses": "actions/upload-artifact@v2", + "uses": "actions/upload-artifact@v3", "with": { "name": f"installed-{software_id}", "path": "~/artefacts-*.tar.gz", @@ -311,10 +311,10 @@ def generate_workflow(config: dict, version_flavor: VersionFlavor): # this job then "if": "success() || failure()", "steps": [ - {"uses": "actions/checkout@v2"}, + {"uses": "actions/checkout@v3"}, { "name": "Download Artifacts", - "uses": "actions/download-artifact@v2", + "uses": "actions/download-artifact@v3", "with": {"path": "artifacts"}, }, { From 297bf2c554ecfe8dbce72f2214526fe3bf924787 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 20 May 2023 20:06:59 +0200 Subject: [PATCH 103/143] inspircd: Use upstream mainloop hack when available (#200) --- .github/workflows/test-devel.yml | 8 ++++++-- .github/workflows/test-devel_release.yml | 8 ++++++-- .github/workflows/test-stable.yml | 8 ++++++-- README.md | 5 ++++- workflows.yml | 8 ++++++-- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 47e23d8..7173c8c 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -149,9 +149,13 @@ jobs: - name: Build InspIRCd run: | cd $GITHUB_WORKSPACE/inspircd/ - patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch + + # Insp3 <= 3.16.0 and Insp4 <= 4.0.0a21 don't support -DINSPIRCD_UNLIMITED_MAINLOOP + patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch || true + ./configure --prefix=$HOME/.local/inspircd --development - make -j 4 + + CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP make -j 4 make install - name: Make artefact tarball run: cd ~; tar -czf artefacts-inspircd.tar.gz .local/ go/ diff --git a/.github/workflows/test-devel_release.yml b/.github/workflows/test-devel_release.yml index 2bdd1b5..2188cfd 100644 --- a/.github/workflows/test-devel_release.yml +++ b/.github/workflows/test-devel_release.yml @@ -61,9 +61,13 @@ jobs: - name: Build InspIRCd run: | cd $GITHUB_WORKSPACE/inspircd/ - patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch + + # Insp3 <= 3.16.0 and Insp4 <= 4.0.0a21 don't support -DINSPIRCD_UNLIMITED_MAINLOOP + patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch || true + ./configure --prefix=$HOME/.local/inspircd --development - make -j 4 + + CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP make -j 4 make install - name: Make artefact tarball run: cd ~; tar -czf artefacts-inspircd.tar.gz .local/ go/ diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 3c6d556..c3863ac 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -189,9 +189,13 @@ jobs: - name: Build InspIRCd run: | cd $GITHUB_WORKSPACE/inspircd/ - patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch + + # Insp3 <= 3.16.0 and Insp4 <= 4.0.0a21 don't support -DINSPIRCD_UNLIMITED_MAINLOOP + patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch || true + ./configure --prefix=$HOME/.local/inspircd --development - make -j 4 + + CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP make -j 4 make install - name: Make artefact tarball run: cd ~; tar -czf artefacts-inspircd.tar.gz .local/ go/ diff --git a/README.md b/README.md index 605c553..e0f278a 100644 --- a/README.md +++ b/README.md @@ -110,8 +110,11 @@ cd /tmp/ git clone https://github.com/inspircd/inspircd.git cd inspircd -# optional, makes tests run considerably faster +# Optional, makes tests run considerably faster. Pick one depending on the InspIRCd version: +# on Insp3 <= 3.16.0 and Insp4 <= 4.0.0a21: patch src/inspircd.cpp < ~/irctest/patches/inspircd_mainloop.patch +# on Insp3 >= 3.17.0 and Insp4 >= 4.0.0a22: +export CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP ./configure --prefix=$HOME/.local/ --development make -j 4 diff --git a/workflows.yml b/workflows.yml index ee2b358..65e27e2 100644 --- a/workflows.yml +++ b/workflows.yml @@ -153,9 +153,13 @@ software: separate_build_job: true build_script: &inspircd_build_script | cd $GITHUB_WORKSPACE/inspircd/ - patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch + + # Insp3 <= 3.16.0 and Insp4 <= 4.0.0a21 don't support -DINSPIRCD_UNLIMITED_MAINLOOP + patch src/inspircd.cpp < $GITHUB_WORKSPACE/patches/inspircd_mainloop.patch || true + ./configure --prefix=$HOME/.local/inspircd --development - make -j 4 + + CXXFLAGS=-DINSPIRCD_UNLIMITED_MAINLOOP make -j 4 make install irc2: name: irc2 From bb8a6b6c3d3e55c1146c3c9f8224983d88a42b17 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Mon, 22 May 2023 22:18:40 -0700 Subject: [PATCH 104/143] add a test for channel +n / -n (#201) * add a test for channel +n / -n * Update irctest/server_tests/chmodes/nooutside.py Co-authored-by: Val Lorentz * Update irctest/server_tests/chmodes/nooutside.py Co-authored-by: Val Lorentz * consistently rename to "no external messages" --------- Co-authored-by: Val Lorentz --- irctest/server_tests/chmodes/no_external.py | 38 +++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 irctest/server_tests/chmodes/no_external.py diff --git a/irctest/server_tests/chmodes/no_external.py b/irctest/server_tests/chmodes/no_external.py new file mode 100644 index 0000000..4858c04 --- /dev/null +++ b/irctest/server_tests/chmodes/no_external.py @@ -0,0 +1,38 @@ +""" +Channel "no external messages" mode (`RFC 1459 +`__, +`Modern `__) +""" + +from irctest import cases +from irctest.numerics import ERR_CANNOTSENDTOCHAN + + +class NoExternalMessagesTestCase(cases.BaseServerTestCase): + @cases.mark_specifications("RFC1459", "Modern") + def testNoExternalMessagesMode(self): + # test the +n channel mode + self.connectClient("chanop", name="chanop") + self.joinChannel("chanop", "#chan") + self.sendLine("chanop", "MODE #chan +n") + self.getMessages("chanop") + + self.connectClient("baz", name="baz") + # this message should be suppressed completely by +n + self.sendLine("baz", "PRIVMSG #chan :hi from baz") + replies = self.getMessages("baz") + reply_cmds = {reply.command for reply in replies} + self.assertIn(ERR_CANNOTSENDTOCHAN, reply_cmds) + self.assertEqual(self.getMessages("chanop"), []) + + # set the channel to -n: baz should be able to send now + self.sendLine("chanop", "MODE #chan -n") + replies = self.getMessages("chanop") + modeLines = [line for line in replies if line.command == "MODE"] + self.assertMessageMatch(modeLines[0], command="MODE", params=["#chan", "-n"]) + self.sendLine("baz", "PRIVMSG #chan :hi again from baz") + self.getMessages("baz") + relays = self.getMessages("chanop") + self.assertMessageMatch( + relays[0], command="PRIVMSG", params=["#chan", "hi again from baz"] + ) From 9b9cfdb2bf05141b6bfe037d223892f0befe98cb Mon Sep 17 00:00:00 2001 From: Mitchell Riley Date: Fri, 26 May 2023 03:41:47 -0400 Subject: [PATCH 105/143] Add tests for MONITOR C and S (#202) --- irctest/server_tests/monitor.py | 46 +++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/irctest/server_tests/monitor.py b/irctest/server_tests/monitor.py index 9305d37..7c2ad70 100644 --- a/irctest/server_tests/monitor.py +++ b/irctest/server_tests/monitor.py @@ -249,6 +249,23 @@ class MonitorTestCase(_BaseMonitorTestCase): extra_format=(messages,), ) + @cases.mark_specifications("IRCv3") + @cases.mark_isupport("MONITOR") + def testMonitorClear(self): + """“Clears the list of targets being monitored. No output will be returned + for use of this command.“ + -- + """ + self.connectClient("foo") + self.check_server_support() + self.sendLine(1, "MONITOR + bar") + self.getMessages(1) + + self.sendLine(1, "MONITOR C") + self.sendLine(1, "MONITOR L") + m = self.getMessage(1) + self.assertEqual(m.command, RPL_ENDOFMONLIST) + @cases.mark_specifications("IRCv3") @cases.mark_isupport("MONITOR") def testMonitorList(self): @@ -284,6 +301,35 @@ class MonitorTestCase(_BaseMonitorTestCase): self.sendLine(1, "MONITOR L") checkMonitorSubjects(self.getMessages(1), "bar", {"bazbat"}) + @cases.mark_specifications("IRCv3") + @cases.mark_isupport("MONITOR") + def testMonitorStatus(self): + """“Outputs for each target in the list being monitored, whether + the client is online or offline. All targets that are online will + be sent using RPL_MONONLINE, all targets that are offline will be + sent using RPL_MONOFFLINE.“ + -- + """ + self.connectClient("foo") + self.check_server_support() + self.connectClient("bar") + self.sendLine(1, "MONITOR + bar,baz") + self.getMessages(1) + + self.sendLine(1, "MONITOR S") + msgs = self.getMessages(1) + self.assertEqual( + len(msgs), + 2, + fail_msg="Expected one RPL_MONONLINE (730) and one RPL_MONOFFLINE (731), got: {}", + extra_format=(msgs,), + ) + + msgs.sort(key=lambda m: m.command) + + self.assertMononline(1, "bar", m=msgs[0]) + self.assertMonoffline(1, "baz", m=msgs[1]) + @cases.mark_specifications("IRCv3") @cases.mark_isupport("MONITOR") def testNickChange(self): From a03e9bb8eabfd07c4216ba68a7de61949dc3d6f6 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Mon, 29 May 2023 09:50:31 +0200 Subject: [PATCH 106/143] Add support for The Lounge (#132) --- .github/workflows/test-devel.yml | 28 ++++++++ .github/workflows/test-stable.yml | 28 ++++++++ Makefile | 12 ++++ irctest/controllers/thelounge.py | 106 ++++++++++++++++++++++++++++++ workflows.yml | 18 +++++ 5 files changed, 192 insertions(+) create mode 100644 irctest/controllers/thelounge.py diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 7173c8c..74b0520 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -400,6 +400,7 @@ jobs: - test-plexus4 - test-solanum - test-sopel + - test-thelounge - test-unrealircd - test-unrealircd-5 - test-unrealircd-anope @@ -994,6 +995,33 @@ jobs: with: name: pytest-results_sopel_devel path: pytest.xml + test-thelounge: + needs: [] + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.7 + uses: actions/setup-python@v4 + with: + python-version: 3.7 + - name: Install dependencies + run: yarn global add https://github.com/thelounge/thelounge.git + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime + - name: Install irctest dependencies + run: |- + python -m pip install --upgrade pip + pip install pytest pytest-xdist -r requirements.txt + - name: Test with pytest + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make + thelounge + timeout-minutes: 30 + - if: always() + name: Publish results + uses: actions/upload-artifact@v3 + with: + name: pytest-results_thelounge_devel + path: pytest.xml test-unrealircd: needs: - build-unrealircd diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index c3863ac..9341a6c 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -443,6 +443,7 @@ jobs: - test-plexus4 - test-solanum - test-sopel + - test-thelounge - test-unrealircd - test-unrealircd-5 - test-unrealircd-anope @@ -1152,6 +1153,33 @@ jobs: with: name: pytest-results_sopel_stable path: pytest.xml + test-thelounge: + needs: [] + runs-on: ubuntu-20.04 + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.7 + uses: actions/setup-python@v4 + with: + python-version: 3.7 + - name: Install dependencies + run: yarn global add thelounge@4.4.0 + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime + - name: Install irctest dependencies + run: |- + python -m pip install --upgrade pip + pip install pytest pytest-xdist -r requirements.txt + - name: Test with pytest + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH make + thelounge + timeout-minutes: 30 + - if: always() + name: Publish results + uses: actions/upload-artifact@v3 + with: + name: pytest-results_thelounge_stable + path: pytest.xml test-unrealircd: needs: - build-unrealircd diff --git a/Makefile b/Makefile index f56a249..bd68631 100644 --- a/Makefile +++ b/Makefile @@ -98,6 +98,13 @@ SOPEL_SELECTORS := \ (foo or not foo) \ $(EXTRA_SELECTORS) +# TheLounge can actually pass all the test so there is none to exclude. +# `(foo or not foo)` serves as a `true` value so it doesn't break when +# $(EXTRA_SELECTORS) is non-empty +THELOUNGE_SELECTORS := \ + (foo or not foo) \ + $(EXTRA_SELECTORS) + # Tests marked with arbitrary_client_tags can't pass because Unreal whitelists which tags it relays # Tests marked with react_tag can't pass because Unreal blocks +draft/react https://github.com/unrealircd/unrealircd/pull/149 # Tests marked with private_chathistory can't pass because Unreal does not implement CHATHISTORY for DMs @@ -253,6 +260,11 @@ sopel: --controller=irctest.controllers.sopel \ -k '$(SOPEL_SELECTORS)' +thelounge: + $(PYTEST) $(PYTEST_ARGS) \ + --controller=irctest.controllers.thelounge \ + -k '$(THELOUNGE_SELECTORS)' + unrealircd: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.unrealircd \ diff --git a/irctest/controllers/thelounge.py b/irctest/controllers/thelounge.py new file mode 100644 index 0000000..7992243 --- /dev/null +++ b/irctest/controllers/thelounge.py @@ -0,0 +1,106 @@ +import json +import os +import subprocess +from typing import Optional, Type + +from irctest import authentication, tls +from irctest.basecontrollers import ( + BaseClientController, + DirectoryBasedController, + NotImplementedByController, +) + +TEMPLATE_CONFIG = """ +"use strict"; + +module.exports = {config}; +""" + + +class TheLoungeController(BaseClientController, DirectoryBasedController): + software_name = "TheLounge" + supported_sasl_mechanisms = { + "PLAIN", + "ECDSA-NIST256P-CHALLENGE", + "SCRAM-SHA-256", + "EXTERNAL", + } + supports_sts = True + + def create_config(self) -> None: + super().create_config() + with self.open_file("bot.conf"): + pass + with self.open_file("conf/users.conf"): + pass + + def run( + self, + hostname: str, + port: int, + auth: Optional[authentication.Authentication], + tls_config: Optional[tls.TlsConfig] = None, + ) -> None: + if tls_config is None: + tls_config = tls.TlsConfig(enable=False, trusted_fingerprints=[]) + if tls_config and tls_config.trusted_fingerprints: + raise NotImplementedByController("Trusted fingerprints.") + if auth and any( + mech.to_string().startswith(("SCRAM-", "ECDSA-")) + for mech in auth.mechanisms + ): + raise NotImplementedByController("ecdsa") + if auth and auth.password and len(auth.password) > 300: + # https://github.com/thelounge/thelounge/pull/4480 + # Note that The Lounge truncates on 300 characters, not bytes. + raise NotImplementedByController("Passwords longer than 300 chars") + # Runs a client with the config given as arguments + assert self.proc is None + self.create_config() + if auth: + mechanisms = " ".join(mech.to_string() for mech in auth.mechanisms) + if auth.ecdsa_key: + with self.open_file("ecdsa_key.pem") as fd: + fd.write(auth.ecdsa_key) + else: + mechanisms = "" + + assert self.directory + with self.open_file("config.js") as fd: + fd.write( + TEMPLATE_CONFIG.format( + config=json.dumps( + dict( + public=False, + host=f"unix:{self.directory}/sock", # prevents binding + ) + ) + ) + ) + with self.open_file("users/testuser.json") as fd: + json.dump( + dict( + networks=[ + dict( + name="testnet", + host=hostname, + port=port, + tls=tls_config.enable if tls_config else "False", + sasl=mechanisms.lower(), + saslAccount=auth.username if auth else "", + saslPassword=auth.password if auth else "", + ) + ] + ), + fd, + ) + with self.open_file("users/testuser.json", "r") as fd: + print("config", json.load(fd)["networks"][0]["saslPassword"]) + self.proc = subprocess.Popen( + [os.environ.get("THELOUNGE_BIN", "thelounge"), "start"], + env={**os.environ, "THELOUNGE_HOME": str(self.directory)}, + ) + + +def get_irctest_controller_class() -> Type[TheLoungeController]: + return TheLoungeController diff --git a/workflows.yml b/workflows.yml index 65e27e2..e7b77a2 100644 --- a/workflows.yml +++ b/workflows.yml @@ -379,6 +379,21 @@ software: run: pip install git+https://github.com/sopel-irc/sopel.git devel_release: null + thelounge: + name: TheLounge + separate_build_job: false + install_steps: + stable: + - name: Install dependencies + run: yarn global add thelounge@4.4.0 + release: + - name: Install dependencies + run: yarn global add thelounge + devel: + - name: Install dependencies + run: yarn global add https://github.com/thelounge/thelounge.git + devel_release: null + tests: bahamut: software: [bahamut] @@ -457,3 +472,6 @@ tests: sopel: software: [sopel] + + thelounge: + software: [thelounge] From 79bbdd29485ecbee78817728d52efb351bf72a3f Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Mon, 29 May 2023 11:53:08 +0200 Subject: [PATCH 107/143] sasl: Add tests for signature failure from the server (#179) --- .github/workflows/test-stable.yml | 2 +- irctest/client_tests/sasl.py | 32 ++++++++++++++++++++++++++++++- workflows.yml | 2 +- 3 files changed, 33 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 9341a6c..97e5409 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -894,7 +894,7 @@ jobs: with: python-version: 3.7 - name: Install dependencies - run: pip install limnoria==2022.03.17 cryptography pyxmpp2-scram + run: pip install limnoria==2023.5.27 cryptography pyxmpp2-scram - name: Install system dependencies run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies diff --git a/irctest/client_tests/sasl.py b/irctest/client_tests/sasl.py index 44f5e76..8e38305 100644 --- a/irctest/client_tests/sasl.py +++ b/irctest/client_tests/sasl.py @@ -228,7 +228,7 @@ class SaslTestCase(cases.BaseClientTestCase): self.assertEqual(m.params, ["+"], m) @cases.skipUnlessHasMechanism("SCRAM-SHA-256") - def testScramBadPassword(self): + def testScramBadPassword(self, server_fakes_success=False, fake_response=None): """Test SCRAM-SHA-256 authentication with a bad password.""" auth = authentication.Authentication( mechanisms=[authentication.Mechanisms.scram_sha_256], @@ -261,6 +261,36 @@ class SaslTestCase(cases.BaseClientTestCase): with self.assertRaises(scram.NotAuthorizedException): authenticator.response(msg) + if server_fakes_success: + self.sendLine(f"AUTHENTICATE :{fake_response}") + + m = self.getMessage() + while m.command == "PING": + self.sendLine(f"PONG server. {m.params[-1]}") + m = self.getMessage() + self.assertMessageMatch( + m, + command="AUTHENTICATE", + params=["*"], + fail_msg="Client did not abort: {msg}", + ) + + @cases.skipUnlessHasMechanism("SCRAM-SHA-256") + @pytest.mark.parametrize( + "fake_response", + [ + "", + "AAAA", + "dj1ubU1mM1FIV2NKUWk5cE1ndHFLU0tQclZueUk2c3FOTzZJN3BFLzBveUdjPQ==", + ], + ) + def testScramMaliciousServer(self, fake_response): + """Test SCRAM-SHA-256 authentication to a server which pretends to know + the password""" + self.testScramBadPassword( + server_fakes_success=True, fake_response=fake_response + ) + class Irc302SaslTestCase(cases.BaseClientTestCase): @cases.skipUnlessHasMechanism("PLAIN") diff --git a/workflows.yml b/workflows.yml index e7b77a2..13b2de5 100644 --- a/workflows.yml +++ b/workflows.yml @@ -355,7 +355,7 @@ software: install_steps: stable: - name: Install dependencies - run: pip install limnoria==2022.03.17 cryptography pyxmpp2-scram + run: pip install limnoria==2023.5.27 cryptography pyxmpp2-scram release: - name: Install dependencies run: pip install limnoria cryptography pyxmpp2-scram From 2fb8ed4000d825f34c4a6dba67ebad1853dc7ede Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Mon, 29 May 2023 14:49:03 +0200 Subject: [PATCH 108/143] dashboard: Use a more concise/readable and tree-like syntax to generate the ASTs (#204) --- irctest/dashboard/format.py | 222 ++++++++++++++++++---------------- irctest/dashboard/shortxml.py | 126 +++++++++++++++++++ 2 files changed, 245 insertions(+), 103 deletions(-) create mode 100644 irctest/dashboard/shortxml.py diff --git a/irctest/dashboard/format.py b/irctest/dashboard/format.py index 51f946c..f7908e4 100644 --- a/irctest/dashboard/format.py +++ b/irctest/dashboard/format.py @@ -16,16 +16,22 @@ from typing import ( Optional, Tuple, TypeVar, + Union, ) import xml.etree.ElementTree as ET from defusedxml.ElementTree import parse as parse_xml import docutils.core +from .shortxml import Namespace + NETLIFY_CHAR_BLACKLIST = frozenset('":<>|*?\r\n#') """Characters not allowed in output filenames""" +HTML = Namespace("http://www.w3.org/1999/xhtml") + + @dataclasses.dataclass class CaseResult: module_name: str @@ -120,33 +126,43 @@ def iter_job_results(job_file_name: Path, job: ET.ElementTree) -> Iterator[CaseR def rst_to_element(s: str) -> ET.Element: html = docutils.core.publish_parts(s, writer_name="xhtml")["html_body"] - htmltree = ET.fromstring(html) + + # Force the HTML namespace on all elements produced by docutils, which are + # unqualified + tree_builder = ET.TreeBuilder( + element_factory=lambda tag, attrib: ET.Element( + "{%s}%s" % (HTML.uri, tag), + {"{%s}%s" % (HTML.uri, k): v for (k, v) in attrib.items()}, + ) + ) + parser = ET.XMLParser(target=tree_builder) + + htmltree = ET.fromstring(html, parser=parser) return htmltree -def append_docstring(element: ET.Element, obj: object) -> None: +def docstring(obj: object) -> Optional[ET.Element]: if obj.__doc__ is None: - return + return None - element.append(rst_to_element(obj.__doc__)) + return rst_to_element(obj.__doc__) def build_job_html(job: str, results: List[CaseResult]) -> ET.Element: jobs = sorted({result.job for result in results}) - root = ET.Element("html") - head = ET.SubElement(root, "head") - ET.SubElement(head, "title").text = job - ET.SubElement(head, "link", rel="stylesheet", type="text/css", href="./style.css") - body = ET.SubElement(root, "body") + table = build_test_table(jobs, results, "job-results test-matrix") - ET.SubElement(body, "h1").text = job - - table = build_test_table(jobs, results) - table.set("class", "job-results test-matrix") - body.append(table) - - return root + return HTML.html( + HTML.head( + HTML.title(job), + HTML.link(rel="stylesheet", type="text/css", href="./style.css"), + ), + HTML.body( + HTML.h1(job), + table, + ), + ) def build_module_html( @@ -154,38 +170,35 @@ def build_module_html( ) -> ET.Element: module = importlib.import_module(module_name) - root = ET.Element("html") - head = ET.SubElement(root, "head") - ET.SubElement(head, "title").text = module_name - ET.SubElement(head, "link", rel="stylesheet", type="text/css", href="./style.css") + table = build_test_table(jobs, results, "module-results test-matrix") - body = ET.SubElement(root, "body") - - ET.SubElement(body, "h1").text = module_name - - append_docstring(body, module) - - table = build_test_table(jobs, results) - table.set("class", "module-results test-matrix") - body.append(table) - - return root + return HTML.html( + HTML.head( + HTML.title(module_name), + HTML.link(rel="stylesheet", type="text/css", href="./style.css"), + ), + HTML.body( + HTML.h1(module_name), + docstring(module), + table, + ), + ) -def build_test_table(jobs: List[str], results: List[CaseResult]) -> ET.Element: +def build_test_table( + jobs: List[str], results: List[CaseResult], class_: str +) -> ET.Element: multiple_modules = len({r.module_name for r in results}) > 1 results_by_module_and_class = group_by( results, lambda r: (r.module_name, r.class_name) ) - table = ET.Element("table") + job_row = HTML.tr( + HTML.th(), # column of case name + [HTML.th(HTML.div(HTML.span(job)), class_="job-name") for job in jobs], + ) - job_row = ET.Element("tr") - ET.SubElement(job_row, "th") # column of case name - for job in jobs: - cell = ET.SubElement(job_row, "th") - ET.SubElement(ET.SubElement(cell, "div"), "span").text = job - cell.set("class", "job-name") + rows = [] for (module_name, class_name), class_results in sorted( results_by_module_and_class.items() @@ -203,20 +216,25 @@ def build_test_table(jobs: List[str], results: List[CaseResult]) -> ET.Element: module = importlib.import_module(module_name) # Header row: class name - header_row = ET.SubElement(table, "tr") - th = ET.SubElement(header_row, "th", colspan=str(len(jobs) + 1)) row_anchor = f"{qualified_class_name}" - section_header = ET.SubElement( - ET.SubElement(th, "h2"), - "a", - href=f"#{row_anchor}", - id=row_anchor, + rows.append( + HTML.tr( + HTML.th( + HTML.h2( + HTML.a( + qualified_class_name, + href=f"#{row_anchor}", + id=row_anchor, + ), + ), + docstring(getattr(module, class_name)), + colspan=str(len(jobs) + 1), + ) + ) ) - section_header.text = qualified_class_name - append_docstring(th, getattr(module, class_name)) # Header row: one column for each implementation - table.append(job_row) + rows.append(job_row) # One row for each test: results_by_test = group_by(class_results, key=lambda r: r.test_name) @@ -227,43 +245,41 @@ def build_test_table(jobs: List[str], results: List[CaseResult]) -> ET.Element: # TODO: only hash test parameter row_anchor = md5sum(row_anchor) - row = ET.SubElement(table, "tr", id=row_anchor) - - cell = ET.SubElement(row, "th") - cell.set("class", "test-name") - cell_link = ET.SubElement(cell, "a", href=f"#{row_anchor}") - cell_link.text = test_name + row = HTML.tr( + HTML.th(HTML.a(test_name, href=f"#{row_anchor}"), class_="test-name"), + id=row_anchor, + ) + rows.append(row) results_by_job = group_by(test_results, key=lambda r: r.job) for job_name in jobs: - cell = ET.SubElement(row, "td") try: (result,) = results_by_job[job_name] except KeyError: - cell.set("class", "deselected") - cell.text = "d" + row.append(HTML.td("d", class_="deselected")) continue - text: Optional[str] + text: Union[str, None, ET.Element] + attrib = {} if result.skipped: - cell.set("class", "skipped") + attrib["class"] = "skipped" if result.type == "pytest.skip": text = "s" elif result.type == "pytest.xfail": text = "X" - cell.set("class", "expected-failure") + attrib["class"] = "expected-failure" else: text = result.type elif result.success: - cell.set("class", "success") + attrib["class"] = "success" if result.type: # dead code? text = result.type else: text = "." else: - cell.set("class", "failure") + attrib["class"] = "failure" if result.type: # dead code? text = result.type @@ -272,14 +288,15 @@ def build_test_table(jobs: List[str], results: List[CaseResult]) -> ET.Element: if result.system_out: # There is a log file; link to it. - a = ET.SubElement(cell, "a", href=f"./{result.output_filename()}") - a.text = text or "?" + text = HTML.a(text or "?", href=f"./{result.output_filename()}") else: - cell.text = text or "?" + text = text or "?" if result.message: - cell.set("title", result.message) + attrib["title"] = result.message - return table + row.append(HTML.td(text, attrib)) + + return HTML.table(*rows, class_=class_) def write_html_pages( @@ -355,15 +372,6 @@ def write_test_outputs(output_dir: Path, results: List[CaseResult]) -> None: def write_html_index(output_dir: Path, pages: List[Tuple[str, str, str]]) -> None: - root = ET.Element("html") - head = ET.SubElement(root, "head") - ET.SubElement(head, "title").text = "irctest dashboard" - ET.SubElement(head, "link", rel="stylesheet", type="text/css", href="./style.css") - - body = ET.SubElement(root, "body") - - ET.SubElement(body, "h1").text = "irctest dashboard" - module_pages = [] job_pages = [] for page_type, title, file_name in sorted(pages): @@ -374,28 +382,36 @@ def write_html_index(output_dir: Path, pages: List[Tuple[str, str, str]]) -> Non else: assert False, page_type - ET.SubElement(body, "h2").text = "Tests by command/specification" + page = HTML.html( + HTML.head( + HTML.title("irctest dashboard"), + HTML.link(rel="stylesheet", type="text/css", href="./style.css"), + ), + HTML.body( + HTML.h1("irctest dashboard"), + HTML.h2("Tests by command/specification"), + HTML.dl( + [ + ( + HTML.dt(HTML.a(module_name, href=f"./{file_name}")), + HTML.dd(docstring(importlib.import_module(module_name))), + ) + for module_name, file_name in sorted(module_pages) + ], + class_="module-index", + ), + HTML.h2("Tests by implementation"), + HTML.ul( + [ + HTML.li(HTML.a(job, href=f"./{file_name}")) + for job, file_name in sorted(job_pages) + ], + class_="job-index", + ), + ), + ) - dl = ET.SubElement(body, "dl") - dl.set("class", "module-index") - - for module_name, file_name in sorted(module_pages): - module = importlib.import_module(module_name) - - link = ET.SubElement(ET.SubElement(dl, "dt"), "a", href=f"./{file_name}") - link.text = module_name - append_docstring(ET.SubElement(dl, "dd"), module) - - ET.SubElement(body, "h2").text = "Tests by implementation" - - ul = ET.SubElement(body, "ul") - ul.set("class", "job-index") - - for job, file_name in sorted(job_pages): - link = ET.SubElement(ET.SubElement(ul, "li"), "a", href=f"./{file_name}") - link.text = job - - write_xml_file(output_dir / "index.xhtml", root) + write_xml_file(output_dir / "index.xhtml", page) def write_assets(output_dir: Path) -> None: @@ -407,12 +423,12 @@ def write_assets(output_dir: Path) -> None: def write_xml_file(filename: Path, root: ET.Element) -> None: - # Hacky: ET expects the namespace to be present in every tag we create instead; - # but it would be excessively verbose. - root.set("xmlns", "http://www.w3.org/1999/xhtml") - # Serialize - s = ET.tostring(root) + if sys.version_info >= (3, 8): + s = ET.tostring(root, default_namespace=HTML.uri) + else: + # default_namespace not supported + s = ET.tostring(root) with filename.open("wb") as fd: fd.write(s) diff --git a/irctest/dashboard/shortxml.py b/irctest/dashboard/shortxml.py new file mode 100644 index 0000000..ee9856d --- /dev/null +++ b/irctest/dashboard/shortxml.py @@ -0,0 +1,126 @@ +# Copyright (c) 2023 Valentin Lorentz +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +"""This module allows writing XML ASTs in a way that is more concise than the default +:mod:`xml.etree.ElementTree` interface. + +For example: + +.. code-block:: python + + from .shortxml import Namespace + + HTML = Namespace("http://www.w3.org/1999/xhtml") + + page = HTML.html( + HTML.head( + HTML.title("irctest dashboard"), + HTML.link(rel="stylesheet", type="text/css", href="./style.css"), + ), + HTML.body( + HTML.h1("irctest dashboard"), + HTML.h2("Tests by command/specification"), + HTML.dl( + [ + ( # elements can be arbitrarily nested in lists + HTML.dt(HTML.a(title, href=f"./{title}.xhtml")), + HTML.dd(defintion), + ) + for title, definition in sorted(definitions) + ], + class_="module-index", + ), + HTML.h2("Tests by implementation"), + HTML.ul( + [ + HTML.li(HTML.a(job, href=f"./{file_name}")) + for job, file_name in sorted(job_pages) + ], + class_="job-index", + ), + ), + ) + + print(ET.tostring(page, default_namespace=HTML.uri)) + + +Attributes can be passed either as dictionaries or as kwargs, and can be mixed +with child elements. +Trailing underscores are stripped from attributes, which allows passing reserved +Python keywords (eg. ``class_`` instead of ``class``) + +Attributes are always qualified, and share the namespace of the element they are +attached to. + +Mixed content (elements containing both text and child elements) is not supported. +""" + +from typing import Dict, Sequence, Union +import xml.etree.ElementTree as ET + + +def _namespacify(ns: str, s: str) -> str: + return "{%s}%s" % (ns, s) + + +_Children = Union[None, Dict[str, str], ET.Element, Sequence["_Children"]] + + +class ElementFactory: + def __init__(self, namespace: str, tag: str): + self._tag = _namespacify(namespace, tag) + self._namespace = namespace + + def __call__(self, *args: Union[str, _Children], **kwargs: str) -> ET.Element: + e = ET.Element(self._tag) + + attributes = {k.rstrip("_"): v for (k, v) in kwargs.items()} + children = [*args, attributes] + + if args and isinstance(children[0], str): + e.text = children[0] + children.pop(0) + + for child in children: + self._append_child(e, child) + + return e + + def _append_child(self, e: ET.Element, child: _Children) -> None: + if isinstance(child, ET.Element): + e.append(child) + elif child is None: + pass + elif isinstance(child, dict): + for k, v in child.items(): + e.set(_namespacify(self._namespace, k), str(v)) + elif isinstance(child, str): + raise ValueError("Mixed content is not supported") + else: + for grandchild in child: + self._append_child(e, grandchild) + + +class Namespace: + def __init__(self, uri: str): + self.uri = uri + + def __getattr__(self, tag: str) -> ElementFactory: + return ElementFactory(self.uri, tag) From 5ec44e1417bc1ef8d10bf5893e15908eb39bd111 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 30 May 2023 21:52:11 +0200 Subject: [PATCH 109/143] thelounge: Build from git repository 'yarn global add https://github.com/thelounge/thelounge.git' doesn't work because we now need to compile TypeScript to JavaScript when not downloading from the package manager --- .github/workflows/test-devel.yml | 14 ++++++++++++-- .github/workflows/test-stable.yml | 14 ++++++++++++-- workflows.yml | 21 +++++++++++---------- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 74b0520..9a03789 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -1004,8 +1004,18 @@ jobs: uses: actions/setup-python@v4 with: python-version: 3.7 - - name: Install dependencies - run: yarn global add https://github.com/thelounge/thelounge.git + - name: Checkout TheLounge + uses: actions/checkout@v3 + with: + path: thelounge + ref: master + repository: thelounge/thelounge + - name: Build TheLounge + run: | + cd $GITHUB_WORKSPACE/thelounge + yarn install + NODE_ENV=production yarn build + ln -s $(pwd)/index.js ~/.local/bin/thelounge - name: Install system dependencies run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 97e5409..98f455d 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -1162,8 +1162,18 @@ jobs: uses: actions/setup-python@v4 with: python-version: 3.7 - - name: Install dependencies - run: yarn global add thelounge@4.4.0 + - name: Checkout TheLounge + uses: actions/checkout@v3 + with: + path: thelounge + ref: v4.4.0 + repository: thelounge/thelounge + - name: Build TheLounge + run: | + cd $GITHUB_WORKSPACE/thelounge + yarn install + NODE_ENV=production yarn build + ln -s $(pwd)/index.js ~/.local/bin/thelounge - name: Install system dependencies run: sudo apt-get install atheme-services faketime - name: Install irctest dependencies diff --git a/workflows.yml b/workflows.yml index 13b2de5..6d353c7 100644 --- a/workflows.yml +++ b/workflows.yml @@ -381,18 +381,19 @@ software: thelounge: name: TheLounge + repository: thelounge/thelounge separate_build_job: false - install_steps: - stable: - - name: Install dependencies - run: yarn global add thelounge@4.4.0 - release: - - name: Install dependencies - run: yarn global add thelounge - devel: - - name: Install dependencies - run: yarn global add https://github.com/thelounge/thelounge.git + refs: + stable: "v4.4.0" + release: "v4.4.0" + devel: "master" devel_release: null + path: thelounge + build_script: | + cd $GITHUB_WORKSPACE/thelounge + yarn install + NODE_ENV=production yarn build + ln -s $(pwd)/index.js ~/.local/bin/thelounge tests: bahamut: From b04db62a9bad488e03f4ef258daff9eb21f006ca Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Wed, 31 May 2023 20:14:17 +0200 Subject: [PATCH 110/143] thelounge: Fix build again --- .github/workflows/test-devel.yml | 1 + .github/workflows/test-stable.yml | 1 + workflows.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 9a03789..1e06f23 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -1015,6 +1015,7 @@ jobs: cd $GITHUB_WORKSPACE/thelounge yarn install NODE_ENV=production yarn build + mkdir -p ~/.local/bin/ ln -s $(pwd)/index.js ~/.local/bin/thelounge - name: Install system dependencies run: sudo apt-get install atheme-services faketime diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 98f455d..fa66e3e 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -1173,6 +1173,7 @@ jobs: cd $GITHUB_WORKSPACE/thelounge yarn install NODE_ENV=production yarn build + mkdir -p ~/.local/bin/ ln -s $(pwd)/index.js ~/.local/bin/thelounge - name: Install system dependencies run: sudo apt-get install atheme-services faketime diff --git a/workflows.yml b/workflows.yml index 6d353c7..996a8b4 100644 --- a/workflows.yml +++ b/workflows.yml @@ -393,6 +393,7 @@ software: cd $GITHUB_WORKSPACE/thelounge yarn install NODE_ENV=production yarn build + mkdir -p ~/.local/bin/ ln -s $(pwd)/index.js ~/.local/bin/thelounge tests: From 22c6743b24f2a85bf79a92fb0c7fab325c047a6c Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Wed, 31 May 2023 13:35:59 -0700 Subject: [PATCH 111/143] test that CAP LS 301 responses are only one line (#205) --- irctest/server_tests/cap.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/irctest/server_tests/cap.py b/irctest/server_tests/cap.py index c079078..95c4027 100644 --- a/irctest/server_tests/cap.py +++ b/irctest/server_tests/cap.py @@ -242,3 +242,31 @@ class CapTestCase(cases.BaseServerTestCase): fail_msg="Sending “CAP LIST” as first message got a reply " "that is not “CAP * LIST :”: {msg}", ) + + @cases.mark_specifications("IRCv3") + def testNoMultiline301Response(self): + """ + Current version: "If the client supports CAP version 302, the server MAY send + multiple lines in response to CAP LS and CAP LIST." This should be read as + disallowing multiline responses to pre-302 clients. + -- + """ # noqa + self.check301ResponsePreRegistration("bar", "CAP LS") + self.check301ResponsePreRegistration("qux", "CAP LS 301") + self.check301ResponsePostRegistration("baz", "CAP LS") + self.check301ResponsePostRegistration("bat", "CAP LS 301") + + def check301ResponsePreRegistration(self, nick, cap_ls): + self.addClient(nick) + self.sendLine(nick, cap_ls) + self.sendLine(nick, "NICK " + nick) + self.sendLine(nick, "USER u s e r") + self.sendLine(nick, "CAP END") + responses = [msg for msg in self.skipToWelcome(nick) if msg.command == "CAP"] + self.assertLessEqual(len(responses), 1, responses) + + def check301ResponsePostRegistration(self, nick, cap_ls): + self.connectClient(nick, name=nick) + self.sendLine(nick, cap_ls) + responses = [msg for msg in self.getMessages(nick) if msg.command == "CAP"] + self.assertLessEqual(len(responses), 1, responses) From 52c22236a6e4cf9ca6db1e47c2d85b8b9dd95d56 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Thu, 1 Jun 2023 09:22:54 -0700 Subject: [PATCH 112/143] use the ratified extended-monitor name (#206) --- irctest/server_tests/monitor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irctest/server_tests/monitor.py b/irctest/server_tests/monitor.py index 7c2ad70..f5bd808 100644 --- a/irctest/server_tests/monitor.py +++ b/irctest/server_tests/monitor.py @@ -373,7 +373,7 @@ class _BaseExtendedMonitorTestCase(_BaseMonitorTestCase): """Tests https://ircv3.net/specs/extensions/extended-monitor.html""" self.connectClient( "foo", - capabilities=["draft/extended-monitor", *watcher_caps], + capabilities=["extended-monitor", *watcher_caps], skip_if_cap_nak=True, ) From 5a5dbdb50dda3dc9e5ed3d542ab75b1b87fccac9 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Thu, 1 Jun 2023 19:17:00 +0200 Subject: [PATCH 113/143] Bump Dlk version --- .github/workflows/test-stable.yml | 2 +- workflows.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index fa66e3e..20e8d7a 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -1350,7 +1350,7 @@ jobs: uses: actions/checkout@v3 with: path: Dlk-Services - ref: effd18652fc1c847d1959089d9cca9ff9837a8c0 + ref: 6db51ea03f039c48fd20427c04cec8ff98df7878 repository: DalekIRC/Dalek-Services - name: Build Dlk run: | diff --git a/workflows.yml b/workflows.yml index 996a8b4..b220238 100644 --- a/workflows.yml +++ b/workflows.yml @@ -332,7 +332,7 @@ software: separate_build_job: false path: Dlk-Services refs: - stable: &dlk_stable "effd18652fc1c847d1959089d9cca9ff9837a8c0" + stable: &dlk_stable "6db51ea03f039c48fd20427c04cec8ff98df7878" release: *dlk_stable devel: "main" devel_release: *dlk_stable From e5f22e8080d215fa9b24b51752fbb8f5d51fbbc9 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 3 Jun 2023 19:32:05 +0200 Subject: [PATCH 114/143] chathistory: Validate BATCH commands more strictly (#208) --- irctest/server_tests/chathistory.py | 91 +++++++++++++++-------------- 1 file changed, 46 insertions(+), 45 deletions(-) diff --git a/irctest/server_tests/chathistory.py b/irctest/server_tests/chathistory.py index 28a201a..873661e 100644 --- a/irctest/server_tests/chathistory.py +++ b/irctest/server_tests/chathistory.py @@ -10,7 +10,7 @@ import pytest from irctest import cases, runner from irctest.irc_utils.junkdrawer import random_name -from irctest.patma import ANYSTR +from irctest.patma import ANYSTR, StrRe CHATHISTORY_CAP = "draft/chathistory" EVENT_PLAYBACK_CAP = "draft/event-playback" @@ -21,28 +21,6 @@ SUBCOMMANDS = ["LATEST", "BEFORE", "AFTER", "BETWEEN", "AROUND"] MYSQL_PASSWORD = "" -def validate_chathistory_batch(msgs): - batch_tag = None - closed_batch_tag = None - result = [] - for msg in msgs: - if msg.command == "BATCH": - batch_param = msg.params[0] - if batch_tag is None and batch_param[0] == "+": - batch_tag = batch_param[1:] - elif batch_param[0] == "-": - closed_batch_tag = batch_param[1:] - elif ( - msg.command == "PRIVMSG" - and batch_tag is not None - and msg.tags.get("batch") == batch_tag - ): - if not msg.prefix.startswith("HistServ!"): # FIXME: ergo-specific - result.append(msg.to_history_message()) - assert batch_tag == closed_batch_tag - return result - - def skip_ngircd(f): @functools.wraps(f) def newf(self, *args, **kwargs): @@ -56,6 +34,26 @@ def skip_ngircd(f): @cases.mark_specifications("IRCv3") @cases.mark_services class ChathistoryTestCase(cases.BaseServerTestCase): + def validate_chathistory_batch(self, msgs, target): + (start, *inner_msgs, end) = msgs + + self.assertMessageMatch( + start, command="BATCH", params=[StrRe(r"\+.*"), "chathistory", target] + ) + batch_tag = start.params[0][1:] + self.assertMessageMatch(end, command="BATCH", params=["-" + batch_tag]) + + result = [] + for msg in inner_msgs: + if ( + msg.command == "PRIVMSG" + and batch_tag is not None + and msg.tags.get("batch") == batch_tag + ): + if not msg.prefix.startswith("HistServ!"): # FIXME: ergo-specific + result.append(msg.to_history_message()) + return result + @staticmethod def config() -> cases.TestCaseControllerConfig: return cases.TestCaseControllerConfig(chathistory=True) @@ -308,6 +306,9 @@ class ChathistoryTestCase(cases.BaseServerTestCase): ) time.sleep(0.002) + self.getMessages(1) + self.getMessages(2) + self.validate_echo_messages(NUM_MESSAGES, echo_messages) self.validate_chathistory(subcommand, echo_messages, 1, c2) self.validate_chathistory(subcommand, echo_messages, 2, c1) @@ -401,15 +402,15 @@ class ChathistoryTestCase(cases.BaseServerTestCase): def _validate_chathistory_LATEST(self, echo_messages, user, chname): INCLUSIVE_LIMIT = len(echo_messages) * 2 self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, INCLUSIVE_LIMIT)) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual(echo_messages, result) self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, 5)) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual(echo_messages[-5:], result) self.sendLine(user, "CHATHISTORY LATEST %s * %d" % (chname, 1)) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual(echo_messages[-1:], result) self.sendLine( @@ -417,7 +418,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase): "CHATHISTORY LATEST %s msgid=%s %d" % (chname, echo_messages[4].msgid, INCLUSIVE_LIMIT), ) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual(echo_messages[5:], result) self.sendLine( @@ -425,7 +426,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase): "CHATHISTORY LATEST %s timestamp=%s %d" % (chname, echo_messages[4].time, INCLUSIVE_LIMIT), ) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual(echo_messages[5:], result) def _validate_chathistory_BEFORE(self, echo_messages, user, chname): @@ -435,7 +436,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase): "CHATHISTORY BEFORE %s msgid=%s %d" % (chname, echo_messages[6].msgid, INCLUSIVE_LIMIT), ) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual(echo_messages[:6], result) self.sendLine( @@ -443,7 +444,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase): "CHATHISTORY BEFORE %s timestamp=%s %d" % (chname, echo_messages[6].time, INCLUSIVE_LIMIT), ) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual(echo_messages[:6], result) self.sendLine( @@ -451,7 +452,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase): "CHATHISTORY BEFORE %s timestamp=%s %d" % (chname, echo_messages[6].time, 2), ) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual(echo_messages[4:6], result) def _validate_chathistory_AFTER(self, echo_messages, user, chname): @@ -461,7 +462,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase): "CHATHISTORY AFTER %s msgid=%s %d" % (chname, echo_messages[3].msgid, INCLUSIVE_LIMIT), ) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual(echo_messages[4:], result) self.sendLine( @@ -469,14 +470,14 @@ class ChathistoryTestCase(cases.BaseServerTestCase): "CHATHISTORY AFTER %s timestamp=%s %d" % (chname, echo_messages[3].time, INCLUSIVE_LIMIT), ) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual(echo_messages[4:], result) self.sendLine( user, "CHATHISTORY AFTER %s timestamp=%s %d" % (chname, echo_messages[3].time, 3), ) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual(echo_messages[4:7], result) def _validate_chathistory_BETWEEN(self, echo_messages, user, chname): @@ -492,7 +493,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase): INCLUSIVE_LIMIT, ), ) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual(echo_messages[1:-1], result) self.sendLine( @@ -505,7 +506,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase): INCLUSIVE_LIMIT, ), ) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual(echo_messages[1:-1], result) # BETWEEN forwards and backwards with a limit, should get @@ -515,7 +516,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase): "CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d" % (chname, echo_messages[0].msgid, echo_messages[-1].msgid, 3), ) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual(echo_messages[1:4], result) self.sendLine( @@ -523,7 +524,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase): "CHATHISTORY BETWEEN %s msgid=%s msgid=%s %d" % (chname, echo_messages[-1].msgid, echo_messages[0].msgid, 3), ) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual(echo_messages[-4:-1], result) # same stuff again but with timestamps @@ -532,28 +533,28 @@ class ChathistoryTestCase(cases.BaseServerTestCase): "CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d" % (chname, echo_messages[0].time, echo_messages[-1].time, INCLUSIVE_LIMIT), ) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual(echo_messages[1:-1], result) self.sendLine( user, "CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d" % (chname, echo_messages[-1].time, echo_messages[0].time, INCLUSIVE_LIMIT), ) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual(echo_messages[1:-1], result) self.sendLine( user, "CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d" % (chname, echo_messages[0].time, echo_messages[-1].time, 3), ) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual(echo_messages[1:4], result) self.sendLine( user, "CHATHISTORY BETWEEN %s timestamp=%s timestamp=%s %d" % (chname, echo_messages[-1].time, echo_messages[0].time, 3), ) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual(echo_messages[-4:-1], result) def _validate_chathistory_AROUND(self, echo_messages, user, chname): @@ -561,14 +562,14 @@ class ChathistoryTestCase(cases.BaseServerTestCase): user, "CHATHISTORY AROUND %s msgid=%s %d" % (chname, echo_messages[7].msgid, 1), ) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual([echo_messages[7]], result) self.sendLine( user, "CHATHISTORY AROUND %s msgid=%s %d" % (chname, echo_messages[7].msgid, 3), ) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertEqual(echo_messages[6:9], result) self.sendLine( @@ -576,7 +577,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase): "CHATHISTORY AROUND %s timestamp=%s %d" % (chname, echo_messages[7].time, 3), ) - result = validate_chathistory_batch(self.getMessages(user)) + result = self.validate_chathistory_batch(self.getMessages(user), chname) self.assertIn(echo_messages[7], result) @pytest.mark.arbitrary_client_tags From 321e254d15fab53fe6c8e3959312f6c0805c770f Mon Sep 17 00:00:00 2001 From: Mitchell Riley Date: Sun, 4 Jun 2023 17:06:53 -0400 Subject: [PATCH 115/143] Add SETNAME tests (#209) * Add SETNAME tests * fix race condition * fix synchronization issue sendLine does not synchronize by itself; call getMessage to synchronize and test the message since we have it * Update irctest/server_tests/setname.py Co-authored-by: Val Lorentz --------- Co-authored-by: Shivaram Lingamneni Co-authored-by: Val Lorentz --- irctest/server_tests/setname.py | 65 +++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 irctest/server_tests/setname.py diff --git a/irctest/server_tests/setname.py b/irctest/server_tests/setname.py new file mode 100644 index 0000000..e328c95 --- /dev/null +++ b/irctest/server_tests/setname.py @@ -0,0 +1,65 @@ +""" +`IRCv3 SETNAME`_ +""" + +from irctest import cases +from irctest.numerics import RPL_WHOISUSER + + +class SetnameMessageTestCase(cases.BaseServerTestCase): + @cases.mark_specifications("IRCv3") + @cases.mark_capabilities("setname") + def testSetnameMessage(self): + self.connectClient("foo", capabilities=["setname"], skip_if_cap_nak=True) + + self.sendLine(1, "SETNAME bar") + self.assertMessageMatch( + self.getMessage(1), + command="SETNAME", + params=["bar"], + ) + + self.sendLine(1, "WHOIS foo") + whoisuser = [m for m in self.getMessages(1) if m.command == RPL_WHOISUSER][0] + self.assertEqual(whoisuser.params[-1], "bar") + + @cases.mark_specifications("IRCv3") + @cases.mark_capabilities("setname") + def testSetnameChannel(self): + """“[Servers] MUST send the server-to-client version of the + SETNAME message to all clients in common channels, as well as + to the client from which it originated, to confirm the change + has occurred. + + The SETNAME message MUST NOT be sent to clients which do not + have the setname capability negotiated.“ + """ + self.connectClient("foo", capabilities=["setname"], skip_if_cap_nak=True) + self.connectClient("bar", capabilities=["setname"], skip_if_cap_nak=True) + self.connectClient("baz") + + self.joinChannel(1, "#chan") + self.joinChannel(2, "#chan") + self.joinChannel(3, "#chan") + self.getMessages(1) + self.getMessages(2) + self.getMessages(3) + + self.sendLine(1, "SETNAME qux") + self.assertMessageMatch( + self.getMessage(1), + command="SETNAME", + params=["qux"], + ) + + self.assertMessageMatch( + self.getMessage(2), + command="SETNAME", + params=["qux"], + ) + + self.assertEqual( + self.getMessages(3), + [], + "Got SETNAME response when it was not negotiated", + ) From 4ee9c9c53a8134bc40285d2e6684cd4db1ad0fa0 Mon Sep 17 00:00:00 2001 From: Sadie Powell Date: Sun, 25 Jun 2023 22:14:08 +0100 Subject: [PATCH 116/143] Update CI to run on Ubuntu 22.04. (#210) * Update workflows to run on Ubuntu 22.04. * Add a patch to fix Bahamut on Ubuntu 22.04. Source: https://github.com/DALnet/bahamut/pull/219 * Add a patch to fix Charybdis on Ubuntu 22.04. --- .github/workflows/test-devel.yml | 65 ++--- .github/workflows/test-devel_release.yml | 12 +- .github/workflows/test-stable.yml | 74 ++--- make_workflows.py | 6 +- patches/bahamut_ubuntu22.patch | 342 +++++++++++++++++++++++ patches/charybdis_ubuntu22.patch | 23 ++ workflows.yml | 2 + 7 files changed, 447 insertions(+), 77 deletions(-) create mode 100644 patches/bahamut_ubuntu22.patch create mode 100644 patches/charybdis_ubuntu22.patch diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 1e06f23..915f6c8 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -3,7 +3,7 @@ jobs: build-anope: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -43,7 +43,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-bahamut: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -72,6 +72,7 @@ jobs: cd $GITHUB_WORKSPACE/Bahamut/ patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch patch src/s_bsd.c < $GITHUB_WORKSPACE/patches/bahamut_mainloop.patch + patch -p1 < $GITHUB_WORKSPACE/patches/bahamut_ubuntu22.patch echo "#undef THROTTLE_ENABLE" >> include/config.h libtoolize --force aclocal @@ -92,7 +93,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-hybrid: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -131,7 +132,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-inspircd: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -166,7 +167,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-ngircd: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -207,7 +208,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-plexus4: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -249,7 +250,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-solanum: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -289,7 +290,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-unrealircd: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -335,7 +336,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-unrealircd-5: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -406,7 +407,7 @@ jobs: - test-unrealircd-anope - test-unrealircd-atheme - test-unrealircd-dlk - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Download Artifacts @@ -433,7 +434,7 @@ jobs: test-bahamut: needs: - build-bahamut - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -467,7 +468,7 @@ jobs: needs: - build-bahamut - build-anope - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -505,7 +506,7 @@ jobs: test-bahamut-atheme: needs: - build-bahamut - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -537,7 +538,7 @@ jobs: path: pytest.xml test-ergo: needs: [] - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -579,7 +580,7 @@ jobs: needs: - build-hybrid - build-anope - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -617,7 +618,7 @@ jobs: test-inspircd: needs: - build-inspircd - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -651,7 +652,7 @@ jobs: needs: - build-inspircd - build-anope - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -688,7 +689,7 @@ jobs: path: pytest.xml test-ircu2: needs: [] - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -727,7 +728,7 @@ jobs: path: pytest.xml test-limnoria: needs: [] - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -755,7 +756,7 @@ jobs: path: pytest.xml test-nefarious: needs: [] - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -794,7 +795,7 @@ jobs: test-ngircd: needs: - build-ngircd - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -828,7 +829,7 @@ jobs: needs: - build-ngircd - build-anope - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -866,7 +867,7 @@ jobs: test-ngircd-atheme: needs: - build-ngircd - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -900,7 +901,7 @@ jobs: needs: - build-plexus4 - build-anope - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -938,7 +939,7 @@ jobs: test-solanum: needs: - build-solanum - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -970,7 +971,7 @@ jobs: path: pytest.xml test-sopel: needs: [] - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -997,7 +998,7 @@ jobs: path: pytest.xml test-thelounge: needs: [] - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -1036,7 +1037,7 @@ jobs: test-unrealircd: needs: - build-unrealircd - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -1069,7 +1070,7 @@ jobs: test-unrealircd-5: needs: - build-unrealircd-5 - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -1103,7 +1104,7 @@ jobs: needs: - build-unrealircd - build-anope - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -1141,7 +1142,7 @@ jobs: test-unrealircd-atheme: needs: - build-unrealircd - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -1174,7 +1175,7 @@ jobs: test-unrealircd-dlk: needs: - build-unrealircd - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 diff --git a/.github/workflows/test-devel_release.yml b/.github/workflows/test-devel_release.yml index 2188cfd..bcfbe62 100644 --- a/.github/workflows/test-devel_release.yml +++ b/.github/workflows/test-devel_release.yml @@ -3,7 +3,7 @@ jobs: build-anope: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -43,7 +43,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-inspircd: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -84,7 +84,7 @@ jobs: - test-inspircd - test-inspircd-anope - test-inspircd-atheme - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Download Artifacts @@ -111,7 +111,7 @@ jobs: test-inspircd: needs: - build-inspircd - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -145,7 +145,7 @@ jobs: needs: - build-inspircd - build-anope - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -183,7 +183,7 @@ jobs: test-inspircd-atheme: needs: - build-inspircd - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 20e8d7a..9bf463b 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -3,7 +3,7 @@ jobs: build-anope: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -43,7 +43,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-bahamut: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -72,6 +72,7 @@ jobs: cd $GITHUB_WORKSPACE/Bahamut/ patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch patch src/s_bsd.c < $GITHUB_WORKSPACE/patches/bahamut_mainloop.patch + patch -p1 < $GITHUB_WORKSPACE/patches/bahamut_ubuntu22.patch echo "#undef THROTTLE_ENABLE" >> include/config.h libtoolize --force aclocal @@ -92,7 +93,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-charybdis: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -119,6 +120,7 @@ jobs: - name: Build Charybdis run: | cd $GITHUB_WORKSPACE/charybdis/ + patch -p1 < $GITHUB_WORKSPACE/patches/charybdis_ubuntu22.patch ./autogen.sh ./configure --prefix=$HOME/.local/ make -j 4 @@ -132,7 +134,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-hybrid: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -171,7 +173,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-inspircd: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -206,7 +208,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-ngircd: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -247,7 +249,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-plexus4: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -289,7 +291,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-solanum: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -329,7 +331,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-unrealircd: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -375,7 +377,7 @@ jobs: path: ~/artefacts-*.tar.gz retention-days: 1 build-unrealircd-5: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Create directories run: cd ~/; mkdir -p .local/ go/ @@ -449,7 +451,7 @@ jobs: - test-unrealircd-anope - test-unrealircd-atheme - test-unrealircd-dlk - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Download Artifacts @@ -476,7 +478,7 @@ jobs: test-bahamut: needs: - build-bahamut - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -510,7 +512,7 @@ jobs: needs: - build-bahamut - build-anope - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -548,7 +550,7 @@ jobs: test-bahamut-atheme: needs: - build-bahamut - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -581,7 +583,7 @@ jobs: test-charybdis: needs: - build-charybdis - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -613,7 +615,7 @@ jobs: path: pytest.xml test-ergo: needs: [] - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -655,7 +657,7 @@ jobs: needs: - build-hybrid - build-anope - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -693,7 +695,7 @@ jobs: test-inspircd: needs: - build-inspircd - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -727,7 +729,7 @@ jobs: needs: - build-inspircd - build-anope - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -765,7 +767,7 @@ jobs: test-inspircd-atheme: needs: - build-inspircd - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -797,7 +799,7 @@ jobs: path: pytest.xml test-irc2: needs: [] - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -847,7 +849,7 @@ jobs: path: pytest.xml test-ircu2: needs: [] - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -886,7 +888,7 @@ jobs: path: pytest.xml test-limnoria: needs: [] - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -913,7 +915,7 @@ jobs: path: pytest.xml test-nefarious: needs: [] - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -952,7 +954,7 @@ jobs: test-ngircd: needs: - build-ngircd - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -986,7 +988,7 @@ jobs: needs: - build-ngircd - build-anope - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -1024,7 +1026,7 @@ jobs: test-ngircd-atheme: needs: - build-ngircd - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -1058,7 +1060,7 @@ jobs: needs: - build-plexus4 - build-anope - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -1096,7 +1098,7 @@ jobs: test-solanum: needs: - build-solanum - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -1128,7 +1130,7 @@ jobs: path: pytest.xml test-sopel: needs: [] - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -1155,7 +1157,7 @@ jobs: path: pytest.xml test-thelounge: needs: [] - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -1194,7 +1196,7 @@ jobs: test-unrealircd: needs: - build-unrealircd - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -1227,7 +1229,7 @@ jobs: test-unrealircd-5: needs: - build-unrealircd-5 - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -1261,7 +1263,7 @@ jobs: needs: - build-unrealircd - build-anope - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -1299,7 +1301,7 @@ jobs: test-unrealircd-atheme: needs: - build-unrealircd - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 @@ -1332,7 +1334,7 @@ jobs: test-unrealircd-dlk: needs: - build-unrealircd - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Set up Python 3.7 diff --git a/make_workflows.py b/make_workflows.py index 5c9f783..f959b9f 100644 --- a/make_workflows.py +++ b/make_workflows.py @@ -116,7 +116,7 @@ def get_build_job(*, software_config, software_id, version_flavor): return None return { - "runs-on": "ubuntu-20.04", + "runs-on": "ubuntu-22.04", "steps": [ { "name": "Create directories", @@ -191,7 +191,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs): unpack = [] return { - "runs-on": "ubuntu-20.04", + "runs-on": "ubuntu-22.04", "needs": needs, "steps": [ {"uses": "actions/checkout@v3"}, @@ -306,7 +306,7 @@ def generate_workflow(config: dict, version_flavor: VersionFlavor): jobs["publish-test-results"] = { "name": "Publish Dashboard", "needs": sorted({f"test-{test_id}" for test_id in config["tests"]} & set(jobs)), - "runs-on": "ubuntu-20.04", + "runs-on": "ubuntu-22.04", # the build-and-test job might be skipped, we don't need to run # this job then "if": "success() || failure()", diff --git a/patches/bahamut_ubuntu22.patch b/patches/bahamut_ubuntu22.patch new file mode 100644 index 0000000..a80f290 --- /dev/null +++ b/patches/bahamut_ubuntu22.patch @@ -0,0 +1,342 @@ +From 42b67ff7218877934abed2a738e164c0dea171b0 Mon Sep 17 00:00:00 2001 +From: "Ned T. Crigler" +Date: Sun, 26 Feb 2023 17:42:29 -0800 +Subject: [PATCH 1/2] Fix compilation on Ubuntu 22.04 + +Starting with glibc 2.34 "The symbols __dn_comp, __dn_expand, +__dn_skipname, __res_dnok, __res_hnok, __res_mailok, __res_mkquery, +__res_nmkquery, __res_nquery, __res_nquerydomain, __res_nsearch, +__res_nsend, __res_ownok, __res_query, __res_querydomain, __res_search, +__res_send formerly in libresolv have been renamed and no longer have a +__ prefix. They are now available in libc." +https://sourceware.org/pipermail/libc-alpha/2021-August/129718.html + +The hex_to_string array in include/dh.h also conflicts with OpenSSL, +which OpenSSL 3.0 now complains about. +--- + configure.in | 4 ++-- + include/dh.h | 2 +- + include/resolv.h | 6 +++++- + src/dh.c | 2 +- + 4 files changed, 9 insertions(+), 5 deletions(-) + +diff --git a/configure.in b/configure.in +index e76dee88..11720419 100644 +--- a/configure.in ++++ b/configure.in +@@ -374,8 +374,7 @@ AC_C_INLINE + dnl Checks for libraries. + dnl Replace `main' with a function in -lnsl: + AC_CHECK_LIB(nsl, gethostbyname) +-AC_CHECK_FUNC(res_mkquery,, AC_CHECK_LIB(resolv, res_mkquery)) +-AC_CHECK_FUNC(__res_mkquery,, AC_CHECK_LIB(resolv, __res_mkquery)) ++AC_SEARCH_LIBS([res_mkquery],[resolv],,AC_SEARCH_LIBS([__res_mkquery],[resolv])) + AC_CHECK_LIB(socket, socket, zlib) + AC_CHECK_FUNC(crypt,, AC_CHECK_LIB(descrypt, crypt,,AC_CHECK_LIB(crypt, crypt,,))) + +@@ -406,6 +405,7 @@ AC_CHECK_FUNCS([strcasecmp strchr strdup strerror strncasecmp strrchr strtol]) + AC_CHECK_FUNCS([strtoul index strerror strtoken strtok inet_addr inet_netof]) + AC_CHECK_FUNCS([inet_aton gettimeofday lrand48 sigaction bzero bcmp bcopy]) + AC_CHECK_FUNCS([dn_skipname __dn_skipname getrusage times break]) ++AC_CHECK_FUNCS([res_init __res_init res_mkquery __res_mkquery dn_expand __dn_expand]) + + dnl check for various OSes + +diff --git a/include/dh.h b/include/dh.h +index 1ca6996a..1817ce1e 100644 +--- a/include/dh.h ++++ b/include/dh.h +@@ -45,7 +45,7 @@ struct session_info + static BIGNUM *ircd_prime; + static BIGNUM *ircd_generator; + +-static char *hex_to_string[256] = ++static char *dh_hex_to_string[256] = + { + "00", "01", "02", "03", "04", "05", "06", "07", + "08", "09", "0a", "0b", "0c", "0d", "0e", "0f", +diff --git a/include/resolv.h b/include/resolv.h +index b5a8aaa1..5b042d43 100644 +--- a/include/resolv.h ++++ b/include/resolv.h +@@ -106,9 +106,13 @@ extern struct state _res; + + extern char *p_cdname(), *p_rr(), *p_type(), *p_class(), *p_time(); + +-#if ((__GNU_LIBRARY__ == 6) && (__GLIBC__ >=2) && (__GLIBC_MINOR__ >= 2)) ++#if !defined(HAVE_RES_INIT) && defined(HAVE___RES_INIT) + #define res_init __res_init ++#endif ++#if !defined(HAVE_RES_MKQUERY) && defined(HAVE___RES_MKQUERY) + #define res_mkquery __res_mkquery ++#endif ++#if !defined(HAVE_DN_EXPAND) && defined(HAVE___DN_EXPAND) + #define dn_expand __dn_expand + #endif + +diff --git a/src/dh.c b/src/dh.c +index cb065a4f..4b5da282 100644 +--- a/src/dh.c ++++ b/src/dh.c +@@ -223,7 +223,7 @@ static void create_prime() + + for(i = 0; i < PRIME_BYTES; i++) + { +- char *x = hex_to_string[dh_prime_1024[i]]; ++ char *x = dh_hex_to_string[dh_prime_1024[i]]; + while(*x) + buf[bufpos++] = *x++; + } + +From 135ebbea4c30e23228d00af762fa7da7ca5016bd Mon Sep 17 00:00:00 2001 +From: "Ned T. Crigler" +Date: Mon, 22 May 2023 15:31:54 -0700 +Subject: [PATCH 2/2] Update the dh code to work with OpenSSL 3.0 + +--- + include/dh.h | 8 ++++ + src/dh.c | 120 ++++++++++++++++++++++++++++++++++++++++++++++++--- + 2 files changed, 123 insertions(+), 5 deletions(-) + +diff --git a/include/dh.h b/include/dh.h +index 1817ce1e..705e6dee 100644 +--- a/include/dh.h ++++ b/include/dh.h +@@ -22,7 +22,11 @@ extern void rc4_destroystate(void *a); + + struct session_info + { ++#if OPENSSL_VERSION_NUMBER < 0x30000000L + DH *dh; ++#else ++ EVP_PKEY *dh; ++#endif + unsigned char *session_shared; + size_t session_shared_length; + }; +@@ -45,6 +49,10 @@ struct session_info + static BIGNUM *ircd_prime; + static BIGNUM *ircd_generator; + ++#if OPENSSL_VERSION_NUMBER >= 0x30000000L ++static EVP_PKEY *ircd_prime_ossl3; ++#endif ++ + static char *dh_hex_to_string[256] = + { + "00", "01", "02", "03", "04", "05", "06", "07", +diff --git a/src/dh.c b/src/dh.c +index 4b5da282..f74d2d76 100644 +--- a/src/dh.c ++++ b/src/dh.c +@@ -36,6 +36,11 @@ + #include + #include "libcrypto-compat.h" + ++#if OPENSSL_VERSION_NUMBER >= 0x30000000L ++#include ++#include ++#endif ++ + #include "memcount.h" + + #define DH_HEADER +@@ -215,7 +220,7 @@ static int init_random() + return 0; + } + +-static void create_prime() ++static int create_prime() + { + char buf[PRIME_BYTES_HEX]; + int i; +@@ -233,6 +238,34 @@ static void create_prime() + BN_hex2bn(&ircd_prime, buf); + ircd_generator = BN_new(); + BN_set_word(ircd_generator, dh_gen_1024); ++ ++#if OPENSSL_VERSION_NUMBER >= 0x30000000L ++ OSSL_PARAM_BLD *paramBuild = NULL; ++ OSSL_PARAM *param = NULL; ++ EVP_PKEY_CTX *primeCtx = NULL; ++ ++ if(!(paramBuild = OSSL_PARAM_BLD_new()) || ++ !OSSL_PARAM_BLD_push_BN(paramBuild, OSSL_PKEY_PARAM_FFC_P, ircd_prime) || ++ !OSSL_PARAM_BLD_push_BN(paramBuild, OSSL_PKEY_PARAM_FFC_G, ircd_generator) || ++ !(param = OSSL_PARAM_BLD_to_param(paramBuild)) || ++ !(primeCtx = EVP_PKEY_CTX_new_from_name(NULL, "DHX", NULL)) || ++ EVP_PKEY_fromdata_init(primeCtx) <= 0 || ++ EVP_PKEY_fromdata(primeCtx, &ircd_prime_ossl3, ++ EVP_PKEY_KEY_PARAMETERS, param) <= 0 || ++ 1) ++ { ++ if(primeCtx) ++ EVP_PKEY_CTX_free(primeCtx); ++ if(param) ++ OSSL_PARAM_free(param); ++ if(paramBuild) ++ OSSL_PARAM_BLD_free(paramBuild); ++ } ++ ++ if(!ircd_prime_ossl3) ++ return -1; ++#endif ++ return 0; + } + + int dh_init() +@@ -241,8 +274,7 @@ int dh_init() + ERR_load_crypto_strings(); + #endif + +- create_prime(); +- if(init_random() == -1) ++ if(create_prime() == -1 || init_random() == -1) + return -1; + return 0; + } +@@ -250,7 +282,7 @@ int dh_init() + int dh_generate_shared(void *session, char *public_key) + { + BIGNUM *tmp; +- int len; ++ size_t len; + struct session_info *si = (struct session_info *) session; + + if(verify_is_hex(public_key) == 0 || !si || si->session_shared) +@@ -261,13 +293,55 @@ int dh_generate_shared(void *session, char *public_key) + if(!tmp) + return 0; + ++#if OPENSSL_VERSION_NUMBER < 0x30000000L + si->session_shared_length = DH_size(si->dh); + si->session_shared = (unsigned char *) malloc(DH_size(si->dh)); + len = DH_compute_key(si->session_shared, tmp, si->dh); ++#else ++ OSSL_PARAM_BLD *paramBuild = NULL; ++ OSSL_PARAM *param = NULL; ++ EVP_PKEY_CTX *peerPubKeyCtx = NULL; ++ EVP_PKEY *peerPubKey = NULL; ++ EVP_PKEY_CTX *deriveCtx = NULL; ++ ++ len = -1; ++ if(!(paramBuild = OSSL_PARAM_BLD_new()) || ++ !OSSL_PARAM_BLD_push_BN(paramBuild, OSSL_PKEY_PARAM_FFC_P, ircd_prime) || ++ !OSSL_PARAM_BLD_push_BN(paramBuild, OSSL_PKEY_PARAM_FFC_G, ircd_generator) || ++ !OSSL_PARAM_BLD_push_BN(paramBuild, OSSL_PKEY_PARAM_PUB_KEY, tmp) || ++ !(param = OSSL_PARAM_BLD_to_param(paramBuild)) || ++ !(peerPubKeyCtx = EVP_PKEY_CTX_new_from_name(NULL, "DHX", NULL)) || ++ EVP_PKEY_fromdata_init(peerPubKeyCtx) <= 0 || ++ EVP_PKEY_fromdata(peerPubKeyCtx, &peerPubKey, ++ EVP_PKEY_PUBLIC_KEY, param) <= 0 || ++ !(deriveCtx = EVP_PKEY_CTX_new(si->dh, NULL)) || ++ EVP_PKEY_derive_init(deriveCtx) <= 0 || ++ EVP_PKEY_derive_set_peer(deriveCtx, peerPubKey) <= 0 || ++ EVP_PKEY_derive(deriveCtx, NULL, &len) <= 0 || ++ !(si->session_shared = malloc(len)) || ++ EVP_PKEY_derive(deriveCtx, si->session_shared, &len) <= 0 || ++ 1) ++ { ++ if(deriveCtx) ++ EVP_PKEY_CTX_free(deriveCtx); ++ if(peerPubKey) ++ EVP_PKEY_free(peerPubKey); ++ if(peerPubKeyCtx) ++ EVP_PKEY_CTX_free(peerPubKeyCtx); ++ if(param) ++ OSSL_PARAM_free(param); ++ if(paramBuild) ++ OSSL_PARAM_BLD_free(paramBuild); ++ } ++#endif + BN_free(tmp); + +- if(len < 0) ++ if(len == -1 || !si->session_shared) ++ { ++ if(si->session_shared) ++ free(si->session_shared); + return 0; ++ } + + si->session_shared_length = len; + +@@ -284,6 +358,7 @@ void *dh_start_session() + + memset(si, 0, sizeof(struct session_info)); + ++#if OPENSSL_VERSION_NUMBER < 0x30000000L + si->dh = DH_new(); + if(si->dh == NULL) + return NULL; +@@ -304,7 +379,23 @@ void *dh_start_session() + MyFree(si); + return NULL; + } ++#else ++ EVP_PKEY_CTX *keyGenCtx = NULL; + ++ if(!(keyGenCtx = EVP_PKEY_CTX_new_from_pkey(NULL, ircd_prime_ossl3, NULL)) || ++ EVP_PKEY_keygen_init(keyGenCtx) <= 0 || ++ EVP_PKEY_generate(keyGenCtx, &si->dh) <= 0 || ++ 1) ++ { ++ if(keyGenCtx) ++ EVP_PKEY_CTX_free(keyGenCtx); ++ } ++ if(!si->dh) ++ { ++ MyFree(si); ++ return NULL; ++ } ++#endif + return (void *) si; + } + +@@ -312,6 +403,7 @@ void dh_end_session(void *session) + { + struct session_info *si = (struct session_info *) session; + ++#if OPENSSL_VERSION_NUMBER < 0x30000000L + if(si->dh) + { + DH_free(si->dh); +@@ -324,6 +416,13 @@ void dh_end_session(void *session) + free(si->session_shared); + si->session_shared = NULL; + } ++#else ++ if(si->dh) ++ { ++ EVP_PKEY_free(si->dh); ++ si->dh = NULL; ++ } ++#endif + + MyFree(si); + } +@@ -333,6 +432,7 @@ char *dh_get_s_public(char *buf, size_t maxlen, void *session) + struct session_info *si = (struct session_info *) session; + char *tmp; + ++#if OPENSSL_VERSION_NUMBER < 0x30000000L + if(!si || !si->dh) + return NULL; + +@@ -343,6 +443,16 @@ char *dh_get_s_public(char *buf, size_t maxlen, void *session) + return NULL; + + tmp = BN_bn2hex(pub_key); ++#else ++ BIGNUM *pub_key = NULL; ++ ++ if(!si || !si->dh) ++ return NULL; ++ if(!EVP_PKEY_get_bn_param(si->dh, OSSL_PKEY_PARAM_PUB_KEY, &pub_key)) ++ return NULL; ++ tmp = BN_bn2hex(pub_key); ++ BN_free(pub_key); ++#endif + if(!tmp) + return NULL; + diff --git a/patches/charybdis_ubuntu22.patch b/patches/charybdis_ubuntu22.patch new file mode 100644 index 0000000..fe93952 --- /dev/null +++ b/patches/charybdis_ubuntu22.patch @@ -0,0 +1,23 @@ +From fa5d445e5e2af735378a1219d2a200ee8aef6561 Mon Sep 17 00:00:00 2001 +From: Sadie Powell +Date: Sun, 25 Jun 2023 21:50:42 +0100 +Subject: [PATCH] Fix Charybdis on Ubuntu 22.04. + +--- + librb/include/rb_lib.h | 2 ++ + 1 file changed, 2 insertions(+) + +diff --git a/librb/include/rb_lib.h b/librb/include/rb_lib.h +index c02dff68..0dd9c378 100644 +--- a/librb/include/rb_lib.h ++++ b/librb/include/rb_lib.h +@@ -258,4 +258,6 @@ pid_t rb_getpid(void); + #include + #include + ++#include ++ + #endif +-- +2.34.1 + diff --git a/workflows.yml b/workflows.yml index b220238..38af377 100644 --- a/workflows.yml +++ b/workflows.yml @@ -18,6 +18,7 @@ software: separate_build_job: true build_script: | cd $GITHUB_WORKSPACE/charybdis/ + patch -p1 < $GITHUB_WORKSPACE/patches/charybdis_ubuntu22.patch ./autogen.sh ./configure --prefix=$HOME/.local/ make -j 4 @@ -106,6 +107,7 @@ software: cd $GITHUB_WORKSPACE/Bahamut/ patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch patch src/s_bsd.c < $GITHUB_WORKSPACE/patches/bahamut_mainloop.patch + patch -p1 < $GITHUB_WORKSPACE/patches/bahamut_ubuntu22.patch echo "#undef THROTTLE_ENABLE" >> include/config.h libtoolize --force aclocal From 7bc8a81f8ae941cc3c6bf990b248342d1a4be3f0 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 9 Jul 2023 20:29:14 +0200 Subject: [PATCH 117/143] Fix compat with Unreal > 6.1.1.1 The '=' syntax is an ircd-hybrid-ism, and Unreal will drop support for it in the next release. More specifically, somewhere between 0af88581d380602bfd58a0cdaa36b714fb7ef3c3 and c8c265790494b908ff397c705855a21e591884de in its Git history. --- irctest/controllers/unrealircd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irctest/controllers/unrealircd.py b/irctest/controllers/unrealircd.py index 8d7e643..923d0d8 100644 --- a/irctest/controllers/unrealircd.py +++ b/irctest/controllers/unrealircd.py @@ -112,7 +112,7 @@ files {{ }} oper "operuser" {{ - password = "operpassword"; + password "operpassword"; mask *; class clients; operclass netadmin; From 2e45f7bfdb523482a2105294df622f5cdc62313c Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Mon, 10 Jul 2023 20:17:01 +0200 Subject: [PATCH 118/143] Fix build against Bahamut's master branch --- .github/workflows/test-devel.yml | 5 ++++- .github/workflows/test-stable.yml | 5 ++++- workflows.yml | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 915f6c8..81af1ae 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -72,7 +72,10 @@ jobs: cd $GITHUB_WORKSPACE/Bahamut/ patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch patch src/s_bsd.c < $GITHUB_WORKSPACE/patches/bahamut_mainloop.patch - patch -p1 < $GITHUB_WORKSPACE/patches/bahamut_ubuntu22.patch + + # <= v2.2.2 + patch -p1 < $GITHUB_WORKSPACE/patches/bahamut_ubuntu22.patch || true + echo "#undef THROTTLE_ENABLE" >> include/config.h libtoolize --force aclocal diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 9bf463b..4f39cf7 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -72,7 +72,10 @@ jobs: cd $GITHUB_WORKSPACE/Bahamut/ patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch patch src/s_bsd.c < $GITHUB_WORKSPACE/patches/bahamut_mainloop.patch - patch -p1 < $GITHUB_WORKSPACE/patches/bahamut_ubuntu22.patch + + # <= v2.2.2 + patch -p1 < $GITHUB_WORKSPACE/patches/bahamut_ubuntu22.patch || true + echo "#undef THROTTLE_ENABLE" >> include/config.h libtoolize --force aclocal diff --git a/workflows.yml b/workflows.yml index 38af377..114cf45 100644 --- a/workflows.yml +++ b/workflows.yml @@ -107,7 +107,10 @@ software: cd $GITHUB_WORKSPACE/Bahamut/ patch src/s_user.c < $GITHUB_WORKSPACE/patches/bahamut_localhost.patch patch src/s_bsd.c < $GITHUB_WORKSPACE/patches/bahamut_mainloop.patch - patch -p1 < $GITHUB_WORKSPACE/patches/bahamut_ubuntu22.patch + + # <= v2.2.2 + patch -p1 < $GITHUB_WORKSPACE/patches/bahamut_ubuntu22.patch || true + echo "#undef THROTTLE_ENABLE" >> include/config.h libtoolize --force aclocal From e38f29befa9b7d56472b67c1643ff2254edb42b6 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 22 Jul 2023 22:12:44 +0200 Subject: [PATCH 119/143] Log unexpected exit codes --- irctest/basecontrollers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index 02b9a34..0652fa2 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -112,7 +112,7 @@ class _BaseController: assert self.proc self.proc.poll() if self.proc.returncode is not None: - raise ProcessStopped() + raise ProcessStopped(f"process returned {self.proc.returncode}") def kill_proc(self) -> None: """Terminates the controlled process, waits for it to exit, and From 59b2cd729b7785c3768e2ed8c4baa89472b45391 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 23 Jul 2023 11:40:01 +0200 Subject: [PATCH 120/143] Configure Unreal with --with-system-argon2 Our Github Workflow builds and runs on different machines, causing argon2 to be built sometimes with some CPU instructions that the machine running it does not support. --- data/unreal/config.settings | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/data/unreal/config.settings b/data/unreal/config.settings index e4d15b1..7d16ac4 100644 --- a/data/unreal/config.settings +++ b/data/unreal/config.settings @@ -19,6 +19,10 @@ SHOWLISTMODES="1" NOOPEROVERRIDE="" OPEROVERRIDEVERIFY="" GENCERTIFICATE="1" -EXTRAPARA="" + +# Use system argon to avoid getting SIGILLed if the build machine has a more recent +# CPU than the one running the tests. +EXTRAPARA="--with-system-argon2" + ADVANCED="" From d190a919603fa5bc2ba2d24318e5a088c78d3d8e Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Tue, 8 Aug 2023 20:19:36 -0700 Subject: [PATCH 121/143] test that PART actually parts (#211) Co-authored-by: Val Lorentz --- irctest/server_tests/part.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/irctest/server_tests/part.py b/irctest/server_tests/part.py index 59a2045..4ee5055 100644 --- a/irctest/server_tests/part.py +++ b/irctest/server_tests/part.py @@ -10,6 +10,7 @@ TODO: cross-reference Modern import time from irctest import cases +from irctest.numerics import RPL_NAMREPLY class PartTestCase(cases.BaseServerTestCase): @@ -84,6 +85,12 @@ class PartTestCase(cases.BaseServerTestCase): self.getMessages(1) self.getMessages(2) + self.sendLine(2, "PRIVMSG #chan :hi everyone") + self.getMessages(2) + self.assertMessageMatch( + self.getMessage(1), command="PRIVMSG", params=["#chan", "hi everyone"] + ) + self.sendLine(1, "PART #chan") # both the PART'ing client and the other channel member should receive # a PART line: @@ -92,6 +99,21 @@ class PartTestCase(cases.BaseServerTestCase): m = self.getMessage(2) self.assertMessageMatch(m, command="PART") + self.sendLine(2, "PRIVMSG #chan :hi again everyone") + self.getMessages(2) + # client 1 has PART'ed and should not receive channel messages: + self.assertEqual(self.getMessages(1), []) + + # client 1 should no longer appear in NAMES responses: + names = set() + self.sendLine(2, "NAMES #chan") + for reply in self.getMessages(2): + if reply.command != RPL_NAMREPLY: + continue + names.update(reply.params[-1].replace("@", "").split()) + self.assertNotIn("bar", names) + self.assertIn("baz", names) + @cases.mark_specifications("RFC2812") def testBasicPartRfc2812(self): """ From 61fb28728065990e1f82fee56fa8fcc593037a51 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Wed, 9 Aug 2023 09:00:51 -0700 Subject: [PATCH 122/143] fix nonexistent user PRIVMSG test (#212) * fix nonexistent user PRIVMSG test * fix single-element tupling issue * test the ERR_NOSUCHNICK params * use patma --- irctest/server_tests/messages.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/irctest/server_tests/messages.py b/irctest/server_tests/messages.py index 8282ee2..441446d 100644 --- a/irctest/server_tests/messages.py +++ b/irctest/server_tests/messages.py @@ -4,6 +4,7 @@ The PRIVMSG and NOTICE commands. from irctest import cases from irctest.numerics import ERR_INPUTTOOLONG +from irctest.patma import ANYSTR class PrivmsgTestCase(cases.BaseServerTestCase): @@ -45,12 +46,12 @@ class PrivmsgTestCase(cases.BaseServerTestCase): @cases.mark_specifications("RFC1459", "RFC2812") def testPrivmsgNonexistentUser(self): - """https://tools.ietf.org/html/rfc2812#section-3.3.1""" + """""" self.connectClient("foo") self.sendLine(1, "PRIVMSG bar :hey there!") msg = self.getMessage(1) - # ERR_NOSUCHNICK - self.assertIn(msg.command, ("401")) + # ERR_NOSUCHNICK: 401 :No such nick + self.assertMessageMatch(msg, command="401", params=["foo", "bar", ANYSTR]) class NoticeTestCase(cases.BaseServerTestCase): From 7255d655146c4dd83c6c7b97f8dca5e7e924cb56 Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Wed, 9 Aug 2023 09:16:32 -0700 Subject: [PATCH 123/143] Test that WHO #chan always returns that channel (#213) * Test that WHO #chan always returns that channel @emersion's test from https://github.com/progval/irctest/pull/190 Co-authored-by: Simon Ser Co-authored-by: Val Lorentz --- irctest/server_tests/who.py | 62 +++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/irctest/server_tests/who.py b/irctest/server_tests/who.py index b3892d6..7a182ba 100644 --- a/irctest/server_tests/who.py +++ b/irctest/server_tests/who.py @@ -361,6 +361,68 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): params=["otherNick", InsensitiveStr(mask), ANYSTR], ) + @cases.mark_specifications("Modern") + def testWhoMultiChan(self): + """ + When WHO <#chan> is sent, the second parameter of RPL_WHOREPLY must + be ``#chan``. See discussion on Modern: + + """ + self._init() + + self.sendLine(1, "JOIN #otherchan") + self.getMessages(1) + + self.sendLine(2, "JOIN #otherchan") + self.getMessages(2) + + for chan in ["#chan", "#otherchan"]: + self.sendLine(2, f"WHO {chan}") + messages = self.getMessages(2) + + self.assertEqual(len(messages), 3, "Unexpected number of messages") + + (*replies, end) = messages + + # Get them in deterministic order + replies.sort(key=lambda msg: msg.params[5]) + + self.assertMessageMatch( + replies[0], + command=RPL_WHOREPLY, + params=[ + "otherNick", + chan, + ANYSTR, + ANYSTR, + "My.Little.Server", + "coolNick", + ANYSTR, + ANYSTR, + ], + ) + + self.assertMessageMatch( + replies[1], + command=RPL_WHOREPLY, + params=[ + "otherNick", + chan, + ANYSTR, + ANYSTR, + "My.Little.Server", + "otherNick", + ANYSTR, + ANYSTR, + ], + ) + + self.assertMessageMatch( + end, + command=RPL_ENDOFWHO, + params=["otherNick", InsensitiveStr(chan), ANYSTR], + ) + @cases.mark_specifications("IRCv3") @cases.mark_isupport("WHOX") def testWhoxFull(self): From a950c724bb61510cc167e80c10d9e73c34e03faf Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Fri, 11 Aug 2023 20:24:26 +0200 Subject: [PATCH 124/143] Bump Python to 3.11 (#214) Sopel dropped support for Python 3.7 --- .github/workflows/test-devel.yml | 36 ++++++++++----------- .github/workflows/test-devel_release.yml | 8 ++--- .github/workflows/test-stable.yml | 40 ++++++++++++------------ make_workflows.py | 4 +-- 4 files changed, 44 insertions(+), 44 deletions(-) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 81af1ae..e9dd4d3 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -17,10 +17,10 @@ jobs: ' - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout Anope uses: actions/checkout@v3 with: @@ -57,10 +57,10 @@ jobs: ' - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout Bahamut uses: actions/checkout@v3 with: @@ -110,10 +110,10 @@ jobs: ' - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout Hybrid uses: actions/checkout@v3 with: @@ -140,10 +140,10 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout InspIRCd uses: actions/checkout@v3 with: @@ -184,10 +184,10 @@ jobs: ' - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout ngircd uses: actions/checkout@v3 with: @@ -225,10 +225,10 @@ jobs: ' - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: clone run: 'curl https://gitlab.com/rizon/plexus4/-/archive/master/plexus4-master.tar.gz | tar -zx @@ -267,10 +267,10 @@ jobs: ' - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout Solanum uses: actions/checkout@v3 with: @@ -307,10 +307,10 @@ jobs: ' - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout UnrealIRCd 6 uses: actions/checkout@v3 with: @@ -353,10 +353,10 @@ jobs: ' - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout UnrealIRCd 5 uses: actions/checkout@v3 with: diff --git a/.github/workflows/test-devel_release.yml b/.github/workflows/test-devel_release.yml index bcfbe62..3da5e00 100644 --- a/.github/workflows/test-devel_release.yml +++ b/.github/workflows/test-devel_release.yml @@ -17,10 +17,10 @@ jobs: ' - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout Anope uses: actions/checkout@v3 with: @@ -48,10 +48,10 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout InspIRCd uses: actions/checkout@v3 with: diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 4f39cf7..4ba290c 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -17,10 +17,10 @@ jobs: ' - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout Anope uses: actions/checkout@v3 with: @@ -57,10 +57,10 @@ jobs: ' - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout Bahamut uses: actions/checkout@v3 with: @@ -110,10 +110,10 @@ jobs: ' - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout Charybdis uses: actions/checkout@v3 with: @@ -151,10 +151,10 @@ jobs: ' - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout Hybrid uses: actions/checkout@v3 with: @@ -181,10 +181,10 @@ jobs: - name: Create directories run: cd ~/; mkdir -p .local/ go/ - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout InspIRCd uses: actions/checkout@v3 with: @@ -225,10 +225,10 @@ jobs: ' - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout ngircd uses: actions/checkout@v3 with: @@ -266,10 +266,10 @@ jobs: ' - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: clone run: 'curl https://gitlab.com/rizon/plexus4/-/archive/20211115_0-611/plexus4-20211115_0-611.tar | tar -x @@ -308,10 +308,10 @@ jobs: ' - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout Solanum uses: actions/checkout@v3 with: @@ -348,10 +348,10 @@ jobs: ' - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout UnrealIRCd 6 uses: actions/checkout@v3 with: @@ -394,10 +394,10 @@ jobs: ' - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout UnrealIRCd 5 uses: actions/checkout@v3 with: diff --git a/make_workflows.py b/make_workflows.py index f959b9f..cc64e82 100644 --- a/make_workflows.py +++ b/make_workflows.py @@ -125,9 +125,9 @@ def get_build_job(*, software_config, software_id, version_flavor): *cache, {"uses": "actions/checkout@v3"}, { - "name": "Set up Python 3.7", + "name": "Set up Python 3.11", "uses": "actions/setup-python@v4", - "with": {"python-version": 3.7}, + "with": {"python-version": 3.11}, }, *install_steps, *upload_steps(software_id), From cb147f46eb3c1f61c12db249edb98ae499bc5193 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sun, 13 Aug 2023 20:09:35 +0200 Subject: [PATCH 125/143] Bump Python to 3.11 on release and devel_release workflows Sopel dropped support for Python 3.7 --- .github/workflows/test-devel.yml | 88 ++++++++++---------- .github/workflows/test-devel_release.yml | 12 +-- .github/workflows/test-stable.yml | 100 +++++++++++------------ make_workflows.py | 4 +- 4 files changed, 102 insertions(+), 102 deletions(-) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index e9dd4d3..33f2ccf 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -440,10 +440,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -474,10 +474,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -512,10 +512,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -544,10 +544,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout Ergo uses: actions/checkout@v3 with: @@ -586,10 +586,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -624,10 +624,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -658,10 +658,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -695,10 +695,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout ircu2 uses: actions/checkout@v3 with: @@ -734,10 +734,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Install dependencies run: pip install git+https://github.com/ProgVal/Limnoria.git@testing cryptography pyxmpp2-scram @@ -762,10 +762,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout nefarious uses: actions/checkout@v3 with: @@ -801,10 +801,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -835,10 +835,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -873,10 +873,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -907,10 +907,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -945,10 +945,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -977,10 +977,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Install dependencies run: pip install git+https://github.com/sopel-irc/sopel.git - name: Install system dependencies @@ -1004,10 +1004,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout TheLounge uses: actions/checkout@v3 with: @@ -1043,10 +1043,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -1076,10 +1076,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -1110,10 +1110,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -1148,10 +1148,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -1181,10 +1181,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: diff --git a/.github/workflows/test-devel_release.yml b/.github/workflows/test-devel_release.yml index 3da5e00..87e85c7 100644 --- a/.github/workflows/test-devel_release.yml +++ b/.github/workflows/test-devel_release.yml @@ -114,10 +114,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -148,10 +148,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -186,10 +186,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 4ba290c..3fa59b4 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -484,10 +484,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -518,10 +518,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -556,10 +556,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -589,10 +589,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -621,10 +621,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout Ergo uses: actions/checkout@v3 with: @@ -663,10 +663,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -701,10 +701,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -735,10 +735,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -773,10 +773,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -805,10 +805,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout irc2 uses: actions/checkout@v3 with: @@ -855,10 +855,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout ircu2 uses: actions/checkout@v3 with: @@ -894,10 +894,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Install dependencies run: pip install limnoria==2023.5.27 cryptography pyxmpp2-scram - name: Install system dependencies @@ -921,10 +921,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout nefarious uses: actions/checkout@v3 with: @@ -960,10 +960,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -994,10 +994,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -1032,10 +1032,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -1066,10 +1066,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -1104,10 +1104,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -1136,10 +1136,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Install dependencies run: pip install sopel==7.1.8 - name: Install system dependencies @@ -1163,10 +1163,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Checkout TheLounge uses: actions/checkout@v3 with: @@ -1202,10 +1202,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -1235,10 +1235,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -1269,10 +1269,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -1307,10 +1307,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: @@ -1340,10 +1340,10 @@ jobs: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - - name: Set up Python 3.7 + - name: Set up Python 3.11 uses: actions/setup-python@v4 with: - python-version: 3.7 + python-version: 3.11 - name: Download build artefacts uses: actions/download-artifact@v3 with: diff --git a/make_workflows.py b/make_workflows.py index cc64e82..8c94179 100644 --- a/make_workflows.py +++ b/make_workflows.py @@ -196,9 +196,9 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs): "steps": [ {"uses": "actions/checkout@v3"}, { - "name": "Set up Python 3.7", + "name": "Set up Python 3.11", "uses": "actions/setup-python@v4", - "with": {"python-version": 3.7}, + "with": {"python-version": 3.11}, }, *downloads, *unpack, From b28820e5620b286310f79ee5fc30d2c2fb0c8bbc Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Wed, 16 Aug 2023 20:12:54 +0200 Subject: [PATCH 126/143] Bump Go again --- .github/workflows/test-devel.yml | 2 +- .github/workflows/test-stable.yml | 2 +- workflows.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 33f2ccf..89574f8 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -556,7 +556,7 @@ jobs: repository: ergochat/ergo - uses: actions/setup-go@v2 with: - go-version: ^1.19.0 + go-version: ^1.21.0 - run: go version - name: Build Ergo run: | diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 3fa59b4..024115a 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -633,7 +633,7 @@ jobs: repository: ergochat/ergo - uses: actions/setup-go@v2 with: - go-version: ^1.19.0 + go-version: ^1.21.0 - run: go version - name: Build Ergo run: | diff --git a/workflows.yml b/workflows.yml index 114cf45..8c187da 100644 --- a/workflows.yml +++ b/workflows.yml @@ -136,7 +136,7 @@ software: pre_deps: - uses: actions/setup-go@v2 with: - go-version: '^1.19.0' + go-version: '^1.21.0' - run: go version separate_build_job: false build_script: | From 10e07aa8008598dec1ee8488171a0ac9a1bb390c Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Thu, 17 Aug 2023 20:15:18 +0200 Subject: [PATCH 127/143] Test that WHO with non-existing nick returns RPL_ENDOFWHO (#215) --- irctest/server_tests/who.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/irctest/server_tests/who.py b/irctest/server_tests/who.py index 7a182ba..7813ee6 100644 --- a/irctest/server_tests/who.py +++ b/irctest/server_tests/who.py @@ -423,6 +423,25 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): params=["otherNick", InsensitiveStr(chan), ANYSTR], ) + @cases.mark_specifications("Modern") + def testWhoNickNotExists(self): + """ + When WHO is sent with a non-existing nickname, the server must reply + with a single RPL_ENDOFWHO. See: + + """ + + self._init() + + self.sendLine(2, "WHO idontexist") + (end,) = self.getMessages(2) + + self.assertMessageMatch( + end, + command=RPL_ENDOFWHO, + params=["otherNick", InsensitiveStr("idontexist"), ANYSTR], + ) + @cases.mark_specifications("IRCv3") @cases.mark_isupport("WHOX") def testWhoxFull(self): From 653d818421d5332db2144a1a8ba57ddda27a881a Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Tue, 29 Aug 2023 20:28:04 +0200 Subject: [PATCH 128/143] testInviteAlreadyInChannel: Fix synchronization --- irctest/server_tests/invite.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irctest/server_tests/invite.py b/irctest/server_tests/invite.py index bb6e6df..5b6c43a 100644 --- a/irctest/server_tests/invite.py +++ b/irctest/server_tests/invite.py @@ -360,8 +360,8 @@ class InviteTestCase(cases.BaseServerTestCase): self.getMessages(2) self.sendLine(1, "JOIN #chan") - self.sendLine(2, "JOIN #chan") self.getMessages(1) + self.sendLine(2, "JOIN #chan") self.getMessages(2) self.getMessages(1) From 360a853bcad8ed43f518f53e19210c992d03ffb8 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 2 Sep 2023 13:43:01 +0200 Subject: [PATCH 129/143] Skip testLabeledPrivmsgResponsesToMultipleClients if PRIVMSG doesn't support multiple targets (#217) --- irctest/server_tests/labeled_responses.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/irctest/server_tests/labeled_responses.py b/irctest/server_tests/labeled_responses.py index 76aa324..b6e8bd9 100644 --- a/irctest/server_tests/labeled_responses.py +++ b/irctest/server_tests/labeled_responses.py @@ -12,6 +12,7 @@ import pytest from irctest import cases from irctest.numerics import ERR_UNKNOWNCOMMAND from irctest.patma import ANYDICT, ANYOPTSTR, NotStrRe, RemainingKeys, StrRe +from irctest.runner import OptionalExtensionNotSupported class LabeledResponsesTestCase(cases.BaseServerTestCase): @@ -22,7 +23,10 @@ class LabeledResponsesTestCase(cases.BaseServerTestCase): capabilities=["echo-message", "batch", "labeled-response"], skip_if_cap_nak=True, ) + if int(self.targmax.get("PRIVMSG", "1") or "4") < 3: + raise OptionalExtensionNotSupported("PRIVMSG to multiple targets") self.getMessages(1) + self.connectClient( "bar", capabilities=["echo-message", "batch", "labeled-response"], From fe24e4b8b832efdf0f02d49667e690e2bc2873d7 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 2 Sep 2023 13:43:15 +0200 Subject: [PATCH 130/143] multi_prefix: Skip test on IRCds that don't support it (#218) --- irctest/server_tests/multi_prefix.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/irctest/server_tests/multi_prefix.py b/irctest/server_tests/multi_prefix.py index 1861ac7..ed57b9c 100644 --- a/irctest/server_tests/multi_prefix.py +++ b/irctest/server_tests/multi_prefix.py @@ -15,7 +15,7 @@ class MultiPrefixTestCase(cases.BaseServerTestCase): These prefixes MUST be in order of ‘rank’, from highest to lowest. """ - self.connectClient("foo", capabilities=["multi-prefix"]) + self.connectClient("foo", capabilities=["multi-prefix"], skip_if_cap_nak=True) self.joinChannel(1, "#chan") self.sendLine(1, "MODE #chan +v foo") self.getMessages(1) From 50d3a8e6dae8ab756ca6d0c483103440f0384c89 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 2 Sep 2023 13:43:35 +0200 Subject: [PATCH 131/143] echo_message: Simplify code (#219) --- irctest/server_tests/echo_message.py | 20 +++++++------------- 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/irctest/server_tests/echo_message.py b/irctest/server_tests/echo_message.py index 063028a..2eb7119 100644 --- a/irctest/server_tests/echo_message.py +++ b/irctest/server_tests/echo_message.py @@ -22,23 +22,17 @@ class EchoMessageTestCase(cases.BaseServerTestCase): @cases.mark_capabilities("echo-message") def testEchoMessage(self, command, solo, server_time): """""" - if server_time: - self.connectClient( - "baz", - capabilities=["echo-message", "server-time"], - skip_if_cap_nak=True, - ) - else: - self.connectClient( - "baz", - capabilities=["echo-message", "server-time"], - skip_if_cap_nak=True, - ) + capabilities = ["server-time"] if server_time else [] + + self.connectClient( + "baz", + capabilities=["echo-message", *capabilities], + skip_if_cap_nak=True, + ) self.sendLine(1, "JOIN #chan") if not solo: - capabilities = ["server-time"] if server_time else None self.connectClient("qux", capabilities=capabilities) self.sendLine(2, "JOIN #chan") From 1c6a7188d6c16ae35dad8cf72a6fd83654310718 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 2 Sep 2023 15:08:29 +0200 Subject: [PATCH 132/143] Add more tests for CAP + allow trailing spaces (#216) --- irctest/controllers/inspircd.py | 1 + irctest/server_tests/cap.py | 239 ++++++++++++++++++++++++++++++-- 2 files changed, 225 insertions(+), 15 deletions(-) diff --git a/irctest/controllers/inspircd.py b/irctest/controllers/inspircd.py index b932ec2..106d8fd 100644 --- a/irctest/controllers/inspircd.py +++ b/irctest/controllers/inspircd.py @@ -73,6 +73,7 @@ TEMPLATE_CONFIG = """ # for testing mute extbans # For multi-prefix + # For userhost-in-names # HELP/HELPOP # for the HELP alias diff --git a/irctest/server_tests/cap.py b/irctest/server_tests/cap.py index 95c4027..b6b0d5e 100644 --- a/irctest/server_tests/cap.py +++ b/irctest/server_tests/cap.py @@ -4,11 +4,32 @@ """ from irctest import cases -from irctest.patma import ANYSTR +from irctest.patma import ANYSTR, StrRe from irctest.runner import CapabilityNotSupported, ImplementationChoice class CapTestCase(cases.BaseServerTestCase): + @cases.mark_specifications("IRCv3") + def testInvalidCapSubcommand(self): + """“If no capabilities are active, an empty parameter must be sent.” + -- + """ # noqa + self.addClient() + self.sendLine(1, "CAP NOTACOMMAND") + self.sendLine(1, "PING test123") + m = self.getRegistrationMessage(1) + self.assertTrue( + self.messageDiffers(m, command="PONG", params=[ANYSTR, "test123"]), + "Sending “CAP NOTACOMMAND” as first message got no reply", + ) + self.assertMessageMatch( + m, + command="410", + params=["*", "NOTACOMMAND", ANYSTR], + fail_msg="Sending “CAP NOTACOMMAND” as first message got a reply " + "that is not ERR_INVALIDCAPCMD: {msg}", + ) + @cases.mark_specifications("IRCv3") def testNoReq(self): """Test the server handles gracefully clients which do not send @@ -23,12 +44,206 @@ class CapTestCase(cases.BaseServerTestCase): self.getCapLs(1) self.sendLine(1, "USER foo foo foo :foo") self.sendLine(1, "NICK foo") + + # Make sure the server didn't send anything yet + self.sendLine(1, "CAP LS 302") + self.getCapLs(1) + self.sendLine(1, "CAP END") m = self.getRegistrationMessage(1) self.assertMessageMatch( m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}." ) + @cases.mark_specifications("IRCv3") + def testReqOne(self): + """Tests requesting a single capability""" + self.addClient(1) + self.sendLine(1, "CAP LS") + self.getCapLs(1) + self.sendLine(1, "USER foo foo foo :foo") + self.sendLine(1, "NICK foo") + self.sendLine(1, "CAP REQ :multi-prefix") + m = self.getRegistrationMessage(1) + self.assertMessageMatch( + m, + command="CAP", + params=[ANYSTR, "ACK", StrRe("multi-prefix ?")], + fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.", + ) + + self.sendLine(1, "CAP LIST") + m = self.getRegistrationMessage(1) + self.assertMessageMatch( + m, + command="CAP", + params=[ANYSTR, "LIST", StrRe("multi-prefix ?")], + fail_msg="Expected CAP LIST after sending CAP LIST, got {msg}.", + ) + + self.sendLine(1, "CAP END") + m = self.getRegistrationMessage(1) + self.assertMessageMatch( + m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}." + ) + + @cases.mark_specifications("IRCv3") + @cases.xfailIfSoftware( + ["ngIRCd"], + "ngIRCd does not support userhost-in-names", + ) + def testReqTwo(self): + """Tests requesting two capabilities at once""" + self.addClient(1) + self.sendLine(1, "CAP LS") + self.getCapLs(1) + self.sendLine(1, "USER foo foo foo :foo") + self.sendLine(1, "NICK foo") + self.sendLine(1, "CAP REQ :multi-prefix userhost-in-names") + m = self.getRegistrationMessage(1) + self.assertMessageMatch( + m, + command="CAP", + params=[ANYSTR, "ACK", StrRe("multi-prefix userhost-in-names ?")], + fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.", + ) + + self.sendLine(1, "CAP LIST") + m = self.getRegistrationMessage(1) + self.assertMessageMatch( + m, + command="CAP", + params=[ + ANYSTR, + "LIST", + StrRe( + "(multi-prefix userhost-in-names|userhost-in-names multi-prefix) ?" + ), + ], + fail_msg="Expected CAP LIST after sending CAP LIST, got {msg}.", + ) + + self.sendLine(1, "CAP END") + m = self.getRegistrationMessage(1) + self.assertMessageMatch( + m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}." + ) + + @cases.mark_specifications("IRCv3") + @cases.xfailIfSoftware( + ["ngIRCd"], + "ngIRCd does not support userhost-in-names", + ) + def testReqOneThenOne(self): + """Tests requesting two capabilities in different messages""" + self.addClient(1) + self.sendLine(1, "CAP LS") + self.getCapLs(1) + self.sendLine(1, "USER foo foo foo :foo") + self.sendLine(1, "NICK foo") + + self.sendLine(1, "CAP REQ :multi-prefix") + m = self.getRegistrationMessage(1) + self.assertMessageMatch( + m, + command="CAP", + params=[ANYSTR, "ACK", StrRe("multi-prefix ?")], + fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.", + ) + + self.sendLine(1, "CAP REQ :userhost-in-names") + m = self.getRegistrationMessage(1) + self.assertMessageMatch( + m, + command="CAP", + params=[ANYSTR, "ACK", StrRe("userhost-in-names ?")], + fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.", + ) + + self.sendLine(1, "CAP LIST") + m = self.getRegistrationMessage(1) + self.assertMessageMatch( + m, + command="CAP", + params=[ + ANYSTR, + "LIST", + StrRe( + "(multi-prefix userhost-in-names|userhost-in-names multi-prefix) ?" + ), + ], + fail_msg="Expected CAP LIST after sending CAP LIST, got {msg}.", + ) + + self.sendLine(1, "CAP END") + m = self.getRegistrationMessage(1) + self.assertMessageMatch( + m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}." + ) + + @cases.mark_specifications("IRCv3") + @cases.xfailIfSoftware( + ["ngIRCd"], + "ngIRCd does not support userhost-in-names", + ) + def testReqPostRegistration(self): + """Tests requesting more capabilities after CAP END""" + self.addClient(1) + self.sendLine(1, "CAP LS") + self.getCapLs(1) + self.sendLine(1, "USER foo foo foo :foo") + self.sendLine(1, "NICK foo") + + self.sendLine(1, "CAP REQ :multi-prefix") + m = self.getRegistrationMessage(1) + self.assertMessageMatch( + m, + command="CAP", + params=[ANYSTR, "ACK", StrRe("multi-prefix ?")], + fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.", + ) + + self.sendLine(1, "CAP LIST") + m = self.getRegistrationMessage(1) + self.assertMessageMatch( + m, + command="CAP", + params=[ANYSTR, "LIST", StrRe("multi-prefix ?")], + fail_msg="Expected CAP LIST after sending CAP LIST, got {msg}.", + ) + + self.sendLine(1, "CAP END") + m = self.getRegistrationMessage(1) + self.assertMessageMatch( + m, command="001", fail_msg="Expected 001 after sending CAP END, got {msg}." + ) + + self.getMessages(1) + + self.sendLine(1, "CAP REQ :userhost-in-names") + m = self.getRegistrationMessage(1) + self.assertMessageMatch( + m, + command="CAP", + params=[ANYSTR, "ACK", StrRe("userhost-in-names ?")], + fail_msg="Expected CAP ACK after sending CAP REQ, got {msg}.", + ) + + self.sendLine(1, "CAP LIST") + m = self.getMessage(1) + self.assertMessageMatch( + m, + command="CAP", + params=[ + ANYSTR, + "LIST", + StrRe( + "(multi-prefix userhost-in-names|userhost-in-names multi-prefix) ?" + ), + ], + fail_msg="Expected CAP LIST after sending CAP LIST, got {msg}.", + ) + @cases.mark_specifications("IRCv3") def testReqUnavailable(self): """Test the server handles gracefully clients which request @@ -45,7 +260,7 @@ class CapTestCase(cases.BaseServerTestCase): self.assertMessageMatch( m, command="CAP", - params=[ANYSTR, "NAK", "foo"], + params=[ANYSTR, "NAK", StrRe("foo ?")], fail_msg="Expected CAP NAK after requesting non-existing " "capability, got {msg}.", ) @@ -78,10 +293,6 @@ class CapTestCase(cases.BaseServerTestCase): ) @cases.mark_specifications("IRCv3") - @cases.xfailIfSoftware( - ["UnrealIRCd"], - "UnrealIRCd sends a trailing space on CAP NAK: https://github.com/unrealircd/unrealircd/pull/148", - ) def testNakWhole(self): """“The capability identifier set must be accepted as a whole, or rejected entirely.” @@ -123,16 +334,12 @@ class CapTestCase(cases.BaseServerTestCase): self.assertMessageMatch( m, command="CAP", - params=[ANYSTR, "ACK", "multi-prefix"], + params=[ANYSTR, "ACK", StrRe("multi-prefix ?")], fail_msg="Expected “CAP ACK :multi-prefix” after " "sending “CAP REQ :multi-prefix”, but got {msg}.", ) @cases.mark_specifications("IRCv3") - @cases.xfailIfSoftware( - ["UnrealIRCd"], - "UnrealIRCd sends a trailing space on CAP NAK: https://github.com/unrealircd/unrealircd/pull/148", - ) def testCapRemovalByClient(self): """Test CAP LIST and removal of caps via CAP REQ :-tagname.""" cap1 = "echo-message" @@ -167,17 +374,19 @@ class CapTestCase(cases.BaseServerTestCase): m = self.getMessage(1) self.assertIn("time", m.tags, m) - # remove the server-time cap + # remove the multi-prefix cap self.sendLine(1, f"CAP REQ :-{cap2}") m = self.getMessage(1) # Must be either ACK or NAK - if self.messageDiffers(m, command="CAP", params=[ANYSTR, "ACK", f"-{cap2}"]): + if self.messageDiffers( + m, command="CAP", params=[ANYSTR, "ACK", StrRe(f"-{cap2} ?")] + ): self.assertMessageMatch( - m, command="CAP", params=[ANYSTR, "NAK", f"-{cap2}"] + m, command="CAP", params=[ANYSTR, "NAK", StrRe(f"-{cap2} ?")] ) raise ImplementationChoice(f"Does not support CAP REQ -{cap2}") - # server-time should be disabled + # multi-prefix should be disabled self.sendLine(1, "CAP LIST") messages = self.getMessages(1) cap_list = [m for m in messages if m.command == "CAP"][0] From 34c78e5d2f739d10c616d28738c91836de598505 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 2 Sep 2023 15:42:18 +0200 Subject: [PATCH 133/143] testCapRemovalByClient: Support multiple CAP LS responses (#220) --- irctest/server_tests/cap.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/irctest/server_tests/cap.py b/irctest/server_tests/cap.py index b6b0d5e..fea4ef0 100644 --- a/irctest/server_tests/cap.py +++ b/irctest/server_tests/cap.py @@ -347,8 +347,13 @@ class CapTestCase(cases.BaseServerTestCase): self.addClient(1) self.connectClient("sender") self.sendLine(1, "CAP LS 302") - m = self.getRegistrationMessage(1) - if not ({cap1, cap2} <= set(m.params[2].split())): + caps = set() + while True: + m = self.getRegistrationMessage(1) + caps.update(m.params[-1].split()) + if m.params[2] != "*": + break + if not ({cap1, cap2} <= caps): raise CapabilityNotSupported(f"{cap1} or {cap2}") self.sendLine(1, f"CAP REQ :{cap1} {cap2}") self.sendLine(1, "nick bar") From c58167b42df888a1092690a7b03b38c9521c6abc Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Sat, 2 Sep 2023 16:24:57 +0200 Subject: [PATCH 134/143] Fix deprecation warning --- conftest.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/conftest.py b/conftest.py index 0939bcf..b06379a 100644 --- a/conftest.py +++ b/conftest.py @@ -106,13 +106,10 @@ def pytest_collection_modifyitems(session, config, items): assert isinstance(item, _pytest.python.Function) # unittest-style test functions have the node of UnitTest class as parent - assert isinstance( - item.parent, - ( - _pytest.python.Class, # pytest >= 7.0.0 - _pytest.python.Instance, # pytest < 7.0.0 - ), - ) + if tuple(map(int, _pytest.__version__.split("."))) >= (7,): + assert isinstance(item.parent, _pytest.python.Class) + else: + assert isinstance(item.parent, _pytest.python.Instance) # and that node references the UnitTest class assert issubclass(item.parent.cls, _IrcTestCase) From ecc560adebc55a4606f39ba9f7c5744e7a1f7c89 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 16 Sep 2023 13:10:56 +0200 Subject: [PATCH 135/143] Make AWAY and away-notify tests stricter (#222) * Make AWAY and away-notify tests stricter * Check AWAY is not echoed on JOIN --- irctest/server_tests/away.py | 16 +++++++++----- irctest/server_tests/away_notify.py | 33 +++++++++++++++++++++++------ 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/irctest/server_tests/away.py b/irctest/server_tests/away.py index d8cd059..36841d6 100644 --- a/irctest/server_tests/away.py +++ b/irctest/server_tests/away.py @@ -11,13 +11,14 @@ from irctest.numerics import ( RPL_USERHOST, RPL_WHOISUSER, ) -from irctest.patma import StrRe +from irctest.patma import ANYSTR, StrRe class AwayTestCase(cases.BaseServerTestCase): @cases.mark_specifications("RFC2812", "Modern") def testAway(self): self.connectClient("bar") + self.getMessages(1) self.sendLine(1, "AWAY :I'm not here right now") replies = self.getMessages(1) self.assertIn(RPL_NOWAWAY, [msg.command for msg in replies]) @@ -29,6 +30,7 @@ class AwayTestCase(cases.BaseServerTestCase): command=RPL_AWAY, params=["qux", "bar", "I'm not here right now"], ) + self.getMessages(1) self.sendLine(1, "AWAY") replies = self.getMessages(1) @@ -47,12 +49,16 @@ class AwayTestCase(cases.BaseServerTestCase): """ self.connectClient("bar") self.sendLine(1, "AWAY :I'm not here right now") - replies = self.getMessages(1) - self.assertIn(RPL_NOWAWAY, [msg.command for msg in replies]) + self.assertMessageMatch( + self.getMessage(1), command=RPL_NOWAWAY, params=["bar", ANYSTR] + ) + self.assertEqual(self.getMessages(1), []) self.sendLine(1, "AWAY") - replies = self.getMessages(1) - self.assertIn(RPL_UNAWAY, [msg.command for msg in replies]) + self.assertMessageMatch( + self.getMessage(1), command=RPL_UNAWAY, params=["bar", ANYSTR] + ) + self.assertEqual(self.getMessages(1), []) @cases.mark_specifications("Modern") def testAwayPrivmsg(self): diff --git a/irctest/server_tests/away_notify.py b/irctest/server_tests/away_notify.py index 3e9034e..0efd021 100644 --- a/irctest/server_tests/away_notify.py +++ b/irctest/server_tests/away_notify.py @@ -3,6 +3,8 @@ """ from irctest import cases +from irctest.numerics import RPL_NOWAWAY, RPL_UNAWAY +from irctest.patma import ANYSTR, StrRe class AwayNotifyTestCase(cases.BaseServerTestCase): @@ -20,13 +22,28 @@ class AwayNotifyTestCase(cases.BaseServerTestCase): self.getMessages(1) self.sendLine(2, "AWAY :i'm going away") - self.getMessages(2) + self.assertMessageMatch( + self.getMessage(2), command=RPL_NOWAWAY, params=["bar", ANYSTR] + ) + self.assertEqual(self.getMessages(2), []) awayNotify = self.getMessage(1) - self.assertMessageMatch(awayNotify, command="AWAY", params=["i'm going away"]) - self.assertTrue( - awayNotify.prefix.startswith("bar!"), - "Unexpected away-notify source: %s" % (awayNotify.prefix,), + self.assertMessageMatch( + awayNotify, + prefix=StrRe("bar!.*"), + command="AWAY", + params=["i'm going away"], + ) + + self.sendLine(2, "AWAY") + self.assertMessageMatch( + self.getMessage(2), command=RPL_UNAWAY, params=["bar", ANYSTR] + ) + self.assertEqual(self.getMessages(2), []) + + awayNotify = self.getMessage(1) + self.assertMessageMatch( + awayNotify, prefix=StrRe("bar!.*"), command="AWAY", params=[] ) @cases.mark_capabilities("away-notify") @@ -45,7 +62,11 @@ class AwayNotifyTestCase(cases.BaseServerTestCase): self.getMessages(2) self.joinChannel(2, "#chan") - self.getMessages(2) + self.assertNotIn( + "AWAY", + [m.command for m in self.getMessages(2)], + "joining user got their own away status when they joined", + ) messages = [msg for msg in self.getMessages(1) if msg.command == "AWAY"] self.assertEqual( From 04d0c8000fde21584fcd4f05d8b794352fff28a5 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sat, 16 Sep 2023 22:56:13 +0200 Subject: [PATCH 136/143] Test TOPIC is echoed on change (#230) * Test TOPIC is echoed on change * black --- irctest/server_tests/topic.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/irctest/server_tests/topic.py b/irctest/server_tests/topic.py index 9489571..d001fe6 100644 --- a/irctest/server_tests/topic.py +++ b/irctest/server_tests/topic.py @@ -11,13 +11,29 @@ from irctest.numerics import ERR_CHANOPRIVSNEEDED, RPL_NOTOPIC, RPL_TOPIC, RPL_T class TopicTestCase(cases.BaseServerTestCase): @cases.mark_specifications("RFC1459", "RFC2812") - def testTopic(self): + def testTopicRfc(self): """“Once a user has joined a channel, he receives information about all commands his server receives affecting the channel. This includes […] TOPIC” -- and """ + self._testTopic(assert_echo=False) + + @cases.mark_specifications("Modern") + def testTopicModern(self): + """ "If the topic of a channel is changed or cleared, every client in that + channel (including the author of the topic change) will receive a TOPIC command + with the new topic as argument (or an empty argument if the topic was cleared) + alerting them to how the topic has changed. + + Clients joining the channel in the future will receive a RPL_TOPIC numeric (or + lack thereof) accordingly." + -- https://modern.ircdocs.horse/#topic-message + """ + self._testTopic(assert_echo=True) + + def _testTopic(self, assert_echo: bool): self.connectClient("foo") self.joinChannel(1, "#chan") @@ -41,6 +57,7 @@ class TopicTestCase(cases.BaseServerTestCase): ) self.assertMessageMatch(m, command="TOPIC") except client_mock.NoMessageException: + self.assertFalse(assert_echo, "TOPIC was not echoed back to the author") # The RFCs do not say TOPIC must be echoed pass m = self.getMessage(2) From 3692f2d79dc7fbd7c90a47dc337a47ca90b44b70 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Mon, 18 Sep 2023 20:31:50 +0200 Subject: [PATCH 137/143] Add various validation tests (#221) * Add various validation tests * skip UTF8ONLY tests on servers that don't support it * Fixes for Ergo * Fixes for Nefarious and ircu2 * xfail for irc2 and workaround for ngIRCd * Bump ngIRCd to the ERR_NOTEXTTOSEND fix --- .github/workflows/test-stable.yml | 2 +- irctest/server_tests/chmodes/key.py | 6 +- .../server_tests/connection_registration.py | 59 ++++++++++++++++++- irctest/server_tests/messages.py | 22 +++++++ irctest/server_tests/utf8.py | 35 +++++++++++ workflows.yml | 2 +- 6 files changed, 121 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 024115a..67334e4 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -233,7 +233,7 @@ jobs: uses: actions/checkout@v3 with: path: ngircd - ref: rel-26.1 + ref: 0714466af88d71d6c395629cd7fb624b099507d4 repository: ngircd/ngircd - name: Build ngircd run: | diff --git a/irctest/server_tests/chmodes/key.py b/irctest/server_tests/chmodes/key.py index fd4c008..26d60da 100644 --- a/irctest/server_tests/chmodes/key.py +++ b/irctest/server_tests/chmodes/key.py @@ -44,8 +44,8 @@ class KeyTestCase(cases.BaseServerTestCase): @pytest.mark.parametrize( "key", - ["passphrase with spaces", "long" * 100, ""], - ids=["spaces", "long", "empty"], + ["passphrase with spaces", "long" * 100, "", " "], + ids=["spaces", "long", "empty", "only-space"], ) @cases.mark_specifications("RFC2812", "Modern") def testKeyValidation(self, key): @@ -84,6 +84,8 @@ class KeyTestCase(cases.BaseServerTestCase): "ngIRCd does not validate channel keys: " "https://github.com/ngircd/ngircd/issues/290" ) + if key == " " and self.controller.software_name == "irc2": + pytest.xfail("irc2 rewrites non-empty keys that contain only spaces") self.connectClient("bar") self.joinChannel(1, "#chan") diff --git a/irctest/server_tests/connection_registration.py b/irctest/server_tests/connection_registration.py index 7399cb9..a3d5038 100644 --- a/irctest/server_tests/connection_registration.py +++ b/irctest/server_tests/connection_registration.py @@ -5,6 +5,8 @@ Tests section 4.1 of RFC 1459. TODO: cross-reference Modern and RFC 2812 too """ +import time + from irctest import cases from irctest.client_mock import ConnectionClosed from irctest.numerics import ERR_NEEDMOREPARAMS, ERR_PASSWDMISMATCH @@ -133,7 +135,7 @@ class ConnectionRegistrationTestCase(cases.BaseServerTestCase): self.assertNotEqual( m.command, "001", - "Received 001 after registering with the nick of a " "registered user.", + "Received 001 after registering with the nick of a registered user.", ) def testEarlyNickCollision(self): @@ -206,3 +208,58 @@ class ConnectionRegistrationTestCase(cases.BaseServerTestCase): command=ERR_NEEDMOREPARAMS, params=[StrRe(r"(\*|foo)"), "USER", ANYSTR], ) + + def testNonutf8Realname(self): + self.addClient() + self.sendLine(1, "NICK foo") + line = b"USER username * * :i\xe8rc\xe9\r\n" + print("1 -> S (repr): " + repr(line)) + self.clients[1].conn.sendall(line) + for _ in range(10): + time.sleep(1) + d = self.clients[1].conn.recv(10000) + self.assertTrue(d, "Server closed connection") + print("S -> 1 (repr): " + repr(d)) + if b" 001 " in d: + break + if b"ERROR " in d or b" FAIL " in d: + # Rejected; nothing more to test. + return + for line in d.split(b"\r\n"): + if line.startswith(b"PING "): + line = line.replace(b"PING", b"PONG") + b"\r\n" + print("1 -> S (repr): " + repr(line)) + self.clients[1].conn.sendall(line) + else: + self.assertTrue(False, "stuck waiting") + self.sendLine(1, "WHOIS foo") + time.sleep(3) # for ngIRCd + d = self.clients[1].conn.recv(10000) + print("S -> 1 (repr): " + repr(d)) + self.assertIn(b"username", d) + + def testNonutf8Username(self): + self.addClient() + self.sendLine(1, "NICK foo") + self.sendLine(1, "USER 😊😊😊😊😊😊😊😊😊😊 * * :realname") + for _ in range(10): + time.sleep(1) + d = self.clients[1].conn.recv(10000) + self.assertTrue(d, "Server closed connection") + print("S -> 1 (repr): " + repr(d)) + if b" 001 " in d: + break + if b" 468" in d or b"ERROR " in d: + # Rejected; nothing more to test. + return + for line in d.split(b"\r\n"): + if line.startswith(b"PING "): + line = line.replace(b"PING", b"PONG") + b"\r\n" + print("1 -> S (repr): " + repr(line)) + self.clients[1].conn.sendall(line) + else: + self.assertTrue(False, "stuck waiting") + self.sendLine(1, "WHOIS foo") + d = self.clients[1].conn.recv(10000) + print("S -> 1 (repr): " + repr(d)) + self.assertIn(b"realname", d) diff --git a/irctest/server_tests/messages.py b/irctest/server_tests/messages.py index 441446d..c0f4ec0 100644 --- a/irctest/server_tests/messages.py +++ b/irctest/server_tests/messages.py @@ -53,6 +53,28 @@ class PrivmsgTestCase(cases.BaseServerTestCase): # ERR_NOSUCHNICK: 401 :No such nick self.assertMessageMatch(msg, command="401", params=["foo", "bar", ANYSTR]) + @cases.mark_specifications("RFC1459", "RFC2812", "Modern") + @cases.xfailIfSoftware( + ["irc2"], + "replies with ERR_NEEDMOREPARAMS instead of ERR_NOTEXTTOSEND", + ) + def testEmptyPrivmsg(self): + self.connectClient("foo") + self.sendLine(1, "JOIN #chan") + self.getMessages(1) # synchronize + self.connectClient("bar") + self.sendLine(2, "JOIN #chan") + self.getMessages(2) # synchronize + self.getMessages(1) # synchronize + self.sendLine(1, "PRIVMSG #chan :") + + self.assertMessageMatch( + self.getMessage(1), + command="412", # ERR_NOTEXTTOSEND + params=["foo", ANYSTR], + ) + self.assertEqual(self.getMessages(2), []) + class NoticeTestCase(cases.BaseServerTestCase): @cases.mark_specifications("RFC1459", "RFC2812") diff --git a/irctest/server_tests/utf8.py b/irctest/server_tests/utf8.py index bb5e77d..12ac129 100644 --- a/irctest/server_tests/utf8.py +++ b/irctest/server_tests/utf8.py @@ -46,3 +46,38 @@ class Utf8TestCase(cases.BaseServerTestCase): if m.command in ("FAIL", "WARN"): self.assertMessageMatch(m, params=["PRIVMSG", "INVALID_UTF8", ANYSTR]) + + def testNonutf8Realname(self): + self.connectClient("foo") + if "UTF8ONLY" not in self.server_support: + raise runner.IsupportTokenNotSupported("UTF8ONLY") + + self.addClient() + self.sendLine(2, "NICK foo") + self.clients[2].conn.sendall(b"USER username * * :i\xe8rc\xe9\r\n") + + d = self.clients[2].conn.recv(1024) + if b" FAIL " in d or b" 468 " in d: # ERR_INVALIDUSERNAME + return # nothing more to test + self.assertIn(b" 001 ", d) + + self.sendLine(2, "WHOIS foo") + self.getMessages(2) + + def testNonutf8Username(self): + self.connectClient("foo") + if "UTF8ONLY" not in self.server_support: + raise runner.IsupportTokenNotSupported("UTF8ONLY") + + self.addClient() + self.sendLine(2, "NICK foo") + self.sendLine(2, "USER 😊😊😊😊😊😊😊😊😊😊 * * :realname") + m = self.getRegistrationMessage(2) + if m.command in ("FAIL", "468"): # ERR_INVALIDUSERNAME + return # nothing more to test + self.assertMessageMatch( + m, + command="001", + ) + self.sendLine(2, "WHOIS foo") + self.getMessages(2) diff --git a/workflows.yml b/workflows.yml index 8c187da..1033a19 100644 --- a/workflows.yml +++ b/workflows.yml @@ -235,7 +235,7 @@ software: name: ngircd repository: ngircd/ngircd refs: - stable: rel-26.1 + stable: 0714466af88d71d6c395629cd7fb624b099507d4 # two years ahead of rel-26.1 release: null devel: master devel_release: null From c3aa97c42826b1cc44f99ba6d37585486d78f458 Mon Sep 17 00:00:00 2001 From: Valentin Lorentz Date: Mon, 18 Sep 2023 22:32:01 +0200 Subject: [PATCH 138/143] Temporary disable daily Dlk tests They are too flaky and I can't debug them until the PHP 8 warnings are fixed. --- .github/workflows/test-stable.yml | 47 ------------------------------- workflows.yml | 4 +-- 2 files changed, 2 insertions(+), 49 deletions(-) diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 67334e4..00394b4 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -453,7 +453,6 @@ jobs: - test-unrealircd-5 - test-unrealircd-anope - test-unrealircd-atheme - - test-unrealircd-dlk runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 @@ -1334,52 +1333,6 @@ jobs: with: name: pytest-results_unrealircd-atheme_stable path: pytest.xml - test-unrealircd-dlk: - needs: - - build-unrealircd - runs-on: ubuntu-22.04 - steps: - - uses: actions/checkout@v3 - - name: Set up Python 3.11 - uses: actions/setup-python@v4 - with: - python-version: 3.11 - - name: Download build artefacts - uses: actions/download-artifact@v3 - with: - name: installed-unrealircd - path: '~' - - name: Unpack artefacts - run: cd ~; find -name 'artefacts-*.tar.gz' -exec tar -xzf '{}' \; - - name: Checkout Dlk - uses: actions/checkout@v3 - with: - path: Dlk-Services - ref: 6db51ea03f039c48fd20427c04cec8ff98df7878 - repository: DalekIRC/Dalek-Services - - name: Build Dlk - run: | - pip install pifpaf - wget -q https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar - wget -q https://wordpress.org/latest.zip -O wordpress-latest.zip - - name: Install system dependencies - run: sudo apt-get install atheme-services faketime - - name: Install irctest dependencies - run: |- - python -m pip install --upgrade pip - pip install pytest pytest-xdist -r requirements.txt - - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH - IRCTEST_DLK_PATH="${{ github.workspace }}/Dlk-Services" IRCTEST_WP_CLI_PATH="${{ - github.workspace }}/wp-cli.phar" IRCTEST_WP_ZIP_PATH="${{ github.workspace - }}/wordpress-latest.zip" make unrealircd-dlk - timeout-minutes: 30 - - if: always() - name: Publish results - uses: actions/upload-artifact@v3 - with: - name: pytest-results_unrealircd-dlk_stable - path: pytest.xml name: irctest with stable versions 'on': pull_request: null diff --git a/workflows.yml b/workflows.yml index 1033a19..87db640 100644 --- a/workflows.yml +++ b/workflows.yml @@ -337,8 +337,8 @@ software: separate_build_job: false path: Dlk-Services refs: - stable: &dlk_stable "6db51ea03f039c48fd20427c04cec8ff98df7878" - release: *dlk_stable + stable: null # disabled because flaky, and hard to debug with all the PHP 8 warnings + release: &dlk_stable "6db51ea03f039c48fd20427c04cec8ff98df7878" devel: "main" devel_release: *dlk_stable build_script: | From e1ff9fd7feab5a49482a226299ff2b8ddd77686b Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Tue, 19 Sep 2023 23:24:26 -0700 Subject: [PATCH 139/143] move no-CTCP channel mode test (#232) --- irctest/server_tests/chmodes/no_ctcp.py | 31 +++++++++++++++++++++++++ irctest/server_tests/quit.py | 29 ----------------------- 2 files changed, 31 insertions(+), 29 deletions(-) create mode 100644 irctest/server_tests/chmodes/no_ctcp.py diff --git a/irctest/server_tests/chmodes/no_ctcp.py b/irctest/server_tests/chmodes/no_ctcp.py new file mode 100644 index 0000000..d01cadb --- /dev/null +++ b/irctest/server_tests/chmodes/no_ctcp.py @@ -0,0 +1,31 @@ +from irctest import cases +from irctest.numerics import ERR_CANNOTSENDTOCHAN + + +class NoCTCPChannelModeTestCase(cases.BaseServerTestCase): + @cases.mark_specifications("Ergo") + def testNoCTCPChannelMode(self): + """Test Ergo's +C channel mode that blocks CTCPs.""" + self.connectClient("bar") + self.joinChannel(1, "#chan") + self.sendLine(1, "MODE #chan +C") + self.getMessages(1) + + self.connectClient("qux") + self.joinChannel(2, "#chan") + self.getMessages(2) + + self.sendLine(1, "PRIVMSG #chan :\x01ACTION hi\x01") + self.getMessages(1) + ms = self.getMessages(2) + self.assertEqual(len(ms), 1) + self.assertMessageMatch( + ms[0], command="PRIVMSG", params=["#chan", "\x01ACTION hi\x01"] + ) + + self.sendLine(1, "PRIVMSG #chan :\x01PING 1473523796 918320\x01") + ms = self.getMessages(1) + self.assertEqual(len(ms), 1) + self.assertMessageMatch(ms[0], command=ERR_CANNOTSENDTOCHAN) + ms = self.getMessages(2) + self.assertEqual(ms, []) diff --git a/irctest/server_tests/quit.py b/irctest/server_tests/quit.py index a62c123..10069bd 100644 --- a/irctest/server_tests/quit.py +++ b/irctest/server_tests/quit.py @@ -10,7 +10,6 @@ TODO: cross-reference RFC 1459 and Modern import time from irctest import cases -from irctest.numerics import ERR_CANNOTSENDTOCHAN from irctest.patma import StrRe @@ -40,31 +39,3 @@ class ChannelQuitTestCase(cases.BaseServerTestCase): m = self.getMessage(1) self.assertMessageMatch(m, command="QUIT", params=[StrRe(".*qux out.*")]) self.assertTrue(m.prefix.startswith("qux")) # nickmask of quitter - - -class NoCTCPTestCase(cases.BaseServerTestCase): - @cases.mark_specifications("Ergo") - def testQuit(self): - self.connectClient("bar") - self.joinChannel(1, "#chan") - self.sendLine(1, "MODE #chan +C") - self.getMessages(1) - - self.connectClient("qux") - self.joinChannel(2, "#chan") - self.getMessages(2) - - self.sendLine(1, "PRIVMSG #chan :\x01ACTION hi\x01") - self.getMessages(1) - ms = self.getMessages(2) - self.assertEqual(len(ms), 1) - self.assertMessageMatch( - ms[0], command="PRIVMSG", params=["#chan", "\x01ACTION hi\x01"] - ) - - self.sendLine(1, "PRIVMSG #chan :\x01PING 1473523796 918320\x01") - ms = self.getMessages(1) - self.assertEqual(len(ms), 1) - self.assertMessageMatch(ms[0], command=ERR_CANNOTSENDTOCHAN) - ms = self.getMessages(2) - self.assertEqual(ms, []) From 805635c839738dd6963182e57c2a31a15e36c0c1 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Thu, 21 Sep 2023 09:18:23 +0200 Subject: [PATCH 140/143] Add Sable (#229) * [WIP] Add support for Sable * tweak sable controller * echo_message: Add missing synchronization for Sable * update sable * whois: Simplify test * WHO: Remove test for oper flag from testWhoChan So it won't fail on Sable, which hides oper status * WHO: Skip/xfail tests for Sable as needed * Skip NakWhole when multi-prefix is not supported * [WIP] Run Sable on CI * working-directory is not setable on actions * this isn't ergo * this really isn't ergo * minimize rust install and cache cargo deps * Need to specify packages to install... * Phony target * Give up on 'cargo install', it seems to ignore the cache * try again to cache the target dir * This isn't Solanum * Comment out BaseServicesController * Parallelize Sable tests * target is relative... * sigh * Fix prefix * Re-add the other software * chathistory: Test TOPIC is not sent unless event-playable is enabled * sable: Dynamically generate certificates This allows using custom server/services names * sable: Enable services * sable: Add support for account registration Sable doesn't support REGISTER via NickServ * sable: Lower log verbosity * Fix lint * Re-add Sable to CI * Fix/skip tests on Sable * Kill sable_services' subprocesses * Bump Sable to include the labeled-response fix * Bump Sable to the channel-rename downgrade fix --- .github/workflows/test-devel.yml | 70 +++- .github/workflows/test-devel_release.yml | 6 +- .github/workflows/test-stable.yml | 70 +++- Makefile | 18 +- irctest/basecontrollers.py | 8 + irctest/cases.py | 4 + irctest/controllers/ergo.py | 3 - irctest/controllers/sable.py | 481 +++++++++++++++++++++++ irctest/irc_utils/junkdrawer.py | 2 +- irctest/server_tests/cap.py | 19 +- irctest/server_tests/chathistory.py | 65 ++- irctest/server_tests/echo_message.py | 3 + irctest/server_tests/messages.py | 1 + irctest/server_tests/who.py | 35 +- irctest/server_tests/whois.py | 20 +- make_workflows.py | 1 + workflows.yml | 31 ++ 17 files changed, 768 insertions(+), 69 deletions(-) create mode 100644 irctest/controllers/sable.py diff --git a/.github/workflows/test-devel.yml b/.github/workflows/test-devel.yml index 89574f8..088e93d 100644 --- a/.github/workflows/test-devel.yml +++ b/.github/workflows/test-devel.yml @@ -402,6 +402,7 @@ jobs: - test-ngircd-anope - test-ngircd-atheme - test-plexus4 + - test-sable - test-solanum - test-sopel - test-thelounge @@ -570,7 +571,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/go/sbin:~/go/bin:$PATH + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/go/sbin:~/go/bin:~/go:$PATH make ergo timeout-minutes: 30 - if: always() @@ -642,7 +643,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH make inspircd timeout-minutes: 30 - if: always() @@ -681,7 +682,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH make + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH make inspircd-anope timeout-minutes: 30 - if: always() @@ -819,7 +820,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:~/.local/:$PATH make ngircd timeout-minutes: 30 - if: always() @@ -858,7 +859,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH make + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:~/.local/:$PATH make ngircd-anope timeout-minutes: 30 - if: always() @@ -891,7 +892,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:~/.local/:$PATH make ngircd-atheme timeout-minutes: 30 - if: always() @@ -939,6 +940,53 @@ jobs: with: name: pytest-results_plexus4_devel path: pytest.xml + test-sable: + needs: [] + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.11 + uses: actions/setup-python@v4 + with: + python-version: 3.11 + - name: Checkout Sable + uses: actions/checkout@v3 + with: + path: sable + ref: master + repository: Libera-Chat/sable + - name: Install rust toolchain + uses: actions-rs/toolchain@v1 + with: + override: true + profile: minimal + toolchain: nightly + - name: Enable Cargo cache + uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + workspaces: sable -> target + - run: rustc --version + - name: Build Sable + run: | + cd $GITHUB_WORKSPACE/sable/ + cargo build + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime + - name: Install irctest dependencies + run: |- + python -m pip install --upgrade pip + pip install pytest pytest-xdist -r requirements.txt + - name: Test with pytest + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=$GITHUB_WORKSPACE/sable/target/debug/sbin:$GITHUB_WORKSPACE/sable/target/debug/bin:$GITHUB_WORKSPACE/sable/target/debug:$PATH + make sable + timeout-minutes: 30 + - if: always() + name: Publish results + uses: actions/upload-artifact@v3 + with: + name: pytest-results_sable_devel + path: pytest.xml test-solanum: needs: - build-solanum @@ -1061,7 +1109,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:~/.local/unrealircd:$PATH make unrealircd timeout-minutes: 30 - if: always() @@ -1094,7 +1142,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:~/.local/unrealircd:$PATH make unrealircd-5 timeout-minutes: 30 - if: always() @@ -1133,7 +1181,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH make + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:~/.local/unrealircd:$PATH make unrealircd-anope timeout-minutes: 30 - if: always() @@ -1166,7 +1214,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:~/.local/unrealircd:$PATH make unrealircd-atheme timeout-minutes: 30 - if: always() @@ -1210,7 +1258,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:~/.local/unrealircd:$PATH IRCTEST_DLK_PATH="${{ github.workspace }}/Dlk-Services" IRCTEST_WP_CLI_PATH="${{ github.workspace }}/wp-cli.phar" IRCTEST_WP_ZIP_PATH="${{ github.workspace }}/wordpress-latest.zip" make unrealircd-dlk diff --git a/.github/workflows/test-devel_release.yml b/.github/workflows/test-devel_release.yml index 87e85c7..dcff7fc 100644 --- a/.github/workflows/test-devel_release.yml +++ b/.github/workflows/test-devel_release.yml @@ -132,7 +132,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH make inspircd timeout-minutes: 30 - if: always() @@ -171,7 +171,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH make + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH make inspircd-anope timeout-minutes: 30 - if: always() @@ -204,7 +204,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH make inspircd-atheme timeout-minutes: 30 - if: always() diff --git a/.github/workflows/test-stable.yml b/.github/workflows/test-stable.yml index 00394b4..b8c5f1d 100644 --- a/.github/workflows/test-stable.yml +++ b/.github/workflows/test-stable.yml @@ -446,6 +446,7 @@ jobs: - test-ngircd-anope - test-ngircd-atheme - test-plexus4 + - test-sable - test-solanum - test-sopel - test-thelounge @@ -646,7 +647,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/go/sbin:~/go/bin:$PATH + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/go/sbin:~/go/bin:~/go:$PATH make ergo timeout-minutes: 30 - if: always() @@ -718,7 +719,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH make inspircd timeout-minutes: 30 - if: always() @@ -757,7 +758,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH make + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH make inspircd-anope timeout-minutes: 30 - if: always() @@ -790,7 +791,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:$PATH + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/inspircd/sbin:~/.local/inspircd/bin:~/.local/inspircd:$PATH make inspircd-atheme timeout-minutes: 30 - if: always() @@ -977,7 +978,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:~/.local/:$PATH make ngircd timeout-minutes: 30 - if: always() @@ -1016,7 +1017,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH make + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:~/.local/:$PATH make ngircd-anope timeout-minutes: 30 - if: always() @@ -1049,7 +1050,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:$PATH + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local//sbin:~/.local//bin:~/.local/:$PATH make ngircd-atheme timeout-minutes: 30 - if: always() @@ -1097,6 +1098,53 @@ jobs: with: name: pytest-results_plexus4_stable path: pytest.xml + test-sable: + needs: [] + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v3 + - name: Set up Python 3.11 + uses: actions/setup-python@v4 + with: + python-version: 3.11 + - name: Checkout Sable + uses: actions/checkout@v3 + with: + path: sable + ref: ff1179512a79eba57ca468a5f83af84ecce08a5b + repository: Libera-Chat/sable + - name: Install rust toolchain + uses: actions-rs/toolchain@v1 + with: + override: true + profile: minimal + toolchain: nightly + - name: Enable Cargo cache + uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + workspaces: sable -> target + - run: rustc --version + - name: Build Sable + run: | + cd $GITHUB_WORKSPACE/sable/ + cargo build + - name: Install system dependencies + run: sudo apt-get install atheme-services faketime + - name: Install irctest dependencies + run: |- + python -m pip install --upgrade pip + pip install pytest pytest-xdist -r requirements.txt + - name: Test with pytest + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=$GITHUB_WORKSPACE/sable/target/debug/sbin:$GITHUB_WORKSPACE/sable/target/debug/bin:$GITHUB_WORKSPACE/sable/target/debug:$PATH + make sable + timeout-minutes: 30 + - if: always() + name: Publish results + uses: actions/upload-artifact@v3 + with: + name: pytest-results_sable_stable + path: pytest.xml test-solanum: needs: - build-solanum @@ -1219,7 +1267,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:~/.local/unrealircd:$PATH make unrealircd timeout-minutes: 30 - if: always() @@ -1252,7 +1300,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:~/.local/unrealircd:$PATH make unrealircd-5 timeout-minutes: 30 - if: always() @@ -1291,7 +1339,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH make + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:~/.local/unrealircd:$PATH make unrealircd-anope timeout-minutes: 30 - if: always() @@ -1324,7 +1372,7 @@ jobs: python -m pip install --upgrade pip pip install pytest pytest-xdist -r requirements.txt - name: Test with pytest - run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:$PATH + run: PYTEST_ARGS='--junit-xml pytest.xml' PATH=$HOME/.local/bin:$PATH PATH=~/.local/unrealircd/sbin:~/.local/unrealircd/bin:~/.local/unrealircd:$PATH make unrealircd-atheme timeout-minutes: 30 - if: always() diff --git a/Makefile b/Makefile index bd68631..1876c51 100644 --- a/Makefile +++ b/Makefile @@ -87,6 +87,13 @@ LIMNORIA_SELECTORS := \ (foo or not foo) \ $(EXTRA_SELECTORS) +SABLE_SELECTORS := \ + not Ergo \ + and not deprecated \ + and not strict \ + and not whowas and not list and not lusers and not userhost and not time and not info \ + $(EXTRA_SELECTORS) + SOLANUM_SELECTORS := \ not Ergo \ and not deprecated \ @@ -118,9 +125,9 @@ UNREALIRCD_SELECTORS := \ and not private_chathistory \ $(EXTRA_SELECTORS) -.PHONY: all flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sopel solanum unrealircd +.PHONY: all flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sable sopel solanum unrealircd -all: flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sopel solanum unrealircd +all: flakes bahamut charybdis ergo inspircd ircu2 snircd irc2 mammon nefarious limnoria sable sopel solanum unrealircd flakes: find irctest/ -name "*.py" -not -path "irctest/scram/*" -print0 | xargs -0 pyflakes3 @@ -249,6 +256,13 @@ ngircd-atheme: -m 'services' \ -k "$(NGIRCD_SELECTORS)" +sable: + $(PYTEST) $(PYTEST_ARGS) \ + --controller=irctest.controllers.sable \ + -n 20 \ + -m 'not services' \ + -k '$(SABLE_SELECTORS)' + solanum: $(PYTEST) $(PYTEST_ARGS) \ --controller=irctest.controllers.solanum \ diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index 0652fa2..b70015e 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -68,6 +68,7 @@ class _BaseController: supports_sts: bool supported_sasl_mechanisms: Set[str] + proc: Optional[subprocess.Popen] _used_ports: Set[Tuple[str, int]] @@ -248,6 +249,12 @@ class BaseServerController(_BaseController): extban_mute_char: Optional[str] = None """Character used for the 'mute' extban""" nickserv = "NickServ" + sync_sleep_time = 0.0 + """How many seconds to sleep before clients synchronously get messages. + + This can be 0 for servers answering all commands in order (all but Sable as of + this writing), as irctest emits a PING, waits for a PONG, and captures all messages + between the two.""" def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) @@ -350,6 +357,7 @@ class BaseServicesController(_BaseController): c.connect(self.server_controller.hostname, self.server_controller.port) c.sendLine("NICK chkNS") c.sendLine("USER chk chk chk chk") + time.sleep(self.server_controller.sync_sleep_time) for msg in c.getMessages(synchronize=False): if msg.command == "PING": # Hi Unreal diff --git a/irctest/cases.py b/irctest/cases.py index 4ddf07f..eea0f8c 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -585,9 +585,13 @@ class BaseServerTestCase( del self.clients[name] def getMessages(self, client: TClientName, **kwargs: Any) -> List[Message]: + if kwargs.get("synchronize", True): + time.sleep(self.controller.sync_sleep_time) return self.clients[client].getMessages(**kwargs) def getMessage(self, client: TClientName, **kwargs: Any) -> Message: + if kwargs.get("synchronize", True): + time.sleep(self.controller.sync_sleep_time) return self.clients[client].getMessage(**kwargs) def getRegistrationMessage(self, client: TClientName) -> Message: diff --git a/irctest/controllers/ergo.py b/irctest/controllers/ergo.py index 5655e82..7a75c87 100644 --- a/irctest/controllers/ergo.py +++ b/irctest/controllers/ergo.py @@ -211,9 +211,6 @@ class ErgoController(BaseServerController, DirectoryBasedController): username: str, password: Optional[str] = None, ) -> None: - # XXX: Move this somewhere else when - # https://github.com/ircv3/ircv3-specifications/pull/152 becomes - # part of the specification if not case.run_services: # Ergo does not actually need this, but other controllers do, so we # are checking it here as well for tests that aren't tested with other diff --git a/irctest/controllers/sable.py b/irctest/controllers/sable.py new file mode 100644 index 0000000..cdd07ba --- /dev/null +++ b/irctest/controllers/sable.py @@ -0,0 +1,481 @@ +import os +from pathlib import Path +import shutil +import signal +import subprocess +import tempfile +import time +from typing import Optional, Type + +from irctest.basecontrollers import ( + BaseServerController, + BaseServicesController, + DirectoryBasedController, + NotImplementedByController, +) +from irctest.cases import BaseServerTestCase +from irctest.exceptions import NoMessageException +from irctest.patma import ANYSTR + +GEN_CERTS = """ +mkdir -p useless_openssl_data/ + +cat > openssl.cnf < useless_openssl_data/serial + +# Generate CA +openssl req -x509 -nodes -newkey rsa:2048 -batch \ + -subj "/CN=Test CA" \ + -outform PEM -out ca_cert.pem \ + -keyout ca_cert.key + +for server in $*; do + openssl genrsa -traditional \ + -out $server.key \ + 2048 + openssl req -nodes -batch -new \ + -addext "subjectAltName = DNS:$server" \ + -key $server.key \ + -outform PEM -out server_$server.req + openssl ca -config openssl.cnf -days 3650 -md sha512 -batch \ + -subj /CN=$server \ + -keyfile ca_cert.key -cert ca_cert.pem \ + -in server_$server.req \ + -out $server.pem + openssl x509 -sha1 -in $server.pem -fingerprint -noout \ + | sed "s/.*=//" | sed "s/://g" | tr '[:upper:]' '[:lower:]' > $server.pem.sha1 +done + +rm -r useless_openssl_data/ +""" + +_certs_dir = None + + +def certs_dir() -> Path: + global _certs_dir + if _certs_dir is None: + certs_dir = tempfile.TemporaryDirectory() + (Path(certs_dir.name) / "gen_certs.sh").write_text(GEN_CERTS) + subprocess.run( + ["bash", "gen_certs.sh", "My.Little.Server", "My.Little.Services"], + cwd=certs_dir.name, + check=True, + ) + _certs_dir = certs_dir + return Path(_certs_dir.name) + + +NETWORK_CONFIG = """ +{ + "fanout": 1, + "ca_file": "%(certs_dir)s/ca_cert.pem", + + "peers": [ + { "name": "My.Little.Services", "address": "%(services_hostname)s:%(services_port)s", "fingerprint": "%(services_cert_sha1)s" }, + { "name": "My.Little.Server", "address": "%(server1_hostname)s:%(server1_port)s", "fingerprint": "%(server1_cert_sha1)s" } + ] +} +""" + +NETWORK_CONFIG_CONFIG = """ +{ + "opers": [ + { + "name": "operuser", + // echo -n "operpassword" | openssl passwd -6 -stdin + "hash": "$6$z5yA.OfGliDoi/R2$BgSsguS6bxAsPSCygDisgDw5JZuo5.88eU3Hyc7/4OaNpeKIxWGjOggeHzOl0xLiZg1vfwxXjOTFN14wG5vNI." + } + ], + + "alias_users": [ + { + "nick": "ChanServ", + "user": "ChanServ", + "host": "services.", + "realname": "Channel services compatibility layer", + "command_alias": "CS" + }, + { + "nick": "NickServ", + "user": "NickServ", + "host": "services.", + "realname": "Account services compatibility layer", + "command_alias": "NS" + } + ], + + "default_roles": { + "builtin:op": [ + "always_send", + "op_self", "op_grant", "voice_self", "voice_grant", + "receive_op", "receive_voice", "receive_opmod", + "topic", "kick", "set_simple_mode", "set_key", + "rename", + "ban_view", "ban_add", "ban_remove_any", + "quiet_view", "quiet_add", "quiet_remove_any", + "exempt_view", "exempt_add", "exempt_remove_any", + "invite_self", "invite_other", + "invex_view", "invex_add", "invex_remove_any" + ], + "builtin:voice": [ + "always_send", + "voice_self", + "receive_voice", + "ban_view", "quiet_view" + ], + "builtin:all": [ + "ban_view", "quiet_view" + ] + }, + + "debug_mode": true +} +""" + +SERVER_CONFIG = """ +{ + "server_id": 1, + "server_name": "My.Little.Server", + + "management": { + "address": "%(server1_management_hostname)s:%(server1_management_port)s", + "client_ca": "%(certs_dir)s/ca_cert.pem", + "authorised_fingerprints": [ + { "name": "user1", "fingerprint": "435bc6db9f22e84ba5d9652432154617c9509370" }, + ], + }, + + "server": { + "listeners": [ + { "address": "%(c2s_hostname)s:%(c2s_port)s" }, + ], + }, + + "event_log": { + "event_expiry": 300, // five minutes, for local testing + }, + + "tls_config": { + "key_file": "%(certs_dir)s/My.Little.Server.key", + "cert_file": "%(certs_dir)s/My.Little.Server.pem", + }, + + "node_config": { + "listen_addr": "%(server1_hostname)s:%(server1_port)s", + "cert_file": "%(certs_dir)s/My.Little.Server.pem", + "key_file": "%(certs_dir)s/My.Little.Server.key", + }, + + "log": { + "dir": "log/server1/", + + "module-levels": { + "": "debug", + "sable_ircd": "trace", + }, + + "targets": [ + { + "target": "stdout", + "level": "trace", + "modules": [ "sable", "audit", "client_listener" ], + }, + ], + }, +} +""" + +SERVICES_CONFIG = """ +{ + "server_id": 99, + "server_name": "My.Little.Services", + + "management": { + "address": "%(services_management_hostname)s:%(services_management_port)s", + "client_ca": "%(certs_dir)s/ca_cert.pem", + "authorised_fingerprints": [ + { "name": "user1", "fingerprint": "435bc6db9f22e84ba5d9652432154617c9509370" } + ] + }, + + "server": { + "database": "test_database.json", + "default_roles": { + "builtin:founder": [ + "founder", "access_view", "access_edit", "role_view", "role_edit", + "op_self", "op_grant", + "voice_self", "voice_grant", + "always_send", + "invite_self", "invite_other", + "receive_op", "receive_voice", "receive_opmod", + "topic", "kick", "set_simple_mode", "set_key", + "rename", + "ban_view", "ban_add", "ban_remove_any", + "quiet_view", "quiet_add", "quiet_remove_any", + "exempt_view", "exempt_add", "exempt_remove_any", + "invex_view", "invex_add", "invex_remove_any" + ], + "builtin:op": [ + "always_send", + "receive_op", "receive_voice", "receive_opmod", + "topic", "kick", "set_simple_mode", "set_key", + "rename", + "ban_view", "ban_add", "ban_remove_any", + "quiet_view", "quiet_add", "quiet_remove_any", + "exempt_view", "exempt_add", "exempt_remove_any", + "invex_view", "invex_add", "invex_remove_any" + ], + "builtin:voice": [ + "always_send", "voice_self", "receive_voice" + ] + } + }, + + "event_log": { + "event_expiry": 300, // five minutes, for local testing + }, + + "tls_config": { + "key_file": "%(certs_dir)s/My.Little.Services.key", + "cert_file": "%(certs_dir)s/My.Little.Services.pem" + }, + + "node_config": { + "listen_addr": "%(services_hostname)s:%(services_port)s", + "cert_file": "%(certs_dir)s/My.Little.Services.pem", + "key_file": "%(certs_dir)s/My.Little.Services.key" + }, + + "log": { + "dir": "log/services/", + + "module-levels": { + "": "debug" + }, + + "targets": [ + { + "target": "stdout", + "level": "debug", + "modules": [ "sable_services" ] + } + ] + } +} +""" + + +class SableController(BaseServerController, DirectoryBasedController): + software_name = "Sable" + supported_sasl_mechanisms = {"PLAIN"} + sync_sleep_time = 0.1 + """Sable processes commands very quickly, but responses for commands changing the + state may be sent after later commands for messages which don't.""" + + def run( + self, + hostname: str, + port: int, + *, + password: Optional[str], + ssl: bool, + run_services: bool, + faketime: Optional[str], + ) -> None: + if password is not None: + raise NotImplementedByController("PASS command") + if ssl: + raise NotImplementedByController("SSL") + assert self.proc is None + self.port = port + self.create_config() + + assert self.directory + + (self.directory / "configs").mkdir() + + c2s_hostname = hostname + c2s_port = port + del hostname, port + # base controller expects this to check for NickServ presence itself + self.hostname = c2s_hostname + self.port = c2s_port + + (server1_hostname, server1_port) = self.get_hostname_and_port() + (services_hostname, services_port) = self.get_hostname_and_port() + + # Sable requires inbound connections to match the configured hostname, + # so we can't configure 0.0.0.0 + server1_hostname = services_hostname = "127.0.0.1" + + ( + server1_management_hostname, + server1_management_port, + ) = self.get_hostname_and_port() + ( + services_management_hostname, + services_management_port, + ) = self.get_hostname_and_port() + + self.template_vars = dict( + certs_dir=certs_dir(), + c2s_hostname=c2s_hostname, + c2s_port=c2s_port, + server1_hostname=server1_hostname, + server1_port=server1_port, + server1_cert_sha1=(certs_dir() / "My.Little.Server.pem.sha1") + .read_text() + .strip(), + server1_management_hostname=server1_management_hostname, + server1_management_port=server1_management_port, + services_hostname=services_hostname, + services_port=services_port, + services_cert_sha1=(certs_dir() / "My.Little.Services.pem.sha1") + .read_text() + .strip(), + services_management_hostname=services_management_hostname, + services_management_port=services_management_port, + ) + + with self.open_file("configs/network.conf") as fd: + fd.write(NETWORK_CONFIG % self.template_vars) + with self.open_file("configs/network_config.conf") as fd: + fd.write(NETWORK_CONFIG_CONFIG % self.template_vars) + with self.open_file("configs/server1.conf") as fd: + fd.write(SERVER_CONFIG % self.template_vars) + + if faketime and shutil.which("faketime"): + faketime_cmd = ["faketime", "-f", faketime] + self.faketime_enabled = True + else: + faketime_cmd = [] + + self.proc = subprocess.Popen( + [ + *faketime_cmd, + "sable_ircd", + "--foreground", + "--server-conf", + self.directory / "configs/server1.conf", + "--network-conf", + self.directory / "configs/network.conf", + "--bootstrap-network", + self.directory / "configs/network_config.conf", + ], + cwd=self.directory, + preexec_fn=os.setsid, + ) + self.pgroup_id = os.getpgid(self.proc.pid) + + if run_services: + self.services_controller = SableServicesController(self.test_config, self) + self.services_controller.run( + protocol="sable", + server_hostname=services_hostname, + server_port=services_port, + ) + + def kill_proc(self) -> None: + os.killpg(self.pgroup_id, signal.SIGKILL) + super().kill_proc() + + def registerUser( + self, + case: BaseServerTestCase, # type: ignore + username: str, + password: Optional[str] = None, + ) -> None: + # XXX: Move this somewhere else when + # https://github.com/ircv3/ircv3-specifications/pull/152 becomes + # part of the specification + if not case.run_services: + raise ValueError( + "Attempted to register a nick, but `run_services` it not True." + ) + assert password + client = case.addClient(show_io=True) + case.sendLine(client, "NICK " + username) + case.sendLine(client, "USER r e g :user") + while case.getRegistrationMessage(client).command != "001": + pass + case.getMessages(client) + case.sendLine( + client, + f"REGISTER * * {password}", + ) + for _ in range(100): + time.sleep(0.1) + try: + msg = case.getMessage(client) + except NoMessageException: + continue + case.assertMessageMatch( + msg, command="REGISTER", params=["SUCCESS", username, ANYSTR] + ) + break + else: + raise NoMessageException() + case.sendLine(client, "QUIT") + case.assertDisconnected(client) + + +class SableServicesController(BaseServicesController): + server_controller: SableController + software_name = "Sable Services" + + def run(self, protocol: str, server_hostname: str, server_port: int) -> None: + assert protocol == "sable" + assert self.server_controller.directory is not None + + with self.server_controller.open_file("configs/services.conf") as fd: + fd.write(SERVICES_CONFIG % self.server_controller.template_vars) + + self.proc = subprocess.Popen( + [ + "sable_services", + "--foreground", + "--server-conf", + self.server_controller.directory / "configs/services.conf", + "--network-conf", + self.server_controller.directory / "configs/network.conf", + ], + cwd=self.server_controller.directory, + preexec_fn=os.setsid, + ) + self.pgroup_id = os.getpgid(self.proc.pid) + + def kill_proc(self) -> None: + os.killpg(self.pgroup_id, signal.SIGKILL) + super().kill_proc() + + +def get_irctest_controller_class() -> Type[SableController]: + return SableController diff --git a/irctest/irc_utils/junkdrawer.py b/irctest/irc_utils/junkdrawer.py index ecbf83f..b34de13 100644 --- a/irctest/irc_utils/junkdrawer.py +++ b/irctest/irc_utils/junkdrawer.py @@ -13,7 +13,7 @@ def ircv3_timestamp_to_unixtime(timestamp: str) -> float: def random_name(base: str) -> str: - return base + "-" + secrets.token_hex(8) + return base + "-" + secrets.token_hex(5) def find_hostname_and_port() -> Tuple[str, int]: diff --git a/irctest/server_tests/cap.py b/irctest/server_tests/cap.py index fea4ef0..a1d3dff 100644 --- a/irctest/server_tests/cap.py +++ b/irctest/server_tests/cap.py @@ -56,6 +56,10 @@ class CapTestCase(cases.BaseServerTestCase): ) @cases.mark_specifications("IRCv3") + @cases.xfailIfSoftware( + ["Sable"], + "does not support multi-prefix", + ) def testReqOne(self): """Tests requesting a single capability""" self.addClient(1) @@ -89,8 +93,8 @@ class CapTestCase(cases.BaseServerTestCase): @cases.mark_specifications("IRCv3") @cases.xfailIfSoftware( - ["ngIRCd"], - "ngIRCd does not support userhost-in-names", + ["ngIRCd", "Sable"], + "does not support userhost-in-names", ) def testReqTwo(self): """Tests requesting two capabilities at once""" @@ -131,8 +135,8 @@ class CapTestCase(cases.BaseServerTestCase): @cases.mark_specifications("IRCv3") @cases.xfailIfSoftware( - ["ngIRCd"], - "ngIRCd does not support userhost-in-names", + ["ngIRCd", "Sable"], + "does not support userhost-in-names", ) def testReqOneThenOne(self): """Tests requesting two capabilities in different messages""" @@ -183,8 +187,8 @@ class CapTestCase(cases.BaseServerTestCase): @cases.mark_specifications("IRCv3") @cases.xfailIfSoftware( - ["ngIRCd"], - "ngIRCd does not support userhost-in-names", + ["ngIRCd", "Sable"], + "does not support userhost-in-names", ) def testReqPostRegistration(self): """Tests requesting more capabilities after CAP END""" @@ -300,7 +304,8 @@ class CapTestCase(cases.BaseServerTestCase): """ # noqa self.addClient(1) self.sendLine(1, "CAP LS 302") - self.assertIn("multi-prefix", self.getCapLs(1)) + if "multi-prefix" not in self.getCapLs(1): + raise CapabilityNotSupported("multi-prefix") self.sendLine(1, "CAP REQ :foo multi-prefix bar") m = self.getRegistrationMessage(1) self.assertMessageMatch( diff --git a/irctest/server_tests/chathistory.py b/irctest/server_tests/chathistory.py index 873661e..cb88bb1 100644 --- a/irctest/server_tests/chathistory.py +++ b/irctest/server_tests/chathistory.py @@ -46,7 +46,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase): result = [] for msg in inner_msgs: if ( - msg.command == "PRIVMSG" + msg.command in ("PRIVMSG", "TOPIC") and batch_tag is not None and msg.tags.get("batch") == batch_tag ): @@ -220,6 +220,47 @@ class ChathistoryTestCase(cases.BaseServerTestCase): self.validate_echo_messages(NUM_MESSAGES, echo_messages) self.validate_chathistory(subcommand, echo_messages, 1, chname) + @skip_ngircd + def testChathistoryNoEventPlayback(self): + """Tests that non-messages don't appear in the chat history when event-playback + is not enabled.""" + + self.connectClient( + "bar", + capabilities=[ + "message-tags", + "server-time", + "echo-message", + "batch", + "labeled-response", + "sasl", + CHATHISTORY_CAP, + ], + skip_if_cap_nak=True, + ) + chname = "#chan" + secrets.token_hex(12) + self.joinChannel(1, chname) + self.getMessages(1) + self.getMessages(1) + + NUM_MESSAGES = 10 + echo_messages = [] + for i in range(NUM_MESSAGES): + self.sendLine(1, "TOPIC %s :this is topic %d" % (chname, i)) + self.getMessages(1) + self.sendLine(1, "PRIVMSG %s :this is message %d" % (chname, i)) + echo_messages.extend( + msg.to_history_message() for msg in self.getMessages(1) + ) + time.sleep(0.002) + + self.validate_echo_messages(NUM_MESSAGES, echo_messages) + self.sendLine(1, "CHATHISTORY LATEST %s * 100" % chname) + (batch_open, *messages, batch_close) = self.getMessages(1) + self.assertMessageMatch(batch_open, command="BATCH") + self.assertMessageMatch(batch_close, command="BATCH") + self.assertEqual([msg for msg in messages if msg.command != "PRIVMSG"], []) + @pytest.mark.parametrize("subcommand", SUBCOMMANDS) @skip_ngircd def testChathistoryEventPlayback(self, subcommand): @@ -244,21 +285,27 @@ class ChathistoryTestCase(cases.BaseServerTestCase): NUM_MESSAGES = 10 echo_messages = [] for i in range(NUM_MESSAGES): + self.sendLine(1, "TOPIC %s :this is topic %d" % (chname, i)) + echo_messages.extend( + msg.to_history_message() for msg in self.getMessages(1) + ) + time.sleep(0.002) + self.sendLine(1, "PRIVMSG %s :this is message %d" % (chname, i)) echo_messages.extend( msg.to_history_message() for msg in self.getMessages(1) ) time.sleep(0.002) - self.validate_echo_messages(NUM_MESSAGES, echo_messages) + self.validate_echo_messages(NUM_MESSAGES * 2, echo_messages) self.validate_chathistory(subcommand, echo_messages, 1, chname) @pytest.mark.parametrize("subcommand", SUBCOMMANDS) @pytest.mark.private_chathistory @skip_ngircd def testChathistoryDMs(self, subcommand): - c1 = "foo" + secrets.token_hex(12) - c2 = "bar" + secrets.token_hex(12) + c1 = random_name("foo") + c2 = random_name("bar") self.controller.registerUser(self, c1, "sesame1") self.controller.registerUser(self, c2, "sesame2") self.connectClient( @@ -313,7 +360,7 @@ class ChathistoryTestCase(cases.BaseServerTestCase): self.validate_chathistory(subcommand, echo_messages, 1, c2) self.validate_chathistory(subcommand, echo_messages, 2, c1) - c3 = "baz" + secrets.token_hex(12) + c3 = random_name("baz") self.connectClient( c3, capabilities=[ @@ -583,8 +630,8 @@ class ChathistoryTestCase(cases.BaseServerTestCase): @pytest.mark.arbitrary_client_tags @skip_ngircd def testChathistoryTagmsg(self): - c1 = "foo" + secrets.token_hex(12) - c2 = "bar" + secrets.token_hex(12) + c1 = random_name("foo") + c2 = random_name("bar") chname = "#chan" + secrets.token_hex(12) self.controller.registerUser(self, c1, "sesame1") self.controller.registerUser(self, c2, "sesame2") @@ -683,8 +730,8 @@ class ChathistoryTestCase(cases.BaseServerTestCase): @skip_ngircd def testChathistoryDMClientOnlyTags(self): # regression test for Ergo #1411 - c1 = "foo" + secrets.token_hex(12) - c2 = "bar" + secrets.token_hex(12) + c1 = random_name("foo") + c2 = random_name("bar") self.controller.registerUser(self, c1, "sesame1") self.controller.registerUser(self, c2, "sesame2") self.connectClient( diff --git a/irctest/server_tests/echo_message.py b/irctest/server_tests/echo_message.py index 2eb7119..cdfcb8c 100644 --- a/irctest/server_tests/echo_message.py +++ b/irctest/server_tests/echo_message.py @@ -32,6 +32,9 @@ class EchoMessageTestCase(cases.BaseServerTestCase): self.sendLine(1, "JOIN #chan") + # Synchronize + self.getMessages(1) + if not solo: self.connectClient("qux", capabilities=capabilities) self.sendLine(2, "JOIN #chan") diff --git a/irctest/server_tests/messages.py b/irctest/server_tests/messages.py index c0f4ec0..520fd13 100644 --- a/irctest/server_tests/messages.py +++ b/irctest/server_tests/messages.py @@ -13,6 +13,7 @@ class PrivmsgTestCase(cases.BaseServerTestCase): """""" self.connectClient("foo") self.sendLine(1, "JOIN #chan") + self.getMessages(1) # synchronize self.connectClient("bar") self.sendLine(2, "JOIN #chan") self.getMessages(2) # synchronize diff --git a/irctest/server_tests/who.py b/irctest/server_tests/who.py index 7813ee6..15bdeb3 100644 --- a/irctest/server_tests/who.py +++ b/irctest/server_tests/who.py @@ -87,7 +87,7 @@ class BaseWhoTestCase: class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): @cases.mark_specifications("Modern") def testWhoStar(self): - if self.controller.software_name == "Bahamut": + if self.controller.software_name in ("Bahamut", "Sable"): raise runner.OptionalExtensionNotSupported("WHO mask") self._init() @@ -118,7 +118,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): ) @cases.mark_specifications("Modern") def testWhoNick(self, mask): - if "*" in mask and self.controller.software_name == "Bahamut": + if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"): raise runner.OptionalExtensionNotSupported("WHO mask") self._init() @@ -148,7 +148,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): ids=["username", "realname-mask", "hostname"], ) def testWhoUsernameRealName(self, mask): - if "*" in mask and self.controller.software_name == "Bahamut": + if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"): raise runner.OptionalExtensionNotSupported("WHO mask") self._init() @@ -201,7 +201,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): ) @cases.mark_specifications("Modern") def testWhoNickAway(self, mask): - if "*" in mask and self.controller.software_name == "Bahamut": + if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"): raise runner.OptionalExtensionNotSupported("WHO mask") self._init() @@ -228,9 +228,14 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): @pytest.mark.parametrize( "mask", ["coolNick", "coolnick", "coolni*"], ids=["exact", "casefolded", "mask"] ) + @cases.xfailIfSoftware( + ["Sable"], + "Sable does not advertise oper status in WHO: " + "https://github.com/Libera-Chat/sable/pull/77", + ) @cases.mark_specifications("Modern") def testWhoNickOper(self, mask): - if "*" in mask and self.controller.software_name == "Bahamut": + if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"): raise runner.OptionalExtensionNotSupported("WHO mask") self._init() @@ -262,9 +267,14 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): @pytest.mark.parametrize( "mask", ["coolNick", "coolnick", "coolni*"], ids=["exact", "casefolded", "mask"] ) + @cases.xfailIfSoftware( + ["Sable"], + "Sable does not advertise oper status in WHO: " + "https://github.com/Libera-Chat/sable/pull/77", + ) @cases.mark_specifications("Modern") def testWhoNickAwayAndOper(self, mask): - if "*" in mask and self.controller.software_name == "Bahamut": + if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"): raise runner.OptionalExtensionNotSupported("WHO mask") self._init() @@ -298,18 +308,11 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): @pytest.mark.parametrize("mask", ["#chan", "#CHAN"], ids=["exact", "casefolded"]) @cases.mark_specifications("Modern") def testWhoChan(self, mask): - if "*" in mask and self.controller.software_name == "Bahamut": + if "*" in mask and self.controller.software_name in ("Bahamut", "Sable"): raise runner.OptionalExtensionNotSupported("WHO mask") self._init() - self.sendLine(1, "OPER operuser operpassword") - self.assertIn( - RPL_YOUREOPER, - [m.command for m in self.getMessages(1)], - fail_msg="OPER failed", - ) - self.sendLine(1, "AWAY :be right back") self.getMessages(1) self.getMessages(2) @@ -335,7 +338,7 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): StrRe(host_re), "My.Little.Server", "coolNick", - "G*@", + "G@", StrRe(realname_regexp(self.realname)), ], ) @@ -589,7 +592,7 @@ class WhoServicesTestCase(BaseWhoTestCase, cases.BaseServerTestCase): class WhoInvisibleTestCase(cases.BaseServerTestCase): @cases.mark_specifications("Modern") def testWhoInvisible(self): - if self.controller.software_name == "Bahamut": + if self.controller.software_name in ("Bahamut", "Sable"): raise runner.OptionalExtensionNotSupported("WHO mask") self.connectClient("evan", name="evan") diff --git a/irctest/server_tests/whois.py b/irctest/server_tests/whois.py index 2853553..51b7ad9 100644 --- a/irctest/server_tests/whois.py +++ b/irctest/server_tests/whois.py @@ -195,18 +195,26 @@ class WhoisTestCase(_WhoisTestMixin, cases.BaseServerTestCase): self.connectClient("otherNick") self.getMessages(2) - self.sendLine(2, f"WHOIS {server} coolnick") + self.sendLine(2, f"WHOIS {server} {nick}") messages = self.getMessages(2) whois_user = messages[0] - self.assertEqual(whois_user.command, RPL_WHOISUSER) - # " * :" - self.assertEqual(whois_user.params[1], nick) - self.assertIn(whois_user.params[2], ("~" + username, username)) + self.assertMessageMatch( + whois_user, + command=RPL_WHOISUSER, + # " * :" + params=[ + "otherNick", + nick, + StrRe("~?" + username), + ANYSTR, + ANYSTR, + realname, + ], + ) # dumb regression test for oragono/oragono#355: self.assertNotIn( whois_user.params[3], [nick, username, "~" + username, realname] ) - self.assertEqual(whois_user.params[5], realname) @pytest.mark.parametrize( "away,oper", diff --git a/make_workflows.py b/make_workflows.py index 8c94179..8ffa6be 100644 --- a/make_workflows.py +++ b/make_workflows.py @@ -151,6 +151,7 @@ def get_test_job(*, config, test_config, test_id, version_flavor, jobs): env += ( f"PATH={software_config['prefix']}/sbin" f":{software_config['prefix']}/bin" + f":{software_config['prefix']}" f":$PATH " ) diff --git a/workflows.yml b/workflows.yml index 87db640..b739260 100644 --- a/workflows.yml +++ b/workflows.yml @@ -250,6 +250,34 @@ software: make -j 4 make install + sable: + name: Sable + repository: Libera-Chat/sable + refs: + stable: ff1179512a79eba57ca468a5f83af84ecce08a5b + release: null + devel: master + devel_release: null + path: sable + prefix: "$GITHUB_WORKSPACE/sable/target/debug" + pre_deps: + - name: Install rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + profile: minimal + override: true + - name: Enable Cargo cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: "sable -> target" + cache-on-failure: true + - run: rustc --version + separate_build_job: false + build_script: | + cd $GITHUB_WORKSPACE/sable/ + cargo build + snircd: name: snircd repository: quakenet/snircd @@ -454,6 +482,9 @@ tests: nefarious: software: [nefarious] + sable: + software: [sable] + # doesn't build because it can't find liblex for some reason #snircd: # software: [snircd] From 558add52298b09d5a490603df9245e0c0ec1d4c2 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Fri, 22 Sep 2023 22:04:27 +0200 Subject: [PATCH 141/143] whox: Add test for individual chars (#227) It makes it easier to debug missing params --- Makefile | 4 ---- irctest/cases.py | 7 +++++-- irctest/server_tests/who.py | 40 +++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 1876c51..e7a68ec 100644 --- a/Makefile +++ b/Makefile @@ -35,22 +35,18 @@ INSPIRCD_SELECTORS := \ and not strict \ $(EXTRA_SELECTORS) -# HelpTestCase fails because it returns NOTICEs instead of numerics IRCU2_SELECTORS := \ not Ergo \ and not deprecated \ and not strict \ $(EXTRA_SELECTORS) -# same justification as ircu2 -# lusers "unregistered" tests fail because NEFARIOUS_SELECTORS := \ not Ergo \ and not deprecated \ and not strict \ $(EXTRA_SELECTORS) -# same justification as ircu2 SNIRCD_SELECTORS := \ not Ergo \ and not deprecated \ diff --git a/irctest/cases.py b/irctest/cases.py index eea0f8c..30b1aed 100644 --- a/irctest/cases.py +++ b/irctest/cases.py @@ -802,7 +802,7 @@ def xfailIf( def decorator(f: Callable[..., _TReturn]) -> Callable[..., _TReturn]: @functools.wraps(f) def newf(self: _TSelf, *args: Any, **kwargs: Any) -> _TReturn: - if condition(self): + if condition(self, *args, **kwargs): try: return f(self, *args, **kwargs) except Exception: @@ -819,7 +819,10 @@ def xfailIf( def xfailIfSoftware( names: List[str], reason: str ) -> Callable[[Callable[..., _TReturn]], Callable[..., _TReturn]]: - return xfailIf(lambda testcase: testcase.controller.software_name in names, reason) + def pred(testcase: _IrcTestCase, *args: Any, **kwargs: Any) -> bool: + return testcase.controller.software_name in names + + return xfailIf(pred, reason) def mark_services(cls: TClass) -> TClass: diff --git a/irctest/server_tests/who.py b/irctest/server_tests/who.py index 15bdeb3..0e4b280 100644 --- a/irctest/server_tests/who.py +++ b/irctest/server_tests/who.py @@ -496,6 +496,46 @@ class WhoTestCase(BaseWhoTestCase, cases.BaseServerTestCase): params=["otherNick", InsensitiveStr("coolNick"), ANYSTR], ) + @pytest.mark.parametrize("char", "cuihsnfdlaor") + @cases.xfailIf( + lambda self, char: bool( + char == "l" and self.controller.software_name == "ircu2" + ), + "https://github.com/UndernetIRC/ircu2/commit/17c539103abbd0055b2297e17854cd0756c85d62", + ) + @cases.xfailIf( + lambda self, char: bool( + char == "l" and self.controller.software_name == "Nefarious" + ), + "https://github.com/evilnet/nefarious2/pull/73", + ) + def testWhoxOneChar(self, char): + self._init() + if "WHOX" not in self.server_support: + raise runner.IsupportTokenNotSupported("WHOX") + + self.sendLine(2, f"WHO coolNick %{char}") + messages = self.getMessages(2) + + self.assertEqual(len(messages), 2, "Unexpected number of messages") + + (reply, end) = messages + + self.assertMessageMatch( + reply, + command=RPL_WHOSPCRPL, + params=[ + "otherNick", + StrRe(".+"), + ], + ) + + self.assertMessageMatch( + end, + command=RPL_ENDOFWHO, + params=["otherNick", InsensitiveStr("coolNick"), ANYSTR], + ) + def testWhoxToken(self): """https://github.com/ircv3/ircv3-specifications/pull/482""" self._init() From 36901c1433eaedd9ab18fb2f7ebf95c19b664bdd Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 24 Sep 2023 08:47:00 +0200 Subject: [PATCH 142/143] Fix lock on the set of used ports (#235) pytest-xdist (well, execnet) re-loads modules after forking, so each process had its own lock, making the lock useless. Co-authored-by: Shivaram Lingamneni --- irctest/basecontrollers.py | 76 ++++++++++++++--------------------- irctest/irc_utils/filelock.py | 19 +++++++++ requirements.txt | 4 +- 3 files changed, 53 insertions(+), 46 deletions(-) create mode 100644 irctest/irc_utils/filelock.py diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index b70015e..716b350 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -1,7 +1,8 @@ from __future__ import annotations +import contextlib import dataclasses -import multiprocessing +import json import os from pathlib import Path import shutil @@ -10,23 +11,13 @@ import subprocess import tempfile import textwrap import time -from typing import ( - IO, - Any, - Callable, - Dict, - List, - MutableMapping, - Optional, - Set, - Tuple, - Type, -) +from typing import IO, Any, Callable, Dict, Iterator, List, Optional, Set, Tuple, Type import irctest from . import authentication, tls from .client_mock import ClientMock +from .irc_utils.filelock import FileLock from .irc_utils.junkdrawer import find_hostname_and_port from .irc_utils.message_parser import Message from .runner import NotImplementedByController @@ -71,41 +62,36 @@ class _BaseController: proc: Optional[subprocess.Popen] - _used_ports: Set[Tuple[str, int]] - """``(hostname, port))`` used by this controller.""" - # the following need to be shared between processes in case we are running in - # parallel (with pytest-xdist) - # The dicts are used as a set of (hostname, port), because _manager.set() doesn't - # exist. - _manager = multiprocessing.Manager() - _port_lock = _manager.Lock() - """Lock for access to ``_all_used_ports`` and ``_available_ports``.""" - _all_used_ports: MutableMapping[Tuple[str, int], None] = _manager.dict() - """``(hostname, port)`` used by all controllers.""" - _available_ports: MutableMapping[Tuple[str, int], None] = _manager.dict() - """``(hostname, port)`` available to any controller.""" + _used_ports_path = Path(tempfile.gettempdir()) / "irctest_ports.json" + _port_lock = FileLock(Path(tempfile.gettempdir()) / "irctest_ports.json.lock") def __init__(self, test_config: TestCaseControllerConfig): self.test_config = test_config self.proc = None - self._used_ports = set() + self._own_ports: Set[Tuple[str, int]] = set() + + @contextlib.contextmanager + def _used_ports(self) -> Iterator[Set[Tuple[str, int]]]: + with self._port_lock: + if not self._used_ports_path.exists(): + self._used_ports_path.write_text("[]") + used_ports = { + (h, p) for (h, p) in json.loads(self._used_ports_path.read_text()) + } + yield used_ports + self._used_ports_path.write_text(json.dumps(list(used_ports))) def get_hostname_and_port(self) -> Tuple[str, int]: - with self._port_lock: - try: - # try to get a known available port - ((hostname, port), _) = self._available_ports.popitem() - except KeyError: - # if there aren't any, iterate while we get a fresh one. - while True: - (hostname, port) = find_hostname_and_port() - if (hostname, port) not in self._all_used_ports: - # double-checking in self._used_ports to prevent collisions - # between controllers starting at the same time. - break + with self._used_ports() as used_ports: + while True: + (hostname, port) = find_hostname_and_port() + if (hostname, port) not in used_ports: + # double-checking in self._used_ports to prevent collisions + # between controllers starting at the same time. + break - # Make this port unavailable to other processes - self._all_used_ports[(hostname, port)] = None + used_ports.add((hostname, port)) + self._own_ports.add((hostname, port)) return (hostname, port) @@ -131,10 +117,10 @@ class _BaseController: if self.proc: self.kill_proc() - # move this controller's ports from _all_used_ports to _available_ports - for hostname, port in self._used_ports: - del self._all_used_ports[(hostname, port)] - self._available_ports[(hostname, port)] = None + with self._used_ports() as used_ports: + for hostname, port in list(self._own_ports): + used_ports.remove((hostname, port)) + self._own_ports.remove((hostname, port)) class DirectoryBasedController(_BaseController): diff --git a/irctest/irc_utils/filelock.py b/irctest/irc_utils/filelock.py new file mode 100644 index 0000000..247dac9 --- /dev/null +++ b/irctest/irc_utils/filelock.py @@ -0,0 +1,19 @@ +""" +Compatibility layer for filelock ( https://pypi.org/project/filelock/ ); +commonly packaged by Linux distributions but might not be available +in some environments. +""" + +import os +from typing import ContextManager + +if os.getenv("PYTEST_XDIST_WORKER"): + # running under pytest-xdist; filelock is required for reliability + from filelock import FileLock +else: + # normal test execution, no port races + import contextlib + from typing import Any + + def FileLock(*args: Any, **kwargs: Any) -> ContextManager[None]: + return contextlib.nullcontext() diff --git a/requirements.txt b/requirements.txt index fe27660..a608954 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,5 @@ +pytest + # The following dependencies are actually optional: ecdsa -pytest +filelock From 00663f15ec43954d0bf3d851ea5aeb71836ae8c2 Mon Sep 17 00:00:00 2001 From: Val Lorentz Date: Sun, 24 Sep 2023 08:47:22 +0200 Subject: [PATCH 143/143] Fix a bunch of synchronization heuristics to work with Sable (#236) --- irctest/basecontrollers.py | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/irctest/basecontrollers.py b/irctest/basecontrollers.py index 716b350..9dda06a 100644 --- a/irctest/basecontrollers.py +++ b/irctest/basecontrollers.py @@ -289,7 +289,7 @@ class BaseServerController(_BaseController): time.sleep(0.01) c.send(b" ") # Triggers BrokenPipeError - except BrokenPipeError: + except (BrokenPipeError, ConnectionResetError): # ircu2 cuts the connection without a message if registration # is not complete. pass @@ -344,13 +344,16 @@ class BaseServicesController(_BaseController): c.sendLine("NICK chkNS") c.sendLine("USER chk chk chk chk") time.sleep(self.server_controller.sync_sleep_time) - for msg in c.getMessages(synchronize=False): - if msg.command == "PING": - # Hi Unreal - c.sendLine("PONG :" + msg.params[0]) - c.getMessages() + got_end_of_motd = False + while not got_end_of_motd: + for msg in c.getMessages(synchronize=False): + if msg.command == "PING": + # Hi Unreal + c.sendLine("PONG :" + msg.params[0]) + if msg.command in ("376", "422"): # RPL_ENDOFMOTD / ERR_NOMOTD + got_end_of_motd = True - timeout = time.time() + 3 + timeout = time.time() + 10 while True: c.sendLine(f"PRIVMSG {self.server_controller.nickserv} :help") @@ -359,11 +362,17 @@ class BaseServicesController(_BaseController): if msg.command == "401": # NickServ not available yet pass + elif msg.command in ("MODE", "221"): # RPL_UMODEIS + pass elif msg.command == "NOTICE": - # NickServ is available - assert "nickserv" in (msg.prefix or "").lower(), msg - print("breaking") - break + assert msg.prefix is not None + if "!" not in msg.prefix and "." in msg.prefix: + # Server notice + pass + else: + # NickServ is available + assert "nickserv" in (msg.prefix or "").lower(), msg + break else: assert False, f"unexpected reply from NickServ: {msg}" else: