Introduction
Any "decent" software company should use a Continuous Integration process and
system. If yours does not — my condolences ;)
The Continuous Integration (CI) concept includes automated software
build/test/deployment/release etc.
A very popular combination for CI tools is CruiseControl.NET — NAnt tandem. While
CruiseControl.NET (CCNet) is the CI engine, providing
all basic functionality, including the CCNet website,
for many more specific tasks it calls NAnt to perform
anything that is tricky and/or customized.
They call NAnt "BAT
file on steroids."It is a powerful and flexible tool. But users keep asking
for more.
Background
I was in bad need for the NANt loop task that
would allow breaking from the loop on a specific condition. There are many
cases when one might need it. In my specific case, NAnt
had to loop through thousands of files in specified folder tree in search of
the file with specific data. And even if it was lucky enough to find the needed
data in, say, the 3rd file it examined, NAnt
still had to loop through the rest of tons of files in idle cycles.
I Googled around for existing solutions and could
not find any. Instead, I found that many more people were asking for the same
functionality. Therefore I just grabbed the source code of NAnt
0.85 and created my own <foreachwhile> task by just subclassing the NAnt <foreach>
task.
When using so popular a tool as NAnt, it is not
nice to have your own heavily customized version. You might have problems
upgrading your NAnt version, migrating your CI
scripts, etc. Therefore I have humbly addressed the NAnt
developer team, using the email distribution list I was able to find, asking if
I may do this small contribution to the NAnt.Contrib
library. No answer so far.
Meanwhile I would like to help those people who are looking for a solution
for this problem.
Using the Code
Properly
Proper usage of this code would be adding this custom task to the NAnt.Contrib library. Where it should stay until the NAnt dev team accepts the solution and includes it into NAnt.Core. Unfortunately I do not have the NAnt.Contrib source (it"™s not a
problem to get it, the thing is I just did not bother). Besides I was doing it
for my personal/test usage. Therefore it is in the NAnt.Core namespace.
Using the Code as
Is
As I mentioned before, the task is in the NAnt.Core namespace. It is easy
to fix — just download the NAnt.Contrib
code from SourceForge: http://sourceforge.net/projects/nantcontrib/
and modify the namespace.
To use it "as is", in the NAnt.Core namespace, just include
the provided LoopWhileTask.cs
into the NAnt solution, rebuild NAnt.Core.dll, and
replace the one in the NAnt installation directory
with your patched version.
Now you can write the following script, for example:
<property name="flag" value="true" readonly="false"/>
<!""- this custom loop will break as soon as filename exactly 24 chars long is found: -->
<foreachwhile item="File" in="C:\Squish\Utils" property="filename" while="flag" >
<echo message="${filename}" />
<echo message="LEN: ${string::get-length(filename)}" />
<property name="flag"
value="${convert::to-boolean(string::get-length(filename) != 24)}"
readonly="false"/>
<echo message="flag = ${flag}" />
</foreachwhile>
<!-"" this core loop will not break; value of each property
of the task is evaluated on the task object creation -->
<foreach item="File" in="C:\Squish\Utils" property="filename" if="${flag}">
<echo message="${filename}" />
<property name="flag" value="false" readonly="false"/>
<echo message="flag = ${flag}" />
</foreach>
...
There are some limitations to the custom task though; they will be described
in the Limitations
section below.
Task Implementation
To keep it simple and to minimize the "disturbance" I have caused in NAnt.core,
I have just inherited the existing NAnt LoopTask:
[TaskName("foreachwhile")]
class LoopWhileTask : LoopTask
{ ... }
LoopTask
has private instance fields:
private string _prop;
private string[] _props;
...
These fields are used in particular to remember initial values of
properties. Private fields of the base class are not
accessible in the derived LoopWhileTask
class.
That leads to the Limitations
section below.
protected override void ExecuteTask()
of the base class was overridden in the derived class. It was a blunt
copy-paste of the base method, minus saving/restoring _props, plus re-evaluating the boolean While condition in every loop:
foreach (FileInfo file in files)
{
if (IsBreak()) break; DoWork(file.FullName);
}
...
I know and respect the style; I know it should be written like this:
if (IsBreak())
{
break;
}
...
But to emphasize my simple addition, I made it into one line of code.
Here goes my evaluation function:
private bool IsBreak()
{
bool isBreak = false;
string value = Project.CurrentTarget.Properties[_while];
try
{
isBreak = !Convert.ToBoolean(value);
}
catch (FormatException)
{
Log(Level.Error, "Unable to convert '{0}' to a Boolean.", value);
}
return isBreak;
}
Finally I had to brazenly copy-paste from the base class private void DoWorkOnFileLines(string filename) method, because it was not
accessible in the derived class.
Limitations
Because of the simplistic while-condition
evaluation function and missing saving/restoring property task attribute value,
there are some limitations on our custom loop task. But in my point of view,
they are not important at all. In my simplistic approach to coding, I would
have avoided using these features whenever possible to keep the script simple,
readable, and reliable.
So, the limitations are as many as two, and they go as follows:
- The
value of the (single) task attribute
property will
not be restored after task execution. Therefore if you are using nested
tasks, avoid using the same name for the property. Like in the script
example above, if you are using property="filename" in the top task, use something like property="filename1" in the embedded task, and so on.
While attribute
should have the value of the boolean NAnt property name that we are using to break
out from the loop, it cannot be an expression. We should use it like while="flag",
and not like while="${flag}" or while="${convert::to-boolean(string::get-length(filename)
!= 24)}".
If someone would kindly point to me cases when these limitations are
critical, we could think of how to bring these missing
functionalities back to our code.
History
No history so far. Let us see if the topic will bring any attention and if
it will require any continuation.