1 /++ 2 Interactive assert statement generation from raw IRC strings, for use in the 3 source code `unittest` blocks. 4 5 Example: 6 7 $(CONSOLE 8 $ dub run :assertgen 9 (...) 10 11 // Paste a raw event string and hit Enter to generate an assert block. Ctrl+C to exit. 12 13 $(I @badge-info=subscriber/15;badges=subscriber/12;color=;display-name=tayk47_mom;emotes=;flags=;id=d6729804-2bf3-495d-80ce-a2fe8ed00a26;login=tayk47_mom;mod=0;msg-id=submysterygift;msg-param-mass-gift-count=1;msg-param-origin-id=49\s9d\s3e\s68\sca\s26\se9\s2a\s6e\s44\sd4\s60\s9b\s3d\saa\sb9\s4c\sad\s43\s5c;msg-param-sender-count=4;msg-param-sub-plan=1000;room-id=71092938;subscriber=1;system-msg=tayk47_mom\sis\sgifting\s1\sTier\s1\sSubs\sto\sxQcOW's\scommunity!\sThey've\sgifted\sa\stotal\sof\s4\sin\sthe\schannel!;tmi-sent-ts=1569013433362;user-id=224578549;user-type= :tmi.twitch.tv USERNOTICE #xqcow) 14 15 { 16 immutable event = parser.toIRCEvent("@badge-info=subscriber/15;badges=subscriber/12;color=;display-name=tayk47_mom;emotes=;flags=;id=d6729804-2bf3-495d-80ce-a2fe8ed00a26;login=tayk47_mom;mod=0;msg-id=submysterygift;msg-param-mass-gift-count=1;msg-param-origin-id=49\\s9d\\s3e\\s68\\sca\\s26\\se9\\s2a\\s6e\\s44\\sd4\\s60\\s9b\\s3d\\saa\\sb9\\s4c\\sad\\s43\\s5c;msg-param-sender-count=4;msg-param-sub-plan=1000;room-id=71092938;subscriber=1;system-msg=tayk47_mom\\sis\\sgifting\\s1\\sTier\\s1\\sSubs\\sto\\sxQcOW's\\scommunity!\\sThey've\\sgifted\\sa\\stotal\\sof\\s4\\sin\\sthe\\schannel!;tmi-sent-ts=1569013433362;user-id=224578549;user-type= :tmi.twitch.tv USERNOTICE #xqcow"); 17 with (event) 18 { 19 assert((type == IRCEvent.Type.TWITCH_BULKGIFT), Enum!(IRCEvent.Type).toString(type)); 20 assert((sender.nickname == "tayk47_mom"), sender.nickname); 21 assert((sender.displayName == "tayk47_mom"), sender.displayName); 22 assert((sender.account == "tayk47_mom"), sender.account); 23 assert((sender.badges == "subscriber/12"), sender.badges); 24 assert((channel == "#xqcow"), channel); 25 assert((content == "tayk47_mom is gifting 1 Tier 1 Subs to xQcOW's community! They've gifted a total of 4 in the channel!"), content); 26 assert((aux == "1000"), aux); 27 assert((tags == "badge-info=subscriber/15;badges=subscriber/12;color=;display-name=tayk47_mom;emotes=;flags=;id=d6729804-2bf3-495d-80ce-a2fe8ed00a26;login=tayk47_mom;mod=0;msg- 28 id=submysterygift;msg-param-mass-gift-count=1;msg-param-origin-id=49\\s9d\\s3e\\s68\\sca\\s26\\se9\\s2a\\s6e\\s44\\sd4\\s60\\s9b\\s3d\\saa\\sb9\\s4c\\sad\\s43\\s5c;msg-param-sender-cou 29 nt=4;msg-param-sub-plan=1000;room-id=71092938;subscriber=1;system-msg=tayk47_mom\\sis\\sgifting\\s1\\sTier\\s1\\sSubs\\sto\\sxQcOW's\\scommunity!\\sThey've\\sgifted\\sa\\stotal\\sof\\s 30 4\\sin\\sthe\\schannel!;tmi-sent-ts=1569013433362;user-id=224578549;user-type="), tags); 31 assert((count == 1), count.to!string); 32 assert((altcount == 4), altcount.to!string); 33 assert((id == "d6729804-2bf3-495d-80ce-a2fe8ed00a26"), id); 34 } 35 } 36 ) 37 38 These can be directly copy/pasted into the appropriate files in `/tests`. 39 They only carry state from the events pasted before it, but the changes made 40 are also expressed as asserts. 41 42 Example: 43 44 $(CONSOLE 45 $ dub run :assertgen 46 (...) 47 48 // Paste a raw event string and hit Enter to generate an assert block. Ctrl+C to exit. 49 50 $(I @badge-info=;badges=;color=#5F9EA0;display-name=Zorael;emote-sets=0,185411,771823,1511983;user-id=22216721;user-type= :tmi.twitch.tv GLOBALUSERSTATE) 51 52 { 53 immutable event = parser.toIRCEvent("@badge-info=;badges=;color=#5F9EA0;display-name=Zorael;emote-sets=0,185411,771823,1511983;user-id=22216721;user-type= :tmi.twitch.tv GLOBALUSERSTATE"); 54 with (event) 55 { 56 assert((type == IRCEvent.Type.GLOBALUSERSTATE), Enum!(IRCEvent.Type).toString(type)); 57 assert((sender.address == "tmi.twitch.tv"), sender.address); 58 assert((sender.class_ == IRCUser.Class.special), Enum!(IRCUser.Class).toString(sender.class_)); 59 assert((target.nickname == "zorael"), target.nickname); 60 assert((target.displayName == "Zorael"), target.displayName); 61 assert((target.class_ == IRCUser.Class.admin), Enum!(IRCUser.Class).toString(target.class_)); 62 assert((target.badges == "*"), target.badges); 63 assert((target.colour == "5F9EA0"), target.colour); 64 assert((tags == "badge-info=;badges=;color=#5F9EA0;display-name=Zorael;emote-sets=0,185411,771823,1511983;user-id=22216721;user-type="), tags); 65 } 66 } 67 68 with (parser.client) 69 { 70 assert((displayName == "Zorael"), displayName); 71 } 72 ) 73 74 This makes it easy to generate tests that verify wanted side-effects 75 incurred by events. 76 +/ 77 module dialect.assertgen; 78 79 version(AssertGeneration): 80 81 private: 82 83 import dialect.defs; 84 import dialect.parsing : IRCParser; 85 import std.range.primitives : isOutputRange; 86 import std.typecons : Flag, No, Yes; 87 88 89 // formatClientAssignment 90 /++ 91 Constructs statement lines for each changed field of an 92 [dialect.defs.IRCClient|IRCClient], including instantiating a fresh one. 93 94 Example: 95 --- 96 IRCClient client; 97 IRCServer server; 98 Appender!(char[]) sink; 99 100 sink.formatClientAssignment(client, server); 101 --- 102 103 Params: 104 sink = Output buffer to write to. 105 client = [dialect.defs.IRCClient|IRCClient] to simulate the assignment of. 106 server = [dialect.defs.IRCServer|IRCServer] to simulate the assignment of. 107 +/ 108 void formatClientAssignment(Sink) 109 (auto ref Sink sink, 110 const IRCClient client, 111 const IRCServer server) pure @safe 112 if (isOutputRange!(Sink, char[])) 113 { 114 import lu.deltastrings : formatDeltaInto; 115 116 sink.put("IRCParser parser;\n\n"); 117 sink.put("with (parser)\n"); 118 sink.put("{\n"); 119 sink.formatDeltaInto(IRCClient.init, client, 1, "client"); 120 sink.formatDeltaInto(IRCServer.init, server, 1, "server"); 121 sink.put('}'); 122 123 static if (!__traits(hasMember, Sink, "data")) 124 { 125 sink.put('\n'); 126 } 127 } 128 129 /// 130 pure @safe unittest 131 { 132 import std.array : Appender; 133 134 Appender!(char[]) sink; 135 sink.reserve(128); 136 137 IRCClient client; 138 IRCServer server; 139 140 with (client) 141 { 142 nickname = "NICKNAME"; 143 user = "UUUUUSER"; 144 server.address = "something.freenode.net"; 145 server.port = 6667; 146 server.daemon = IRCServer.Daemon.unreal; 147 server.aModes = "eIbq"; 148 } 149 150 sink.formatClientAssignment(client, server); 151 152 assert(sink.data == 153 `IRCParser parser; 154 155 with (parser) 156 { 157 client.nickname = "NICKNAME"; 158 client.user = "UUUUUSER"; 159 server.address = "something.freenode.net"; 160 server.port = 6667; 161 server.daemon = IRCServer.Daemon.unreal; 162 server.aModes = "eIbq"; 163 }`, '\n' ~ sink.data); 164 } 165 166 167 // formatEventAssertBlock 168 /++ 169 Constructs assert statement blocks for each changed field of an 170 [dialect.defs.IRCEvent|IRCEvent]. 171 172 Example: 173 --- 174 IRCEvent event; 175 Appender!(char[]) sink; 176 sink.formatEventAssertBlock(event); 177 --- 178 179 Params: 180 sink = Output buffer to write to. 181 event = [dialect.defs.IRCEvent|IRCEvent] to construct assert statements for. 182 +/ 183 void formatEventAssertBlock(Sink) 184 (auto ref Sink sink, 185 const IRCEvent event) pure @safe 186 if (isOutputRange!(Sink, char[])) 187 { 188 import lu.deltastrings : formatDeltaInto; 189 import lu.string : tabs; 190 import std.array : replace; 191 import std.format : format, formattedWrite; 192 193 immutable raw = event.tags.length ? 194 "@%s %s".format(event.tags, event.raw) : event.raw; 195 196 immutable escaped = raw 197 .replace('\\', `\\`) 198 .replace('"', `\"`); 199 200 sink.put("{\n"); 201 if (escaped != raw) sink.formattedWrite("%s// %s\n", 1.tabs, raw); 202 sink.formattedWrite("%simmutable event = parser.toIRCEvent(\"%s\");\n", 1.tabs, escaped); 203 sink.formattedWrite("%swith (event)\n", 1.tabs); 204 sink.formattedWrite("%s{\n", 1.tabs); 205 sink.formatDeltaInto!(Yes.asserts)(IRCEvent.init, event, 2); 206 sink.formattedWrite("%s}\n", 1.tabs); 207 sink.put("}"); 208 209 static if (!__traits(hasMember, Sink, "data")) 210 { 211 sink.put('\n'); 212 } 213 } 214 215 /// 216 unittest 217 { 218 import lu.deltastrings : formatDeltaInto; 219 import lu.string : tabs; 220 import std.array : Appender; 221 import std.format : formattedWrite; 222 223 Appender!(char[]) sink; 224 sink.reserve(1024); 225 226 IRCClient client; 227 IRCServer server; 228 auto parser = IRCParser(client, server); 229 230 immutable event = parser.toIRCEvent(":zorael!~NaN@2001:41d0:2:80b4:: PRIVMSG #flerrp :kameloso: 8ball"); 231 232 // copy/paste the above 233 sink.put("{\n"); 234 sink.formattedWrite("%simmutable event = parser.toIRCEvent(\"%s\");\n", 1.tabs, event.raw); 235 sink.formattedWrite("%swith (event)\n", 1.tabs); 236 sink.formattedWrite("%s{\n", 1.tabs); 237 sink.formatDeltaInto!(Yes.asserts)(IRCEvent.init, event, 2); 238 sink.formattedWrite("%s}\n", 1.tabs); 239 sink.put("}"); 240 241 assert(sink.data == 242 `{ 243 immutable event = parser.toIRCEvent(":zorael!~NaN@2001:41d0:2:80b4:: PRIVMSG #flerrp :kameloso: 8ball"); 244 with (event) 245 { 246 assert((type == IRCEvent.Type.CHAN), Enum!(IRCEvent.Type).toString(type)); 247 assert((sender.nickname == "zorael"), sender.nickname); 248 assert((sender.ident == "~NaN"), sender.ident); 249 assert((sender.address == "2001:41d0:2:80b4::"), sender.address); 250 assert((channel == "#flerrp"), channel); 251 assert((content == "kameloso: 8ball"), content); 252 } 253 }`, '\n' ~ sink.data); 254 } 255 256 257 // inputServerInformation 258 /++ 259 Asks the user to input server information via standard input. 260 261 Params: 262 parser = [dialect.parsing.IRCParser] to populate with information. 263 +/ 264 void inputServerInformation(ref IRCParser parser) @system 265 { 266 import dialect.common : typenumsOf; 267 import lu.conv : Enum; 268 import lu.string : advancePast, stripped; 269 import std.range : chunks, only; 270 import std.traits : EnumMembers; 271 import std.stdio : readln, stdin, stdout, write, writefln, writeln; 272 import std.uni : toLower; 273 274 writeln("-- Available daemons --"); 275 writefln("%(%(%-14s%)\n%)", EnumMembers!(IRCServer.Daemon).only.chunks(3)); 276 writeln(); 277 278 write("Enter daemon [optional daemon literal] (solanum): "); 279 stdout.flush(); 280 stdin.flush(); 281 string slice = readln().stripped; // mutable so we can advancePast it 282 immutable daemonstring = slice.advancePast(' ', Yes.inherit).toLower; 283 immutable daemonLiteral = slice.length ? slice : daemonstring; 284 285 parser.server.daemon = daemonstring.length ? 286 Enum!(IRCServer.Daemon).fromString(daemonstring) : IRCServer.Daemon.solanum; 287 parser.typenums = typenumsOf(parser.server.daemon); 288 parser.server.daemonstring = daemonLiteral; 289 290 write("Enter network (Libera.Chat): "); 291 stdout.flush(); 292 stdin.flush(); 293 parser.server.network = readln().stripped; 294 if (!parser.server.network.length) parser.server.network = "Libera.Chat"; 295 296 write("Enter server address (irc.libera.chat): "); 297 stdout.flush(); 298 stdin.flush(); 299 parser.server.address = readln().stripped; 300 if (!parser.server.address.length) parser.server.address = "irc.libera.chat"; 301 } 302 303 304 public: 305 306 307 // main 308 /++ 309 Entry point when compiling the `assertgen` dub configuration. 310 311 Reads raw server strings from `stdin`, parses them into 312 [dialect.defs.IRCEvent|IRCEvent]s and constructs assert blocks of their contents. 313 +/ 314 version(unittest) {} 315 else 316 int main(string[] args) @system 317 { 318 import dialect.defs : IRCServer; 319 import lu.deltastrings : formatDeltaInto; 320 import lu.string : strippedLeft; 321 import std.array : Appender; 322 import std.getopt : GetOptException, config, getopt; 323 import std.stdio : File, readln, stdin, stdout, write, writefln, writeln; 324 import std.string : chomp; 325 326 enum defaultOutputFilename = "unittest.log"; 327 328 string nicknameOverride; 329 string userOverride; 330 string identOverride; 331 string outputFile = defaultOutputFilename; 332 bool overwrite; 333 bool twitch; 334 335 try 336 { 337 version(TwitchSupport) 338 { 339 enum twitchString = "Shortcut to Twitch input"; 340 } 341 else 342 { 343 enum twitchString = "(Only available when compiled with Twitch support)"; 344 } 345 346 auto results = getopt(args, 347 config.caseSensitive, 348 config.bundling, 349 "n|nickname", 350 "Override initial nickname", 351 &nicknameOverride, 352 "u|user", 353 "Override initial user", 354 &userOverride, 355 "i|ident", 356 "Override initial ident", 357 &identOverride, 358 "o|output", 359 "Output file (specify '-' to disable) [" ~ defaultOutputFilename ~ "]", 360 &outputFile, 361 "O|overwrite", 362 "Overwrite file instead of appending to it", 363 &overwrite, 364 "twitch", 365 twitchString, 366 &twitch, 367 ); 368 369 if (results.helpWanted) 370 { 371 import std.getopt : defaultGetoptPrinter; 372 defaultGetoptPrinter("Available flags:\n", results.options); 373 return 0; 374 } 375 } 376 catch (GetOptException e) 377 { 378 writeln(e.msg); 379 return 1; 380 } 381 382 IRCParser parser; 383 parser.initPostprocessors(); // Normally done in IRCParser(IRCClient) constructor 384 385 Appender!(char[]) buffer; 386 buffer.reserve(2048); 387 388 if (outputFile == "-") 389 { 390 outputFile = string.init; 391 } 392 393 if (outputFile.length) 394 { 395 writefln("Writing output to %s, overwrite:%s", outputFile, overwrite); 396 } 397 398 version (TwitchSupport) 399 { 400 if (twitch) 401 { 402 import dialect.common : typenumsOf; 403 404 parser.server.daemon = IRCServer.Daemon.twitch; 405 parser.typenums = typenumsOf(IRCServer.Daemon.twitch); 406 parser.server.network = "Twitch"; 407 parser.server.daemonstring = "twitch"; 408 parser.server.address = "irc.chat.twitch.tv"; 409 parser.server.maxNickLength = 25; 410 411 // Provide skeletal user defaults. 412 with (parser.client) 413 { 414 // nickname, user and ident are always identical 415 nickname = nicknameOverride.length ? nicknameOverride : "kameloso"; 416 user = nickname; 417 ident = nickname; 418 //realName = "kameloso IRC bot"; // Not used on Twitch 419 } 420 421 writeln("Server set to Twitch as per command-line argument."); 422 } 423 } 424 425 if (!twitch) 426 { 427 import std.conv : ConvException; 428 429 try 430 { 431 inputServerInformation(parser); 432 } 433 catch (ConvException e) 434 { 435 writeln(); 436 writeln("-- Conversion exception caught when parsing daemon: ", e.msg); 437 version(PrintStacktraces) writeln(e.info); 438 stdout.flush(); 439 return 1; 440 } 441 442 // Provide skeletal user defaults. 443 with (parser.client) 444 { 445 nickname = nicknameOverride.length ? nicknameOverride : "kameloso"; 446 user = userOverride.length ? userOverride : "kameloso"; 447 ident = identOverride.length ? identOverride : "~kameloso"; 448 realName = "kameloso IRC bot"; 449 } 450 451 // Provide Libera.Chat defaults here, now that they're no longer in IRCServer.init 452 // If we need different values we'll have to provide a RPL_MYINFO event. 453 with (parser.server) 454 { 455 aModes = "eIbq"; 456 bModes = "k"; 457 cModes = "flj"; 458 dModes = "CFLMPQScgimnprstuz"; 459 prefixes = "ov"; 460 prefixchars = [ 'o' : '@', 'v' : '+' ]; 461 } 462 } 463 464 enum scissors = "// 8< -- 8< -- 8< -- 8< -- 8< -- 8< -- 8< -- 8< -- 8<\n"; 465 466 buffer.formatClientAssignment(parser.client, parser.server); 467 buffer.put("\n\nparser.typenums = typenumsOf(parser.server.daemon);\n"); 468 469 writeln(); 470 writeln(scissors); 471 writeln(buffer.data); 472 writeln(scissors); 473 writeln("// Paste a raw event string and hit Enter to generate an assert block. " ~ 474 "Ctrl+C to exit."); 475 writeln(); 476 stdout.flush(); 477 478 File file; 479 480 if (outputFile.length) 481 { 482 import std.datetime.systime : Clock; 483 import std.file : exists; 484 import core.time : msecs; 485 486 if (overwrite) 487 { 488 file = File(outputFile, "w"); 489 } 490 else 491 { 492 immutable shouldPad = outputFile.exists; 493 494 file = File(outputFile, "a"); 495 496 if (shouldPad) 497 { 498 file.writeln('\n'); 499 } 500 } 501 502 auto now = Clock.currTime; 503 now.fracSecs = 0.msecs; 504 file.writeln("// ========== ", args[0], ": ", now, '\n'); 505 file.writeln(buffer.data); 506 file.flush(); 507 } 508 509 buffer.clear(); 510 511 IRCClient oldClient = parser.client; 512 IRCServer oldServer = parser.server; 513 string input; 514 515 while ((input = readln()) !is null) 516 { 517 import dialect.common : IRCParseException; 518 import lu.string : unquoted; 519 import std.format : formattedWrite; 520 521 scope(exit) 522 { 523 // Reset input so double enter doesn't display the same event 524 input = string.init; 525 stdin.flush(); 526 stdout.flush(); 527 } 528 529 input = input 530 .strippedLeft(" /") // Remove indents and commentating slashes 531 .chomp //strippedRight; 532 .unquoted; 533 534 if (input.length && (input[$-1] == '$')) input = input[0..$-1]; 535 if (!input.length) continue; 536 537 try 538 { 539 IRCEvent event = parser.toIRCEvent(input); 540 541 buffer.formatEventAssertBlock(event); 542 543 if (parser.updates != IRCParser.Update.nothing) 544 { 545 buffer.put("\n\nwith (parser)\n{\n"); 546 buffer.formatDeltaInto!(Yes.asserts)(oldClient, parser.client, 1, "client"); 547 buffer.formatDeltaInto!(Yes.asserts)(oldServer, parser.server, 1, "server"); 548 buffer.put("}\n"); 549 550 oldClient = parser.client; 551 oldServer = parser.server; 552 parser.updates = IRCParser.Update.nothing; 553 } 554 } 555 catch (IRCParseException e) 556 { 557 buffer.formattedWrite("\n// IRC Parse Exception at %s:%d: %s\n", e.file, e.line, e.msg); 558 559 version(PrintStacktraces) 560 { 561 import std.conv : text; 562 563 buffer.put("/*\n"); 564 buffer.put(e.info.text); 565 buffer.put("\n*/\n"); 566 } 567 } 568 catch (Exception e) 569 { 570 buffer.formattedWrite("\n// Exception at %s:%d: %s\n", e.file, e.line, e.msg); 571 572 version(PrintStacktraces) 573 { 574 buffer.put("/*\n"); 575 buffer.put(e.toString); 576 buffer.put("\n*/\n"); 577 } 578 } 579 580 if (outputFile.length) 581 { 582 file.writeln(buffer.data); 583 file.flush(); 584 } 585 586 writeln(); 587 writeln(buffer.data); 588 writeln(); 589 buffer.clear(); 590 } 591 592 return 0; 593 }