Source code management with darcs: a first look

Posted by Tom Moertel Sat, 12 Feb 2005 17:00:00 GMT

I have been managing the LectroTest project with the monotone revision control system. For the last few months, monotone has been undergoing some growing pains that have made it less stable than I would like for everyday use. Thus I thought that I would give darcs a try.

I have been following the progress of darcs since it was first announced on the Haskell-Cafe mailing list on 9 April 2003. Darcs is written in Haskell, one of my favorite programming languages, and that was my initial draw. Still, until yesterday I had never used it for any of my projects because I felt it was immature and needed some more real-world testing before I committed work to it.

In the last three months, Darcs has gained mainstream attention (triggered by a favorable write-up in Linux Weekly News) and a growing user base. Under the gaze of these new eyeballs, darcs has matured much. I thought it was time for another look.

Darcs has a small, easy-to-understand command set and yet offers "modern" source-code management features such as distributed development (via HTTP, ssh, and email), change sets, and cherry picking. Want to start an experimental branch of your project? Just check out another copy and use it for the branch. Each working copy is a complete, independent repo. Want to publish a project repository to the world? Just copy it to a public web server. Want to start working on someone else's project? A single "darcs get http://other.project.com/project" gives you a complete, stand-alone copy. Your own personal branch. Start hacking.

To try darcs on something I was familiar with, I decided manage my LectroTest development with it. The first thing I did was change to the LT working directory and use "darcs init" to create a darcs repository there.

[tom@bifur Flippi]$ cd ~/work/research/perl/qc/  # LT root dir
[tom@bifur qc]$ darcs init
[tom@bifur qc]$ l
blib/                       mt.db
_build/                     mtdb.dump
Build*                      perl-Test-LectroTest-0.2007-1.src.rpm
Build.PL                    pod2htmd.tmp
Build.PL~                   pod2htmi.tmp
buildrpm*                   posts/
buildrpm~                   prop2.pl
Changes                     prop2.pl~
Changes~                    README
checkpods*                  t/
checkpods~                  Test-LectroTest-0.2001.tar.gz
ctime.pl                    Test-LectroTest-0.2002.tar.gz
ctime.pl~                   Test-LectroTest-0.2003.tar.gz
CVS/                        Test-LectroTest-0.2004.tar
_darcs/                     Test-LectroTest-0.2004.tar.gz
Example1.pl~                Test-LectroTest-0.2005.tar.gz
lib/                        Test-LectroTest-0.2006.tar.gz
Makefile.PL                 Test-LectroTest-0.2007.tar.gz
MANIFEST                    Test-LectroTest-0.2008.tar.gz
MANIFEST~                   Test-LectroTest-0.2009.tar.gz
MANIFEST.bak                Test-LectroTest-0.201.tar.gz
MANIFEST.SKIP               tex/
MANIFEST.SKIP~              THANKS
META.yml                    THANKS~
monotone.db                 TODO
monotone.db.bak             TODO~
monotone.db.pre-changesets  toms-notes.txt
monotone.db-pre-sql3        toms-notes.txt~
MT/

You can see that there is a lot of accumulated cruft in my working directory, including CVS, monotone, and now darcs revision-control artifacts. To prevent Perl’s Module::Build from thinking the _darcs directory is meaningful, I added it to the manifest-skip file.

[tom@bifur qc]$ echo '\b_darcs\b' >> MANIFEST.SKIP

Next I added my LectroTest sources, docs, and related files to the darcs repo.

[tom@bifur qc]$ darcs add Build.PL buildrpm Changes \
    checkpods MANIFEST MANIFEST.SKIP tex THANKS TODO \
    toms-notes.txt tex/Makefile tex/titlepage.ltx

The “darcs whatsnew” command asks darcs to tell me what is changed in the working directory with respect to the repository state.

[tom@bifur qc]$ darcs whatsnew -s
A ./Build.PL
A ./Changes
A ./MANIFEST
A ./MANIFEST.SKIP
A ./THANKS
A ./TODO
A ./buildrpm
A ./checkpods
A ./tex/
A ./tex/Makefile
A ./tex/titlepage.ltx
A ./toms-notes.txt

The files that I added are new because I had not yet recorded them to the repository. Before I did that, I added the remaining LT assets.

[tom@bifur qc]$ darcs add t  # add the tests dir
[tom@bifur qc]$ darcs add t/*.t
[tom@bifur qc]$ darcs add lib
[tom@bifur qc]$ cd lib
[tom@bifur lib]$ l
Test/
[tom@bifur lib]$ darcs add Test
[tom@bifur lib]$ cd Test
[tom@bifur Test]$ l
LectroTest/    LectroTest.pm~     LectroTest::Tutorial.pod~
LectroTest.pm  LectroTest.pm.bak
[tom@bifur Test]$ darcs add LectroTest LectroTest.pm
[tom@bifur Test]$ cd LectroTest
[tom@bifur LectroTest]$ l
Compat.pm      Generator.pm~     Simple.pm~         Tutorial.pod
Compat.pm~     Generator.pm.bak  Test.pm~           Tutorial.pod~
Compat.pm.bak  Property.pm       TestRunner.pm      Tutorial.pod.bak
CVS/           Property.pm~      TestRunner.pm~
Generator.pm   Property.pm.bak   TestRunner.pm.bak
[tom@bifur LectroTest]$ darcs add *.pm *.pod

At this point, it looked like I had all of the files under darcs’s watchful eye.

[tom@bifur LectroTest]$ darcs w -s  # abbreviated: w -> whatsnew
A ./Build.PL
A ./Changes
A ./MANIFEST
A ./MANIFEST.SKIP
A ./THANKS
A ./TODO
A ./buildrpm
A ./checkpods
A ./lib/
A ./lib/Test/
A ./lib/Test/LectroTest/
A ./lib/Test/LectroTest.pm
A ./lib/Test/LectroTest/Compat.pm
A ./lib/Test/LectroTest/Generator.pm
A ./lib/Test/LectroTest/Property.pm
A ./lib/Test/LectroTest/TestRunner.pm
A ./lib/Test/LectroTest/Tutorial.pod
A ./t/
A ./t/001.t
A ./t/002.t
A ./t/003.t
A ./t/004.t
A ./t/005.t
A ./t/compat.t
A ./tex/
A ./tex/Makefile
A ./tex/titlepage.ltx
A ./toms-notes.txt

[tom@bifur LectroTest]$ cd ../../..  # back up to project home

That looked right. It was time to record my changes. This was straightforward.

[tom@bifur qc]$ darcs record --all    # record all changes

Darcs needs to know what name (conventionally an email address) to use
as the patch author, e.g. 'Fred Bloggs <fred@bloggs.invalid>'.
If you provide one now it will be stored in the file
'_darcs/prefs/author' and used as a default in the future.  To change
your preferred author address, simply delete or edit this file.

What is your email address? Tom Moertel <tom@moertel.com>
What is the patch name? Initial checkin of sources
Do you want to add a long comment? [yn] n
Finished recording patch 'Initial checkin of sources'

Now what did darcs think has changed?

[tom@bifur qc]$ darcs w -s
No changes!

Excellent.

One cool feature of darcs is that every working directory is also a complete, independent repository. To make a branch, then, is as simple as checking out a new repository.

Of course, because there is no central repository in the darcs model, “checking out” is a concept that does not really apply. Rather, what I must do is set up a new repository and then “push” my existing repository’s patches to it. I can push in many ways, including via ssh to a remotely hosted repository, but here I will just set up a new repo in /tmp and push to it on the local filesystem.

[tom@bifur qc]$ mkdir /tmp/lt && pushd /tmp/lt
/tmp/lt ~/work/research/perl/qc
[tom@bifur lt]$ darcs init   # set up new repo at /tmp/lt
[tom@bifur lt]$ popd
~/work/research/perl/qc
[tom@bifur qc]$ darcs push /tmp/lt   # push to repo at /tmp/lt
Sat Feb 12 01:26:15 EST 2005  Tom Moertel <tom@moertel.com>
  * Initial checkin of sources
Shall I push this patch? (1/1) [ynWvxqadjk], or ? for help: y
Finished applying...

Now, I can begin working on my new branch in the /tmp/lt working directory.

[tom@bifur qc]$ cd /tmp/lt
[tom@bifur lt]$ l
Build.PL  Changes    _darcs/  MANIFEST       posts/  tex/    TODO
buildrpm  checkpods  lib/     MANIFEST.SKIP  t/      THANKS  toms-notes.txt
[tom@bifur lt]$ emacs lib/Test/LectroTest.pm   # fix typo
[tom@bifur lt]$ darcs record
hunk ./lib/Test/LectroTest.pm 38
-of your software.  LectroTest then checks your software see whether
+of your software.  LectroTest then checks your software to see whether
Shall I record this patch? (1/1) [ynWsfqadjk], or ? for help: y

What is the patch name? Fixed stupid typo in intro text of T::LectroTest.pm
Do you want to add a long comment? [yn] n
Finished recording patch 'Fixed stupid typo in intro text of T::LectroTest.pm'

Now my branch repository contains two patches:

[tom@bifur lt]$ darcs changes
Sat Feb 12 13:20:07 EST 2005  Tom Moertel <tom@moertel.com>
  * Fixed stupid typo in intro text of T::LectroTest.pm
Sat Feb 12 01:26:15 EST 2005  Tom Moertel <tom@moertel.com>
  * Initial checkin of sources

Because the typo that I fixed is not unique to my new branch, I ought to make sure that the original branch gets the fix, too. To do so, I just push it:

[tom@bifur lt]$ darcs push ~/work/research/perl/qc
Pushing to /home/thor/work/research/perl/qc...
Sat Feb 12 13:20:07 EST 2005  Tom Moertel <tom@moertel.com>
  * Fixed stupid typo in intro text of T::LectroTest.pm
Shall I push this patch? (1/1) [ynWvxqadjk], or ? for help: y
Finished applying...

And now my patch has been pushed back up to the mainstream branch! This is an attractive development model.

So far, I like darcs. Its source code–management model is simple and powerful. Its command set is small enough to actually grok. Using darcs has me wondering why other SCM systems have made the problem seem so complicated. My life is complicated enough as it is.

I think I just switched to darcs.

Posted in
Tags , , , ,
no comments
no trackbacks
Reddit Delicious

When worlds collide and then dance: Test::LectroTest meets Test::Builder

Posted by Tom Moertel Tue, 08 Feb 2005 17:00:00 GMT

It seems that LectroTest is picking up in popularity because I am starting to get regular requests and feedback. Recently, two related requests came in regarding something that I had put off: Figuring out how to merge specification-based testing, which samples thousands of trials per property check, with case-based testing, which runs one trial per case.

The rub is that case-base testing in Perl is managed by Test::Builder and a related family of modules that are designed to make that kind of testing easy. One convenience they offer is that calling test functions like cmp_ok not only performs a test, in this case a general comparison, but also reports the result of the test to the test harness.

So what happens if somebody wants to perform a cmp_ok test from within a LectroTest property specification? When the property is checked, LectroTest will test whether the property holds by sampling a thousand random trials (by default), each of which will end up calling cmp_ok. Can you see where this is going? Yup, the test harness will end up seeing one thousand separate tests instead of the single test of the property.

The solution to this problem, as to all problems of distinction, is hackery. The Test::* family of modules ends up filtering all calls to test functions such as cmp_ok down to the Test::Builder module’s ok method. This method does two things. First, it reports the result of the test to the test harness without giving us a chance to say otherwise. (Naughty!) Second, it returns the result of the test back to the original caller. As far as property checks are concerned, the first part is bad, and the second is good.

To get rid of the bad part, I redefine the ok method during property checks. I put the implementation into a new module, Test::LectroTest::Compat, that exports a single function holds. This function is used to inject a property check into a plain-old Test::Simple- or Test::More-style test plan. For example, here’s a test plan that uses the new module:

use Test::More tests => 2;
use Test::LectroTest::Compat;

my $prop_nonnegative = Property {
    ##[ x <- Int, y <- Int ]##
    cmp_ok(my_function( $x, $y ), '>=', 0);
}, name => "my_function output is non-negative" ;

holds( $prop_nonnegative ); # assert that the prop holds
cmp_ok( 0, '<', 1, "trivial 0<1 test" ); # a "normal" assertion
# ... and so on ...

What does holds do when called? It redefines Test::Builder’s ok method, runs the property-check trials, restores ok, and finally reports the property check’s result via the newly-restored ok. From there, Test::Builder takes over and does the magic necessary to incorporate the result into the test session’s TAP output.

The code that does all this is as follows:

sub holds {
    my ($diag_store, $results) = check_property(@_);
    my $success = $results->success;
    (my $name   = $results->summary) =~ s/^.*?- /property /;
    $Test->ok($success, $name);
    my $details = $results->details;
    $details =~ s/^.*?\n//;     # remove summary line
    $details =~ s/^\# /    /mg; # replace commenting w/ indent
    $Test->diag(@$diag_store) if @$diag_store;
    $Test->diag($details) if $details;
    return $success;
}

sub check_property {
    no strict 'refs';
    no warnings;
    my $diag_store = [];
    my $property = shift;
    local *Test::Builder::ok = \&disconnected_ok;
    local *Test::Builder::diag = sub { shift; push @$diag_store, @_ };
    return ( $diag_store, 
             Test::LectroTest::TestRunner->new(@_)->run($property) );
}

sub disconnected_ok { $_[1] ? 1 : 0 }

You can see that there is one extra bit of hackery going on. I also redefine Test::Builder’s diag method to capture any diagnostic output that may be emitted during the trials. Typically, this occurs only when a trial fails, and in that case the output is almost certainly worth passing back to the user in context. To ensure that the user sees it in context, I hold on to the captured output until the property check is complete and then roll it into the normal LectroTest diagnostic output. It looks great:

not ok 3 - property 'x is a natural number' falsified in 2 attempts
#     Failed test (t/compat.t at line 32)
#     '0'
#         >
#     '0'
#     Counterexample:
#     $x = 0;

In the first part you see the typical cmp_ok output that the assertion 0 > 0 failed. The second part is the LectroTest counterexample that shows at what part of the test space the assertion failed.

It takes some complicated footwork, but the resulting dance is beautiful. To see two markedly different testing systems – and in some ways markedly different testing philosophies – working in step with one another is gratifying. I suspect that most real-world testing problems can be solved better by a combination of the two approaches than by either alone. Thus I am especially happy about this integration.

After some more refinement, I am going to incorporate Test::LectroTest::Compat into the main distribution. For now, you can get a copy in the LectroTest/FAQs section of the Community Projects site.

Posted in ,
Tags , ,
no comments
no trackbacks
Reddit Delicious