I'm going to remix the rakefile/tasks from your own answer. There are some Ruby/Rake conventions that you should follow to appeal to a broader audience. And I have some opinions on how to write awesome rakefiles. In particular...
1. Don't invoke/execute Rake tasks directly
Rake::Task[:unitTestWithCoverage].execute( testAssembly )
There's all sorts of reasons why you don't want to mess with direct Rake invoke
or execute
. One of them doesn't invoke dependent tasks, one only runs dependent tasks once... it gets goofy. There should always be a way to construct properly defined and dependent tasks.
2. Don't parameterize "internal" tasks
exec :unitTestWithCoverage, [:testAssembly] do |cmd, testAssembly|
You probably have a static list or wildcard-matching list of test assemblies. You should be able to construct concrete tasks without using parameters. I only use parameterized tasks when the user can invoke them with custom input from the command line.
3. No need to create paths inside each task
testAssemblyRealPath = Pathname.new(testAssembly).realpath
testAssemblyName = File.basename(testAssemblyRealPath)
We're gonna explore the Rake FileList
to figure out how to create custom, lazy, mapped lists of filenames/paths/arbitrary-strings!
The Remix (updated)
I made a critical mistake in my first answer (which I maintain at the bottom, for reference). I'll explain what went wrong in that section for your/my education!
What follows is my new recommendation. This should have been obvious to me because I made the same mistake with an mspec test runner task in my own builds.
dotcover_path = 'path/to/dotcover.exe'
xunit_runner_path = 'path/to/xunitrunner.exe'
test_assemblies = FileList['path/to/output/**/*.test.dll']
coverage_results = "#{test_results_path}/coverage_results.dcvr"
task :cover_all => [ :tests_with_coverage, :publish_coverage_results ]
exec :tests_with_coverage do |cmd|
cmd.comand = dotcover_path
cmd.parameters = [
"cover",
"/AnalyseTargetArguments=False",
"/TargetExecutable=\"#{xunit_runner_path}\"",
"/TargetArguments=\"#{test_assemblies.join ','}\"",
"/Output=\"#{coverage_results}\""
]
end
task :publish_coverage_results => [ :tests_with_coverage ] do
import_data 'dotNetCoverage', 'dotCover', coverage_results
end
def import_data(type, tool, file)
puts "##teamcity[importData type='#{type}' tool='#{tool}' path='#{file}']"
end
The Explanation
I default to absolute paths (usually by using File.expand_path
and the __FILE__
constant). There are tools/tasks that require relative paths, but you can always play with methods like File.basename
.
dotcover_path = 'path/to/dotcover.exe'
xunit_runner_path = 'path/to/xunitrunner.exe'
We can still use a FileList
of built assemblies to define the target assemblies. We won't evaluate it until the body of the test task executes.
test_assemblies = FileList['path/to/output/**/*.test.dll']
The coverage runner supports multiple assemblies with a single results file. This way we don't have another complicated pathmap
.
coverage_results = "#{test_results_path}/coverage_results.dcvr"
Call this from your CI server to run the tests and the coverage results published.
task :cover_all => [ :tests_with_coverage, :publish_coverage_results ]
This task is now plain and simple. Some notes:
1. Use join
to turn the list of targets into a string of the correct format.
2. I tend to quote exec task parameters that have file paths (which requires escaping, \"
).
exec :tests_with_coverage do |cmd|
cmd.command = dotcover_path
cmd.parameters = [
"cover",
"/AnalyseTargetArguments=False",
"/TargetExecutable=\"#{xunit_runner_path}\"",
"/TargetArguments=\"#{test_assemblies.join ','}\"",
"/Output=\"#{coverage_results}\""
]
end
Same old publish task/method.
task publish_coverage_results => [ :tests_with_coverage ] do
import_data 'dotNetCoverage', 'dotCover', coverage_results
end
def import_data(type, tool, file)
puts "##teamcity[importData type='#{type}' tool='#{tool}' path='#{file}']"
end
The Old Remix
Snipped to show the problem area, assume the rest was uninteresting or exists in the new solution, too.
The test assemblies won't exist until after the build task. That's normally not a problem, since FileList
is lazy. It won't evaluate until you enumerate over it (for example, by using each
, map
, or zip
).
However, we immediately each
over it to generate the test tasks... so this won't work. It'll have nothing in the list and generate no tasks. Or, even worse, it'll pickup the previous build's output and possibly do bad things (if you didn't completely clean the output directory).
test_assemblies = FileList['path/to/output/**/*.test.dll']
coverage_results = test_assemblies.pathmap "#{test_results_path}/%n.dcvr"
cover_task_names = test_assemblies.pathmap "cover_%n"
test_assemblies.zip(coverage_results, cover_task_names) do |assembly, results, task_name|
exec task_name do |cmd|
cmd.command = dotcover_path
cmd.parameters = [
"cover",
"/AnalyseTargetArguments=False",
"/TargetExecutable=#{xunit_path}",
"/TargetArguments=#{assembly}",
"/Output=#{results}"
]
end
end