Showing posts with label NAnt. Show all posts
Showing posts with label NAnt. Show all posts

Friday, March 09, 2007

NAnt's Filtered Trigger

I have to say that I found the documentation on this feature somewhat baffling, but that might just be me. We use Subversion here, and I'd like the build not to run if the only files committed since the last build are the ones that the last build created, e.g. the version.txt file, the generated CommonAssemblyInfo.cs into which the incremented build number is written, the backups of CruiseControl.NET's various .config and .xsl files, etc. Feel free to write me if my example doesn't help you out. The problem that cost me close to a day is that relative paths within the block don't seem to be resolved as I expected. See my comments...


<cruisecontrol>
<project name="Our Project 3.0">

<workingDirectory>E:\Builds\Our Project 3.0</workingDirectory>
<artifactDirectory>E:\Builds\Our Project 3.0\artifacts</artifactDirectory>
<!--
CruiseControl.NET doesn't handle spaces in paths consistently, so let's
keep ourselves out of trouble.
-->
<webURL>http://localhost/ccnet/OurProject3.0</webURL>

<!-- Check every 10 minutes. -->
<triggers>
<intervalTrigger seconds="600" />
</triggers>

<sourcecontrol type="filtered">
<sourceControlProvider type="svn">
<trunkUrl>svn://aMachine/omega/trunk/OurProject</trunkUrl>
<workingDirectory>E:\Builds\Our Project 3.0</workingDirectory>
<username>tnassar</username>
<password></password>
</sourceControlProvider>
<inclusionFilters>
<pathFilter>
<!-- Note that this is the full pathname, and apparently has to be. -->
<pattern>/trunk/Our Project/**/*.*</pattern>
</pathFilter>
</inclusionFilters>
<exclusionFilters>
<!-- Changes to these files should *not* cause a new build. -->
<pathFilter>
<pattern>/trunk/Our Project/version.txt</pattern>
</pathFilter>
<pathFilter>
<!-- These are serialization assemblies generated during the build process,
using xsd.exe.
-->
<pattern>/trunk/Our Project/Serialization/Bin/*.dll</pattern>
</pathFilter>
<pathFilter>
<!-- The build number is boosted by a NAnt task. -->
<pattern>/trunk/Omega LP/Applications/CommonAssemblyInfo.cs</pattern>
</pathFilter>
<pathFilter>
<pattern>/trunk/Our Project/Build/**/*</pattern>
</pathFilter>
</exclusionFilters>
</sourcecontrol>
<tasks>
<!-- And so on... -->
</tasks>

Saturday, February 03, 2007

Generating LOC from NAnt, Part 1

Last March I my team took over a failing project for our most important client. The domain to which the software applied is very interesting, and very difficult, and I can't deny that the previous contractor had employed some smart people...at some point in the project's history! The code was a mess. Simply passing FxCop over it revealed that about 5% of it was dead code. After a few months of working with it, we realized that another significant part of it would only be called if certain events fired, and they never did. This is often not so easy to prove, unfortunately. Since I use ReSharper, I found it easier simply to delete unused methods, but the overuse of dynamic binding (!) in the code base meant that I could not be sure, without regression tests (which of course didn't exist until I wrote them) that certain classes were never instantiated. And so on.

Anyway, we began by simply counting the lines of text in the entire code base. This measure decreased daily, and the project manager finally suggested that we do this more systematically so that the client could cite these figures when trying to justify his additional expenditures on this already "working" code to his superiors. I want to make clear that we are not somehow charging for KLOC. Notwithstanding that we're doing this work for a gov't agency, we're not working under some burdensome methodology. The distinction between physical and logical LOC is not so important to us. We are supposed to be making the code more maintainable; a steady decrease in the relative LOC counts as success. I imposed the requirement on myself that the LOC metric be generated from CruiseControl.NET via NAnt. None of the tools that I could use in this way were entirely satisfactory, however. NDepend measured the LOC in files that didn't interest me (e.g., MyForm.Designer.cs); GeroneSoft's Code Counter likewise. Both utilites allow me to create include and exclude lists, but none of them as neatly as NAnt! I would really like to be able to edit the .build file, quickly create another <fileset>, say for ".cs files generated by the Windows Forms Designer," measure the LOC in that set, and arrange for those results to be published by CruiseControl.NET.

NAntContrib does supply a <codestats> task, which accepts a fileset as input. Then, unfortunately, it outputs the statistics as a flat list, which is not what I wanted. What I need is to turn that list into a tree again. This turns out to be unspeakably difficult without XSLT 2.0. Don't recommend Muenchian groupings, please! My initial thought that it would be neat to write this transform cost me at least a day. Then I set about writing a custom NAnt task in C#, but once again I was sidetracked by C#'s relatively poor support for list comprehensions. Moreover, one is discouraged by "performance considerations" from repeatedly splitting and rejoining strings, slicing lists, etc. That's what happens to you when you write too much C, as opposed to learning LISP at a good school. After about two wasted days, on the bus going home, I pulled out a pad of paper and wrote out some Python. I tried it out when I got home, and it worked right out of the box. Then, I admit with some embarrassment, I retrofitted the unit tests, and implemented the special functions such as __len__(), for the sake of the tests.

import os
import itertools
import unittest

class Node:
def __init__(self, path = [], children = []):
assert isinstance(path, list)
assert isinstance(children, list)
self.path = path
self.children = {}
for child in children:
self.add(child)

def __len__(self):
return 1 + sum([len(child) for child in self.children.values()])

def __getitem__(self, key):
return self.children.__getitem__(key)

def __setitem__(self, key):
return self.children.__setitem__(key)

def __iter__(self):
yield self.path
for child in self.children.values():
for i in child:
yield self.path + i

def __repr__(self):
return os.sep.join(self.path)

def add(self, path):
assert len(path), "Cannot add an empty path."
if len(path) == 1:
self.children[path[0]] = Node(path)
elif self.children.has_key(path[0]):
self.children[path[0]].add(path[1:])
else:
self.children[path[0]] = Node([path[0]], [path[1:]])

def allsame(seq):
# An empty sequence is true because is has no mismatches.
# A 1-length sequence should always be true, i.e., imap(...) will
# return an empty list, of which False is not a member (duh!).
# Otherwise, compare all elements to the first. The iteration
# over the sequence should stop as soon as False becomes
# a member of the output sequence.
return not seq or False not in itertools.imap(lambda x: x == seq[0], itertools.islice(seq, 1, None))

def createNodeSorter(paths):
assert paths, "Cannot create a base node for 0 paths"
# Split the paths along the directory separator character.
splitFiles = [f.split(os.sep) for f in paths]
# Now "pivot" these lists in order to find out if they share a common
# base path. Note that os.commonprefix() is character-based; it is
# not suitable.
zipped = zip(*splitFiles)
# How many sequences consist of identical elements?
# Concatenate those, and you've got the common base path.
common = [f[0] for f in itertools.takewhile(allsame, zipped)]
print "Common base path: " + str(common)
# Now strip the common base path.
splitFiles = [f[len(common):] for f in splitFiles if f != common]
print "\tchildren: " + str(splitFiles)
return Node(common, splitFiles)

class AllSameTest(unittest.TestCase):
def testEmptySequence(self):
self.assertTrue(allsame([]))

def testSingleElement(self):
self.assertTrue(allsame([1]))
self.assertTrue(allsame(['test']))

def testTwoIntegers(self):
self.assertTrue(allsame([1, 1]))
self.assertFalse(allsame([0, 1]))

def testTwoStrings(self):
self.assertTrue(allsame(['test', 'test']))
self.assertFalse(allsame(['test', 'fail']))

class CreatorTest(unittest.TestCase):
def testEmptyFileSet(self):
self.assertRaises(AssertionError, lambda: createNodeSorter([]))

def testSinglePath(self):
n = createNodeSorter([r'D:\trunk\Projects\Build\NAnt\LineCounter\LineCount.cs'])
self.assertEqual(1, len(n))

def testTwoPaths(self):
files = [
r'D:\trunk\Projects\Build\NAnt\LineCounter\LineCount.cs',
r'D:\trunk\Projects\Build\NAnt\LineCounter\LineCountCollection.cs']
n = createNodeSorter(files)
self.assertEqual(3, len(n))
self.assertEqual(['D:', 'trunk', 'Projects', 'Build', 'NAnt', 'LineCounter'], n.path)

def testOverlappingPaths(self):
files = [
r'D:\trunk\Projects\Build\NAnt\LineCounter',
r'D:\trunk\Projects\Build\NAnt\LineCounter\LineCountCollection.cs']
n = createNodeSorter(files)
self.assertEqual(2, len(n))
self.assertEqual(['D:', 'trunk', 'Projects', 'Build', 'NAnt', 'LineCounter'], n.path)

class NodeSorterTest(unittest.TestCase):
def testBlankNode(self):
n = Node()
self.assertEqual('', repr(n))
self.assertEqual(1, len(n))

def testSingleChild(self):
n = Node(['base'], [['child1']])
self.assertEqual(2, len(n))
self.assertEqual('base', `n`)

def testNoChildren(self):
n = Node(['C:'])
self.assertEqual(1, len(n))

def testInvalidPath(self):
self.assertRaises(AssertionError, lambda: Node('base'))

def testIteratorBlankNode(self):
n = Node()
nodes = [node for node in n]
self.assertEqual([[]], nodes)

def testIteratorNodeWithPath(self):
n = Node(['C:', 'Program Files'])
nodes = [node for node in n]
self.assertEqual([['C:', 'Program Files']], nodes)

def testIteratorWithOneChild(self):
n = Node(['C:'], [['Program Files']])
nodes = [node for node in n]
self.assertEqual(['C:'], nodes[0])
self.assertEqual(['C:', 'Program Files'], nodes[1])

def testThreeLevels(self):
n = Node(['C:'], [['Program Files', 'Adobe', 'Acrobat 7.0'], ['Program Files', "CruiseControl.NET"]])
self.assertEqual(5, len(n))

def testNoCommonBase(self):
n = Node([], [['C:', 'Program Files', 'Adobe', 'Acrobat 7.0'], ['D:', 'Program Files', "CruiseControl.NET"]])
self.assertEquals(8, len(n))

if __name__ == '__main__':
unittest.main()

Friday, January 19, 2007

Boosting Your Assembly.cs Revision Number from NAnt

I know that others have blogged about this, esp. Scott Hanselman did, somewhere...I'm digressing already. I wanted the simplest possible solution: I wanted to leverage already existing NAnt or NAntContrib tasks, rather than write my own; I did not want to use the CruiseControl.NET label, since a new CC.NET installation starts from zero, as I found out after I hosed my own, and in any case I would want CC.NET ultimately to do no more than call a bunch of NAnt tasks, pass all the artifacts through some XSLT, and create the Web pages. What I ended up with was 1, a file called version.txt, containing only (at the moment) the text 3.0.0.1401; 2, a file called CommonAssemblyInfo.cs, including assembly-level metadata common to the entire application, e.g. [assembly: AssemblyVersionAttribute("3.0.0.1412")]; 3, the following NAnt task:

<target name="createAsmInfo" description="Overwrite the rev. #" >
<if test="${not property::exists('version.txt')}">
<property name="version.txt" value="version.txt" />
</if>
<if test="${not property::exists('common.assembly.info')}">
<property
name="common.assembly.info"
value="Applications/CommonAssemblyInfo.cs" />
</if>
<!-- I.e., increment the revision number and *not* the build number. -->
<version path="${version.txt}"
buildtype="NoIncrement"
revisiontype="Increment" />
<asminfo output="${common.assembly.info}" language="CSharp">
<imports>
<import namespace="System.Reflection" />
</imports>
<attributes>

<attribute type="AssemblyVersionAttribute"

value="${buildnumber.major}.${buildnumber.minor}.${buildnumber.build}.${buildnumber.revision}" />
<attribute type="AssemblyCopyrightAttribute" value="Copyright © 2006 MDi" />
<attribute type="AssemblyCompanyAttribute" value="MegaDyne, Inc." />
<attribute type="AssemblyProductAttribute" value="KillerApp" />
</attributes>
</asminfo>
<!-- The svn task is inadequate. It doesn't handle commits! -->
<exec program='${svn.exe}'
commandline='commit --non-interactive --username=${svn.username} --password=${svn.password} -m "Updated by CruiseControl.NET" ' />
<exec program='${svn.exe}' commandline='update' />
</target>


The <asminfo> task should really not overwrite existing metadata unless so instructed, but I haven't tested that yet. Note that I immediately commit the changed files back into the repository; this might be the wrong thing to do, unless the build succeeds. If it does fail, then CruiseControl.NET will be unable to update the source code from the repository; i.e., there files I've changed will be in conflict. This suggests that I really do all my work from NAnt, but I haven't gotten around to that yet. A further problem is that version.txt and CommonAssemblyInfo.cs will look new to CC.NET the next time it checks, triggering another build. CC.NET offers a "filter trigger" for this sort of thing, but I found the documentation incomprehensible, so I haven't been able to use it yet.


The one additional step is to remove the common metadata from AssemblyInfo.cs files throughout the solution, and add CommonAssemblyInfo.cs as a link. This turned out to be somewhat tricky for me. I have an old habit of configuring Windows Explorer to open files with a single click; a more experienced developer once told me that he was "more productive" with this option; I fell for it, and now I'm stuck with this wierd tic. Anyway, I would select "Add existing item" from a project's context menu, navigate to CommonAssemblyInfo.cs, click on it...and of course a copy would be added to the project. You won't have this problem! Anyway, I had to right-click on the file from the File Open dialog, select "Select" from the context menu, and only then would the little triangle be enabled:

That's pretty much it. You have to remember to change every project this way, but that's about it. Some (Scott Hanselman) prefer to edit the .csproj files themselves; I am probably revealing myself as a non-guru by this admission, but I hate doing that.