Step 5 Add source code analysis

Another great addition for a build server is to ensure that the code in source control is 'clean' and maintainable.
We already know that is compiles, it passes tests and that we cover a lot of the code. All this is nice, but sadly enough it does not mean that the code is maintainable.

Simple example : there is an update required to an existing function, but someone just copies it, adding an extra argument to the copy and wires to this copy when needed. And if that person adds tests for this extra copy, the build is green. This results however in code that has lots of similar code lines. It would be best that no code duplication would exist(Utopia).

Another item is code quality : example : naming convention, dead-code detection, unused arguments or variables, ...
In VS2010 there is a build in rule-checker. Older visual studio did not have this, so one had to call external tools (fxcop, gendarme, ... for example). Even the build in Rule-Checker (which is just an updated version of FXCop) does not give the same overview that a dedicated program like NDepend does.

So lets start by adding these things into our builds.

Checking for code-duplication

The best program I've found for this is Simian It's the very fast, easy to set up, and free for open source(a reasonable price for other projects.)

Build script update

This a rather simple update, add a simian target, and define a few properties for ease of referencing. We also need to delete the generated output in the clean task. Shown in the full Build Script below. The new simian task :

 1<property name="Simian_exe"            value="c:\Tools\simian\bin\simian-2.3.32.exe"  />
 2<property name="Simian_Threshold"      value="10"  />
 3<property name="Simian_output"         value="simian.xml"  />
 4
 5<target name="simian" description="find duplicate code" >        
 6    <exec program="${Simian_exe}" failonerror="false">
 7        <arg value="-includes=**/*.cs" />
 8        <arg value="-excludes=**/*Designer.cs" />
 9        <arg value="-excludes=**/*Generated.cs" />
10        <arg value="-excludes=**/*Reference.cs" />
11        <arg value="-excludes=**/obj/*" />
12        <arg value="-threshold=${Simian_Threshold}" />
13        <arg value="-formatter=xml:${Simian_output}" />
14    </exec>
15</target>

CCNet config change

Here we only need to add the simian target in the QA-build definition : <target>simian</target>
The nant_target_QA block looks now like this :

1 <cb:define name="nant_target_qa">
2    <targetList>
3         <target>clean</target>
4         <target>simian</target>
5         <target>compile</target>
6         <target>cover</target>      
7    </targetList>
8 </cb:define> 

VS2010 Code Analysis

You can enable this option on the 'Code Analysis' tab in the VisualStudio project(Visual Studio 2010 Premium or Ultimate). Take for example the code from Step 4 Add Coverage - Code for converting coverage to XML and enable Code analysis.
Just take the 'Microsoft Minimum Recommend Rules' for now.
You'll see that there are 2 warnings about System.IDisposable.Dispose. You can create your own ruleset, and specify that a rule is a warning or an error. Errors will break the build.

Tip Code analysis adds up a lot of compile time, so I added a new build configuration for it in Visual Studio : QA, which is just a copy of Debug but with Code Analysis enabled.
Tip Code analysis rules are set per project. Each project can have different rule sets. For example a UI program will have different needs than a framework library.

CCNet config change

Because of the new build configuration QA, we'll introduce a new setting : nant_QA and use this in the QA-build definition.

1<cb:define name="nant_QA">
2   <executable>c:\Tools\nant\bin\nant.exe</executable>
3   <nologo>true</nologo>
4   <buildTimeoutSeconds>1800</buildTimeoutSeconds>
5   <buildArgs>-D:useExtraMsbuildLogger=true  -listener:CCNetListener,CCNetListener -D:configuration=QA</buildArgs>
6</cb:define> 

Code Section

CCNet config

  1<cruisecontrol xmlns:cb="urn:ccnet.config.builder">
  2  <!-- preprocessor settings -->
  3  <cb:define WorkingDir="D:\WorkingFolders\" />
  4  <cb:define WorkingMainDir="D:\ArtifactFolders\" />
  5  <cb:define ArtifactsDir="\Artifacts" />
  6
  7  <!-- TFS settings for a CI build -->
  8  <cb:define name="vsts_ci">
  9      <server>http://tfs-server:8080/tfs/default/</server>
 10      <username>cruise</username>
 11      <password>**********</password>
 12      <domain>tfs-server</domain>
 13      <autoGetSource>true</autoGetSource>
 14      <cleanCopy>true</cleanCopy>
 15      <force>true</force>
 16      <deleteWorkspace>true</deleteWorkspace>
 17  </cb:define>
 18
 19  <!-- all projects will have at least these publishers  -->
 20  <cb:define name="common_publishers">
 21     <merge>
 22        <files>
 23            <file>MStest_UnitTestResults.xml</file>
 24         </files>
 25      </merge>
 26      <xmllogger />
 27      <statistics />
 28      <modificationHistory  onlyLogWhenChangesFound="true" />
 29      <artifactcleanup      cleanUpMethod="KeepLastXSubDirs"   cleanUpValue="2" />
 30      <artifactcleanup      cleanUpMethod="KeepLastXBuilds"    cleanUpValue="25000" />
 31      <!-- email the breakers on a broken build and when it's fixed -->
 32      <email from="CruiseControl@TheBuilder.com" 
 33            mailhost="TheMailer.Company.com" 
 34            includeDetails="TRUE">
 35          <groups/>
 36          <users/>
 37          <converters>
 38              <ldapConverter domainName="Company"  />
 39          </converters> 
 40          <modifierNotificationTypes>
 41             <NotificationType>Failed</NotificationType>
 42             <NotificationType>Fixed</NotificationType>
 43          </modifierNotificationTypes>
 44      </email> 
 45  </cb:define>
 46
 47  <!-- the Nant build arguments for a CI build  -->
 48  <cb:define name="nant_CI">
 49    <executable>c:\Tools\nant\bin\nant.exe</executable>
 50    <nologo>true</nologo>
 51    <buildTimeoutSeconds>1800</buildTimeoutSeconds>
 52    <buildArgs>-D:useExtraMsbuildLogger=true  -D:isCI=true  -listener:CCNetListener,CCNetListener -D:configuration=Debug</buildArgs>
 53  </cb:define> 
 54
 55  <!-- the Nant build arguments for a QA build  -->
 56  <cb:define name="nant_QA">
 57    <executable>c:\Tools\nant\bin\nant.exe</executable>
 58    <nologo>true</nologo>
 59    <buildTimeoutSeconds>1800</buildTimeoutSeconds>
 60    <buildArgs>-D:useExtraMsbuildLogger=true  -listener:CCNetListener,CCNetListener -D:configuration=QA</buildArgs>
 61  </cb:define> 
 62
 63  <!-- the nant  targets for a CI build -->
 64  <cb:define name="nant_target_CI">
 65     <targetList>
 66          <target>clean</target>
 67          <target>compile</target>
 68          <target>unit_test</target>
 69     </targetList>
 70  </cb:define> 
 71
 72 <!-- the nant  targets for a QA build -->
 73  <cb:define name="nant_target_qa">
 74     <targetList>
 75          <target>clean</target>
 76          <target>simian</target>
 77          <target>compile</target>
 78          <target>cover</target>      
 79     </targetList>
 80  </cb:define>
 81
 82  <!-- end preprocessor settings -->
 83
 84  <!-- Projects -->
 85   <cb:scope ProjectName="ProjectX">
 86     <cb:define ProjectType="_CI" />
 87      <project name="$(ProjectName)$(ProjectType)" queue="Q1" queuePriority="901">
 88        <workingDirectory>$(WorkingDir)$(ProjectName)$(ProjectType)</workingDirectory>
 89        <artifactDirectory>$(WorkingMainDir)$(ProjectName)$(ProjectType)$(ArtifactsDir)</artifactDirectory>
 90
 91        <labeller type="defaultlabeller" />
 92
 93        <intervalTrigger name="continuous" seconds="30" buildCondition="IfModificationExists" initialSeconds="30" />
 94
 95        <sourcecontrol type="vsts">
 96            <workspace>$(ProjectName)</workspace>
 97            <project>$/$(ProjectName)/Trunk</project>
 98            <cb:vsts_ci/>          
 99        </sourcecontrol> 
100
101        <tasks>
102           <nant>
103              <cb:nant_CI/>
104              <cb:nant_target_CI />
105           </nant>    
106        </tasks>
107
108        <publishers>
109           <cb:common_publishers />
110        </publishers>
111
112      </project>
113    </cb:scope>
114
115     <cb:scope ProjectName="ProjectX">
116       <cb:define ProjectType="_QA" />
117        <project name="$(ProjectName)$(ProjectType)" queue="Q1" queuePriority="801">
118          <workingDirectory>$(WorkingDir)$(ProjectName)$(ProjectType)</workingDirectory>
119          <artifactDirectory>$(WorkingMainDir)$(ProjectName)$(ProjectType)$(ArtifactsDir)</artifactDirectory>
120
121          <scheduleTrigger time="06:30" buildCondition="ForceBuild" name="QA_Scheduled">      
122
123          <labeller type="defaultlabeller" />
124
125          <sourcecontrol type="vsts">
126              <workspace>$(ProjectName)</workspace>
127              <project>$/$(ProjectName)/Main</project>
128              <cb:vsts_ci/>             
129          </sourcecontrol> 
130
131          <tasks>
132             <nant>
133                <cb:nant_QA/>
134                <cb:nant_target_qa />
135             </nant>    
136          </tasks>
137
138          <publishers>
139             <cb:common_publishers />
140          </publishers>
141
142        </project>
143     </cb:scope>
144
145  <!-- End Projects -->
146  </cruisecontrol>

Nant Build Script

  1 <project default="help">
  2       <property name="solution"                           unless="${property::exists('solution')}"                            value="ProjectX.sln" />    
  3       <property name="configuration"                      unless="${property::exists('configuration')}"                       value="Debug"        />    
  4       <property name="CCNetListenerFile"                  unless="${property::exists('CCNetListenerFile')}"                   value="listen.xml"    />    
  5       <property name="msbuildverbose"                     unless="${property::exists('msbuildverbose')}"                      value="normal"       />    
  6       <property name="CCNetLabel"                         unless="${property::exists('CCNetLabel')}"                          value="0.0.0.0"      />     
  7       <property name="WantedCoveragePercent"              value="75"  />      
  8
  9       <property name="mstest_metadatafile"                value="${string::replace(solution, '.sln', '.vsmdi')}" />    
 10       <property name="mstest_unit_test_file"              value="MStest_UnitTestResults.xml" />    
 11       <property name="mstest_CoverageFile"                value="MStest_Cover.xml"  />
 12       <property name="TurnCoverageFileIntoXml_exe"        value="C:\Tools\TurnCoverageFileIntoXml\TurnCoverageFileIntoXml.exe"  />
 13       <property name="Simian_exe"                         value="c:\Tools\simian\ bin\simian-2.3.32.exe"  />
 14       <property name="Simian_Threshold"                   value="10"  />
 15       <property name="Simian_output"                      value="simian.xml"  />
 16
 17       <property overwrite="false" name="msbuildlogger"    value="C:\Program Files\CruiseControl.NET\server\MSBuildListener.dll"                     />
 18       <property overwrite="false" name="versionInfofile"  value="VersionInfo.cs" />
 19
 20       <!-- custom scripts --> 
 21        <script language="C#" prefix="RuWi">
 22             <references>
 23                 <include name="System.Xml.dll" />
 24                 <include name="System.dll" />
 25             </references>
 26             <imports>
 27                 <import namespace="System.Text" />
 28             </imports>
 29             &lt;code&gt;
 30               &lt;![CDATA[
 31                   [Function("UpdateVersionFile")]
 32                   public static bool UpdateVersionFile(string inputFile, string newVersion, bool debugMode)
 33                   {
 34                       bool ok = true;
 35                       try
 36                       {
 37                           System.IO.StreamReader versionFile = new System.IO.StreamReader(inputFile, System.Text.Encoding.ASCII);
 38                           string line = "";
 39                           System.Text.StringBuilder result = new StringBuilder();
 40                           string searchPatternVersion = @"(\d+\.\d+\.\d+\.\d+)";
 41                           string searchPatternAssemblyProduct = string.Format(@"AssemblyProduct\({0}(.*?)\{0}", "\"");
 42                           string replacePatternAssemblyProduct = string.Format(@"AssemblyProduct({0}(Debug)${1}1{2}{0}", "\"", "{", "}");
 43
 44                           while (!versionFile.EndOfStream)
 45                           {
 46                               line = versionFile.ReadLine();
 47
 48                               if (System.Text.RegularExpressions.Regex.IsMatch(line, searchPatternVersion) && (line.Contains("AssemblyFileVersion")))
 49                               {
 50                                   line = System.Text.RegularExpressions.Regex.Replace(line, searchPatternVersion, newVersion);
 51                               }
 52
 53                               if (debugMode && System.Text.RegularExpressions.Regex.IsMatch(line, searchPatternAssemblyProduct))
 54                               {
 55                                   line = System.Text.RegularExpressions.Regex.Replace(line, searchPatternAssemblyProduct, replacePatternAssemblyProduct);
 56                               }
 57
 58                               result.AppendLine(line);
 59                           }
 60
 61                           versionFile.Close();
 62
 63                           System.IO.StreamWriter updatedVersionfile = new System.IO.StreamWriter(inputFile);
 64                           updatedVersionfile.Write(result.ToString());
 65                           updatedVersionfile.Close();
 66                       }
 67                       catch (Exception ex)
 68                       {
 69                           ok = false;
 70                           Console.WriteLine(ex.ToString());
 71                       }
 72                       return ok;
 73                   }
 74                   ]]&gt;
 75             &lt;/code&gt;
 76        &lt;/script&gt;
 77
 78          &lt;script language="C#" prefix="RuWi"&gt;
 79               &lt;code&gt;
 80                 &lt;![CDATA[
 81                     [Function("FindFile")]
 82                     public static string FindFile(string folderName, string searchPattern)
 83                     {
 84                         var f =  System.IO.Directory.GetFiles(folderName, searchPattern, System.IO.SearchOption.AllDirectories);
 85                         if (f.Length &gt; 0)
 86                         {
 87                             return f[0];
 88                         }
 89
 90                         return "";
 91                     }                            
 92                 ]]&gt;
 93           &lt;/code&gt;
 94       &lt;/script&gt;       
 95
 96       &lt;!-- end custom scripts --&gt; 
 97
 98       &lt;target name="help" &gt;   
 99           &lt;echo message="Removed for keeping the file shorter." /&gt;
100       &lt;/target&gt;
101
102       &lt;target name="clean" description="deletes all created files"&gt;
103           &lt;delete &gt;
104               &lt;fileset&gt;
105                   &lt;patternset &gt;
106                       &lt;include name="**/bin/**"  /&gt;
107                       &lt;include name="**/obj/**"  /&gt;
108                       &lt;include name="${mstest_unit_test_file}" /&gt;  
109                       &lt;include name="${mstest_CoverageFile}" /&gt;  
110                   &lt;/patternset&gt;
111               &lt;/fileset&gt;
112           &lt;/delete&gt;
113       &lt;/target&gt;
114
115       &lt;target name="adjustversion" description="Adjusts the version in the version.info file"&gt;
116           &lt;if test="${not file::exists(versionInfofile)}"&gt;
117               &lt;fail message="file: ${versionInfofile}  which must contains the version info was NOT found" /&gt;
118           &lt;/if&gt;
119
120           &lt;echo message="Setting version to ${CCNetLabel}" /&gt;        
121
122           &lt;property name="debugMode" value = "False" /&gt;
123           &lt;property name="debugMode" value = "True"  if="${configuration=='Debug'}"  /&gt;
124           &lt;if test="${not RuWi::UpdateVersionFile(versionInfofile,CCNetLabel,debugMode)}"&gt;
125               &lt;fail message="updating file: ${versionInfofile}  which must contains the version info failed" /&gt;
126           &lt;/if&gt;
127       &lt;/target&gt;
128
129       &lt;target name="compile" description="compiles the solution in the wanted configuration" depends="adjustversion"&gt;                         
130           &lt;msbuild  project="${solution}" &gt;
131               &lt;arg value="/p:Configuration=${configuration}" /&gt;
132               &lt;arg value="/p:CCNetListenerFile=${CCNetListenerFile}" /&gt;
133               &lt;arg value="/v:${msbuildverbose}" /&gt;
134               &lt;arg value="/l:${msbuildlogger}" /&gt; 
135           &lt;/msbuild&gt;
136       &lt;/target&gt;
137
138      &lt;target name="unit_test" description="runs the unit tests"    depends="compile"&gt;
139          &lt;if test="${file::exists(mstest_metadatafile)}"&gt;
140             &lt;exec program="${mstest_exe}"&gt;
141                &lt;arg value="/testmetadata:${mstest_metadatafile}" /&gt;
142                &lt;arg value="/resultsfile:${mstest_unit_test_file}" /&gt;
143                &lt;arg value="/testlist:UnitTests" /&gt;
144             &lt;/exec&gt;             
145          &lt;/if&gt;
146      &lt;/target&gt;
147
148     &lt;target name = "cover" description="runs the tests with coverage" &gt;     
149     &lt;!-- 
150         company rule : code coverage settings must be set via the file CodeCoverage.testsettings 
151         with the following NamingScheme : baseName="cover_me" appendTimeStamp="false" useDefault="false" 
152     --&gt;
153          &lt;if test="${file::exists('CodeCoverage.testsettings')}"&gt; 
154
155             &lt;exec program="${mstest_exe}" failonerror="false" resultproperty="testresult.temp" &gt;
156               &lt;arg value="/testmetadata:${mstest_metadatafile}" /&gt;
157               &lt;arg value="/resultsfile:${mstest_unit_test_file}" /&gt;
158               &lt;arg value="/testsettings:CodeCoverage.testsettings" /&gt;
159               &lt;arg value="/testlist:UnitTests" /&gt;
160             &lt;/exec&gt;             
161
162             &lt;property name="TestsOK" value="false" unless="${int::parse(testresult.temp)==0}"/&gt; 
163             &lt;property name="DataCoverageFilePath" value="${RuWi::FindFile('cover_me','data.coverage')}"  /&gt;
164
165             &lt;fail message="No data.coverage found in cover_me folder" unless="${string::get-length(DataCoverageFilePath)&gt;0}" /&gt;
166
167             &lt;echo message="DataCoverageFilePath : ${DataCoverageFilePath}" /&gt;               
168
169             &lt;exec program="${TurnCoverageFileIntoXml_exe}" &gt;
170                 &lt;arg value="${DataCoverageFilePath}" /&gt;
171                 &lt;arg value="cover_me\Out" /&gt;
172                 &lt;arg value="${mstest_CoverageFile}" /&gt;
173                                 &lt;arg value="${WantedCoveragePercent}" /&gt;
174             &lt;/exec&gt;
175
176             &lt;fail message="Failures reported in unit tests." unless="${TestsOK}" /&gt;
177         &lt;/if&gt;
178     &lt;/target&gt;
179
180     &lt;target name="simian" description="find duplicate code" &gt;        
181         &lt;exec program="${Simian_exe}" failonerror="false"&gt;
182             &lt;arg value="-includes=**/*.cs" /&gt;
183             &lt;arg value="-excludes=**/*Designer.cs" /&gt;
184             &lt;arg value="-excludes=**/*Generated.cs" /&gt;
185             &lt;arg value="-excludes=**/*Reference.cs" /&gt;
186             &lt;arg value="-excludes=**/obj/*" /&gt;
187             &lt;arg value="-threshold=${Simian_Threshold}" /&gt;
188             &lt;arg value="-formatter=xml:${Simian_output}" /&gt;
189         &lt;/exec&gt;
190    &lt;/target&gt; 
191
192   &lt;/project&gt;   
193