Sunday, August 28, 2005

Rake Experiences Continue

I've been working on the NanoContainer rake file recently. The development seemed to move in 3 significant directions.

  1. It was very procedural and used standard tasks exclusively.

  2. I incorporated many directory and file tasks to take advantage of only updating files when their dependant files were updated.

  3. The final (for now) version is back to being rather procedural; however, I use uptodate? often to get the benefits of the file task.

The final version is both shorter and more readable than version 2. This version is the easiest to follow because of it's procedural nature, and the use of uptodate? gives the most efficient builds.
require 'CSProjFile.rb'

def build(*relative_path)
File.join("src","build",relative_path)
end

task :default => [:compile, :test]
task :all => [:clean, :default]

task :clean do
rm_rf(build)
rm_rf("src/NanoContainer.Tests/bin")
rm_rf("src/TestComp/bin")
end

task :precompile do
mkdir_p(build) unless File.exists?(build)
def _lib(relative_path)
File.join("lib",relative_path)
end
def _precomp(files)
files.each {|f| cp(_lib(f), build) unless uptodate?(build(f), _lib(f))}
end
_precomp(%w(NUnit.Framework.dll PicoContainer.dll Castle.DynamicProxy.dll NMock.dll))
end

task :compile => :precompile do
def _compile(project)
projFile = CSProjFile.new(File.new("src/#{project}/#{project}.csproj"))
unless uptodate?("#{build(project)}.dll",projFile.files.collect {|f| "src/#{project}/#{f}" })
cd "src/#{project}"
sh projFile.create_csc("../build")
cd "../.."
end
end
%w(NanoContainer NanoContainer.Tests TestComp TestComp2 NotStartable).each {|project| _compile(project)}
end

task :pretest do
def tcVsOutput(*relative_path)
File.join("src","TestComp","bin","Debug",relative_path)
end
mkdir_p(tcVsOutput) unless File.exists?(tcVsOutput)
tcdll = "TestComp.dll"
cp(build(tcdll), tcVsOutput) unless uptodate?(tcVsOutput(tcdll), build(tcdll))
def nanoVsOutput(*relative_path)
File.join("src","NanoContainer.Tests","bin","Debug",relative_path)
end
mkdir_p(nanoVsOutput) unless File.exists?(nanoVsOutput)
def _pretest(files)
files.each {|f| cp(build(f),nanoVsOutput) unless uptodate?(nanoVsOutput(f), build(f))}
end
_pretest(%w(NMock.dll PicoContainer.dll Castle.DynamicProxy.dll NUnit.Framework.dll NanoContainer.dll NanoContainer.Tests.dll))
end

task :test => [:compile,:pretest] do
cd nanoVsOutput
sh "../../../../lib/nunit-console.exe NanoContainer.Tests.dll"
end

Thursday, August 25, 2005

Controlling Subversion with Ruby and irb

I previously blogged about Controlling Subversion with Ruby. In theory it seemed like a good idea; however, in practice I used Ruby's Interactive Ruby Shell (irb). Irb allows me to mass add or delete quickly without needing a Ruby file. If you have Ruby installed, typing irb at the command line should drop you right into irb. Once in irb, it's easy to work with subversion using Ruby:

Add all files with "?" status:
`svn st`.split(/\n/).each { |line| `svn add #{line.delete("?").lstrip}` if line[0,1] =~ /\?/ }
Delete all files with "!" status:
`svn st`.split(/\n/).each { |line| `svn rm #{line.delete("!").lstrip}` if line[0,1] =~ /\!/ }

Wednesday, August 24, 2005

Ruby C# Project File parser (CSProjFile)

In response to my blogs about using rake with .net and how to add an embedded resource, Jeremy Miller asks:

Isn't there an equivalent to the "solution" task in NAnt for Rake?
I don't see how it would be better if I had to manually specify things
like including resource files in the build script.

As far as I know, there is no built in support for .net in rake. However, a .csproj file is just an XML file and should therefore be easy to parse.

I've never done any XML parsing in Ruby so I contacted Jeremy Stell-Smith for a recommendation. His answer was REXML. REXML must have come standard in my version of Ruby because I didn't need to install anything. After about a minute looking at the tutorial I was set.

Of course, being test-driven, I started out with the tests. I didn't feel like creating a Foo.csproj, so I used the NanoContainer.csproj to create a XML string I would use for testing.
require 'test/unit'
require 'csprojfile'

class CSProjFileTest < Test::Unit::TestCase
def CSProjFileTest.NanoConfig
%q!
<VisualStudioProject>
<CSHARP
ProjectType = "Local"
ProductVersion = "7.10.3077"
SchemaVersion = "2.0"
ProjectGuid = "{10C07279-0C4B-49AC-8DA5-54062116C2ED}"
>
<Build>
<Settings
ApplicationIcon = ""
AssemblyKeyContainerName = ""
AssemblyName = "NanoContainer"
AssemblyOriginatorKeyFile = ""
DefaultClientScript = "JScript"
DefaultHTMLPageLayout = "Grid"
DefaultTargetSchema = "IE50"
DelaySign = "false"
OutputType = "Library"
PreBuildEvent = ""
PostBuildEvent = ""
RootNamespace = "NanoContainer"
RunPostBuildEvent = "OnBuildSuccess"
StartupObject = ""
>
<Config
Name = "Debug"
AllowUnsafeBlocks = "false"
BaseAddress = "285212672"
CheckForOverflowUnderflow = "false"
ConfigurationOverrideFile = ""
DefineConstants = "DEBUG;TRACE"
DocumentationFile = ""
DebugSymbols = "true"
FileAlignment = "4096"
IncrementalBuild = "false"
NoStdLib = "false"
NoWarn = ""
Optimize = "false"
OutputPath = "bin\Debug\"
RegisterForComInterop = "false"
RemoveIntegerChecks = "false"
TreatWarningsAsErrors = "false"
WarningLevel = "4"
/>
<Config
Name = "Release"
AllowUnsafeBlocks = "false"
BaseAddress = "285212672"
CheckForOverflowUnderflow = "false"
ConfigurationOverrideFile = ""
DefineConstants = "TRACE"
DocumentationFile = ""
DebugSymbols = "false"
FileAlignment = "4096"
IncrementalBuild = "false"
NoStdLib = "false"
NoWarn = ""
Optimize = "true"
OutputPath = "bin\Release\"
RegisterForComInterop = "false"
RemoveIntegerChecks = "false"
TreatWarningsAsErrors = "false"
WarningLevel = "4"
/>
</Settings>
<References>
<Reference
Name = "System"
AssemblyName = "System"
HintPath = "..\..\..\..\..\WINDOWS\Microsoft.NET\Framework\v1.1.4322\System.dll"
/>
<Reference
Name = "System.Data"
AssemblyName = "System.Data"
HintPath = "..\..\..\..\..\WINDOWS\Microsoft.NET\Framework\v1.1.4322\System.Data.dll"
/>
<Reference
Name = "System.XML"
AssemblyName = "System.Xml"
HintPath = "..\..\..\..\..\WINDOWS\Microsoft.NET\Framework\v1.1.4322\System.XML.dll"
/>
<Reference
Name = "PicoContainer"
AssemblyName = "PicoContainer"
HintPath = "..\..\lib\PicoContainer.dll"
/>
<Reference
Name = "Microsoft.JScript"
AssemblyName = "Microsoft.JScript"
HintPath = "..\..\..\..\..\WINDOWS\Microsoft.NET\Framework\v1.1.4322\Microsoft.JScript.dll"
/>
<Reference
Name = "VJSharpCodeProvider"
AssemblyName = "VJSharpCodeProvider"
HintPath = "..\..\..\..\..\WINDOWS\Microsoft.NET\Framework\v1.1.4322\VJSharpCodeProvider.DLL"
/>
</References>
</Build>
<Files>
<Include>
<File
RelPath = "AssemblyInfo.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "DefaultNanoContainer.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Attributes\AssemblyUtil.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Attributes\AttributeBasedContainerBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Attributes\ComponentAdapterType.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Attributes\DependencyInjectionType.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Attributes\RegisterWithContainerAttribute.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "IntegrationKit\ContainerBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "IntegrationKit\LifeCycleContainerBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "IntegrationKit\PicoCompositionException.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\AbstractFrameworkContainerBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\FrameworkCompiler.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\ScriptedContainerBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\CSharp\CSharpBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\JS\JSBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\JSharp\JSharpBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\VB\VBBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\Xml\ComposeMethodBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\Xml\Constants.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\Xml\ContainerStatementBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "Script\Xml\XmlContainerBuilder.cs"
SubType = "Code"
BuildAction = "Compile"
/>
<File
RelPath = "TestScripts\test.js"
BuildAction = "EmbeddedResource"
/>
<File
RelPath = "TestScripts\test.vb"
BuildAction = "EmbeddedResource"
/>
</Include>
</Files>
</CSHARP>
</VisualStudioProject>!
end

def testOutputType
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
assert_equal(projFile.output_type, "library")
end

def testAssemblyName
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
assert_equal(projFile.assembly_name, "NanoContainer")
end

def testFiles
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
expectedFiles = %w(AssemblyInfo.cs DefaultNanoContainer.cs Attributes\\AssemblyUtil.cs Attributes\\AttributeBasedContainerBuilder.cs Attributes\\ComponentAdapterType.cs Attributes\\DependencyInjectionType.cs Attributes\\RegisterWithContainerAttribute.cs IntegrationKit\\ContainerBuilder.cs IntegrationKit\\LifeCycleContainerBuilder.cs IntegrationKit\\PicoCompositionException.cs Script\\AbstractFrameworkContainerBuilder.cs Script\\FrameworkCompiler.cs Script\\ScriptedContainerBuilder.cs Script\\CSharp\\CSharpBuilder.cs Script\\JS\\JSBuilder.cs Script\\JSharp\\JSharpBuilder.cs Script\\VB\\VBBuilder.cs Script\\Xml\\ComposeMethodBuilder.cs Script\\Xml\\Constants.cs Script\\Xml\\ContainerStatementBuilder.cs Script\\Xml\\XmlContainerBuilder.cs)
assert_equal(projFile.files, expectedFiles)
end

def testReferences
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
expectedRefs = %w(System System.Data System.XML PicoContainer Microsoft.JScript VJSharpCodeProvider)
assert_equal(projFile.references, expectedRefs)
end

def testCscOutput
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
expectedCsc = "csc /out:../build/NanoContainer.dll /target:library /lib:../build /r:'System.dll;System.Data.dll;System.XML.dll;PicoContainer.dll;Microsoft.JScript.dll;VJSharpCodeProvider.dll' /res:'TestScripts\\test.js,NanoContainer.TestScripts.test.js' /res:'TestScripts\\test.vb,NanoContainer.TestScripts.test.vb' AssemblyInfo.cs DefaultNanoContainer.cs /recurse:AssemblyUtil.cs /recurse:AttributeBasedContainerBuilder.cs /recurse:ComponentAdapterType.cs /recurse:DependencyInjectionType.cs /recurse:RegisterWithContainerAttribute.cs /recurse:ContainerBuilder.cs /recurse:LifeCycleContainerBuilder.cs /recurse:PicoCompositionException.cs /recurse:AbstractFrameworkContainerBuilder.cs /recurse:FrameworkCompiler.cs /recurse:ScriptedContainerBuilder.cs /recurse:CSharpBuilder.cs /recurse:JSBuilder.cs /recurse:JSharpBuilder.cs /recurse:VBBuilder.cs /recurse:ComposeMethodBuilder.cs /recurse:Constants.cs /recurse:ContainerStatementBuilder.cs /recurse:XmlContainerBuilder.cs"
assert_equal(projFile.create_csc("../build"), expectedCsc)
end

def testEmbeddedResource
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
expectedResources = %w(TestScripts\test.js TestScripts\test.vb)
assert_equal(projFile.embedded_resources, expectedResources)
end

def testConvertToResource
projFile = CSProjFile.new(CSProjFileTest.NanoConfig)
assert_equal(projFile.convert_to_resource("a\\b.c"),"/res:'a\\b.c,NanoContainer.a.b.c'")
end
end
After getting those tests to pass I felt like my CSProjFile class was well enough tested.
require 'rexml/document'

class CSProjFile
def initialize(projFile)
@projXml = REXML::Document.new projFile
@extensions = {"library"=>"dll"}
end

def output_type
@projXml.elements["VisualStudioProject/CSHARP/Build/Settings"].attributes["OutputType"].downcase
end

def assembly_name
@projXml.elements["VisualStudioProject/CSHARP/Build/Settings"].attributes["AssemblyName"]
end

def files
result = []
path = "VisualStudioProject/CSHARP/Files/Include/File"
@projXml.elements.each(path) { |element| result << element.attributes["RelPath"] if compiledFile?(element) }
result
end

def compiledFile?(element)
element.attributes["BuildAction"]=="Compile"
end

def embedded_resources
result = []
path = "VisualStudioProject/CSHARP/Files/Include/File"
@projXml.elements.each(path) { |element| result << element.attributes["RelPath"] if embeddedResourceFile?(element) }
result
end

def embeddedResourceFile?(element)
element.attributes["BuildAction"]=="EmbeddedResource"
end

def references
result = []
@projXml.elements.each("VisualStudioProject/CSHARP/Build/References/Reference") { |element| result << element.attributes["Name"] }
result
end

def create_csc(outDir)
result = []
result << "csc"
result << "/out:#{outDir}/#{assembly_name}.#{@extensions[output_type]}"
result << "/target:#{output_type}"
result << "/lib:#{outDir}"
refs = references.collect {|ref| ref + ".dll"}
result << "/r:'#{refs.join(";")}'"
embeddedFiles = embedded_resources.collect { |item| convert_to_resource(item)}
result << embeddedFiles.join(" ")
recurseFiles = files.each { |item| item.sub!(/.+\\/,"/recurse:") }
result << recurseFiles.join(" ")
result.join(" ")
end

def convert_to_resource(item)
file = item.to_s
id = "#{assembly_name}.#{item.to_s.sub(/\\/,".")}"
"/res:'#{file},#{id}'"
end
end
So, the final (successful) test was changing my NanoContainer rakefile to use the CSProjFile class.
require 'CSProjFile.rb'

buildDir = "src/build"
nanoDll = "build/NanoContainer.dll"
nanoTestsDll = "build/NanoContainer.Tests.dll"
testCompDll = "build/TestComp.dll"
testComp2Dll = "build/TestComp2.dll"
notStartableDll = "build/NotStartable.dll"
nanoTestVsBinDir = "src/NanoContainer.Tests/bin"
nanoTestVsBinDebugDir = "src/NanoContainer.Tests/bin/Debug"
testCompBinDir = "src/TestComp/bin"
testCompBinDebugDir = "src/TestComp/bin/Debug"

task :default => [:compileInit, :compile, :test]
task :all => [:clear, :removeBuildDir, :removeVsDirs, :default]

task :clear do sh "clear" end

task :removeBuildDir => :clear do rm_rf(buildDir) end
task :removeVsDirs => :clear do
rm_rf(nanoTestVsBinDir)
rm_rf(testCompBinDir)
end

directory buildDir

task :compileInit => [buildDir] do
copyToDir(%w(lib/NUnit.Framework.dll lib/PicoContainer.dll lib/Castle.DynamicProxy.dll),buildDir)
copyToDir(%w(lib/NMock.dll lib/NUnit.Framework.dll),buildDir)
end

file nanoDll => :compileInit do compileProj("src/NanoContainer","NanoContainer.csproj") end
file nanoTestsDll => [:compileInit, nanoDll] do compileProj("src/NanoContainer.Tests","NanoContainer.Tests.csproj") end
file testCompDll => :compileInit do compileProj("src/TestComp","TestComp.csproj") end
file testComp2Dll => [:compileInit, testCompDll] do compileProj("src/TestComp2","TestComp2.csproj") end
file notStartableDll => [:compileInit, testCompDll] do compileProj("src/NotStartable","NotStartable.csproj") end
task :compile => [nanoDll,nanoTestsDll,testCompDll,testComp2Dll,notStartableDll]

directory testCompBinDir
directory testCompBinDebugDir
directory nanoTestVsBinDir
directory nanoTestVsBinDebugDir

task :testInit => [testCompBinDir,testCompBinDebugDir,nanoTestVsBinDir,nanoTestVsBinDebugDir] do
cp "src/Build/TestComp.dll", testCompBinDebugDir
copyToDir(%w(src/Build/NMock.dll src/Build/PicoContainer.dll src/Build/Castle.DynamicProxy.dll),nanoTestVsBinDebugDir)
copyToDir(%w(src/Build/NUnit.Framework.dll src/Build/NanoContainer.dll src/Build/NanoContainer.Tests.dll),nanoTestVsBinDebugDir)
end

task :test => [:compile,:testInit] do
cd nanoTestVsBinDebugDir
sh "../../../../lib/nunit-console.exe NanoContainer.Tests.dll"
end

def copyToDir(fileArray, outputDir)
fileArray.each { |file| cp file, outputDir }
end

def compileProj(workingDir, projFileName)
cd workingDir
projFile = CSProjFile.new(File.new(projFileName))
sh projFile.create_csc("../build")
cd "../.."
end


There's much room for improvement, but it work well for a first attempt. Feedback welcome.

Tuesday, August 23, 2005

Add Events to NMock

While using View Observer on my last project we needed a way to raise events for unit testing. The best solution turned out to be stubs; however, while we were evaluating options Levi Khatskevitch created a DynamicMockWithEvents class.

DynamicMockWithEvents inherits from NMock's DynamicMock, but it also contains support for raising events from a mock. To raise an event from a mock simply call the RaiseEvent method with the event name and any optional args.
public class DynamicMockWithEvents : DynamicMock
{
private const string ADD_PREFIX = "add_";
private const string REMOVE_PREFIX = "remove_";

private readonly EventHandlerList handlers;
private readonly Type mockedType;

public DynamicMockWithEvents(Type type) : base(type)
{
handlers = new EventHandlerList();
mockedType = type;
}

public override object Invoke(string methodName, params object[] args)
{
if (methodName.StartsWith(ADD_PREFIX))
{
handlers.AddHandler(GetKey(methodName, ADD_PREFIX), (Delegate) args[0]);
return null;
}
if (methodName.StartsWith(REMOVE_PREFIX))
{
handlers.RemoveHandler(GetKey(methodName, REMOVE_PREFIX), (Delegate) args[0]);
return null;
}
return base.Invoke(methodName, args);
}

private static string GetKey(string methodName, string prefix)
{
return string.Intern(methodName.Substring(prefix.Length));
}

public void RaiseEvent(string eventName, params object[] args)
{
Delegate handler = handlers[eventName];
if (handler == null)
{
if (mockedType.GetEvent(eventName) == null)
{
throw new MissingMemberException("Event " + eventName + " is not defined");
}
else if (Strict)
{
throw new ApplicationException("Event " + eventName + " is not handled");
}
}
handler.DynamicInvoke(args);
}
}

Monday, August 22, 2005

Adding an Embedded Resource to a csc command line compiled assembly

While using Rake to compile and execute NanoContainer.Tests, 4 of my tests kept failing. They were relying on 4 embedded resources that I was not embedding. In Visual Studio it's easy to embed a resource. DevHood and CodeProject both give good information on embedding using Visual Studio.net; however, I wanted to embed a resource using the command line.

MSDN2 provided some good info specific to the command line, but everything I found seemed to detail how to add a .resources or .resx file to an assembly. I needed to add .cs, .js, .java, and .vb files to the assembly. Using csc -? and resgen -? didn't seem to lead me in the right direction either.

Finally I gave up on finding the documentation I needed and starting shooting in the dark. I used /res, /win32res, and /linkresource. The tests kept failing. /res seemed like the answer, but the documentation focuses on .resources files so there was no way to be sure. Enter James Johnson's demo executable. I'd done everything I could think of and the tests were still failing. It was time to fire up an app that could show me what resources were contained in my assembly.

It turned out that my assembly was embedding the resource correctly when I used /res; however, an identifier needed to be given to match the identifier specified in the NanoContainer tests. Once I added the identifier all the tests passed.

Final correct csc (broken to multiple lines for readablity):
csc /out:../build/NanoContainer.Tests.dll /res:'TestScripts/test.cs,NanoContainer.Tests.TestScripts.test.cs' /res:'TestScripts/test.js,NanoContainer.Tests.TestScripts.test.js' /res:'TestScripts/test.java,NanoContainer.Tests.TestScripts.test.java' /res:'TestScripts/test.vb,NanoContainer.Tests.TestScripts.test.vb' /target:library /recurse:*.cs /lib:'../build' /r:'PicoContainer.dll;Microsoft.JScript.dll;VJSharpCodeProvider.dll;Castle.DynamicProxy.dll;NanoContainer.dll;NMock.dll;NUnit.Framework.dll'