tag:blogger.com,1999:blog-51591032008-10-12T13:02:49.048-05:00Mundane Essaysmunesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comBlogger648125tag:blogger.com,1999:blog-5159103.post-49004680417118730072008-09-21T12:12:00.003-05:002008-09-21T13:00:55.765-05:00Mingle, meet Git<p> Several weeks back, <a href="http://www.adammonago.com/blog/blog.html">Adam Monago</a>, <a href="http://studios.thoughtworks.com/mingle-project-intelligence">Mingle</a>'s product manager was visiting our office in Chapel Hill. One of the topics that came up was Git integration with Mingle. Alas, it sounded like it was a ways out. But he explained that the SCM integration was pluggable and that we could write the code and drop it in place, echoing things I'd heard from other ThoughtWorkers. </p> <p>A couple of days later, <a href="http://www.donmullen.net/">Don</a> and I were curious as to just what it'd take to implement the integration. Reverse engineering the interface we had to implement from the Subversion and Perforce plug-ins was anything but fun, but before the day was over, we had rudimentary integration working: we could see commit messages and the list of files modified per check-in. Since then, Don implemented the rest of it including Mingle-based source code browsing.</p> <p>We've been using the <a href="http://github.com/donmullen/mingle_git/">mingle_git</a> plugin for a little while with no problems. For performance reasons, we're not using the source code browsing from inside Mingle. Instead, we use GitHub's source browsing: see the <a href="http://github.com/donmullen/mingle_git/tree/master%2FREADME.rdoc?raw=true">README</a> for instructions on wiring things that way too.</p> <p> Installation is documented in the README. We've only used this on Mac OS, FreeBSD and Linux. Windows users, you may have luck by using <a href="http://code.google.com/p/msysgit/">msysgit</a>. </p> <p> <strong>Caveat emptor</strong>: if you browse around the code, you'll quickly conclude that it is a <strong>spike</strong>: we started with the subversion plugin and evolved it to this. Since the SCM integration API is not documented, we can't even be positive that we implemented it correctly (though the evidence suggests that we have). Also note that Mingle makes the assumption that check-in numbers are sequential, and that's an assumption that is not Git-friendly and it shows in the code. </p> <p> Enjoy, but keep in mind that this is unsupported, use-at-your-own-risk software. For me, that's better than no Git integration. ;) </p>munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-31483837277857140202008-08-17T11:35:00.007-05:002008-08-19T07:22:04.212-05:00Encrypt your client data in 53 minutes<p>Thanks to <a href="http://aaronbedra.com">Aaron</a>'s continued efforts to make us ever more security conscious, I've been encrypting client data on my laptop. This came up in conversation during <a href="http://erubycon.com">e<strong>ruby</strong>con</a> (thanks <a href="http://theedgecase.com">EdgeCase</a> for a fun conference with engaging <a href="http://twitter.com/muncman/statuses/889085361">evening events</a>). A couple of people asked me to get them started on encrypting their data too. <p>Here are instructions. They're written for Mac users, but they'd be nearly identical for Linux and Windows users since TrueCrypt is available there too. </p> <ol> <li>Install <a href=http://www.truecrypt.org/downloads.php>TrueCrypt</a>.</li> <li>Create a container based encrypted file. Pick FAT as the file system. I use both keyfiles and a password.</li> <li>Erase the FAT partition and then format as HFS/ext2/[fs of choice] using Disk Utility/[partition manager of choice]. Name it <code>client_data</code>.</li> <li>Mount the newly created partition.</li> <li>Move your sensitive data to <code>/Volumes/client_data</code>. This is by far the slowest part of the process. While you wait, watch <a href="http://video.google.com/videoplay?docid=-2293483151556804649">The Enemies of Reason</a> (48 minutes -- everything else should take 5 minutes or less).</li> <li>Modify TrueCrypt preferences to suit your needs. Some suggestions: leave encrypted volumes mounted when quitting TrueCrypt, set Auto-dismount volume after 30 minutes of inactivity in the partition.</li> </ol> <p>As mentioned in <a href="http://muness.blogspot.com/2008/06/lazy-bash-cd-aliaes.html">my previous blog entry</a>, I use a script to automatically create aliases to allow easy switching to project directories. All I had to do was switch that entry to point to <code>/Volumes/client_data</code> instead of the old <code>~/work/client_data</code>. Now, when I want to work on client projects or contracts, I launch TrueCrypt, mount the container and open a new terminal window. All my old aliases work just like they used to.</p> <p>Mac users: if you don't care about the cross-platform compatibility that TrueCrypt offers, and trust Apple to encrypt your data, you could use Disk Utility to <a href="http://support.apple.com/kb/HT1578">create encrypted partitions</a> instead. Some people insist it's more convenient (<a href="http://jasonrudolph.com/blog">You</a> <a href="http://tech.hickorywind.org/">know</a> who you are!).</p>munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-78213390180351224582008-06-29T13:04:00.010-05:002008-09-05T21:38:42.498-05:00Lazy bash cd aliasesThose of you who've found value in my last couple of bash specific posts may also like the <a href="http://github.com/relevance/etc/tree/master/bash/project_aliases.sh">latest addition</a> to my <code>~/.bash_profile</code>. <p> This one iterates through the one or more directories and creates aliases to the subdirectories so I don't have to. Here's the scenario: I've got a directory ~/work/ where I keep work projects, a ~/writings/ where I keep all the writing projects and so on. I used to have aliases to each subdirectory. e.g. <code>alias project1="cd /Users/muness/work/project1"</code>. With shame, I admit that I maintained each of these manually. No more!</p> <p> Install instructions: <code> <pre> curl http://github.com/relevance/etc/tree/master%2Fbash%2Fproject_aliases.sh?raw=true?raw=true > ~/.project_aliases.sh echo "source ~/. project_aliases.sh" >> ~/.bash_profile </pre> </code> </p> <p> Usage instructions: <ul> <li>Move your work projects to <code>~/work</code>.</li> <li>Above the <code>source ~/. project_aliases.sh</code> add <code>PROJECT_PARENT_DIRS[0]="$HOME/work"</code>. </ul> </p> <p>You may be interested in my blog post over at <a href="http://pragmactic-osxer.blogspot.com/2008/06/pimp-my-shell.html">PragMactic OS-Xer</a> where I describe my motivation for these recent shell scripts.munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-59155768129942248922008-06-16T19:56:00.007-05:002008-06-17T14:16:39.947-05:00bash: don't make me think<p> <a href="http://muness.blogspot.com/2008/06/stop-presses-bash-said-to-embrace.html">Last week</a> I figured out a way to make my life a little bit easier by abstracting the scm I was using and having the prompt indicate whether I was in a Subversion or Git. Thanks to Mike Hommey whose <A HREF=http://glandium.org/blog/?p=170>script</A> I tweaked for my needs. </p> <p> For a long time, I've wanted my iTerm tab title to be more useful. I started by having it display the last process I executed in that tab (this also shows the full path in iTerm's window title bar): <pre> <code> PS1='\[\e]2;\h::\]${PWD/$HOME/~}\[\a\]\[\e]1;\]$(history 1 | sed -e "s/^[ ]*[0-9]*[ ]*//g")\a\]\$ ' </code> </pre> </p> <p> Next up I wanted to show the currently running command. Google showed <a href="http://www.davidpashley.com/articles/xterm-titles-with-bash.html">me how</a>: <pre> <code> trap 'echo -e "\e]1;$BASH_COMMAND\007\c"' DEBUG </code> </pre> <p>Instead of trying to explain this code I'll quote the Bash man page: <blockquote> BASH_COMMAND<br/> The command currently being executed or about to be executed, unless the shell is executing a command as the result of a trap, in which case it is the command executing at the time of the trap. </blockquote> </p> <p> Some more tweaks followed: <ul> <li>Distinguish between a previously executed command and a currently executing command by decorating them (I chose surrounding the former with braces and the latter with &gt;/&lt;. e.g. <code>[ls] and &gt;vi&lt;</code>). <li>Show the context of the executing command in the tab. This is just the repository where I executed the command. <li>Make this coexist with TextMate. </ul> </p> <p> And a screenshot to summarize: <a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp1.blogger.com/_X2VtRiH_7DA/SFcPaHEgkdI/AAAAAAAAArw/xVgpaEFFg94/s1600-h/dont-make-me-think-iterm.jpg"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;" src="http://bp1.blogger.com/_X2VtRiH_7DA/SFcPaHEgkdI/AAAAAAAAArw/xVgpaEFFg94/s800/dont-make-me-think-iterm.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5212652034953613778" /></a> </p> <p>The code is in github<a href="http://github.com/relevance/etc/tree/master/bash/bash_vcs.sh">/relevance/etc/tree/master/bash/bash_vcs.sh</a>. </p> <p> Install instructions: <pre> <code> curl http://github.com/relevance/etc/tree/master%2Fbash%2Fbash_vcs.sh?raw=true > ~/.bash_dont_think.sh echo "source ~/.bash_dont_think.sh" >> ~/.bash_profile </code> </pre> </p> <p>Enjoy! Tested with iTerm on Leopard.</p>munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-88303956015763007302008-06-10T19:41:00.009-05:002008-08-05T19:30:49.209-05:00Stop the presses: bash said to embrace subversion and git<p><strong>Update:</strong> I've since updated and moved this script. See <a href="http://muness.blogspot.com/2008/06/bash-dont-make-me-think.html">my new blog post</a>. </p> <p>I had a chance to pair with <a href="http://robsanheim.com">Rob</a> today. On his console window, I noticed something I wanted: his bash prompt had an indication that he was in a Git repository along with the branch he was on. Later, after he'd left, I found myself wondering how that worked and that I simply had to have it, and I wasn't willing to wait a whole day to figure out how he'd done it. Patience is <em>not</em> a virtue. (In that spirit, click <a href="http://github.com/relevance/etc/tree/master%2Fbash%2Fbash_vcs.sh?raw=true">here</a> for the code if you're too impatient to read the rest of this blog entry which I spent countless hours writing. Uphill both ways. In the snow.)</p> <p> A few minutes of googling turned up a blog entry with relevant <a href="http://glandium.org/blog/?p=170">bash fu</a>. The code there was for changing the prompt such that it indicated whether you were using svn, git, svk or Mercurial along with some repository metadata. A couple of minutes later (it took a while to copy and paste the code since it was interweaved with comments), I found out it didn't work on Leopard. <code>readlink</code> turned out to be the culprit. Instead of figuring out why it wasn't working, I replaced it with something simpler (specifically I used: <code>base_dir=`cd $base_dir; pwd`</code>). Voila, things worked: I now had a hella cool prompt that showed me if I was in a Subversion or a Git repository - I never used Mercurial, and left SVK for Git a couple of months ago. Yay. </p> <p> Being who I am I had to do something more with this new ability to distinguish between Subversion and Git. And I knew exactly which itch to scratch: 90% of the time I use the same scm commands. I even had shell aliases for them so I wouldn't have to type them. A few minutes later I'd converted my previously Subversion specific aliases to generic ones that worked with Git too. Screenshot says it all: </p> <a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp2.blogger.com/_X2VtRiH_7DA/SE8kdQ5Uc_I/AAAAAAAAAro/qLjIzmgKkCg/s1600-h/screen-capture.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;" src="http://bp2.blogger.com/_X2VtRiH_7DA/SE8kdQ5Uc_I/AAAAAAAAAro/qLjIzmgKkCg/s800/screen-capture.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5210423379061142514" /></a> <p> If you want the same, download <a href="http://github.com/relevance/etc/tree/master%2Fbash%2Fbash_vcs.sh?raw=true">the script</a> as <code>~/.bash_vcs</code> and add <code>source ~/.bash_vcs</code> at the end of your <code>~/.bash_profile</code>. </p>munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-52425408469206156392008-04-25T10:23:00.012-05:002008-06-26T10:03:29.833-05:00Buckets of mice<center> <a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp2.blogger.com/_X2VtRiH_7DA/SBH4DqhRgAI/AAAAAAAAAqQ/Rnde9NuPB1U/s1600-h/20080418-P1000587-1.jpg"><img style="cursor:pointer; cursor:hand;" src="http://bp2.blogger.com/_X2VtRiH_7DA/SBH4DqhRgAI/AAAAAAAAAqQ/Rnde9NuPB1U/s800/20080418-P1000587-1.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5193204587172036610" /></a> </center> <p> <a href="http://thinkrelevance.com">We</a> have 24" and 30" pairing stations, an intern, <a href="http://3cups.net/">freshly roasted coffee</a> delivered weekly, a fridge stocked with caffeine, more caffeine, beer, and <a href="http://en.wikipedia.org/wiki/Dave's_Gourmet">Dave's Insanity sauce</a>. We have cool <a href="http://store.xkcd.com/">t-shirts</a> and all manner of radiators up on the walls, whiteboards and the <a href="http://www.somethingnimble.com/bliki/radiator">Beta Brite</a>. Music <a href="http://www.alteclansing.com/index.php?file=north_product_detail&iproduct_id=57">is served</a> by our CI/Music server. </p> <center> <a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp3.blogger.com/_X2VtRiH_7DA/SBIJkqhRgCI/AAAAAAAAAqg/o1nd76e6dpI/s1600-h/20080418-P1000588.jpg"><img style="cursor:pointer; cursor:hand;" src="http://bp3.blogger.com/_X2VtRiH_7DA/SBIJkqhRgCI/AAAAAAAAAqg/o1nd76e6dpI/s200/20080418-P1000588.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5193223845805391906" /></a> </center> <p> The bookshelves are stocked with reference materials, the occasional bottle of scotch, and recommended readings. On the list of recommended readings we've got The Carpet Makers, The Company, The Omnivore's Dilemma, The Eyre Affair, Behind Closed Doors, Bloodsucking Fiends and The Deadline, Men In Hats, Volume I, Don't Make me Think, The Insane are Running the Asylum. </p> <p> Our favorite radiator is the Friday radiator. <a name="fridays">Fridays</a> are special: we don't do billable work. Instead we open source, blog (self reference makes this blog entry extra cool), watch <a href="http://opuszine.com/blog/entry/cinematic_titanic_vs_the_oozing_skull/">educational movies</a>, play Wii on the projector. Here's today's: </p> <a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp1.blogger.com/_X2VtRiH_7DA/SBH4TahRgBI/AAAAAAAAAqY/za276x3InPo/s1600-h/20080425-P1000606.jpg"><img style="cursor:pointer; cursor:hand;" src="http://bp1.blogger.com/_X2VtRiH_7DA/SBH4TahRgBI/AAAAAAAAAqY/za276x3InPo/s800/20080425-P1000606.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5193204857754976274" /></a> <p>Oh, let's not forget the buckets of mice.</p> <center> <a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp3.blogger.com/_X2VtRiH_7DA/SBH3g6hRf_I/AAAAAAAAAqI/uEQJJaHopkI/s1600-h/20080425-P1000603.jpg"><img style="cursor:pointer; cursor:hand;" src="http://bp3.blogger.com/_X2VtRiH_7DA/SBH3g6hRf_I/AAAAAAAAAqI/uEQJJaHopkI/s400/20080425-P1000603.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5193203990171582450" /></a> </center> <p> This blog entry inspired by <a href="http://www.gapingvoid.com/Moveable_Type/archives/004493.html">another excellent business card cartoon</a> from Hugh Macleod. </p> <a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp0.blogger.com/_X2VtRiH_7DA/SBISH6hRgGI/AAAAAAAAArA/3PCIOE5gTd0/s1600-h/20080418-P1000597.jpg"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;" src="http://bp0.blogger.com/_X2VtRiH_7DA/SBISH6hRgGI/AAAAAAAAArA/3PCIOE5gTd0/s800/20080418-P1000597.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5193233247488802914" /></a> <br/> <center> <a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp2.blogger.com/_X2VtRiH_7DA/SBIJ7ahRgEI/AAAAAAAAAqw/dKiEw5vOAns/s1600-h/20080418-P1000577.jpg"><img style="cursor:pointer; cursor:hand;" src="http://bp2.blogger.com/_X2VtRiH_7DA/SBIJ7ahRgEI/AAAAAAAAAqw/dKiEw5vOAns/s200/20080418-P1000577.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5193224236647415874" /></a> <a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp2.blogger.com/_X2VtRiH_7DA/SBIJyahRgDI/AAAAAAAAAqo/r1_-8d__DTU/s1600-h/20080418-P1000575.jpg"><img style="cursor:pointer; cursor:hand;" src="http://bp2.blogger.com/_X2VtRiH_7DA/SBIJyahRgDI/AAAAAAAAAqo/r1_-8d__DTU/s200/20080418-P1000575.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5193224082028593202" /></a> <a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp0.blogger.com/_X2VtRiH_7DA/SBILP6hRgFI/AAAAAAAAAq4/-86yDjW_FtQ/s1600-h/20080418-P1000573.jpg"><img style="cursor:pointer; cursor:hand;" src="http://bp0.blogger.com/_X2VtRiH_7DA/SBILP6hRgFI/AAAAAAAAAq4/-86yDjW_FtQ/s200/20080418-P1000573.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5193225688346361938" /></a> </center>munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-64210230397446628152008-04-16T09:06:00.002-05:002008-04-16T09:08:57.216-05:00cmd line history meme<p><a href=http://robsanheim.com/2008/04/16/history-meme-onwards/>Rob tagged me</a>. So: <code> <pre> muness$ history 1000 | awk '{a[$2]++}END{for(i in a){print a[i] " " i}}' | sort -rn | head 102 git 72 ls 63 cd 40 ss 35 rake 24 m 17 up 14 st 12 sc 11 ri </pre> </code> </p> <p>Tag, you're it.</p>munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-52046727934460177812008-04-11T12:10:00.012-05:002008-04-21T14:08:35.499-05:00Distributed Retrospectives<p> <em><a href="http://www.amazon.com/gp/redirect.html?ie=UTF8&location=http%3A%2F%2Fwww.amazon.com%2FAgile-Retrospectives-Making-Teams-Great%2Fdp%2F0977616649%3Fie%3DUTF8%26s%3Dbooks%26qid%3D1208008472%26sr%3D8-1&tag=mundaneessays-20&linkCode=ur2&camp=1789&creative=9325">Agile Retrospectives</a><img src="http://www.assoc-amazon.com/e/ir?t=mundaneessays-20&amp;l=ur2&amp;o=1" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" /></em> (Esther Derby, Diana Larsen) and <em><a href="http://www.amazon.com/gp/redirect.html?ie=UTF8&location=http%3A%2F%2Fwww.amazon.com%2FProject-Retrospectives-Handbook-Team-Reviews%2Fdp%2F0932633447%3Fie%3DUTF8%26s%3Dbooks%26qid%3D1208008482%26sr%3D8-1&tag=mundaneessays-20&linkCode=ur2&camp=1789&creative=9325">Project Retrospectives</a><img src="http://www.assoc-amazon.com/e/ir?t=mundaneessays-20&amp;l=ur2&amp;o=1" width="1" height="1" border="0" alt="" style="border:none !important; margin:0px !important;" /></em> (Norman L. Kerth) have been a great resource in facilitating - or helping others facilitate - retrospectives. I'll often leaf through them a day or two before a retrospective to remind myself of the framework (more on that in an forthcoming post), and to introduce new activities or create my own. </p> <p> Something that neither of those books tackle directly is how to facilitate a distributed retrospective. Given that <a href="http://thinkrelevance.com">we've</a> been running a few of those lately, I wanted to share some tips: </p> <ul> <li><strong>Your equipment matters</strong>. It took us a while to find a speaker phone that worked well for those in the office and those on the other end. It's hard enough to communicate effectively without being in the same room, without muffled voices. We use a Polycom unit with two extensions for a 20x16 room.</li> <li>Prepare. There's a lot of preparation that goes into running a retrospective in person. You have to <strong>familiarize yourself with the project, look for a theme, send out invitations, pick activities (and backup activities in case one isn't working), and collect the tools/resources you need for those activities.</strong></li> <li><a href="http://docs.google.com">Google Docs</a> is your friend. With its near real time updates for viewers and collaborators, it's been an indispensable tool to take notes. The facilitator or a scribe can update the document and others can see what's happening, on the fly.</li> <li>It's not enough to use a Google Doc, you need to turn it into an online workspace that makes others feel like they're part of the process. For example, I prepare a google document with activities scheduled for the retrospective. I take care to: <ul> <li>Clearly state activity names. When we move to the next one, it's enough to say its name and everyone can easily find the appropriate section.</li> <li>Describe the activity. <strong>Having a written description allows people who miss the spoken description to follow the intent as well as the process involved</strong>. Missing a description over a conference call can happen for all sorts of reasons, from people being more easily distracted, to phone problems.</li> <li>State the targeted time for the activity. People are even more restless on a conference call. This gives them a sense of the overall meeting's timeline.</li> <li><strong>Copy or summarize information from previous retrospective documents</strong> for ease of reference.</li> </ul> <li>Share with others. Sharing the document with others as collaborators (rather than just viewers) gives them the confidence that they can correct mistakes.</li> <li>Don't forget to <strong>send an email shortly before the meeting that serves as a reminder, repeats the phone number/conference line details and includes a link to the Google Doc</strong> for that meeting.</li> </ul></li> <p> I've shared a <a href="http://docs.google.com/Doc?id=dchgzgwd_20cnnwffgs">Google Doc template</a> for a 2 hour iteration retrospective for 4 to 8 people you can refer to or use as a starting point for your own living retrospective document. Credit goes to Agile Retrospectives for the activities. </p>munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-194281142686383102008-04-11T09:20:00.007-05:002008-04-12T09:00:29.348-05:00Testing declarative code<p>For a while now, I've had my doubts about the usefulness of one aspect of test driven development (which applies equally well to behavior driven development): that it should be done 100% of the time.</p> <p>The first time I came across this, was at ThoughtWorks. The question was whether we should TDD declarative delegations. I remember a several hour long conversation about the merits of tests for Forwardable based delegations. <code> <pre> class A extend Forwardable def initialize(b) @b = b end def_delegator :@b, :some_method # Should I write a test before writing this declaration?! end </pre> </code> </p> <p> A specification for this declaration would be: <code> <pre> describe "A" do it "delegates some_method to b" do a = A.new(stub(:some_method => :x)) a.some_method.should == :x # or you could use the <a href=http://handoff.rubyforge.org/>Handoff</a> assertion: assert_handoff.from(A.new).to(:@b).for_method(:some_method) end end </pre> </code> </p> <p>I didn't get the fuss -- it was clear to me that testing such delegations was wasteful and a maintenance hassle. But I didn't realize that this was part of a larger argument.</p> <h1>Not an isolated case</h1> <p>Since then, I've learned that to some developers this is a general rule. Here's another example, re testing stock ActiveRecord validation declarations, e.g.: <code> <pre> class Model < ActiveRecord::Base validates_presence_of :attr_1 end </pre> </code> </p> <p> The test for this would be: <code> <pre> describe "Model" do it "validates_presence_of :attr_1" do model = Model.new model.valid? model.errors.on(:attr_1).should == "must not be blank" end # or you might write a helper that reflects on validates_presence_of declarations test_model_validates_presence_of Model, :attr_1 end </pre> </code> </p> <p>It took me a while (too long) to get the general pattern: these discussions I kept finding myself in all had to do with testing declarations.</p> <h1>How did we get here?</h1> <p>Test driven development as best as I can tell is a - pragmatic - descendant of formal specifications: <blockquote> "A formal specification is a mathematical description of software or hardware that may be used to develop an implementation. It describes what the system should do, not how the system should do it. Given such a specification, it is possible to use formal verification techniques to demonstrate that a candidate system design is correct with respect to the specification." </blockquote> </p> <p> <strong>Writing formal specifications for the majority of the software isn't practical. Writing tests first provides us a simple, practical way to write executable specifications.</strong> True, they don't give us the ability to use "formal verification techniques" to prove anything about our code, but they do give us confidence that our code does what our tests say the code does (as much confidence as we have in our tests). </p> <h1>The rule</h1> <p>The rule regarding TDD is: If you can TDD it and it's destined to be production code, you should TDD it.</p> <p> The problem I find with this rule is that it causes us to write tests like those above that duplicates the declarative code. At best case it goes like this: <code> <pre> # We write this code first in the spec: test_model_validates_presence_of Model, :attr_1 # we run it and watch it fail... # and then we write this in the model class: validates_presence_of :attr_1 # rerun the test and watch it pass... </pre> </code> </p> <p> I find this wasteful: We first implement the <code>test_declaration_is_called</code> (in this case, <code>test_model_validates_presence_of</code>) for every declaration. After all that work (and not to mention code that we have to maintain) we end up with a test that tells us what the declaration itself does. But with less readability and intent. Indeed, <strong>declarative code is in fact formal specification</strong>. </p> <h1>Guidelines not rules</h1> <p>Writing tests first is a great guideline. I do it <strong>almost</strong> all the time. But I don't do it all the time. It makes me <em>angry</em> when I change declarative code only to have a test that looks exactly the same fail as well.</p> <p>OK, tests for declarations don't really make me angry. But they do make me wonder if the developer who wrote the test really likes to answer the question, "are you sure that you're sure that you want to do what you just said you want to do?"</p>munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-77505040159053585212008-03-14T10:59:00.005-05:002008-03-14T12:37:03.839-05:00Where there's smoke signals...In a <a href=http://muness.blogspot.com/2008/02/ccrb-campfire-notifier-released.html>previous post</a>, I briefly described how we were using cc_campfire_notifier to get build success/failure notifications in campfire. cc_campfire_notifier has lost momentum over the last couple of months. Since we actively rely on the notifier for several projects, we've now forked it as <A HREF=http://opensource.thinkrelevance.com/wiki/smoke_signals>smoke_signals</a>. <p>The first release offers the following improvements: <ul> <li>Each notification includes a link back to the CruiseControl.rb build (so you have one-click access to the full details for each build). <li>Recognizes apr_error errors as SVN errors (so you'll spend less time scratching your head over generic build failure messages). <li>Speaks once into the campfire room whenever an unexpected error occurs (as opposed to speaking once per line in the backtrace). </ul> <p>Check out the notifier's <a href=http://opensource.thinkrelevance.com/wiki/smoke_signals>home page</a> for installation instructions or head on over to <a href=http://github.com/relevance/smoke_signals/tree>GitHub to browse the code</a>.munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-32662221723705671122008-03-06T08:46:00.003-05:002008-03-06T09:44:29.245-05:00Release early, release often, business editionOne of the things we preach as software developers is <a href=http://catb.org/~esr/writings/cathedral-bazaar/cathedral-bazaar/ar01s04.html>release early, release often</a>. I think ESR is right in that our primary motivation for this is often related to quality: <blockquote> In the bazaar view, on the other hand, you assume that bugs are generally shallow phenomena—or, at least, that they turn shallow pretty quickly when exposed to a thousand eager co-developers pounding on every single new release. <strong>Accordingly you release often in order to get more corrections</strong>, and as a beneficial side effect you have less to lose if an occasional botch gets out the door. </blockquote> <p> As I've done more project consulting and management, I've realized that the benefits extend well beyond the technical ones. But I've had a difficult time articulating the business benefits to incremental, frequent releases. <p>Last week, Ross posted an article that describes one of the primary business benefits to frequent, early releases, <a href=http://agilemanager.blogspot.com/2008/02/minimising-speculative-risk-of-it.html>Minimising the Speculative Risk of IT Investments</a>: <blockquote> With each incremental delivery, and every increase in tangible value, the intangible or speculative value decreases. <strong>The reduction in speculative value at risk represents a reduction in the total value that can be depressed through delays in delivery</strong>. Thus, early delivery reduces the risk of speculative value not being realised. Simultaneously, it reduces the volatility of returns. </blockquote> <p>What other benefits do you find to the business from releasing early, releasing often?munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-41932235045562816432008-02-07T18:19:00.001-05:002008-04-11T15:03:40.175-05:00CC.rb Campfire Notifier Released<strong>Note</strong>: I've forked this notifier to add features and fix some bugs at in <a href=http://muness.blogspot.com/2008/03/where-theres-smoke-signals.html>Smoke Signals</a>. At Relevance, we make heavy use of Campfire for our distributed projects. One room per project, and we expect everyone to "be in" the relevant room associated with the project they're working on at the time. We use it to communicate: to ask for a pair (we've tried Leopard's Remote Desktop Sharing, but have no settled mostly on emacs + screen), ask questions, request code reviews (we've taken to using the topic for posting ad hoc requests like that), and make announcements. <p> This virtual space has many disadvantages over being in the same room as I'd been used to on my previous couple of projects, but it did have a nice perk: SVN commit notifications were right there alongside our own discussions. The notices were more useful than I expected, a quick, clear indicator of our pace on a given day. <p> Naturally, I wanted our other major automated task to notify us of <A HREF=http://ccmenu.sourceforge.net/>CCMenu</A> is OK, but it's disconnected from our primary virtual space (Campfire, as discussed above). Being <a href=http://undefined.com/ia/2006/10/24/the-fourteen-types-of-programmers-type-4-lazy-ones/>lazy</a>, I didn't want to build it myself, so I first looked around for tools that suited my needs. Much to my chagrin, none did, so I took the one that was closest and added to it what I needed: <a href=http://campfire-ccrb.rubyforge.org/svn/test/campfire_notifier_test.rb>specs</a>. Then came the stuff that was missing: ssl support and support for one room per project. And last but not least, to help deal with configuration hassles and debugging, I added logging. <p> If you're using CruiseControl.rb and Campfire, give it a whirl. Instructions are in the <a href=http://campfire-ccrb.rubyforge.org/svn/CampfireNotifier.README>README</A>.munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-38242453866757631122008-02-05T16:53:00.000-05:002008-02-05T17:31:18.543-05:00Pi<a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://www.wired.com/science/discoveries/news/2008/02/dayintech_0205"><img style="cursor:pointer; cursor:hand;" src="http://bp0.blogger.com/_X2VtRiH_7DA/R6jjj2hXloI/AAAAAAAAAoc/31Ehh2U21FU/s640/TextMateScreenSnapz002.jpg" border="0" alt=""id="BLOGGER_PHOTO_ID_5163627177850672770" /></a>munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-21585641866795547782008-01-20T18:46:00.001-05:002008-04-14T10:34:39.128-05:00Managers and rules<A HREF=http://www.geraldmweinberg.com/BIOStuff/EachBIO/bio.Jerry.html>Jerry Weinberg</A>, if you don't know him, is the author of more than a dozen books on software development and memorable laws such as the <A HREF=http://en.wikipedia.org/wiki/Weinberg%27s_Law_of_Twins>Law of Twins</A>. In Quality Software Management: Systems Thinking, published in '92, he described what we might today call an agile team. (He lists several levels of software team cultures, ranging from Oblivious to Congruent. Along that spectrum, an agile team would fall somewhere along Steering, Anticipating and Congruent.) <p> At <a href=http://www.ayeconference.com/>AYE</A> 2007, I had the pleasure of meeting Jerry in person. Something that I was not aware of was his remarkable skill at helping others. Instead of answering a request for advice directly, he'd delve into the details, applying something like the <A HREF=http://en.wikipedia.org/wiki/5_Whys>5 Whys</A>. Within minutes, he would hone in on a rule that the person had imposed on themselves that was at the root of the problem. After expressing the problematic rule, he'd help them relax the rule into guidelines. Several people who've spent more time with him have echoed my gut feel that they too found this ability to help others rare and invaluable. <p> His approach to helping individuals is based on <A HREF=http://en.wikipedia.org/wiki/Virginia_Satir>Virginia Satir</A>'s work in family therapy on survival rule transformation. An excerpt from The New People Making: <blockquote> After you have written down all the rules your family thinks exist and cleared up any misunderstanding about them, go on to the next phase. Try to discover which of your rules are still up to date and which are not. As fast as the world changes, it is easy to have out-of-date rules. Are you driving a modern car with Model T rules? Many families are doing just this. If you find that you are, can you bring your rules up to date and throw away the old ones? One characteristic of a nurturing family is the ability to keep its rules up to date. <p> Now ask yourself if your rules are helping or obstructing. What do you want them to accomplish? Good rules facilitate instead of limit. </blockquote> <p> Last weekend, while attending a Satir workshop I was struck by the applicability of Satir's ideas on rules to teams at work. There too, rules can be unstated, ambiguous or inapplicable sometimes hampering our ability to get things done, other times making everyone miserable. <p> (For a valuable discussion on rules suitable for software development teams, check out the section on Rules in chapter 21 of Software Teamwork:Taking Ownership for Success. To summarize, the rules should be focused on ensuring that a team has "the capabilities and resources to do the right things, rather than on stepwise procedures to be dogmatically followed".) <p> In big companies the problems are magnified as fully connected networks of people become prohibitively expensive. For example, it'd take each person over a day out of every week for 25 people to stay in meaningful weekly one-on-one contact. (Indeed, the few who take the time to build an extensive network can become <a href=http://blog.splitbody.com/2007/6/27/social-networks-in-large-companies>stars</a> capable of getting things done faster than their peers could.) Additionally, in a big organization, it's likely that a rule that works for one group will be applied to ones where it doesn't fit. <p> The role of a manager is to engender an enabling environment. But how? Virginia Satir's approach to rule transformation provides one way. One which fits in well with the management advice in many of the management books I am familiar with, including Behind Closed Doors, Peopleware and Slack. <p> Here's how I describe the process: <ol> <li>Identify implicit rules and restate the explicit ones. <li>Explore how they came about. <li>Express the rules explicitly. <li>Clear up ambiguities in the team's understanding of each rule. <li>Evaluate each rule's applicability. </ol> <p> Having done this, you might find that a rule is: <ul> <li>Helpful and reasonable. So, apply it! <li>Helpful but impossible or untenable. Then transform it into one or more guidelines that aren't prohibitive. <li>Limiting or obstructing your team. If so, the rule should be challenged, publicly. </ul> <p> Again, quoting The New People Making: <blockquote> What do you think about your rules? Are they overt, human [i.e. reasonable] and up to date? Or are they covert, inhuman [i.e. prohibitive, impossible] and out of date? If your rules are mostly of the second variety, I think you realize that you and your family [or team] have some important and necessary work to do. If your rules are of the first category, you are probably all having a ball. </blockquote>munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-16096940793274892512008-01-19T02:24:00.000-05:002008-01-27T03:06:48.164-05:00spec-converter 0.0.3Inspired by a link <a href=http://robsanheim.com/>Rob</a> sent to <a href=http://pivots.pivotallabs.com/users/chad/blog/articles/249-using-search-and-replace-regular-expressions-to-convert-from-test-unit-to-rspec>a blog entry on converting Test::Unit to Rspec</a>, I added the ability to convert assertions in <a href=http://opensource.thinkrelevance.com/wiki/spec-converter>spec-converter</a>, as follows: <ul> <li><code>assert !foo</code> with <code>foo.should == false</code> <li><code>assert foo > 20</code> with <code>foo.should > 20</code> <li><code>assert foo</code> with <code>foo.should == true</code> <li><code>assert_equal y, x</code> with <code>x.should == y</code> <li><code>assert_false foo</code> with <code>foo.should == false</code> <li><code>assert_nil foo</code> with <code>foo.should == nil</code> <li><code>assert_not_nil foo</code> with <code>foo.should.not == nil</code> <li><code>assert_true foo </code> with <code>foo.should == true</code> </ul> <p>Enjoy your conversion. ;)munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-57829319000973219062008-01-18T22:39:00.000-05:002008-01-19T14:30:45.407-05:00If software could make resolutions...Two weeks ago I got what at first glance was yet another electronic greeting, the sort I've come to barely look at for couple of reasons: 1) like everyone else, I never seem to have enough time for everything, and 2) I guess I've become jaded - if someone wants to say hi, I'd rather get a personalized SMS or one sentence email than getting yet another irrelevant "funny" greeting lacking personality. Seconds after hitting the delete button I restored it from my trash to visit later. <p> <a onblur="try {parent.deselectBloggerImageGracefully();} catch(e) {}" href="http://bp1.blogger.com/_X2VtRiH_7DA/R5F0J6xrZYI/AAAAAAAAAn4/IXhwqTEaN9k/s1600-h/screen-capture-1.png"><img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;" src="http://bp1.blogger.com/_X2VtRiH_7DA/R5F0J6xrZYI/AAAAAAAAAn4/IXhwqTEaN9k/s320/screen-capture-1.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5157030762061194626" /></a> <p>I finally got around to it this evening while catching up on my email, curious as to why I had undeleted it. For one, it's simple, with lots of white space and only two, clear links: one smack in the middle and an unsubscribe link - noteworthy because unlike most unsubscribe links, it was obvious. <p>For another it was from Cooper, the interaction design firm. Over the holidays I'd read Alan Cooper's provocative The Insane are Running the Asylum which resonated with me. (Just ask the folks at work who must be tired of me ranting, "what do you get when you cross a phone with a computer? A computer!" A phone that takes 3 minutes to reboot is more computer than phone, no matter what it looks like.) <p>I then <a href=http://cdn2.goldmail.com/?GMID=7i8xhec3csrt>clicked the link</a> and was pleasantly surprised, for what I found was a short, witty presentation with thoughtful design tips, paired with music composed by Philip Glass. The tips included: <ul> <li>Remember users' preferences. <li>Aesthetics matter. <li>Stop dialog-boxing your users. <li>Communicate with them. <li>Don't follow, lead. </ul> <p>But don't settle for my summary of it, <a href=http://cdn2.goldmail.com/?GMID=7i8xhec3csrt>check it out for yourself</a>. <p>These tips are indicative of the ideas in <a href=http://cooper.com/insights/books/>his books</a>. The Insane are Running the Asylum lucidly illustrates various problems that arise when design is merely an afterthought. (Note: his primary focus is on <em>interaction</em>, not aesthetics.) <p>One such problem is one I alluded to earlier, about the convergence of various electronics with computers. When everyone else's focus is on technology and features rather than on user interaction, Apple handily beats them at their own game time and again. By the by, I contend that this is not because Apple is doing interaction design well as it is because everyone else isn't doing any. <p>The Insane are Running the Asylum also contained an intriguing discussion of confirmation dialog boxes. Alan points out that confirmation dialog boxes were a misguided "solution" to a user losing their data. By adding a dialog box, such a loss was no longer due to poor design, instead it became the user's fault. Why these miserable dialog boxes persist in the days of unlimited undo and Recycle bin recovery is beyond me. <p>About Face (which I am still reading) focuses on the how instead of the why of interaction design. It's an interaction design tome. (I want to point out that it includes an excellent primer on business analysis, covering persona and scenario composition.) It distinguishes between <em>implementation</em>, <em>mental</em> and <em>represented</em> models. Simply, the implementation model is the set of classes that encodes the business domain, the mental model is the user's picture of the domain and the represented model is the way the implementation model is presented to the user. <p>This is in stark contrast to <A HREF=http://domainlanguage.com/ddd/index.html>Domain Driven Design</A>'s "One Team, One Language" mantra. By insisting that the implementation must match up with the mental model, it can make our job of building software even more difficult than inherently is. Or we do the reverse, forcing the implementation model on our user, who thank you very much, already understands her business and will vehemently resent being told what her business <em>really</em> entails. <p>By introducing a represented model, we acknowledge that a mental and an implementation model are not equivalent. The represented model, the result of interaction design, can map from one to the other, facilitating usage and implementation. <p>If software could make resolutions, it might resolve to be considerate. Interaction design is part of what will get us there.munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-12821770416662187792008-01-04T22:12:00.000-05:002008-01-19T10:24:04.978-05:00spec_converter releasedTired of converting your <A HREF=http://ruby-doc.org/stdlib/libdoc/test/unit/rdoc/classes/Test/Unit.html>Test::Unit </a> tests over to <a href=http://test-spec.rubyforge.org/test-spec/>test/spec</a> specs by hand? So were <a href=http://thinkrelevance.com>we</a>. So we made it as <a href=http://spec-converter.rubyforge.org/>easy</a> as 1,2,3: <blockquote> <code> <pre> sudo gem install spec-converter cd ~/work/my_project spec_converter </pre> </code> </blockquote> <p>Note that it works on files in place, so be sure to check in or backup first! <p>It only does the basics right now, converting old style specs to new(<code>context</code> with <code>describe</code>, <code>spec</code> with <code>it</code>) and things like: <pre><code> def setup @o = Object.new end def test_should_do_something_cool assert_equal 1+1, 2 end </code></pre> <p>to: <pre><code> before do @o = Object.new end it "should do something cool" do assert_equal 1+1, 2 end </code></pre> <p> See <A HREF=http://opensource.thinkrelevance.com/browser/spec_converter/trunk/test/spec_converter_test.rb>its spec</A> for a full list of features. If you'd like to grab the latest, get it at <A href=https://opensource.thinkrelevance.com/svn/spec_converter/trunk>the subversion repository</a>.munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-33458571225629056832007-12-28T12:11:00.000-05:002007-12-28T15:29:33.308-05:00Joining the 5%In a <a href="http://www.artima.com/weblogs/viewpost.jsp?thread=221622">compelling speech</a> by Bruce Eckel: <blockquote> ... Roughly 80% of programmers don't read books, don't go to conferences, don't continue learning, don't do anything but what they covered in college. Maybe they've gotten a job in a big company where they can do the same thing over and over. The other 20% struggle with their profession: they read, try to learn things, listen to podcasts, go to user group meetings and sometimes a conference. 80% of this 20% are not very successful yet; they're still beginning, still trying. The other 20% of this 20% -- <span style="font-weight:bold;">that's about 5% of the whole who are 20x more productive</span>. <p> So how do you become one of these mythical 5%? <p> These people are not those who can remember all the moves and have fingers that fly over the keyboard erupting system commands. In my experience those in the 5% must struggle to get there, and struggle to stay there, and it's the process of continuous learning that makes the difference. </blockquote> <p>It took me a long time to realize that there was such a significant productivity difference. It took me longer still to realize <a href="http://www.npr.org/templates/story/story.php?storyId=7406521">that I could - and should - aspire to achieving that increased level of productivity</a>, and that it wasn't based on innate abilities or otherwise fixed. Bruce lists some things to do to get there: <ul> <li>Read. A lot. [Books rife with advice on how to grow as a technical person include Jerry's <em>On Becoming a Technical Leader</em> and <em>Secrets of Consulting</em>, the Prags' <em>The Pragmatic Programmer</em>, and Chad's <em>My Job Went to India</em>.] <li>Go to conferences. But be selective about which ones to go to. <li>Listen to podcasts. <li>"Using the best tools, techniques, and ideas at your disposal. Always doing your best." <li>Attend user group meetings. <li>Keep the big picture in mind: "... people will still spend all their time on one decision while something else might actually have a far greater influence. Architectural decisions, for example." <li>Remember Jerry's maxim: "no matter what they tell you, it's always a people problem." <li>Learn the difference between targets and goals (see the excellent <em>Waltzing with Bears</em>). And communicate both. <li>Never fall for <a href="http://en.wikipedia.org/wiki/No_Silver_Bullet">silver bullets</a>. <li>Keep in mind that "things are the way they are because they got that way ... one logical step at a time." </ul> <p>Tips not in Eckel's speech: <ul> <li><a href="http://headrush.typepad.com/">Create passionate users</a>. (The blogosphere isn't the same without you, Kathy!). This means giving <a href="http://www.cooper.com/insights/books/">interaction design</a> the respect and resources that it deserves. <li><a href="http://www.zedshaw.com/rants/programmer_stats.html">Learn statistics</a>. <li><a href=http://muness.blogspot.com/2005/01/bio-and-not-enough-cross-disciplinary.html>Learn about things that have - apparently - nothing to do with programming</a>. For example, I've learned much from <A href="http://sethgodin.typepad.com/">Seth Godin</a>, who taught me the importance of emotions when delivering a speech, marketing yourself and your team, honesty in that marketing and much more. <li>Learn <a href=http://www.squidoo.com/thedipbook>when to quit and when to stick</a>. <li>Learn how to communicate better. (Less is more!) <li>Strive to be the worst member of your team (see <em>My Job Went to India</em> for a good explanation of what this is all about). <li>"<a href="http://en.wikipedia.org/wiki/Even_Cowgirls_Get_the_Blues">Embrace failure!</a> Seek it out. Learn to love it. That may be the only way any of us will ever be free." And learn from it. <li><a href="http://www.smartlemming.com/blog/index.php/2007/02/12-tips-to-learn-how-to-be-curious/">Be curious</a>. <li>Remember: "no matter what they tell you, it's always a people problem." Because it bears repeating. </ul> <p>I'd like to close the same way Bruce did his speech: <blockquote> You'll need to make a lot of mistakes in order to figure things out. So be humble, and keep asking questions. </blockquote>munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-2477204345821092642007-12-25T13:11:00.000-05:002007-12-25T14:01:56.337-05:00The Carpet MakersAs you probably know, <a href="http://allconsuming.net/summaries/view/muness/2007/12">I love reading</a>. Sometimes, I come across delightfully engaging books. This year that's included the masterful Nassim Taleb's <em><a type="amzn" asin="1400063515">The Black Swan</a></em>, a <a href="http://en.wikipedia.org/wiki/Black_swan_theory">provocative look at unexpected, high impact events</a> and Terry Pratchett's <em><a type="amzn" asin="0061161640">Making Money</a></em>, a fantastical treatise on money and how we interact with it (not to mention a fun excursion into the world of systems thinking and modeling). <p>Today, I finished another excellent book, <em><a type="amzn" asin="0765305933">The Carpet Makers</a></em> by Andreas Eschbach (translated from German). It retells an epic story across planets, solar systems and even dimensions that spans millenniums. Yet it manages to weave that story with that of individual characters and their personal plight. The story and his story telling are exciting and engaging making the book difficult to put down. Highly recommended.munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-85031669416698333922007-12-25T12:26:00.000-05:002007-12-25T13:11:59.782-05:00Agile Retrospectives<p>At <A href="http://thinkrelevance.com">Relevance</A>, we recently added a twist to the way we run our retrospective. We have a non-team member lead the meeting. We use this to provide a fresh and impartial perspective give each of a chance to learn about other projects, and introduce more of us to clients. Here, I wanted to share a primary resource we use for planning and running retrospectives. <p>Some time back, <a href="http://robsanheim.com/">Rob</a> asked me to lead a retrospective for his project. I’d built up several tools over time to help run a retrospective, but I’d never been quite satisfied with the typical format, consisting of the typical exercise asking participants what they felt was going well and what could be done better. So I consulted with <a type="amzn" asin="0977616649">Agile Retrospectives: Making Good Teams Great</a> and picked out a few exercises from the book including a Check-In, Team Radar, and Learning Matrix. The book explains the purpose of each exercise, time requirements, a description, necessary materials or preparation as well as an example. Here’s my summary of the Check-In activity: <blockquote> This exercise is used to set the stage and help participants focus on the retrospective. The format was to go around the room answering the question, "In one or two sentences, what are your hopes for the retrospective?" </blockquote> <p>And of the Team Radar: <blockquote> We use this to uncover important factors to the success of the project. It then is used to measure how well the team members think we're doing in each area by ranking each on a scale from 1 to 10 with higher numbers indicating better performance. This can be done in future retrospectives and tracked over the life the project. </blockquote> <p>(Check out the <A HREF="http://media.pragprog.com/titles/dlret/Activities.pdf">Selected Activities (pdf)</A> on the <A HREF="http://pragprog.com/titles/dlret">book’s web page</A> for examples straight out of the book.) <p>Such exercises have helped me get a lot more out of retrospectives. Specifically, they've helped our teams get on the same page in terms of important factors to the project and perceptions of how each member thought the team was doing. We've been able to extract and prioritize action items and track team vitals over time. Indeed, we've had projects that have had quick, radical changes in project direction and deliverables due to issues uncovered in such meetings. <p>What resources do you use for running a retrospective?munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-22449753964833374492007-12-12T22:50:00.000-05:002007-12-12T23:39:59.822-05:00Validate your fixturesIf you join a Rails project, odds are fixtures are already in place. And it's not at all unlikely that they'd be invalid. Here's a test (written in test-spec) to detect those errors. (Also a <a href="http://pastie.caboo.se/127894">pastie</a>) <p> <code><pre class="sunburst"><span class="meta meta_require meta_require_ruby"><span class="keyword keyword_other keyword_other_special-method keyword_other_special-method_ruby">require</span> <span class="support support_class support_class_ruby">File</span><span class="meta meta_function-call meta_function-call_method meta_function-call_method_with-arguments meta_function-call_method_with-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">dirname</span></span>(<span class="variable variable_language variable_language_ruby">__FILE__</span>) <span class="keyword keyword_operator keyword_operator_arithmetic keyword_operator_arithmetic_ruby">+</span> <span class="string string_quoted string_quoted_single string_quoted_single_ruby">'/functional_test_helper'</span></span> describe <span class="string string_quoted string_quoted_double string_quoted_double_ruby">"Fixtures"</span> <span class="keyword keyword_control keyword_control_ruby keyword_control_ruby_start-block">do</span> <span class="variable variable_other variable_other_constant variable_other_constant_ruby">ALL_FIXTURES</span> <span class="keyword keyword_operator keyword_operator_assignment keyword_operator_assignment_ruby">=</span> <span class="support support_class support_class_ruby">Dir</span>[<span class="string string_quoted string_quoted_double string_quoted_double_ruby">"<span class="source source_ruby source_ruby_embedded source_ruby_embedded_source">#{<span class="variable variable_other variable_other_constant variable_other_constant_ruby">RAILS_ROOT</span>}</span>/test/fixtures/**/*.yml"</span>]<span class="meta meta_function-call meta_function-call_method meta_function-call_method_without-arguments meta_function-call_method_without-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">collect</span></span>{|<span class="variable variable_other variable_other_block variable_other_block_ruby">yml_file</span>|<span class="support support_class support_class_ruby">File</span><span class="meta meta_function-call meta_function-call_method meta_function-call_method_with-arguments meta_function-call_method_with-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">basename</span></span>(yml_file, <span class="string string_quoted string_quoted_single string_quoted_single_ruby">'.yml'</span>)<span class="meta meta_function-call meta_function-call_method meta_function-call_method_without-arguments meta_function-call_method_without-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">to_sym</span></span>} fixtures <span class="variable variable_other variable_other_constant variable_other_constant_ruby">ALL_FIXTURES</span> <span class="variable variable_other variable_other_constant variable_other_constant_ruby">ALL_MODELS</span> <span class="keyword keyword_operator keyword_operator_assignment keyword_operator_assignment_ruby">=</span> <span class="support support_class support_class_ruby">Dir</span>[<span class="string string_quoted string_quoted_double string_quoted_double_ruby">"<span class="source source_ruby source_ruby_embedded source_ruby_embedded_source">#{<span class="variable variable_other variable_other_constant variable_other_constant_ruby">RAILS_ROOT</span>}</span>/app/models/**/*.rb"</span>]<span class="meta meta_function-call meta_function-call_method meta_function-call_method_without-arguments meta_function-call_method_without-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">collect</span></span>{|<span class="variable variable_other variable_other_block variable_other_block_ruby">ruby_file</span>|<span class="support support_class support_class_ruby">File</span><span class="meta meta_function-call meta_function-call_method meta_function-call_method_with-arguments meta_function-call_method_with-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">basename</span></span>(ruby_file, <span class="string string_quoted string_quoted_single string_quoted_single_ruby">'.rb'</span>)<span class="meta meta_function-call meta_function-call_method meta_function-call_method_without-arguments meta_function-call_method_without-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">classify</span></span><span class="meta meta_function-call meta_function-call_method meta_function-call_method_without-arguments meta_function-call_method_without-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">constantize</span></span>} it <span class="string string_quoted string_quoted_double string_quoted_double_ruby">"should be valid"</span> <span class="keyword keyword_control keyword_control_ruby keyword_control_ruby_start-block">do</span> errors <span class="keyword keyword_operator keyword_operator_assignment keyword_operator_assignment_ruby">=</span> <span class="constant constant_numeric constant_numeric_ruby">0</span> <span class="variable variable_other variable_other_constant variable_other_constant_ruby">ALL_MODELS</span><span class="meta meta_function-call meta_function-call_method meta_function-call_method_without-arguments meta_function-call_method_without-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">each</span></span> <span class="keyword keyword_control keyword_control_ruby keyword_control_ruby_start-block">do </span>|<span class="variable variable_other variable_other_block variable_other_block_ruby">model_class</span>| <span class="keyword keyword_control keyword_control_ruby">if</span> model_class<span class="meta meta_function-call meta_function-call_method meta_function-call_method_with-arguments meta_function-call_method_with-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">respond_to?</span></span>(<span class="constant constant_other constant_other_symbol constant_other_symbol_ruby">:find</span>) model_class<span class="meta meta_function-call meta_function-call_method meta_function-call_method_with-arguments meta_function-call_method_with-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">find</span></span>(<span class="constant constant_other constant_other_symbol constant_other_symbol_ruby">:all</span>)<span class="meta meta_function-call meta_function-call_method meta_function-call_method_without-arguments meta_function-call_method_without-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">each</span></span> <span class="keyword keyword_control keyword_control_ruby keyword_control_ruby_start-block">do </span>|<span class="variable variable_other variable_other_block variable_other_block_ruby">instance</span>| <span class="keyword keyword_control keyword_control_ruby">unless</span> (instance<span class="meta meta_function-call meta_function-call_method meta_function-call_method_without-arguments meta_function-call_method_without-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">class</span></span> <span class="keyword keyword_operator keyword_operator_comparison keyword_operator_comparison_ruby">!=</span> model_class) <span class="keyword keyword_operator keyword_operator_logical keyword_operator_logical_ruby">||</span> instance<span class="meta meta_function-call meta_function-call_method meta_function-call_method_without-arguments meta_function-call_method_without-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">valid?</span></span> errors <span class="keyword keyword_operator keyword_operator_assignment keyword_operator_assignment_augmented keyword_operator_assignment_augmented_ruby">+=</span> instance<span class="meta meta_function-call meta_function-call_method meta_function-call_method_without-arguments meta_function-call_method_without-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">errors</span></span><span class="meta meta_function-call meta_function-call_method meta_function-call_method_without-arguments meta_function-call_method_without-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">size</span></span> puts <span class="string string_quoted string_quoted_double string_quoted_double_ruby">"<span class="source source_ruby source_ruby_embedded source_ruby_embedded_source">#{instance<span class="meta meta_function-call meta_function-call_method meta_function-call_method_without-arguments meta_function-call_method_without-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">class</span></span>}</span> <span class="source source_ruby source_ruby_embedded source_ruby_embedded_source">#{instance<span class="meta meta_function-call meta_function-call_method meta_function-call_method_without-arguments meta_function-call_method_without-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">id</span></span>}</span> has <span class="source source_ruby source_ruby_embedded source_ruby_embedded_source">#{instance<span class="meta meta_function-call meta_function-call_method meta_function-call_method_without-arguments meta_function-call_method_without-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">errors</span></span><span class="meta meta_function-call meta_function-call_method meta_function-call_method_without-arguments meta_function-call_method_without-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">size</span></span>}</span> error(s):"</span> instance<span class="meta meta_function-call meta_function-call_method meta_function-call_method_without-arguments meta_function-call_method_without-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">errors</span></span><span class="meta meta_function-call meta_function-call_method meta_function-call_method_without-arguments meta_function-call_method_without-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">each</span></span>{|<span class="variable variable_other variable_other_block variable_other_block_ruby">attr</span>,<span class="variable variable_other variable_other_block variable_other_block_ruby">msg</span>| puts <span class="string string_quoted string_quoted_double string_quoted_double_ruby">" <span class="source source_ruby source_ruby_embedded source_ruby_embedded_source">#{<span class="keyword keyword_other keyword_other_special-method keyword_other_special-method_ruby">attr</span>}</span> - <span class="source source_ruby source_ruby_embedded source_ruby_embedded_source">#{msg}</span>"</span> } <span class="keyword keyword_control keyword_control_ruby">end</span> <span class="keyword keyword_control keyword_control_ruby">end</span> <span class="keyword keyword_control keyword_control_ruby">end</span> <span class="keyword keyword_control keyword_control_ruby">end</span> errors<span class="meta meta_function-call meta_function-call_method meta_function-call_method_without-arguments meta_function-call_method_without-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">should</span></span><span class="meta meta_function-call meta_function-call_method meta_function-call_method_with-arguments meta_function-call_method_with-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">blaming</span></span>(<span class="string string_quoted string_quoted_double string_quoted_double_ruby">"No errors expected"</span>)<span class="meta meta_function-call meta_function-call_method meta_function-call_method_without-arguments meta_function-call_method_without-arguments_ruby">.<span class="entity entity_name entity_name_function entity_name_function_ruby">equal</span></span> <span class="constant constant_numeric constant_numeric_ruby">0</span> <span class="keyword keyword_control keyword_control_ruby">end</span> <span class="keyword keyword_control keyword_control_ruby">end</span> </pre></code>munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-3522347864868416352007-11-05T01:55:00.000-05:002007-11-05T02:19:55.753-05:00AYE - Tutorial Day<p> Here are my notes, unedited from the optional warm up tutorial at AYE. I am posting them here to share my first impressions of the conference, to give you a glimpse at just how different this conference is from traditional software-related conferences. </p> <p> The first activity of the day was: Connect. Tutorial attendees (only 24 signed up for this optional day) broke out into groups of 3 and got to know each other. As a result, they then put together a one minute presentation about themselves to share with everyone. The nice thing here was the prompt - how did you end up here? The exercise was interesting; a couple where the husband is a programmer at Google. A Test manager at Adobe. A tester at Microsoft who works on yet to be announced products. There was a former rocket science turned software tester. There were a couple of attendees from paris and one from New Zealand and 4 from Canada. Almost everyone was here because of personal recommendations from colleagues. </p> <p> After a break, we took a Myers Briggs Type Indicator test. This time around I was an INFP (Introvert vs Extrovert, Intuiting vs Sensing, Feeling vs Thinking, Perceiving vs Judging). We then went through an exercise showing what the indicators mean: a preference one direction or another. The example they used was along the scale, from "It's always playtime" to "work before play" and highlighted that NFs generally try to make work fun and then cluster around "It's always playtime". </p> <p> After lunch, we did exercises to highlight how to use type, and how the different types behave (1. what thinkers and feelers said about love, 2. an exercise where each group of type got together to map the first floor of the hotel). This showed strengths of each type and how much more effective we can be if we team together people of different types together. Also, it can be used to allow individuals to know what they're good at and what they're not. </p> <p> Next up was the congruence tutorial. To illustrate congruence and it's relevance, we started with an exercise where one person delivers a message that doesn't match up with what they're saying: nodding their heads while saying "I agree with you" at the same time. Congruence is behaving in a manner compatible with the words you use, what you believe. </p> <p> Communication is a message from a 1. self to 2. other in 3. context. What happens if one of these is removed from the equation? Incongruence: <ul> <li>Placating (removing 1. self). <li>Blame (removing 2. other in response) <li>Remove 1. self and 2. other. This is being Super-Reasonable. ("It's all about the product, we don't matter", "we're only working 10 hours a day, we should start working 12 hour days") <li>Remove all. All of a sudden the conversation diverges and talks about nothing and is utterly irrelevant. </ul> </p> <p> In order to be congruent, we have to take everything into account, 1. self, 2. other and 3. context. You are then centered, and balanced. </p> <p> The last thing we got around to was an introduction to Satir's temperature readings, meetings that are suitable for measuring a team's state. Here's how the agenda flows: <ol> <li>Appreciations: start by something positive about the past. <li>New Information: share something that happened that only you know about. <li>Puzzles: share and note puzzles. <li>Share complaints w/ Recommendations: DON'T share complaints that don't have at least one recommendation. <li>Hopes and wishes: share them. </ol> </p>munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-31991442090572122242007-11-03T22:36:00.000-05:002007-12-28T13:22:57.918-05:00Conference? Participate!<p> This weekend started going with my attending the first day of RubyConf 2007, a conference familiar to me - I've was there in 2005 and 2006 as well. It's one of my favorite conferences - with smallish crowd of passionate people (as you probably know, it <a href="http://duncandavidson.com/archives/171">sells out within hours</a> of being announced). The talks I attended, were great; instead of the standard technical fare, we were treated to talks on the <A HREF="http://dotavery.com/blog/archive/2007/11/02/149439.aspx">role of beauty in code</A>, a call to innovate and push favorite DSLs further (<A HREF="http://railstips.org/2007/11/2/jim-weirich-advanced-ruby-class-design">with Jim presenting LINQ-like improvements to ActiveRecord</A>), and <A HREF="http://kfahlgren.com/blog/2007/11/02/rubyconf-2007-first-day-afternoon/">a call to improve ourselves and our code</A>. </p> <p> Now, I am on my way to yet another conference, <A HREF="http://www.ayeconference.com/">AYE</A>, Amplifying Your Effectiveness. AYE is very different than most conferences: the sessions are not presentations, but rather participatory. I've done the same sort of thing this year during my presentations at eRubycon and at NFJS. Others are doing that as well, take <A HREF="http://jasonrudolph.com/blog/2007/11/03/evan-phoenix-on-testing-private-methods-in-ruby/">Stuart's Refactotum</A> is a notable example. Yet participatory sessions are still the exception. </p> <p> This, I am sure is familiar: sitting through a presentation in college, the professor yet again read her slides, word for word, as she does every week of. Those are the worst presentations, and mercifully, most presenters are learning that they can't get away with this. Indeed, there are lots of other ways to present as explored in <A HREF="http://sethgodin.typepad.com/seths_blog/2007/01/really_bad_powe.html">Really Bad Powerpoint</A>, <A HREF="http://www.wired.com/wired/archive/11.09/ppt2.html">Edward Tufte's</A> work, and <A HREF="http://www.presentationzen.com/">Presentation Zen</A>. I love attending presentations using <A HREF="http://en.wikipedia.org/wiki/The_Takahashi_method">the Takahashi method</A>, with Zero Powerpoint (ZePo presentations, as <A HREF="http://www.agiledeveloper.com/blog">Venkat </A>likes to call them), and am looking forward to see <A HREF="http://memeagora.blogspot.com/">Neal</A> present using mind maps. </p> <p> Yet, when I've successfully sparked participation, the experience has been quite effective. We explored avenues I would have completely neglected. War stories, especially, offer additional avenues for participants to relate to a technique or pattern. Besides, learning in not something that happens to you; it's something you do. And generally, people don't do much enough when they're listening. </p> <p> Markets, thanks to self-publishing have <A HREF="http://www.cluetrain.com/book/markets.html">evolved into conversations</A>. We no longer buy things because of ads, but instead thanks to word of mouth (be it the physical sort or the electronic). The news, is not consumed, but explored as seen by news outlets leaning more on blogs, and on comment sections at the bottom of news stories. I watch TV as much for the entertainment it offers as for the ensuing conversations it spurs. And the only reason I watch something is because someone recommends it. </p> <p> It's this sort of exploration that I seek when I am learning something new (or relearn something old). I am tired of teaching being portrayed as a broadcast activity, with wisdom emanating out of the speaker. Knowledge, like markets and the news is about conversations. </p> <p> I am happy to see that people are becoming fed up with conferences where they end up learning more from the ancillary activities than from the main events. <A HREF="http://barcamp.org/">Bar Camp</A>, comes to mind, where self-organization and participation are the <A HREF="http://barcamp.org/TheRulesOfBarCamp">rule</A>. </p> <p> I have no specific conclusions to offer, but questions instead: do you want to see more participatory sessions? Why is it that the broadcast model persists to dominate conferences and classrooms? Far too many talks are mere tutorials, which I could just as easily read in a HOWTO. What am I missing? </p>munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-81668511530126214052007-10-31T12:47:00.000-05:002007-10-31T21:59:04.616-05:00Declaring Active Record Foreign KeysSo, you just used <a href="http://muness.blogspot.com/2007/10/documenting-your-schema.html">a tool to automatically generate your schema's documentation</a> for a rails application and you see no relationships? Alas, since ActiveRecord declares all associations in classes, not in the migrations there are no foreign keys on the schema. <a href="http://www.redhillonrails.org/#redhillonrails_core">RedHill on Rails Core plugin</a> adds foreign key support to ActiveRecord migrations, allowing you to, for example: <code> <pre> create_table :orders do |t| ... t.foreign_key :customer_id, :customers, :id end # or add_foreign_key(:orders, :customer_id, :customers, :id) </pre> </code> <p>Nice! But, when I tried to use it on a current project, it caused a couple of issues. The first was with our unit tests. We use <a href="http://unit-test-ar.rubyforge.org/">unit_record</a> to disconnect the database from our unit tests and naturally, it doesn't support these added features. Easy enough to fix: just add empty implementations of the methods that RedHill on Rails Core adds. But there was another problem -- our fixtures violated these constraints, if only because they were added in the wrong order. I could have tried to adjust all fixture loading to get things to load in the right order, but we've been slowly, painfully removing fixtures from all our tests, and spending more time on them seemed ill-advised. </p><p>Besides, I wanted the ability to generate the schema sans the foreign key constraints and optionally add them when I needed them (e.g. when running schema spy) or on as need basis when we had time to get rid of more fixtures, say. So, I created a parallel migrations directory for foreign key declarations, <code>db/foreign_keys</code> and a rake task (slightly modified from <code>db:migrate</code>) to add them: <code> <pre> desc "Add the forgein keys through scripts in db/foreign_keys. Target specific version with VERSION=x" task :fk => :environment do ActiveRecord::Migrator.migrate("db/foreign_keys/", ENV["VERSION"] ? ENV["VERSION"].to_i : nil) end </pre> </code> </p> <p>I start those migrations at a large number (1000 in my case) and then I run <code>rake db:fk</code> when I want to add in the foreign keys to my schema. (And use <code>rake db:drop db:create db:migrate</code> when I need to recreate the schema without them). </p> <p><strong>Note:</strong>Don't run <code>rake db:fk</code> to add foreign keys to your production schema! future migration runs won't execute as expected because the version in schema_info will be set to 1000. I'll post another update with a solution suitable for running against a production schema.</p>munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.comtag:blogger.com,1999:blog-5159103.post-66089138030741290972007-10-30T14:24:00.000-05:002007-10-31T12:43:10.069-05:00Documenting your schema<p>Next time you have to document your schema, try this first:</p> <ol> <li>Install Graphviz (on my Mac Book Pro, I used sudo port install graphviz)</li> <li><a href="http://downloads.sourceforge.net/schemaspy/schemaSpy_3.1.1.jar?modtime=1166439383&big_mirror=0">Download SchemaSpy</a></li> <li><code><pre>java -jar ~/jars/schemaSpy_3.1.1.jar -t mysql -cp ~/jars/mysql-connector-java-5.0.7-bin.jar -host &lt;my_host> -db &lt;my_database> -u &lt;user> -o schema_spy</pre></code></li> </ol> <p>Of course if this is a rails app (which this was), you'll have to declare your foreign key relationships (I use the <A HREF=http://www.redhillonrails.org/#redhillonrails_core>RedHill on Rails Core plugin</A>).</p>munesshttp://www.blogger.com/profile/13080591937269765506noreply@blogger.com