nandi/frqpublic Fork 0
main
Commits
Clone
git clone https://git.rickub.com/nandi/frq.git
git clone ssh://git@rickub.com/nandi/frq.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

tircparse.nim · 137 lines · 4.8 KBNim Blame HistoryRaw
One frontend where there were three, and a core that is not Clojure 438b247 nandi 10h ago1## The IRC wire format, tested against the cases that cost something.
2##
3## These are not a transcription of the Clojure's tests, because it had none —
4## which is half the argument for the move. Every case here is either a rule
5## IRCv3 states or a bug the comments in `common/frq/irc/parse.cljc` record
6## having been paid for once already.
7
8import std/unittest
9import frq/ircparse
10
11suite "parseLine":
12 test "a bare line":
13 let p = parseLine("PING :12345")
14 check p.command == "PING"
15 check p.params == @["12345"]
16 check not p.hasPrefix
17 check not p.hasTags
18
19 test "a prefix is split off and the command upcased":
20 let p = parseLine(":nick!user@host privmsg #chan :hello there")
21 check p.prefix == "nick!user@host"
22 check p.hasPrefix
23 check p.command == "PRIVMSG"
24 check p.params == @["#chan", "hello there"]
25
26 test "the trailing parameter keeps its spaces and its colons":
27 let p = parseLine(":a!b@c PRIVMSG #chan :look: a b c")
28 check p.params == @["#chan", "look: a b c"]
29
30 test "no trailing parameter at all":
31 let p = parseLine(":a!b@c JOIN #chan")
32 check p.params == @["#chan"]
33
34 test "an empty trailing parameter is a parameter":
35 # `:` with nothing after it is how a client sends an empty topic, and
36 # dropping it turns a clear into a no-op.
37 let p = parseLine(":a!b@c TOPIC #chan :")
38 check p.params == @["#chan", ""]
39
40 test "tags are taken off the front":
41 let p = parseLine("@time=2026-01-01T00:00:00Z;account=alice :a!b@c PRIVMSG #chan :hi")
42 check p.hasTags
43 check p.tags == "time=2026-01-01T00:00:00Z;account=alice"
44 check p.account == "alice"
45 check p.command == "PRIVMSG"
46 check p.params == @["#chan", "hi"]
47
48 test "a line with tags and no account":
49 let p = parseLine("@time=x :a!b@c PRIVMSG #chan :hi")
50 check p.hasTags
51 check not p.hasAccount
52
53 test "trailing whitespace is trimmed but leading structure is not":
54 let p = parseLine("PING :12345\r\n")
55 check p.raw == "PING :12345"
56 check p.params == @["12345"]
57
58 test "raw is what arrived, not what we made of it":
59 let p = parseLine("@a=1 :n!u@h PRIVMSG #c :x ")
60 check p.raw == "@a=1 :n!u@h PRIVMSG #c :x"
61
62 test "runs of spaces in the head do not become empty parameters":
63 let p = parseLine(":a!b@c PRIVMSG #chan :hi")
64 check p.params == @["#chan", "hi"]
65
66 test "a line that is only tags does not throw":
67 # The Clojure indexes with `subs` on a nil `index-of` here and dies. It
68 # cannot arrive from a conforming server, but a parser reading a socket
69 # answers malformed input with a value rather than an exception.
70 let p = parseLine("@only=tags")
71 check p.hasTags
72 check p.tags == "only=tags"
73 check p.command == ""
74
75 test "an empty line":
76 let p = parseLine("")
77 check p.command == ""
78 check p.params.len == 0
79
80suite "unescapeTag":
81 test "the five escapes":
82 check unescapeTag("a\\:b") == "a;b"
83 check unescapeTag("a\\sb") == "a b"
84 check unescapeTag("a\\\\b") == "a\\b"
85 check unescapeTag("a\\rb") == "a\rb"
86 check unescapeTag("a\\nb") == "a\nb"
87
88 test "an unknown escape is the character itself":
89 check unescapeTag("a\\qb") == "aqb"
90
91 test "a trailing backslash is kept rather than eating the terminator":
92 check unescapeTag("ab\\") == "ab\\"
93
94 test "a reaction tally survives the trip":
95 # The bug this function exists for: three tallies, not one.
96 check unescapeTag("x\\:alice\\:bob") == "x;alice;bob"
97
98suite "escapeTagValue":
99 test "round-trips everything unescapeTag undoes":
100 for s in ["plain", "a;b", "a b", "a\\b", "a\r\nb", "", "😀", "a;;b"]:
101 check unescapeTag(escapeTagValue(s)) == s
102
103 test "a backslash is not double-escaped":
104 check escapeTagValue("a\\b") == "a\\\\b"
105
106suite "tagValue":
107 test "a present value":
108 check tagValue("a=1;b=2", "b") == ("2", true)
109
110 test "an absent tag":
111 check tagValue("a=1", "b") == ("", false)
112
113 test "an empty value and a bare key are both absent":
114 # IRCv3 says `key` and `key=` mean the same thing, and a caller asking
115 # `tagValue` is asking whether a fact is there. `+reply=` on a line
116 # answering nothing used to put a reply chip above it.
117 check tagValue("a=;b=2", "a") == ("", false)
118 check tagValue("a;b=2", "a") == ("", false)
119
120 test "the value is unescaped":
121 check tagValue("t=a\\sb", "t") == ("a b", true)
122
123 test "a key that is a prefix of another does not match it":
124 check tagValue("account-x=1;account=2", "account") == ("2", true)
125
126 test "a value containing = keeps the rest of it":
127 check tagValue("t=a=b", "t") == ("a=b", true)
128
129suite "nickOf":
130 test "a full prefix":
131 check nickOf("nick!user@host") == "nick"
132
133 test "a prefix that is only a nick":
134 check nickOf("nick") == "nick"
135
136 test "a server prefix has no bang and comes back whole":
137 check nickOf("irc.freeq.at") == "irc.freeq.at"