In this post I'll describe how one can set up CCNet to work with VS2010. Not everyone has the full blown version of TFS at their disposal.
Scenario setup
I always use the following setup at work :
- Project_CI : for Continuous Integration (compile, unit-test)
- Project_MakePackage : this makes the install package (compile, unit-test,integration-test, make package)
- Project_QA : this does unit-test, integration_test, coverage and code analysis.
This is a pragmatic approach: a 'fix' can be deployed even when for example coverage is still below X percent, as long as all tests are passed. It's convention that QA must be fixed ASAP!
The
CI must be as fast as possible, so it runs only the unit-tests. The CI project has an
interval trigger checking the repo every 5 minutes. This project does NOT label TFS.
The makePackage project has a
schedule trigger : every day at 20:00, and a labeler so we can easily branch via a label.
The QA project also has a
schedule trigger : every day at 21:00. This project does NOT label TFS.
Step 1 : Setting up the source control part
In Tfs itself I have the following layout :
ProjectName
\__Main
| \Lib
| \Src
\__Releases
\__1_0_0_3450
| \Lib
| \Src
\__1_0_1_5678
\Lib
\Src
This allows for easy branching.
Step 2 : Setting up CCNet.config
I use the
pre-processor to reduce a lot of the configuration. This allows me to define a CCNet project in just 30 lines!
You can read the full configuration for the tfs source control at the wiki
Tfs Source ControlMy advise : set the deleteworkspace and cleancopy to true, this prevents a lot of problems.
A full example of ccnet.config with comparable layout is at the bottom of this post.
The preprocessor declaration : <cb:define name="vsts_ci"> <server>http://tfs-server:8080/tfs/default/</server> <username>cruise</username> <password>**********</password> <domain>tfs-server</domain> <autoGetSource>true</autoGetSource> <cleanCopy>true</cleanCopy> <force>true</force> <deleteWorkspace>true</deleteWorkspace> </cb:define>
The source control block inside a project : <sourcecontrol type="vsts"> <workspace>$(ProjectName)</workspace> <project>$/$(ProjectName)/Main</project> <cb:vsts_ci/> </sourcecontrol>
|
Step 3 : Setting up the build script
The main action lays of course in the build script, for which I use
Nant. The reason I (still) use Nant is that I know it rather well, and it works. For compiling I just call the MSBuild task from
NantContrib pointing to the VS2010 solution, but all other logic is in Nant.
An example of the Nant build script is also at the bottom of this post.
Step 4 : Testing with Ms-test
Like I said in the beginning, I have 2 kind of tests, UnitTests and Integration Tests. In MS-test I create a test-list with the name
UnitTests directly under the root item. All tests in this list, and in test-lists beneath it will be ran when I specify
UnitTests. The 'Integration Tests' (slow running ones, going to the database, ...) are in a test-list named
IntegrationTests also directly under the root item. Here's an example of calling MS-Test via nant :
<exec program="${mstest_exe}"> <arg value="/testmetadata:${mstest_metadatafile}" /> <arg value="/resultsfile:MStest_Results.xml" /> <arg value="/testlist:UnitTests" /> <arg value="/testlist:IntegrationTests" if="${CCNetBuildCondition=='ForceBuild'}" /> </exec> |
Step 5 : Using Ms-Test with coverage
In Ms-test you can specify that you also want coverage to run, see
for setting it up.
I just made a company rule that for code coverage to be ran via ccnet, the testsettings file must be named :
CodeCoverage.testsettings,
with a specific base name(cover_me) and no timestamps appended. Just to make things easier for me.
If you want MS-Test to run coverage, just pass the testsettings as an extra argument :
<exec program="${mstest_exe}" failonerror="false" resultproperty="testresult.temp" > <arg value="/testmetadata:${mstest_metadatafile}" /> <arg value="/resultsfile:MStest_Results.xml" /> <arg value="/testsettings:CodeCoverage.testsettings" /> <arg value="/testlist:UnitTests" /> <arg value="/testlist:IntegrationTests" if="${CCNetBuildCondition=='ForceBuild'}" /> </exec> |
There is a catch : Ms-Test from VS2010 does not produce XML anymore, see this
post for a solution. You really need the dll from VS2008 for it to work, the VS2010 has another interface sadly enough. So best to digg up you DVD of VS2008. I've updated that program a bit so that is also removes the Lines from the coverage result file, making it a lot smaller to merge. Below is my source code (its VB.Net)
Showing the results
I've added 2 new xsl files (MsTestReport2010.xsl and MsTest2010Cover.xsl) to CCNet, you can use these in the dashboard in the build plugins.
<buildPlugins> ... <xslReportBuildPlugin description="Ms Test" actionName="MSTest" xslFileName="xsl\MsTestReport2010.xsl" /> <xslReportBuildPlugin description="MS Test Coverage" actionName="MSTest2008Cover" xslFileName="xsl\MsTestCover2010.xsl"/> ... </buildPlugins> |
Attachments
CCNet.config
<cruisecontrol xmlns:cb="urn:ccnet.config.builder"> <!-- preprocessor settings --> <cb:define WorkingDir="D:\WorkingFolders\" /> <cb:define WorkingMainDir="D:\ArtifactFolders\" /> <cb:define ArtifactsDir="\Artifacts" />
<cb:define name="vsts_ci"> <server>http://tfs-server:8080/tfs/default/</server> <username>cruise</username> <password>**********</password> <domain>tfs-server</domain> <autoGetSource>true</autoGetSource> <cleanCopy>true</cleanCopy> <force>true</force> <deleteWorkspace>true</deleteWorkspace> </cb:define>
<cb:define name="vsts_package"> <server>http://tfs-server:8080/tfs/default/</server> <username>cruise</username> <password>**********</password> <domain>tfs-server</domain> <autoGetSource>true</autoGetSource> <cleanCopy>true</cleanCopy> <force>true</force> <applyLabel>true</applyLabel> <deleteWorkspace>true</deleteWorkspace> </cb:define>
<cb:define name="common_publishers"> <merge> <files> <file>Coverage.xml</file> <file>MStest_Results.xml</file> <file>simian.xml</file> </files> </merge> <xmllogger /> <statistics /> <modificationHistory onlyLogWhenChangesFound="true" /> <artifactcleanup cleanUpMethod="KeepLastXSubDirs" cleanUpValue="2" /> <artifactcleanup cleanUpMethod="KeepLastXBuilds" cleanUpValue="25000" /> <email from="CruiseControl@TheBuilder.com" mailhost="TheMailer.Company.com" includeDetails="TRUE"> <groups/> <users/> <converters> <ldapConverter domainName="Company" /> </converters> <modifierNotificationTypes> <NotificationType>Failed</NotificationType> <NotificationType>Fixed</NotificationType> </modifierNotificationTypes> </email> </cb:define>
<cb:define name="nant_common"> <executable>c:\Tools\nant\bin\nant.exe</executable> <nologo>true</nologo> <buildTimeoutSeconds>1800</buildTimeoutSeconds> <buildArgs>-D:useExtraMsbuildLogger=true -D:isCI=true -listener:CCNetListener,CCNetListener -D:configuration=Debug</buildArgs> </cb:define>
<cb:define name="nant_package"> <executable>c:\Tools\nant\bin\nant.exe</executable> <nologo>true</nologo> <buildTimeoutSeconds>1800</buildTimeoutSeconds> <buildArgs> -D:useExtraMsbuildLogger=true -D:CreateInstallZips=true -listener:CCNetListener,CCNetListener -D:configuration=Release</buildArgs> </cb:define>
<cb:define name="nant_qa"> <executable>c:\Tools\nant\bin\nant.exe</executable> <nologo>true</nologo> <buildTimeoutSeconds>3600</buildTimeoutSeconds> <buildArgs>-D:useExtraMsbuildLogger=true -listener:CCNetListener,CCNetListener -D:configuration=DebugCA</buildArgs> </cb:define>
<cb:define name="nant_target_CI"> <targetList> <target>clean</target> <target>compile</target> <target>test</target> </targetList> </cb:define> <cb:define name="nant_target_qa"> <targetList> <target>clean</target> <target>simian</target> <target>compile</target> <target>cover</target> </targetList> </cb:define>
<cb:define name="nant_target_package"> <targetList> <target>clean</target> <target>compile</target> <target>test</target> <target>make_package</target> <target>makehelp</target> </targetList> </cb:define> <!-- end preprocessor settings -->
<!-- Projects --> <cb:scope ProjectName="ProjectX"> <cb:define ProjectType="_CI" /> <project name="$(ProjectName)$(ProjectType)" queue="Q1" queuePriority="901"> <workingDirectory>$(WorkingDir)$(ProjectName)$(ProjectType)</workingDirectory> <artifactDirectory>$(WorkingMainDir)$(ProjectName)$(ProjectType)$(ArtifactsDir)</artifactDirectory>
<labeller type="defaultlabeller" />
<sourcecontrol type="vsts"> <workspace>$(ProjectName)</workspace> <project>$/$(ProjectName)/Main</project> <cb:vsts_ci/> </sourcecontrol>
<tasks> <nant> <cb:nant_common/> <cb:nant_target_CI /> </nant> </tasks>
<publishers> <cb:common_publishers /> </publishers>
</project> </cb:scope>
<cb:scope ProjectName="ProjectX"> <cb:define ProjectType="_Package" /> <project name="$(ProjectName)$(ProjectType)" queue="Q1" queuePriority="801"> <workingDirectory>$(WorkingDir)$(ProjectName)$(ProjectType)</workingDirectory> <artifactDirectory>$(WorkingMainDir)$(ProjectName)$(ProjectType)$(ArtifactsDir)</artifactDirectory>
<labeller type="defaultlabeller"> <prefix>1.0.1.</prefix> <incrementOnFailure>false</incrementOnFailure> </labeller>
<sourcecontrol type="vsts"> <workspace>$(ProjectName)</workspace> <project>$/$(ProjectName)/Main</project> <cb:vsts_package/> </sourcecontrol>
<tasks> <nant> <cb:nant_package/> <cb:nant_target_package /> </nant> </tasks>
<publishers> <cb:common_publishers /> </publishers>
</project> </cb:scope>
<cb:scope ProjectName="ProjectX"> <cb:define ProjectType="_QA" /> <project name="$(ProjectName)$(ProjectType)" queue="Q1" queuePriority="801"> <workingDirectory>$(WorkingDir)$(ProjectName)$(ProjectType)</workingDirectory> <artifactDirectory>$(WorkingMainDir)$(ProjectName)$(ProjectType)$(ArtifactsDir)</artifactDirectory>
<labeller type="defaultlabeller" />
<sourcecontrol type="vsts"> <workspace>$(ProjectName)</workspace> <project>$/$(ProjectName)/Main</project> <cb:vsts_package/> </sourcecontrol>
<tasks> <nant> <cb:nant_common/> <cb:nant_target_qa /> </nant> </tasks>
<publishers> <cb:common_publishers /> </publishers>
</project> </cb:scope> </cruisecontrol> |
Nant Build Script
<project default="help"> <property name="solution" unless="${property::exists('solution')}" value="ProjectX.sln" /> <property name="configuration" unless="${property::exists('configuration')}" value="Debug" /> <property name="CCNetListenerFile" unless="${property::exists('CCNetListenerFile')}" value="listen.xml" /> <property name="msbuildverbose" unless="${property::exists('msbuildverbose')}" value="normal" /> <property name="CCNetLabel" unless="${property::exists('CCNetLabel')}" value="0.0.0.0" />
<property name="mstest_metadatafile" value="ProjectX.vsmdi" /> <property overwrite="false" name="Simian_exe" value="c:\Tools\simian\bin\simian-2.3.32.exe" /> <property overwrite="false" name="msbuildlogger" value="C:\Program Files\CruiseControl.NET\server\MSBuildListener.dll" /> <property overwrite="false" name="versionInfofile" value="VersionInfo.cs" /> <property overwrite="false" name="mstest_exe" value="C:\Program Files\Microsoft Visual Studio 10.0\Common7\IDE\mstest.exe" />
<!-- custom scripts --> <script language="C#" prefix="RuWi"> <references> <include name="System.Xml.dll" /> <include name="System.dll" /> </references> <imports> <import namespace="System.Text" /> </imports> <code> <![CDATA[ [Function("UpdateVersionFile")] public static bool UpdateVersionFile(string inputFile, string newVersion, bool debugMode) { bool ok = true; try { System.IO.StreamReader versionFile = new System.IO.StreamReader(inputFile, System.Text.Encoding.ASCII); string line = ""; System.Text.StringBuilder result = new StringBuilder(); string searchPatternVersion = @"(\d+\.\d+\.\d+\.\d+)"; string searchPatternAssemblyProduct = string.Format(@"AssemblyProduct\({0}(.*?)\{0}", "\""); string replacePatternAssemblyProduct = string.Format(@"AssemblyProduct({0}(Debug)${1}1{2}{0}", "\"", "{", "}");
while (!versionFile.EndOfStream) { line = versionFile.ReadLine();
if (System.Text.RegularExpressions.Regex.IsMatch(line, searchPatternVersion) && (line.Contains("AssemblyFileVersion"))) { line = System.Text.RegularExpressions.Regex.Replace(line, searchPatternVersion, newVersion); }
if (debugMode && System.Text.RegularExpressions.Regex.IsMatch(line, searchPatternAssemblyProduct)) { line = System.Text.RegularExpressions.Regex.Replace(line, searchPatternAssemblyProduct, replacePatternAssemblyProduct); }
result.AppendLine(line); }
versionFile.Close();
System.IO.StreamWriter updatedVersionfile = new System.IO.StreamWriter(inputFile); updatedVersionfile.Write(result.ToString()); updatedVersionfile.Close(); } catch (Exception ex) { ok = false; Console.WriteLine(ex.ToString()); } return ok; } ]]> </code> </script>
<target name="help" > <echo message="Removed for keeping the file shorter." /> </target> <target name="clean" description="deletes all created files"> <delete > <fileset> <patternset > <include name="**/bin/**" /> <include name="**/obj/**" /> <include name="Coverage*.xml" /> <include name="*.zip" /> <include name="MStest_Results.xml" /> <include name="simian.xml" /> </patternset> </fileset> </delete> </target> <target name="adjustversion" description="Adjusts the version in the version.info file"> <if test="${not file::exists(versionInfofile)}"> <fail message="file: ${versionInfofile} which must contains the version info was NOT found" /> </if>
<echo message="Setting version to ${CCNetLabel}" /> <property name="debugMode" value = "False" /> <property name="debugMode" value = "True" if="${configuration=='Debug'}" /> <if test="${not RuWi::UpdateVersionFile(versionInfofile,CCNetLabel,debugMode)}"> <fail message="updating file: ${versionInfofile} which must contains the version info failed" /> </if> </target> <target name="compile" description="compiles the solution in the wanted configuration" depends="adjustversion"> <msbuild project="${solution}" > <arg value="/p:Configuration=${configuration}" /> <arg value="/p:CCNetListenerFile=${CCNetListenerFile}" /> <arg value="/v:${msbuildverbose}" /> <arg value="/l:${msbuildlogger}" /> </msbuild> </target>
<target name="test" description="runs the tests" depends="deploy.services"> <if test="${string::get-length(mstest_metadatafile)>0}" > <exec program="${mstest_exe}"> <arg value="/testmetadata:${mstest_metadatafile}" /> <arg value="/resultsfile:MStest_Results.xml" /> <arg value="/testlist:UnitTests" /> <arg value="/testlist:IntegrationTests" if="${CCNetBuildCondition=='ForceBuild'}" /> </exec> </if> </target>
<target name = "cover" description="runs the tests with coverage" > <if test="${string::get-length(mstest_metadatafile)>0}" > <!-- company rule : code coverage settings must be set via this file with the following NamingScheme : baseName="cover_me" appendTimeStamp="false" useDefault="false" --> <if test="${file::exists('CodeCoverage.testsettings')}">
<exec program="${mstest_exe}" failonerror="false" resultproperty="testresult.temp" > <arg value="/testmetadata:${mstest_metadatafile}" /> <arg value="/resultsfile:MStest_Results.xml" /> <arg value="/testsettings:CodeCoverage.testsettings" /> <arg value="/testlist:UnitTests" /> <arg value="/testlist:IntegrationTests" if="${CCNetBuildCondition=='ForceBuild'}" /> </exec> <property name="TestsOK" value="false" unless="${int::parse(testresult.temp)==0}"/>
<property name="DataCoverageFilePath" value="${RuWi::FindFile('cover_me','data.coverage')}" /> <property name="TurnCoverageFileIntoXml_exe" value="C:\Tools\TurnCoverageFileIntoXml\TurnCoverageFileIntoXml.exe" />
<fail message="No data.coverage found in cover_me folder" unless="${string::get-length(DataCoverageFilePath)>0}" />
<echo message="DataCoverageFilePath : ${DataCoverageFilePath}" />
<exec program="${TurnCoverageFileIntoXml_exe}" > <arg value="${DataCoverageFilePath}" /> <arg value="cover_me\Out" /> <arg value="NCoverExplorer.xml" /> </exec> <fail message="Failures reported in unit tests." unless="${TestsOK}" /> </if> </if>
</target> <target name="simian" description="find duplicate code" > <exec program="${Simian_exe}" failonerror="false"> <arg value="-includes=**/*.cs" /> <arg value="-excludes=**/*Designer.*" /> <arg value="-excludes=**/*Generated.*" /> <arg value="-excludes=**/*Reference.*" /> <arg value="-excludes=**/obj/*" /> <arg value="-threshold=10" /> <arg value="-formatter=xml:simian.xml" /> </exec> </target> <target name="deploy.services" description="deploys all service (web/wcf)" /> <!-- company specific, just copies files to the iis folder --> <target name="make_package" description="makes install packages" /> <!-- company specific, creates install packages and zips them --> <target name="makehelp" description="makes install packages" /> <!-- company specific, makes user help with custom tool --> </project> |
Source code for Ms-Test binary2Xml
Imports Microsoft.VisualStudio.CodeCoverage
Module Module1
Sub Main() Dim Arguments As String() Dim obc = Console.BackgroundColor Dim returnValue As Integer = 0
Try Arguments = Environment.GetCommandLineArgs
If Arguments.Length <> 4 Then
Console.BackgroundColor = ConsoleColor.Blue Console.WriteLine("Usage : {0} DataCoverageFilePath CoveredFilesPath ResultXmlFilePath", Arguments(0)) Console.BackgroundColor = ConsoleColor.DarkGreen Console.WriteLine(" {0} In\LTREMRUBEN\data.coverage Out d:\codecover.xml", Arguments(0)) Console.BackgroundColor = obc returnValue = 1 Exit Try End If
Dim DataCoverageFilePath As String = Arguments(1) Dim CoveredFilesPath As String = Arguments(2) Dim ResultXmlFilePath As String = Arguments(3)
CoverageInfoManager.ExePath = CoveredFilesPath CoverageInfoManager.SymPath = CoveredFilesPath
Console.WriteLine("converting {0}", DataCoverageFilePath) Dim coverage = CoverageInfoManager.CreateInfoFromFile(DataCoverageFilePath)
Dim CoverResult = coverage.BuildDataSet(Nothing)
Dim CoverResultStream As New IO.MemoryStream CoverResult.WriteXml(CoverResultStream)
Console.WriteLine("Initial Size in bytes : {0}", CoverResultStream.Length) CoverResultStream.Position = 0
Console.WriteLine("Cleaning up xml info ...") Dim CoverResultXmlDoc As New Xml.XmlDocument() CoverResultXmlDoc.Load(CoverResultStream)
Dim LineInfos = CoverResultXmlDoc.SelectNodes("//Lines")
For Each lineInfo As Xml.XmlNode In LineInfos lineInfo.RemoveAll() Next
Dim SourceFileNameInfos = CoverResultXmlDoc.SelectNodes("//SourceFileNames") For Each SourceFileNameInfo As Xml.XmlNode In SourceFileNameInfos SourceFileNameInfo.RemoveAll() Next
CoverResultXmlDoc.PreserveWhitespace = False CoverResultXmlDoc.Normalize() CoverResultXmlDoc.Save(ResultXmlFilePath)
Console.WriteLine("Compressed Size in bytes : {0}", New IO.FileInfo(ResultXmlFilePath).Length)
Console.WriteLine("Done.")
Catch ex As Exception Console.WriteLine(ex.ToString) End Try End Sub
End Module |