Gray Soft / Ruby Tutorials / Unit Testers Get More Chickstag:graysoftinc.com,2014-03-20:/posts/152014-03-29T03:53:18ZJames Edward Gray IIThe 8th Comment on "Unit Testers Get More Chicks"tag:graysoftinc.com,2008-12-20:/comments/2432014-03-29T03:53:18ZThanks for the high praise.
There are links to the books I've written in the footer of this page, but I haven't done a book just on testing.<p>Thanks for the high praise.</p>
<p>There are links to the books I've written in the footer of this page, but I haven't done a book just on testing.</p>James Edward Gray IIThe 7th Comment on "Unit Testers Get More Chicks"tag:graysoftinc.com,2008-12-20:/comments/2422014-03-29T03:53:18ZExcellent!!!A very understandable way of explaining Ruby testing. It would great to have more of such posts in Ruby testing.
Great way of explaining the topic.
Can you please provide us your second post on Ruby Testing or some similar links el...<p>Excellent!!!A very understandable way of explaining Ruby testing. It would great to have more of such posts in Ruby testing.</p>
<p>Great way of explaining the topic.<br>
Can you please provide us your second post on Ruby Testing or some similar links elsewhere.Or do you have any books in plan or already published.</p>
<p>Really looking forward to it.</p>NabinThe 6th Comment on "Unit Testers Get More Chicks"tag:graysoftinc.com,2007-11-29:/comments/1412014-03-29T03:52:14ZA good place to view IRC details is [this site](http://www.networksorcery.com/enp/protocol/irc.htm#RFCs).<p>A good place to view IRC details is <a href="http://www.networksorcery.com/enp/protocol/irc.htm#RFCs">this site</a>.</p>James Edward Gray IIThe 5th Comment on "Unit Testers Get More Chicks"tag:graysoftinc.com,2007-11-29:/comments/1402014-03-29T03:52:14ZThank you very much for the excellent article. This helped me where other tutorials failed by actually verbosely demonstrating what ought to be tested and what kinds of test one would perform. Most tutorials simply toss out the methods supported b...<p>Thank you very much for the excellent article. This helped me where other tutorials failed by actually verbosely demonstrating what ought to be tested and what kinds of test one would perform. Most tutorials simply toss out the methods supported by <code>Test::Unit</code>, provide 1-2 simple cases and move on. My appreciation, sir.</p>
<p>As a suggestion, I would provide a link to an IRC reference/specification toward the top of the article. The testing aspect was reasonably clear but at times it would've been nice to have something to look at for more information. While this information is Google-able, one source for everyone reading and playing along could be nice. (Shrug)</p>Daniel LindsleyThe 4th Comment on "Unit Testers Get More Chicks"tag:graysoftinc.com,2007-07-14:/comments/1102014-03-29T03:51:03ZWow, that's helpful. Finally, Ruby unit testing made understandable. Thanks a bunch.
<p>Wow, that's helpful. Finally, Ruby unit testing made understandable. Thanks a bunch.</p>Benjamin KudriaThe 3rd Comment on "Unit Testers Get More Chicks"tag:graysoftinc.com,2006-04-21:/comments/372014-03-27T01:38:23ZI've only been through the first 1/5th of the article, but so far I am loving it! Finally, a sufficiently complete unit testing example. I didn't need to be sold on the unit testing idea since I loved it the very second I heard about it, but this ...<p>I've only been through the first 1/5th of the article, but so far I am loving it! Finally, a sufficiently complete unit testing example. I didn't need to be sold on the unit testing idea since I loved it the very second I heard about it, but this long example was much needed. Thanks!</p>Haris SkiadasThe 2nd Comment on "Unit Testers Get More Chicks"tag:graysoftinc.com,2006-04-21:/comments/362014-03-27T01:38:23ZI sent you my rather lengthy coments for the lengthy article via email, but I really enjoyed this post! There a bunch of neat tricks in there and I also liked the step by step approach.
Very cool!<p>I sent you my rather lengthy coments for the lengthy article via email, but I really enjoyed this post! There a bunch of neat tricks in there and I also liked the step by step approach.</p>
<p>Very cool!</p>Gregory BrownThe 1st Comment on "Unit Testers Get More Chicks"tag:graysoftinc.com,2006-04-21:/comments/352014-03-27T01:38:23ZNice article, James. It reminded me of a perpetually on-hold toy project I have to recreate Spip (can't find a link, but he was based off of Infobot - [http://www.infobot.org/](http://www.infobot.org/)) in Ruby.
Especially liked the step-by-ste...<p>Nice article, James. It reminded me of a perpetually on-hold toy project I have to recreate Spip (can't find a link, but he was based off of Infobot - <a href="http://www.infobot.org/">http://www.infobot.org/</a>) in Ruby.</p>
<p>Especially liked the step-by-step approach - for me one of the biggest gains in unit testing is that it enforces small steps but often articles don't fully get across the short turnaround time that can give, but here it's plain to see. Good stuff.</p>Ross BamfordUnit Testers Get More Chickstag:graysoftinc.com,2006-04-21:/posts/152014-03-29T03:53:18ZA complete example of Test-Driven Development from start to finish. Make sure you have a little free time when you tackle this one, it's pretty long.<p>Just recently, a developer I respect very much was caught uttering the this surprising statement: "Unit tests just really aren't my thing." Now, I still respect this developer very much and I can tell you that the person single-handedly created one of my very favorite pieces of software. However, I do think the developer is dead wrong on this point. This is my attempt to change the mind of everyone that thinks similar things about unit testing.</p>
<p>My belief is that unit testing is for everyone and, in fact, I'll go so far as to say that I believe becoming a test-driven developer is the single best change a programmer can make in their day to day routine. Here are just some of the reasons why:</p>
<ul>
<li>Though counterintuitive, I swear that it makes you code faster. No one tends to believe me on this, but most test-driven developers come to this realization eventually. The reason is simple: you spend <em>much</em> less time debugging the 935 errors from your two hour code sprees.</li>
<li>The tests are my memory. My head is too full of all the languages, frameworks, APIs, and family birthdays I am expected to know to remember everything I've ever done on top of that. This week at work I've touched three separate projects all with over 1,000 lines of code. I'm sure I had good ideas when I wrote those lines, but now I doubt I can tell you what they are. My tests can though. They remember so I don't have to. If I go into the code and change something I don't remember was needed or why, my tests will remind me immediately.</li>
<li>In team development, my test powered memory even travels to the machines of the other developers! How cool is that? When someone goes into the code and says, "Why on Earth did James do this? We don't need this. I'm going to change that…" my tests will look after my interests for me.</li>
<li>I'm a lot more confident in my software. I use to say things like, "I just
finished this, so it hasn't been used much yet and probably still has plenty
of issues." Now by the time I finish something my tests have been using the
heck out of it. I won't kid you and tell you that my software now springs
fully formed from the head of my tests, but it definitely comes out farther
along the track.</li>
<li>Environment changes seldom surprise me anymore. When I move my software to
a different box and it can't cope for whatever reason, I not only know the
first time I run the tests, I have an excellent idea of exactly where the
problem is.</li>
</ul><p>Believe me I could go on and on. I have to the poor developers who have set me off in the past. I'll spare you my sermon though and try something else. Let's test drive the creation of some software!</p>
<h4>Scribe</h4>
<p>I've recently discovered the joys of IRC. (IRC, or Internet Relay Chat, is kind-of an underground chat network for the Internet. You can Google for more details if you need them.) Yes, I know I am the last geek on Earth to be enlightened, but late or not I finally made it to the party.</p>
<p>Shortly after this discovery, I did what any good programmer would do: I built an IRC bot. I learned just enough of the protocol to get something running and turned it loose. Just between you and me, that bot sucks. But I now know enough to build a better one!</p>
<p>Let's do just that. I will call my new creation Scribe. Let's start sketching out some tests for Scribe…</p>
<h4>Mock Socket</h4>
<p>IRC is a pretty simple protocol over TCP/IP sockets. What makes it easy to deal with is that it is a line based protocol, so we can just print command lines to it and read response lines back very similar to how we often deal with ordinary files.</p>
<p>Now, we don't want the bot to actually connect to an IRC server when we are testing it. There are a lot of reasons for that, but my biggest one is that we don't want to be waiting on the network. Tests need to be fast, so they are painless to run and we don't get bogged down using them.</p>
<p>If we just had a trivial fake server we could feed lines to and then it would slowly handout those lines to our bot as requested, we could be reasonably sure it is working as intended. This won't be a completely real IRC environment, but it should allow us to ensure the core behaviors. We can work out the networking oddities once we get that far. So, I created a file <code>scribe/test/mock/tcp_socket.rb</code> with the following contents:</p>
<div class="highlight highlight-ruby"><pre><span class="c1">#!/usr/local/bin/ruby -w</span>
<span class="vg">$server_responses</span> <span class="o">=</span> <span class="nb">Array</span><span class="o">.</span><span class="n">new</span>
<span class="vg">$client_output</span> <span class="o">=</span> <span class="nb">String</span><span class="o">.</span><span class="n">new</span>
<span class="k">class</span> <span class="nc">MockTCPSocket</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">host</span><span class="p">,</span> <span class="n">port</span><span class="p">)</span>
<span class="c1"># we don't need +host+ or +port+--just keeping the interface</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">gets</span>
<span class="k">if</span> <span class="vg">$server_responses</span><span class="o">.</span><span class="n">empty?</span>
<span class="k">raise</span> <span class="s2">"The server is out of responses."</span>
<span class="k">else</span>
<span class="vg">$server_responses</span><span class="o">.</span><span class="n">shift</span> <span class="o">+</span> <span class="s2">"</span><span class="se">\r\n</span><span class="s2">"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">print</span><span class="p">(</span><span class="n">line</span><span class="p">)</span>
<span class="vg">$client_output</span> <span class="o"><<</span> <span class="n">line</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>Alright, before you guys can fire up the mail clients and pour on the hate mail, let's address the two sins I just committed in that tiny bit of code. First, I used global variables. Yes, I hesitated for at least half a second when I typed them and I'm sure someone will be quick to point out that <code>$server_responses</code> could be a class variable with an append method to use it. That's true. Of course, it's a touch longer to build that way, and worse, a little bit longer to use each time since I need the class name followed by a method call. And for that added annoyance what do you gain? Zip. It's still global to all the connections. Some might say it's easier to clobber the global data than go through the explicit class interface but I'm writing the mock, the tests, and the actual bot and I promise to be good. Given that, I choose the easy way out.</p>
<p>Second sin: there are no tests for this code! Here I am bragging about how tests cure cancer, and the first thing I do is write code without tests. It's a world gone mad. OK, let's clear this up early or it is going to be a very bumpy ride through the rest of this process: tests are for covering risks. Above I made an <code>Array</code> and a <code>String</code>, and provided a very, very trivial interface to them.</p>
<p>The risk factor so far is about 0.125%. I'm a gambling man so I'm willing to take my chances there. It cracks me up when I see someone write an <code>attr_reader()</code> call, then go write some tests for it. That is Ruby's concern not yours. Stick to your own challenges, because there will be plenty of those to keep you busy.</p>
<h4>Login Tests</h4>
<p>When we first connect to an IRC server, we have to start the conversation with two messages. One is the <code>NICK</code> command, for the nickname you want the bot to use. It looks like this:</p>
<pre><code>NICK scribe
</code></pre>
<p>Let's get a connection sending just that much and call it a start. I start with the tests, because it makes me think about the interface before I write the code to do it. I find that saves me the trouble of building something only to find out that it's a pain to use and I need to rethink it. Let's create another file <code>scribe/test/tc_irc_connection.rb</code> and add a test:</p>
<div class="highlight highlight-ruby"><pre><span class="c1">#!/usr/local/bin/ruby -w</span>
<span class="nb">require</span> <span class="s2">"test/unit"</span>
<span class="nb">require</span> <span class="s2">"scribe/irc/connection"</span>
<span class="nb">require</span> <span class="s2">"mock/tcp_socket"</span>
<span class="k">class</span> <span class="nc">TestIRCConnection</span> <span class="o"><</span> <span class="no">Test</span><span class="o">::</span><span class="no">Unit</span><span class="o">::</span><span class="no">TestCase</span>
<span class="k">def</span> <span class="nf">test_construction</span>
<span class="vi">@con</span> <span class="o">=</span> <span class="no">Scribe</span><span class="o">::</span><span class="no">IRC</span><span class="o">::</span><span class="no">Connection</span><span class="o">.</span><span class="n">new</span><span class="p">(</span> <span class="s2">"some.host.net"</span><span class="p">,</span>
<span class="ss">:socket_class</span> <span class="o">=></span> <span class="no">MockTCPSocket</span> <span class="p">)</span>
<span class="n">assert_not_nil</span><span class="p">(</span><span class="vi">@con</span><span class="p">)</span>
<span class="n">assert_instance_of</span><span class="p">(</span><span class="no">Scribe</span><span class="o">::</span><span class="no">IRC</span><span class="o">::</span><span class="no">Connection</span><span class="p">,</span> <span class="vi">@con</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>Oops, I went to build a <code>nick()</code> method and realized I need to construct a connection first. These are just a couple of tests to make sure we get a connection to use in all the other tests.</p>
<p>Now let's add one more file <code>scribe/lib/scribe/irc/connection.rb</code>. Here's enough code to create a connection:</p>
<div class="highlight highlight-ruby"><pre><span class="c1">#!/usr/local/bin/ruby -w</span>
<span class="nb">require</span> <span class="s2">"socket"</span>
<span class="k">module</span> <span class="nn">Scribe</span>
<span class="k">module</span> <span class="nn">IRC</span>
<span class="k">class</span> <span class="nc">Connection</span>
<span class="no">DEFAULT_OPTIONS</span> <span class="o">=</span> <span class="p">{</span> <span class="ss">:port</span> <span class="o">=></span> <span class="mi">6667</span><span class="p">,</span>
<span class="ss">:socket_class</span> <span class="o">=></span> <span class="no">TCPSocket</span> <span class="p">}</span><span class="o">.</span><span class="n">freeze</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">host</span><span class="p">,</span> <span class="n">options</span> <span class="o">=</span> <span class="no">Hash</span><span class="o">.</span><span class="n">new</span><span class="p">)</span>
<span class="n">options</span> <span class="o">=</span> <span class="no">DEFAULT_OPTIONS</span><span class="o">.</span><span class="n">merge</span><span class="p">(</span><span class="n">options</span><span class="p">)</span>
<span class="vi">@host</span><span class="p">,</span> <span class="vi">@port</span> <span class="o">=</span> <span class="n">host</span><span class="p">,</span> <span class="n">options</span><span class="o">[</span><span class="ss">:port</span><span class="o">]</span>
<span class="vi">@irc_server</span> <span class="o">=</span> <span class="n">options</span><span class="o">[</span><span class="ss">:socket_class</span><span class="o">].</span><span class="n">new</span><span class="p">(</span><span class="vi">@host</span><span class="p">,</span> <span class="vi">@port</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>There shouldn't be any surprises in there. I provided an option to allow you to switch classes for testing purposes. Since I already need two options and I am not sure if it is going to grow more, I went ahead and used an option <code>Hash</code> to give myself plenty of room for expansion.</p>
<p>That should be enough to get us going. Let's see if they run:</p>
<pre><code>$ ruby -I lib:test test/tc_irc_connection.rb
Loaded suite test/tc_irc_connection
Started
.
Finished in 0.000344 seconds.
1 tests, 2 assertions, 0 failures, 0 errors
</code></pre>
<p>No mistakes yet? I deserve a cookie! Be right back… OK, I have my treat and am getting crumbs all over the keyboard. We can move on now.</p>
<p>I would love to claim that all just worked on the first try because I am a brilliant programmer, but the truth is that it works because I am moving in small steps. There's not a lot of room for error. This is yet another benefit of unit testing: it encourages you to write small methods. Small methods mean you make less mistakes and when you do screw up, you only have a few lines to bug hunt in. It's easy.</p>
<p>OK, my stalling worked. I finished the cookie. Let's move ahead.</p>
<p>Now I am really ready for the <code>NICK</code> tests:</p>
<div class="highlight highlight-ruby"><pre><span class="k">class</span> <span class="nc">TestIRCConnection</span> <span class="o"><</span> <span class="no">Test</span><span class="o">::</span><span class="no">Unit</span><span class="o">::</span><span class="no">TestCase</span>
<span class="k">def</span> <span class="nf">setup</span>
<span class="n">test_construction</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">test_nick</span>
<span class="n">assert_nothing_raised</span><span class="p">(</span><span class="no">Exception</span><span class="p">)</span> <span class="p">{</span> <span class="vi">@con</span><span class="o">.</span><span class="n">nick</span><span class="p">(</span><span class="s2">"scribe"</span><span class="p">)</span> <span class="p">}</span>
<span class="n">assert_equal</span><span class="p">(</span><span class="s2">"NICK scribe</span><span class="se">\r\n</span><span class="s2">"</span><span class="p">,</span> <span class="vg">$client_output</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>I'm using new testing trick here. You can define a <code>setup()</code> method and it will be called before every single test method is executed. Since I already have a method for building an object, we can just run that. (That's why I had that method use an instance variable instead of a local, but you already knew that didn't you?) This has another side effect, it artificially inflates the test count which is just great for my ego!</p>
<p>Alright, let's see if we can add enough code to the library to get that running:</p>
<div class="highlight highlight-ruby"><pre><span class="k">module</span> <span class="nn">Scribe</span>
<span class="k">module</span> <span class="nn">IRC</span>
<span class="k">class</span> <span class="nc">Connection</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">nick</span><span class="p">(</span><span class="nb">name</span><span class="p">)</span>
<span class="n">send_command</span> <span class="s2">"NICK </span><span class="si">#{</span><span class="nb">name</span><span class="si">}</span><span class="s2">"</span>
<span class="k">end</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">send_command</span><span class="p">(</span><span class="n">cmd</span><span class="p">)</span>
<span class="vi">@irc_server</span><span class="o">.</span><span class="n">print</span> <span class="s2">"</span><span class="si">#{</span><span class="n">cmd</span><span class="si">}</span><span class="se">\r\n</span><span class="s2">"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>How are we doing?</p>
<pre><code>$ ruby -I lib:test test/tc_irc_connection.rb
Loaded suite test/tc_irc_connection
Started
..
Finished in 0.000854 seconds.
2 tests, 8 assertions, 0 failures, 0 errors
</code></pre>
<p>Eight passing tests and it feels like they are just flying right by. We rock.</p>
<p>The other half of an IRC login is much the same. We need to send a <code>USER</code> command, in this format:</p>
<pre><code>USER scribe <host_name> <server_name> :"scribe" IRC bot
</code></pre>
<p>We don't need to dwell on <code>host_name</code> and <code>server_name</code> too long since the IRC RCF literally says that they are normally ignored from clients for security reasons. Let's create a test for this method:</p>
<div class="highlight highlight-ruby"><pre><span class="k">class</span> <span class="nc">TestIRCConnection</span> <span class="o"><</span> <span class="no">Test</span><span class="o">::</span><span class="no">Unit</span><span class="o">::</span><span class="no">TestCase</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">test_user</span>
<span class="n">assert_nothing_raised</span><span class="p">(</span><span class="no">Exception</span><span class="p">)</span> <span class="k">do</span>
<span class="vi">@con</span><span class="o">.</span><span class="n">user</span><span class="p">(</span><span class="s2">"scribe"</span><span class="p">,</span> <span class="s2">"127.0.0.1"</span><span class="p">,</span> <span class="s2">"127.0.0.1"</span><span class="p">,</span> <span class="s1">'"scribe" IRC bot'</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">assert_equal</span><span class="p">(</span> <span class="s2">"USER scribe 127.0.0.1 127.0.0.1 :</span><span class="se">\"</span><span class="s2">scribe</span><span class="se">\"</span><span class="s2"> IRC bot</span><span class="se">\r\n</span><span class="s2">"</span><span class="p">,</span>
<span class="vg">$client_output</span> <span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>The library code for that is also pretty trivial:</p>
<div class="highlight highlight-ruby"><pre><span class="k">module</span> <span class="nn">Scribe</span>
<span class="k">module</span> <span class="nn">IRC</span>
<span class="k">class</span> <span class="nc">Connection</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">user</span><span class="p">(</span><span class="n">nick</span><span class="p">,</span> <span class="n">host_name</span><span class="p">,</span> <span class="n">server_name</span><span class="p">,</span> <span class="n">full_name</span><span class="p">)</span>
<span class="n">send_command</span> <span class="s2">"USER </span><span class="si">#{</span><span class="n">nick</span><span class="si">}</span><span class="s2"> </span><span class="si">#{</span><span class="n">host_name</span><span class="si">}</span><span class="s2"> </span><span class="si">#{</span><span class="n">server_name</span><span class="si">}</span><span class="s2"> :</span><span class="si">#{</span><span class="n">full_name</span><span class="si">}</span><span class="s2">"</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>Great. Let's see how we did:</p>
<pre><code>$ ruby -I lib:test test/tc_irc_connection.rb
Loaded suite test/tc_irc_connection
Started
..F
Finished in 0.01194 seconds.
1) Failure:
test_user(TestIRCConnection) [test/tc_irc_connection.rb:34]:
<"USER scribe 127.0.0.1 127.0.0.1 :\"scribe\" IRC bot\r\n"> expected but was
<"NICK scribe\r\nUSER scribe 127.0.0.1 127.0.0.1 :\"scribe\" IRC bot\r\n">.
3 tests, 12 assertions, 1 failures, 0 errors
</code></pre>
<p>Ouch! Our first failure. What went wrong?</p>
<p>Luckily, the failure includes all the info we need to get right to the heart of the problem. The line number shows us that <code>$client_output</code> didn't hold what we expected it to. Reading on, the failure message shows that the line we wanted is in there, but the <code>NICK</code> line is also still in there. Oops, we never cleared the globals. Let's add a method to the tests that handles that:</p>
<div class="highlight highlight-ruby"><pre><span class="k">class</span> <span class="nc">TestIRCConnection</span> <span class="o"><</span> <span class="no">Test</span><span class="o">::</span><span class="no">Unit</span><span class="o">::</span><span class="no">TestCase</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">teardown</span>
<span class="vg">$server_responses</span><span class="o">.</span><span class="n">clear</span>
<span class="vg">$client_output</span><span class="o">.</span><span class="n">clear</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span>
</pre></div>
<p>This method is the opposite of <code>setup()</code>. It is called after each test method is run, and thus can handle cleanup tasks like the one we use it for here.</p>
<p>Does that fix our tests?</p>
<pre><code>$ ruby -I lib:test test/tc_irc_connection.rb
Loaded suite test/tc_irc_connection
Started
...
Finished in 0.001129 seconds.
3 tests, 12 assertions, 0 failures, 0 errors
</code></pre>
<p>Sure does.</p>
<p>Now that I have put both of those in, I saw a way to refactor them a bit. What's great is that I can do this and use the tests to make sure I don't screw anything up. Let's make the change. First I comment out <code>nick()</code> and <code>user()</code>. Then I add:</p>
<div class="highlight highlight-ruby"><pre><span class="k">module</span> <span class="nn">Scribe</span>
<span class="k">module</span> <span class="nn">IRC</span>
<span class="k">class</span> <span class="nc">Connection</span>
<span class="c1"># ...</span>
<span class="no">COMMAND_METHODS</span> <span class="o">=</span> <span class="o">[</span><span class="ss">:nick</span><span class="p">,</span> <span class="ss">:user</span><span class="o">].</span><span class="n">freeze</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">method_missing</span><span class="p">(</span><span class="n">meth</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">&</span><span class="n">block</span><span class="p">)</span>
<span class="k">if</span> <span class="no">COMMAND_METHODS</span><span class="o">.</span><span class="n">include?</span><span class="p">(</span><span class="n">meth</span><span class="p">)</span>
<span class="n">params</span> <span class="o">=</span> <span class="n">args</span><span class="o">.</span><span class="n">dup</span>
<span class="n">params</span><span class="o">[-</span><span class="mi">1</span><span class="o">]</span> <span class="o">=</span> <span class="s2">":</span><span class="si">#{</span><span class="n">params</span><span class="o">.</span><span class="n">last</span><span class="si">}</span><span class="s2">"</span> <span class="k">if</span> <span class="n">params</span><span class="o">[-</span><span class="mi">1</span><span class="o">]</span> <span class="o">=~</span> <span class="sr">/\A[^:].* /</span>
<span class="n">send_command</span> <span class="s2">"</span><span class="si">#{</span><span class="n">meth</span><span class="o">.</span><span class="n">to_s</span><span class="o">.</span><span class="n">upcase</span><span class="si">}</span><span class="s2"> </span><span class="si">#{</span><span class="n">params</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s1">' '</span><span class="p">)</span><span class="si">}</span><span class="s2">"</span>
<span class="k">else</span>
<span class="k">super</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>That's not a huge change for just <code>nick()</code> and <code>user()</code>, but as the software grows and we add more output methods it is now as simple as adding a new method name to the Array. (I use this <code>Array</code> to keep <code>method_missing()</code> from swallowing too many methods.) That should pay us back eventually.</p>
<p>Let's make sure the tests didn't change:</p>
<pre><code>$ ruby -I lib:test test/tc_irc_connection.rb
Loaded suite test/tc_irc_connection
Started
...
Finished in 0.001103 seconds.
3 tests, 12 assertions, 0 failures, 0 errors
</code></pre>
<p>Perfect. I'm happy and the tests are happy. I can now delete the two obsolete methods.</p>
<h4>IRC Talks Back</h4>
<p>When we get this far, things are about to get a lot more interesting. IRC is about to start talking back to us. Now we need to listen as well as send. Getting a command from the IRC server is easy enough, so let's start there:</p>
<div class="highlight highlight-ruby"><pre><span class="k">class</span> <span class="nc">TestIRCConnection</span> <span class="o"><</span> <span class="no">Test</span><span class="o">::</span><span class="no">Unit</span><span class="o">::</span><span class="no">TestCase</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">test_recv_command</span>
<span class="n">mes</span> <span class="o">=</span> <span class="s2">":calvino.freenode.net 001 scribe "</span> <span class="o">+</span>
<span class="s2">":Welcome to the freenode IRC Network scribe"</span>
<span class="vg">$server_responses</span> <span class="o"><<</span> <span class="n">mes</span>
<span class="n">assert_equal</span><span class="p">(</span><span class="n">mes</span><span class="p">,</span> <span class="vi">@con</span><span class="o">.</span><span class="n">recv_command</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>And here is the library code for that method:</p>
<div class="highlight highlight-ruby"><pre><span class="k">module</span> <span class="nn">Scribe</span>
<span class="k">module</span> <span class="nn">IRC</span>
<span class="k">class</span> <span class="nc">Connection</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">recv_command</span>
<span class="n">cmd</span> <span class="o">=</span> <span class="vi">@irc_server</span><span class="o">.</span><span class="n">gets</span>
<span class="n">cmd</span><span class="o">.</span><span class="n">nil?</span> <span class="p">?</span> <span class="n">cmd</span> <span class="p">:</span> <span class="n">cmd</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sr">/\r\n\Z/</span><span class="p">,</span> <span class="s2">""</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>Checking the tests:</p>
<pre><code>$ ruby -I lib:test test/tc_irc_connection.rb
Loaded suite test/tc_irc_connection
Started
....
Finished in 0.001335 seconds.
4 tests, 15 assertions, 0 failures, 0 errors
</code></pre>
<p>Looking good. Except I don't like the idea of using that command all the time. Those messages have a set format and I would rather get them bundled up in an easy to deal with Ruby object. Let's see if we can add a programmer friendly layer of command wrapping on there. Now how would I want that to look? (Tests are just thinking-in-code…)</p>
<div class="highlight highlight-ruby"><pre><span class="k">class</span> <span class="nc">TestIRCConnection</span> <span class="o"><</span> <span class="no">Test</span><span class="o">::</span><span class="no">Unit</span><span class="o">::</span><span class="no">TestCase</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">test_recv</span>
<span class="vg">$server_responses</span> <span class="o"><<</span> <span class="s2">":calvino.freenode.net 001 scribe "</span> <span class="o">+</span>
<span class="s2">":Welcome to the freenode IRC Network scribe"</span>
<span class="n">cmd</span> <span class="o">=</span> <span class="vi">@con</span><span class="o">.</span><span class="n">recv</span>
<span class="n">assert_not_nil</span><span class="p">(</span><span class="n">cmd</span><span class="p">)</span>
<span class="n">assert_instance_of</span><span class="p">(</span><span class="no">Scribe</span><span class="o">::</span><span class="no">IRC</span><span class="o">::</span><span class="no">Connection</span><span class="o">::</span><span class="no">Command</span><span class="p">,</span> <span class="n">cmd</span><span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span><span class="s2">"calvino.freenode.net"</span><span class="p">,</span> <span class="n">cmd</span><span class="o">.</span><span class="n">prefix</span><span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span><span class="s2">"001"</span><span class="p">,</span> <span class="n">cmd</span><span class="o">.</span><span class="n">command</span><span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span> <span class="o">[</span><span class="s2">"scribe"</span><span class="p">,</span> <span class="s2">"Welcome to the freenode IRC Network scribe"</span><span class="o">]</span><span class="p">,</span>
<span class="n">cmd</span><span class="o">.</span><span class="n">params</span> <span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span> <span class="s2">"Welcome to the freenode IRC Network scribe"</span><span class="p">,</span>
<span class="n">cmd</span><span class="o">.</span><span class="n">last_param</span> <span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>Notice that I did sneak in a little duplication with that last test. The last parameter of an IRC command is often special, since it is the only one that can contain spaces. Because of that, it might be nice to be able to treat that one a little differently.</p>
<p>Also notice that the colons have been removed from the prefix and the final parameter. Those are part of the IRC protocol, not the items themselves.</p>
<p>Let's add the code to the library for this:</p>
<div class="highlight highlight-ruby"><pre><span class="k">module</span> <span class="nn">Scribe</span>
<span class="k">module</span> <span class="nn">IRC</span>
<span class="k">class</span> <span class="nc">Connection</span>
<span class="no">Command</span> <span class="o">=</span> <span class="no">Struct</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="ss">:prefix</span><span class="p">,</span> <span class="ss">:command</span><span class="p">,</span> <span class="ss">:params</span><span class="p">,</span> <span class="ss">:last_param</span><span class="p">)</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">recv</span>
<span class="n">cmd</span> <span class="o">=</span> <span class="n">recv_command</span>
<span class="k">return</span> <span class="n">cmd</span> <span class="k">if</span> <span class="n">cmd</span><span class="o">.</span><span class="n">nil?</span>
<span class="n">cmd</span> <span class="o">=~</span> <span class="sr">/ \A (?::([^\040]+)\040)? # prefix</span>
<span class="sr"> ([A-Za-z]+|\d{3}) # command</span>
<span class="sr"> ((?:\040[^:][^\040]+)*) # params, minus last</span>
<span class="sr"> (?:\040:?(.*))? # last param</span>
<span class="sr"> \Z /x</span> <span class="ow">or</span> <span class="k">raise</span> <span class="s2">"Malformed IRC command."</span>
<span class="no">Command</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="vg">$1</span><span class="p">,</span> <span class="vg">$2</span><span class="p">,</span> <span class="vg">$3</span><span class="o">.</span><span class="n">split</span> <span class="o">+</span> <span class="o">[</span><span class="vg">$4</span><span class="o">]</span><span class="p">,</span> <span class="vg">$4</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>I know, I know, scary Regexp usage in there. I really took it pretty much right out of the IRC RFC though. (Hint for breaking it down: <code>\040</code> is just a space, but needed when using the <code>/x</code> Regexp modifier.)</p>
<p>It even seems to work:</p>
<pre><code>$ ruby -I lib:test test/tc_irc_connection.rb
Loaded suite test/tc_irc_connection
Started
.....
Finished in 0.001553 seconds.
5 tests, 23 assertions, 0 failures, 0 errors
</code></pre>
<p>Now remember what I said about risk? I'm not feeling super sure about this code yet, since the difficultly just ramped up a notch. The risk went up, so I'm going to counter with some more tests:</p>
<div class="highlight highlight-ruby"><pre><span class="k">class</span> <span class="nc">TestIRCConnection</span> <span class="o"><</span> <span class="no">Test</span><span class="o">::</span><span class="no">Unit</span><span class="o">::</span><span class="no">TestCase</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">test_recv</span>
<span class="vg">$server_responses</span> <span class="o"><<</span> <span class="s2">":calvino.freenode.net 001 scribe "</span> <span class="o">+</span>
<span class="s2">":Welcome to the freenode IRC Network scribe"</span>
<span class="n">cmd</span> <span class="o">=</span> <span class="vi">@con</span><span class="o">.</span><span class="n">recv</span>
<span class="n">assert_not_nil</span><span class="p">(</span><span class="n">cmd</span><span class="p">)</span>
<span class="n">assert_instance_of</span><span class="p">(</span><span class="no">Scribe</span><span class="o">::</span><span class="no">IRC</span><span class="o">::</span><span class="no">Connection</span><span class="o">::</span><span class="no">Command</span><span class="p">,</span> <span class="n">cmd</span><span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span><span class="s2">"calvino.freenode.net"</span><span class="p">,</span> <span class="n">cmd</span><span class="o">.</span><span class="n">prefix</span><span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span><span class="s2">"001"</span><span class="p">,</span> <span class="n">cmd</span><span class="o">.</span><span class="n">command</span><span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span> <span class="o">[</span><span class="s2">"scribe"</span><span class="p">,</span> <span class="s2">"Welcome to the freenode IRC Network scribe"</span><span class="o">]</span><span class="p">,</span>
<span class="n">cmd</span><span class="o">.</span><span class="n">params</span> <span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span> <span class="s2">"Welcome to the freenode IRC Network scribe"</span><span class="p">,</span>
<span class="n">cmd</span><span class="o">.</span><span class="n">last_param</span> <span class="p">)</span>
<span class="vg">$server_responses</span> <span class="o"><<</span> <span class="s2">"TIME"</span>
<span class="n">cmd</span> <span class="o">=</span> <span class="vi">@con</span><span class="o">.</span><span class="n">recv</span>
<span class="n">assert_not_nil</span><span class="p">(</span><span class="n">cmd</span><span class="p">)</span>
<span class="n">assert_instance_of</span><span class="p">(</span><span class="no">Scribe</span><span class="o">::</span><span class="no">IRC</span><span class="o">::</span><span class="no">Connection</span><span class="o">::</span><span class="no">Command</span><span class="p">,</span> <span class="n">cmd</span><span class="p">)</span>
<span class="n">assert_nil</span><span class="p">(</span><span class="n">cmd</span><span class="o">.</span><span class="n">prefix</span><span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span><span class="s2">"TIME"</span><span class="p">,</span> <span class="n">cmd</span><span class="o">.</span><span class="n">command</span><span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span><span class="nb">Array</span><span class="o">.</span><span class="n">new</span><span class="p">,</span> <span class="n">cmd</span><span class="o">.</span><span class="n">params</span><span class="p">)</span>
<span class="n">assert_nil</span><span class="p">(</span><span class="n">cmd</span><span class="o">.</span><span class="n">last_param</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>Let's see if it can handle that correctly as well:</p>
<pre><code>$ ruby -I lib:test test/tc_irc_connection.rb
Loaded suite test/tc_irc_connection
Started
..F..
Finished in 0.012277 seconds.
1) Failure:
test_recv(TestIRCConnection) [test/tc_irc_connection.rb:63]:
<[]> expected but was
<[nil]>.
5 tests, 28 assertions, 1 failures, 0 errors
</code></pre>
<p>Bingo. My finely tuned rocky code instincts triumph again! (It could also be dumb luck, but I'm an optimist.)</p>
<p>Let's see if we can snap that edge case into shape:</p>
<div class="highlight highlight-ruby"><pre><span class="k">module</span> <span class="nn">Scribe</span>
<span class="k">module</span> <span class="nn">IRC</span>
<span class="k">class</span> <span class="nc">Connection</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">recv</span>
<span class="n">cmd</span> <span class="o">=</span> <span class="n">recv_command</span>
<span class="k">return</span> <span class="n">cmd</span> <span class="k">if</span> <span class="n">cmd</span><span class="o">.</span><span class="n">nil?</span>
<span class="n">cmd</span> <span class="o">=~</span> <span class="sr">/ \A (?::([^\040]+)\040)? # prefix</span>
<span class="sr"> ([A-Za-z]+|\d{3}) # command</span>
<span class="sr"> ((?:\040[^:][^\040]+)*) # params, minus last</span>
<span class="sr"> (?:\040:?(.*))? # last param</span>
<span class="sr"> \Z /x</span> <span class="ow">or</span> <span class="k">raise</span> <span class="s2">"Malformed IRC command."</span>
<span class="n">params</span> <span class="o">=</span> <span class="vg">$3</span><span class="o">.</span><span class="n">split</span> <span class="o">+</span> <span class="p">(</span><span class="vg">$4</span><span class="o">.</span><span class="n">nil?</span> <span class="p">?</span> <span class="nb">Array</span><span class="o">.</span><span class="n">new</span> <span class="p">:</span> <span class="o">[</span><span class="vg">$4</span><span class="o">]</span><span class="p">)</span>
<span class="no">Command</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="vg">$1</span><span class="p">,</span> <span class="vg">$2</span><span class="p">,</span> <span class="n">params</span><span class="p">,</span> <span class="n">params</span><span class="o">.</span><span class="n">last</span><span class="p">)</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>That got us there:</p>
<pre><code>$ ruby -I lib:test test/tc_irc_connection.rb
Loaded suite test/tc_irc_connection
Started
.....
Finished in 0.001852 seconds.
5 tests, 29 assertions, 0 failures, 0 errors
</code></pre>
<p>I added one more test just to temp fate again, but it passed and I felt like I had now applied the proper level of paranoia. I won't bore you with that code though, since it looks the same as the last two. I also flipped <code>recv_command()</code> to a private method (<code>recv()</code> became the public interface) and adjusted the test to deal with that, but that wasn't too exciting. Let's move on.</p>
<p>Now, we still have a tiny problem with input. Let me show you the first few commands I see from an actual IRC connection (sending <code>NICK</code>, then <code>USER</code>, then just reading):</p>
<pre><code>NOTICE AUTH :*** Looking up your hostname...
NOTICE AUTH :*** Found your hostname, welcome back
NOTICE AUTH :*** Checking ident
NOTICE AUTH :*** No identd (auth) response
:calvino.freenode.net 001 scribe :Welcome to the freenode IRC Network scribe
</code></pre>
<p>The server did OK my login (command <code>001</code> is <code>RPL_WELCOME</code>), but while I was sending <code>NICK</code> and <code>USER</code>, the sever sent me a little chatter and now the response I am looking for is buried. We need to deal with that. Here are some tests that flesh out the interface I have in mind:</p>
<div class="highlight highlight-ruby"><pre><span class="k">class</span> <span class="nc">TestIRCConnection</span> <span class="o"><</span> <span class="no">Test</span><span class="o">::</span><span class="no">Unit</span><span class="o">::</span><span class="no">TestCase</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">test_recv_until</span>
<span class="vg">$server_responses</span> <span class="o"><<</span>
<span class="s2">"NOTICE AUTH :*** Looking up your hostname..."</span> <span class="o"><<</span>
<span class="s2">"NOTICE AUTH :*** Found your hostname, welcome back"</span> <span class="o"><<</span>
<span class="s2">"NOTICE AUTH :*** Checking ident"</span> <span class="o"><<</span>
<span class="s2">"NOTICE AUTH :*** No identd (auth) response"</span> <span class="o"><<</span>
<span class="s2">":calvino.freenode.net 001 scribe "</span> <span class="o">+</span>
<span class="s2">":Welcome to the freenode IRC Network scribe"</span>
<span class="n">cmd</span> <span class="o">=</span> <span class="vi">@con</span><span class="o">.</span><span class="n">recv_until</span> <span class="p">{</span> <span class="o">|</span><span class="n">c</span><span class="o">|</span> <span class="n">c</span><span class="o">.</span><span class="n">command</span> <span class="o">=~</span> <span class="sr">/\A(?:43[1236]|46[12]|001)\Z/</span> <span class="p">}</span>
<span class="n">assert_not_nil</span><span class="p">(</span><span class="n">cmd</span><span class="p">)</span>
<span class="n">assert_instance_of</span><span class="p">(</span><span class="no">Scribe</span><span class="o">::</span><span class="no">IRC</span><span class="o">::</span><span class="no">Connection</span><span class="o">::</span><span class="no">Command</span><span class="p">,</span> <span class="n">cmd</span><span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span><span class="s2">"calvino.freenode.net"</span><span class="p">,</span> <span class="n">cmd</span><span class="o">.</span><span class="n">prefix</span><span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span><span class="s2">"001"</span><span class="p">,</span> <span class="n">cmd</span><span class="o">.</span><span class="n">command</span><span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span> <span class="o">[</span><span class="s2">"scribe"</span><span class="p">,</span> <span class="s2">"Welcome to the freenode IRC Network scribe"</span><span class="o">]</span><span class="p">,</span>
<span class="n">cmd</span><span class="o">.</span><span class="n">params</span> <span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span> <span class="s2">"Welcome to the freenode IRC Network scribe"</span><span class="p">,</span>
<span class="n">cmd</span><span class="o">.</span><span class="n">last_param</span> <span class="p">)</span>
<span class="n">cmd</span> <span class="o">=</span> <span class="vi">@con</span><span class="o">.</span><span class="n">recv_until</span> <span class="p">{</span> <span class="o">|</span><span class="n">c</span><span class="o">|</span> <span class="n">c</span><span class="o">.</span><span class="n">last_param</span><span class="o">.</span><span class="n">include?</span> <span class="s2">"welcome"</span> <span class="p">}</span>
<span class="n">assert_not_nil</span><span class="p">(</span><span class="n">cmd</span><span class="p">)</span>
<span class="n">assert_instance_of</span><span class="p">(</span><span class="no">Scribe</span><span class="o">::</span><span class="no">IRC</span><span class="o">::</span><span class="no">Connection</span><span class="o">::</span><span class="no">Command</span><span class="p">,</span> <span class="n">cmd</span><span class="p">)</span>
<span class="n">assert_nil</span><span class="p">(</span><span class="n">cmd</span><span class="o">.</span><span class="n">prefix</span><span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span><span class="s2">"NOTICE"</span><span class="p">,</span> <span class="n">cmd</span><span class="o">.</span><span class="n">command</span><span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span> <span class="o">[</span><span class="s2">"AUTH"</span><span class="p">,</span> <span class="s2">"*** Found your hostname, welcome back"</span><span class="o">]</span><span class="p">,</span>
<span class="n">cmd</span><span class="o">.</span><span class="n">params</span> <span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span><span class="s2">"*** Found your hostname, welcome back"</span><span class="p">,</span> <span class="n">cmd</span><span class="o">.</span><span class="n">last_param</span><span class="p">)</span>
<span class="n">remaining</span> <span class="o">=</span> <span class="o">[</span> <span class="o">[</span><span class="s2">"NOTICE"</span><span class="p">,</span> <span class="s2">"AUTH"</span><span class="p">,</span> <span class="s2">"*** Looking up your hostname..."</span><span class="o">]</span><span class="p">,</span>
<span class="o">[</span><span class="s2">"NOTICE"</span><span class="p">,</span> <span class="s2">"AUTH"</span><span class="p">,</span> <span class="s2">"*** Checking ident"</span><span class="o">]</span><span class="p">,</span>
<span class="o">[</span><span class="s2">"NOTICE"</span><span class="p">,</span> <span class="s2">"AUTH"</span><span class="p">,</span> <span class="s2">"*** No identd (auth) response"</span><span class="o">]</span> <span class="o">]</span>
<span class="n">remaining</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">raw</span><span class="o">|</span>
<span class="n">cmd</span> <span class="o">=</span> <span class="no">Scribe</span><span class="o">::</span><span class="no">IRC</span><span class="o">::</span><span class="no">Connection</span><span class="o">::</span><span class="no">Command</span><span class="o">.</span><span class="n">new</span><span class="p">(</span> <span class="kp">nil</span><span class="p">,</span>
<span class="n">raw</span><span class="o">[</span><span class="mi">0</span><span class="o">]</span><span class="p">,</span>
<span class="n">raw</span><span class="o">[</span><span class="mi">1</span><span class="o">.</span><span class="n">.</span><span class="o">-</span><span class="mi">1</span><span class="o">]</span><span class="p">,</span>
<span class="n">raw</span><span class="o">[-</span><span class="mi">1</span><span class="o">]</span> <span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="vi">@con</span><span class="o">.</span><span class="n">recv</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">assert_raise</span><span class="p">(</span><span class="no">RuntimeError</span><span class="p">)</span> <span class="p">{</span> <span class="vi">@con</span><span class="o">.</span><span class="n">recv</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>Don't let all that code fool you, I'm not working anywhere near that hard. I'm making liberal use of copy and paste, then just tweaking the expected values. While frowned upon in library or application code, there's no problem with writing tests that way.</p>
<p>We're ready to implement <code>recv_until()</code> now, but it requires changes to a couple of methods:</p>
<div class="highlight highlight-ruby"><pre><span class="k">module</span> <span class="nn">Scribe</span>
<span class="k">module</span> <span class="nn">IRC</span>
<span class="k">class</span> <span class="nc">Connection</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">host</span><span class="p">,</span> <span class="n">options</span> <span class="o">=</span> <span class="no">Hash</span><span class="o">.</span><span class="n">new</span><span class="p">)</span>
<span class="c1"># ...</span>
<span class="vi">@cmd_buffer</span> <span class="o">=</span> <span class="nb">Array</span><span class="o">.</span><span class="n">new</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">recv</span>
<span class="k">return</span> <span class="vi">@cmd_buffer</span><span class="o">.</span><span class="n">shift</span> <span class="k">unless</span> <span class="vi">@cmd_buffer</span><span class="o">.</span><span class="n">empty?</span>
<span class="c1"># ...</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">recv_until</span>
<span class="n">skipped_commands</span> <span class="o">=</span> <span class="nb">Array</span><span class="o">.</span><span class="n">new</span>
<span class="k">while</span> <span class="n">cmd</span> <span class="o">=</span> <span class="n">recv</span>
<span class="k">if</span> <span class="k">yield</span><span class="p">(</span><span class="n">cmd</span><span class="p">)</span>
<span class="vi">@cmd_buffer</span><span class="o">.</span><span class="n">unshift</span><span class="p">(</span><span class="o">*</span><span class="n">skipped_commands</span><span class="p">)</span>
<span class="k">return</span> <span class="n">cmd</span>
<span class="k">else</span>
<span class="n">skipped_commands</span> <span class="o"><<</span> <span class="n">cmd</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>The additions to <code>initialize()</code> and <code>recv()</code> just add and make use of a command buffer, to hold input we bypassed. We aren't interested in it, but others might be and we don't want to discard it. From there, <code>recv_until()</code> is easy enough. Read input until we find what we are after and toss the extra commands in the buffer for future reading.</p>
<p>How are the tests looking now?</p>
<pre><code>$ ruby -I lib:test test/tc_irc_connection.rb
Loaded suite test/tc_irc_connection
Started
......
Finished in 0.003009 seconds.
6 tests, 53 assertions, 0 failures, 0 errors
</code></pre>
<p>Good enough for the girl I go with.</p>
<p>Last step for a login, I promise. All we need to do now is wrap <code>nick()</code>, <code>user()</code>, and the verification call of <code>recv_until()</code> in an easy-to-use whole. Here's the test for the method:</p>
<div class="highlight highlight-ruby"><pre><span class="k">class</span> <span class="nc">TestIRCConnection</span> <span class="o"><</span> <span class="no">Test</span><span class="o">::</span><span class="no">Unit</span><span class="o">::</span><span class="no">TestCase</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">test_login</span>
<span class="vg">$server_responses</span> <span class="o"><<</span>
<span class="s2">"NOTICE AUTH :*** Looking up your hostname..."</span> <span class="o"><<</span>
<span class="s2">"NOTICE AUTH :*** Found your hostname, welcome back"</span> <span class="o"><<</span>
<span class="s2">"NOTICE AUTH :*** Checking ident"</span> <span class="o"><<</span>
<span class="s2">"NOTICE AUTH :*** No identd (auth) response"</span> <span class="o"><<</span>
<span class="s2">":calvino.freenode.net 001 scribe "</span> <span class="o">+</span>
<span class="s2">":Welcome to the freenode IRC Network scribe"</span>
<span class="n">assert_nothing_raised</span><span class="p">(</span><span class="no">Exception</span><span class="p">)</span> <span class="k">do</span>
<span class="vi">@con</span><span class="o">.</span><span class="n">login</span><span class="p">(</span><span class="s2">"scribe"</span><span class="p">,</span> <span class="s2">"127.0.0.1"</span><span class="p">,</span> <span class="s2">"127.0.0.1"</span><span class="p">,</span> <span class="s1">'"scribe" IRC bot'</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">assert_equal</span><span class="p">(</span> <span class="s2">"NICK scribe</span><span class="se">\r\n</span><span class="s2">"</span> <span class="o">+</span>
<span class="s2">"USER scribe 127.0.0.1 127.0.0.1 :</span><span class="se">\"</span><span class="s2">scribe</span><span class="se">\"</span><span class="s2"> IRC bot</span><span class="se">\r\n</span><span class="s2">"</span><span class="p">,</span>
<span class="vg">$client_output</span> <span class="p">)</span>
<span class="n">remaining</span> <span class="o">=</span> <span class="o">[</span> <span class="o">[</span><span class="s2">"NOTICE"</span><span class="p">,</span> <span class="s2">"AUTH"</span><span class="p">,</span> <span class="s2">"*** Looking up your hostname..."</span><span class="o">]</span><span class="p">,</span>
<span class="o">[</span> <span class="s2">"NOTICE"</span><span class="p">,</span> <span class="s2">"AUTH"</span><span class="p">,</span>
<span class="s2">"*** Found your hostname, welcome back"</span> <span class="o">]</span><span class="p">,</span>
<span class="o">[</span><span class="s2">"NOTICE"</span><span class="p">,</span> <span class="s2">"AUTH"</span><span class="p">,</span> <span class="s2">"*** Checking ident"</span><span class="o">]</span><span class="p">,</span>
<span class="o">[</span><span class="s2">"NOTICE"</span><span class="p">,</span> <span class="s2">"AUTH"</span><span class="p">,</span> <span class="s2">"*** No identd (auth) response"</span><span class="o">]</span> <span class="o">]</span>
<span class="n">remaining</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">raw</span><span class="o">|</span>
<span class="n">cmd</span> <span class="o">=</span> <span class="no">Scribe</span><span class="o">::</span><span class="no">IRC</span><span class="o">::</span><span class="no">Connection</span><span class="o">::</span><span class="no">Command</span><span class="o">.</span><span class="n">new</span><span class="p">(</span> <span class="kp">nil</span><span class="p">,</span>
<span class="n">raw</span><span class="o">[</span><span class="mi">0</span><span class="o">]</span><span class="p">,</span>
<span class="n">raw</span><span class="o">[</span><span class="mi">1</span><span class="o">.</span><span class="n">.</span><span class="o">-</span><span class="mi">1</span><span class="o">]</span><span class="p">,</span>
<span class="n">raw</span><span class="o">[-</span><span class="mi">1</span><span class="o">]</span> <span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span><span class="n">cmd</span><span class="p">,</span> <span class="vi">@con</span><span class="o">.</span><span class="n">recv</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">assert_raise</span><span class="p">(</span><span class="no">RuntimeError</span><span class="p">)</span> <span class="p">{</span> <span class="vi">@con</span><span class="o">.</span><span class="n">recv</span> <span class="p">}</span>
<span class="vg">$server_responses</span> <span class="o"><<</span>
<span class="s2">"NOTICE AUTH :*** Looking up your hostname..."</span> <span class="o"><<</span>
<span class="s2">"NOTICE AUTH :*** Found your hostname, welcome back"</span> <span class="o"><<</span>
<span class="s2">"NOTICE AUTH :*** Checking ident"</span> <span class="o"><<</span>
<span class="s2">"NOTICE AUTH :*** No identd (auth) response"</span> <span class="o"><<</span>
<span class="s2">":niven.freenode.net 431 :No nickname given"</span>
<span class="n">assert_raise</span><span class="p">(</span><span class="no">RuntimeError</span><span class="p">)</span> <span class="k">do</span>
<span class="vi">@con</span><span class="o">.</span><span class="n">login</span><span class="p">(</span><span class="s2">""</span><span class="p">,</span> <span class="s2">"127.0.0.1"</span><span class="p">,</span> <span class="s2">"127.0.0.1"</span><span class="p">,</span> <span class="s1">'"scribe" IRC bot'</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>Here's the method that does what we need:</p>
<div class="highlight highlight-ruby"><pre><span class="k">module</span> <span class="nn">Scribe</span>
<span class="k">module</span> <span class="nn">IRC</span>
<span class="k">class</span> <span class="nc">Connection</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">login</span><span class="p">(</span><span class="n">nickname</span><span class="p">,</span> <span class="n">host_name</span><span class="p">,</span> <span class="n">server_name</span><span class="p">,</span> <span class="n">full_name</span><span class="p">)</span>
<span class="n">nick</span><span class="p">(</span><span class="n">nickname</span><span class="p">)</span>
<span class="n">user</span><span class="p">(</span><span class="n">nickname</span><span class="p">,</span> <span class="n">host_name</span><span class="p">,</span> <span class="n">server_name</span><span class="p">,</span> <span class="n">full_name</span><span class="p">)</span>
<span class="n">rpl</span> <span class="o">=</span> <span class="n">recv_until</span> <span class="p">{</span> <span class="o">|</span><span class="n">c</span><span class="o">|</span> <span class="n">c</span><span class="o">.</span><span class="n">command</span> <span class="o">=~</span> <span class="sr">/\A(?:43[1236]|46[12]|001)\Z/</span> <span class="p">}</span>
<span class="k">if</span> <span class="n">rpl</span><span class="o">.</span><span class="n">nil?</span> <span class="o">||</span> <span class="n">rpl</span><span class="o">.</span><span class="n">command</span> <span class="o">!=</span> <span class="s2">"001"</span>
<span class="k">raise</span> <span class="s2">"Login error: </span><span class="si">#{</span><span class="n">rpl</span><span class="o">.</span><span class="n">last_param</span><span class="si">}</span><span class="s2">."</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>Finally, here's the proof that we did it right:</p>
<pre><code>$ ruby -I lib:test test/tc_irc_connection.rb
Loaded suite test/tc_irc_connection
Started
.......
Finished in 0.003969 seconds.
7 tests, 63 assertions, 0 failures, 0 errors
</code></pre>
<h4>Playing PING PONG</h4>
<p>There is one more little issue we need to deal with, to maintain an IRC connection for a sustained period of time. IRC servers regularly "ping" their connections, to make sure you are still connected. The server will send <code>PING</code> commands with some random parameter content, and the client is expected to return a <code>PONG</code> command with the same content.</p>
<p>Here's an example of what I am talking about:</p>
<div class="highlight highlight-ruby"><pre><span class="k">class</span> <span class="nc">TestIRCConnection</span> <span class="o"><</span> <span class="no">Test</span><span class="o">::</span><span class="no">Unit</span><span class="o">::</span><span class="no">TestCase</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">test_ping</span>
<span class="n">commands</span> <span class="o">=</span> <span class="o">[</span> <span class="s2">"*** Looking up your hostname..."</span><span class="p">,</span>
<span class="s2">"*** Found your hostname, welcome back"</span><span class="p">,</span>
<span class="s2">"*** Checking ident"</span><span class="p">,</span>
<span class="s2">"*** No identd (auth) response"</span> <span class="o">]</span>
<span class="n">commands</span><span class="o">.</span><span class="n">each</span> <span class="p">{</span> <span class="o">|</span><span class="n">cmd</span><span class="o">|</span> <span class="vg">$server_responses</span> <span class="o"><<</span> <span class="s2">"NOTICE AUTH :</span><span class="si">#{</span><span class="n">cmd</span><span class="si">}</span><span class="s2">"</span> <span class="p">}</span>
<span class="vg">$server_responses</span><span class="o">.</span><span class="n">insert</span><span class="p">(</span> <span class="nb">rand</span><span class="p">(</span><span class="vg">$server_responses</span><span class="o">.</span><span class="n">size</span><span class="p">),</span>
<span class="s2">"PING :calvino.freenode.net"</span> <span class="p">)</span>
<span class="n">commands</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">expected</span><span class="o">|</span>
<span class="n">cmd</span> <span class="o">=</span> <span class="vi">@con</span><span class="o">.</span><span class="n">recv</span>
<span class="n">assert_not_nil</span><span class="p">(</span><span class="n">cmd</span><span class="p">)</span>
<span class="n">assert_instance_of</span><span class="p">(</span><span class="no">Scribe</span><span class="o">::</span><span class="no">IRC</span><span class="o">::</span><span class="no">Connection</span><span class="o">::</span><span class="no">Command</span><span class="p">,</span> <span class="n">cmd</span><span class="p">)</span>
<span class="n">assert_nil</span><span class="p">(</span><span class="n">cmd</span><span class="o">.</span><span class="n">prefix</span><span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span><span class="s2">"NOTICE"</span><span class="p">,</span> <span class="n">cmd</span><span class="o">.</span><span class="n">command</span><span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span><span class="o">[</span><span class="s2">"AUTH"</span><span class="p">,</span> <span class="n">expected</span><span class="o">]</span><span class="p">,</span> <span class="n">cmd</span><span class="o">.</span><span class="n">params</span><span class="p">)</span>
<span class="n">assert_equal</span><span class="p">(</span><span class="n">expected</span><span class="p">,</span> <span class="n">cmd</span><span class="o">.</span><span class="n">last_param</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">assert_raise</span><span class="p">(</span><span class="no">RuntimeError</span><span class="p">)</span> <span class="p">{</span> <span class="vi">@con</span><span class="o">.</span><span class="n">recv</span> <span class="p">}</span>
<span class="n">assert_equal</span><span class="p">(</span><span class="s2">"PONG :calvino.freenode.net</span><span class="se">\r\n</span><span class="s2">"</span><span class="p">,</span> <span class="vg">$client_output</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>We can sneak this functionality in at the lowest level so the user never needs to be bothered by it:</p>
<div class="highlight highlight-ruby"><pre><span class="k">module</span> <span class="nn">Scribe</span>
<span class="k">module</span> <span class="nn">IRC</span>
<span class="k">class</span> <span class="nc">Connection</span>
<span class="c1"># ...</span>
<span class="kp">private</span>
<span class="k">def</span> <span class="nf">recv_command</span>
<span class="n">cmd</span> <span class="o">=</span> <span class="vi">@irc_server</span><span class="o">.</span><span class="n">gets</span>
<span class="k">if</span> <span class="ow">not</span> <span class="n">cmd</span><span class="o">.</span><span class="n">nil?</span> <span class="o">&&</span> <span class="n">cmd</span> <span class="o">=~</span> <span class="sr">/\APING (.*?)\r\n\Z/</span>
<span class="n">send_command</span><span class="p">(</span><span class="s2">"PONG </span><span class="si">#{</span><span class="vg">$1</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="n">recv_command</span>
<span class="k">else</span>
<span class="n">cmd</span><span class="o">.</span><span class="n">nil?</span> <span class="p">?</span> <span class="n">cmd</span> <span class="p">:</span> <span class="n">cmd</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sr">/\r\n\Z/</span><span class="p">,</span> <span class="s2">""</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>Nothing too tough there and we are still passing the tests:</p>
<pre><code>$ ruby -I lib:test test/tc_irc_connection.rb
Loaded suite test/tc_irc_connection
Started
.........
Finished in 0.005391 seconds.
9 tests, 93 assertions, 0 failures, 0 errors
</code></pre>
<h4>Getting Social</h4>
<p>Now if we want our bot to do anything social on the IRC networks, we need to be able to join channels and send messages. Let's add tests for those operations:</p>
<div class="highlight highlight-ruby"><pre><span class="k">class</span> <span class="nc">TestIRCConnection</span> <span class="o"><</span> <span class="no">Test</span><span class="o">::</span><span class="no">Unit</span><span class="o">::</span><span class="no">TestCase</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">test_join</span>
<span class="vg">$server_responses</span> <span class="o"><<</span> <span class="s2">":niven.freenode.net 403 scribe junk "</span> <span class="o">+</span>
<span class="s2">":That channel doesn't exist"</span>
<span class="n">assert_raise</span><span class="p">(</span><span class="no">RuntimeError</span><span class="p">)</span> <span class="p">{</span> <span class="vi">@con</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">"junk"</span><span class="p">)</span> <span class="p">}</span>
<span class="vg">$server_responses</span> <span class="o"><<</span>
<span class="s2">":herbert.freenode.net 332 GrayBot ##textmate "</span> <span class="o">+</span>
<span class="s2">":r961 available as ‘cutting edge’ || "</span> <span class="o">+</span>
<span class="s2">"http://macromates.com/wiki/Polls/WhichLanguageDoYouUse"</span>
<span class="n">assert_nothing_raised</span><span class="p">(</span><span class="no">Exception</span><span class="p">)</span> <span class="p">{</span> <span class="vi">@con</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">"##textmate"</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">test_privmsg</span>
<span class="n">assert_nothing_raised</span><span class="p">(</span><span class="no">Exception</span><span class="p">)</span> <span class="k">do</span>
<span class="vi">@con</span><span class="o">.</span><span class="n">privmsg</span><span class="p">(</span><span class="s2">"##textmate"</span><span class="p">,</span> <span class="s2">"hello all"</span><span class="p">)</span>
<span class="k">end</span>
<span class="n">assert_equal</span><span class="p">(</span><span class="s2">"PRIVMSG ##textmate :hello all</span><span class="se">\r\n</span><span class="s2">"</span><span class="p">,</span> <span class="vg">$client_output</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>And here is the matching library code:</p>
<div class="highlight highlight-ruby"><pre><span class="k">module</span> <span class="nn">Scribe</span>
<span class="k">module</span> <span class="nn">IRC</span>
<span class="k">class</span> <span class="nc">Connection</span>
<span class="c1"># ...</span>
<span class="no">COMMAND_METHODS</span> <span class="o">=</span> <span class="o">[</span><span class="ss">:nick</span><span class="p">,</span> <span class="ss">:join</span><span class="p">,</span> <span class="ss">:privmsg</span><span class="p">,</span> <span class="ss">:user</span><span class="o">].</span><span class="n">freeze</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">join_channel</span><span class="p">(</span><span class="n">channel</span><span class="p">)</span>
<span class="n">join</span><span class="p">(</span><span class="n">channel</span><span class="p">)</span>
<span class="n">rpl</span> <span class="o">=</span> <span class="n">recv_until</span> <span class="k">do</span> <span class="o">|</span><span class="n">c</span><span class="o">|</span>
<span class="n">c</span><span class="o">.</span><span class="n">command</span> <span class="o">=~</span> <span class="sr">/\A(?:461|47[13456]|40[35]|332|353)\Z/</span>
<span class="k">end</span>
<span class="k">if</span> <span class="n">rpl</span><span class="o">.</span><span class="n">nil?</span> <span class="o">||</span> <span class="n">rpl</span><span class="o">.</span><span class="n">command</span> <span class="o">!~</span> <span class="sr">/\A3(?:32|53)\Z/</span>
<span class="k">raise</span> <span class="s2">"Join error: </span><span class="si">#{</span><span class="n">rpl</span><span class="o">.</span><span class="n">last_param</span><span class="si">}</span><span class="s2">."</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
</pre></div>
<p>Let's check my work:</p>
<pre><code>$ ruby -I lib:test test/tc_irc_connection.rb
Loaded suite test/tc_irc_connection
Started
.F........
Finished in 0.017689 seconds.
1) Failure:
test_join(TestIRCConnection) [test/tc_irc_connection.rb:178]:
<RuntimeError> exception expected but none was thrown.
10 tests, 98 assertions, 1 failures, 0 errors
</code></pre>
<p>Oops, I blew it. That's what I get for trying to skip steps, I suppose. Luckily, the failure tells me it's <code>join()</code> that I didn't get right. Actually, I did. I'm just not testing it correctly. <code>join()</code> is the low-level method and I should really be testing <code>join_channel()</code>. Let's fix that:</p>
<div class="highlight highlight-ruby"><pre><span class="k">class</span> <span class="nc">TestIRCConnection</span> <span class="o"><</span> <span class="no">Test</span><span class="o">::</span><span class="no">Unit</span><span class="o">::</span><span class="no">TestCase</span>
<span class="c1"># ...</span>
<span class="k">def</span> <span class="nf">test_join_channel</span>
<span class="vg">$server_responses</span> <span class="o"><<</span> <span class="s2">":niven.freenode.net 403 scribe junk "</span> <span class="o">+</span>
<span class="s2">":That channel doesn't exist"</span>
<span class="n">assert_raise</span><span class="p">(</span><span class="no">RuntimeError</span><span class="p">)</span> <span class="p">{</span> <span class="vi">@con</span><span class="o">.</span><span class="n">join_channel</span><span class="p">(</span><span class="s2">"junk"</span><span class="p">)</span> <span class="p">}</span>
<span class="vg">$server_responses</span> <span class="o"><<</span>
<span class="s2">":herbert.freenode.net 332 GrayBot ##textmate "</span> <span class="o">+</span>
<span class="s2">":r961 available as ‘cutting edge’ || "</span> <span class="o">+</span>
<span class="s2">"http://macromates.com/wiki/Polls/WhichLanguageDoYouUse"</span>
<span class="n">assert_nothing_raised</span><span class="p">(</span><span class="no">Exception</span><span class="p">)</span> <span class="p">{</span> <span class="vi">@con</span><span class="o">.</span><span class="n">join_channel</span><span class="p">(</span><span class="s2">"##textmate"</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="c1"># ...</span>
<span class="k">end</span>
</pre></div>
<p>Make sure I got it right:</p>
<pre><code>$ ruby -I lib:test test/tc_irc_connection.rb
Loaded suite test/tc_irc_connection
Started
..........
Finished in 0.005689 seconds.
10 tests, 99 assertions, 0 failures, 0 errors
</code></pre>
<p>Bingo.</p>
<h4>Summary</h4>
<p>Though this post is quite long, detailing my process and showing each little step, the library code is still under 100 lines. I also wrote it very fast, if you don't count the time spent with this write-up. I'm confident software is working well, though I've intentionally left out error handling code to keep things simple. Don't take my word for it though. Here's a trivial greeter bot showing off the code:</p>
<div class="highlight highlight-ruby"><pre><span class="c1">#!/usr/local/bin/ruby -w</span>
<span class="nb">require</span> <span class="s2">"scribe/irc/connection"</span>
<span class="k">unless</span> <span class="no">ARGV</span><span class="o">.</span><span class="n">size</span> <span class="o">==</span> <span class="mi">3</span>
<span class="nb">abort</span> <span class="s2">"Usage </span><span class="si">#{</span><span class="vg">$PROGRAM_NAME</span><span class="si">}</span><span class="s2"> IRC_URL NICKNAME CHANNEL"</span>
<span class="k">end</span>
<span class="n">irc</span><span class="p">,</span> <span class="n">nick</span><span class="p">,</span> <span class="n">channel</span> <span class="o">=</span> <span class="no">ARGV</span>
<span class="k">begin</span>
<span class="n">server</span> <span class="o">=</span> <span class="no">Scribe</span><span class="o">::</span><span class="no">IRC</span><span class="o">::</span><span class="no">Connection</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">irc</span><span class="p">)</span>
<span class="n">server</span><span class="o">.</span><span class="n">login</span><span class="p">(</span><span class="n">nick</span><span class="p">,</span> <span class="s2">"hostname"</span><span class="p">,</span> <span class="s2">"servername"</span><span class="p">,</span> <span class="s2">"</span><span class="se">\"</span><span class="si">#{</span><span class="n">nick</span><span class="si">}</span><span class="se">\"</span><span class="s2"> Greeter Bot"</span><span class="p">)</span>
<span class="n">server</span><span class="o">.</span><span class="n">join_channel</span><span class="p">(</span><span class="n">channel</span><span class="p">)</span>
<span class="k">while</span> <span class="n">cmd</span> <span class="o">=</span> <span class="n">server</span><span class="o">.</span><span class="n">recv</span>
<span class="k">if</span> <span class="n">cmd</span><span class="o">.</span><span class="n">command</span> <span class="o">==</span> <span class="s2">"JOIN"</span>
<span class="n">who</span> <span class="o">=</span> <span class="n">cmd</span><span class="o">.</span><span class="n">prefix</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sr">/[!@].*\Z/</span><span class="p">,</span> <span class="s1">''</span><span class="p">)</span>
<span class="k">next</span> <span class="k">if</span> <span class="n">who</span> <span class="o">==</span> <span class="n">nick</span>
<span class="k">if</span> <span class="nb">rand</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span><span class="o">.</span><span class="n">zero?</span>
<span class="n">server</span><span class="o">.</span><span class="n">privmsg</span><span class="p">(</span> <span class="n">channel</span><span class="p">,</span> <span class="s2">"I know you... "</span> <span class="o">+</span>
<span class="s2">"You're the one my master covets!"</span> <span class="p">)</span>
<span class="k">else</span>
<span class="n">server</span><span class="o">.</span><span class="n">privmsg</span><span class="p">(</span><span class="n">channel</span><span class="p">,</span> <span class="s2">"Howdy </span><span class="si">#{</span><span class="n">who</span><span class="si">}</span><span class="s2">."</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">rescue</span>
<span class="nb">puts</span> <span class="s2">"An error has occurred: </span><span class="si">#{</span><span class="vg">$!</span><span class="o">.</span><span class="n">message</span><span class="si">}</span><span class="s2">"</span>
<span class="k">end</span>
</pre></div>
<p>If I put that guy in some room and join it a few times, we can see his quirky sense of humor at work:</p>
<pre><code>greet_bot: Howdy JEG2.
greet_bot: Howdy JEG2.
greet_bot: I know you... You're the one my master covets!
</code></pre>
<p>Now go forth my friends spreading unit tests into the world…</p>James Edward Gray II