Simon Green's Developer Blog
Developing .NET in the cold white north ...

Build versioning with NAnt, Draco.NET and SourceGear Vault

Friday, 21 October 2005 16:33 by Simon

One aim of continuous integration should be to automate as much of the build process as possible. If something is left to human intervention then inevitably it will at some point be completed incorrectly or inconsistently or forgotten completely.

Part of a good build process is to update the version number of the application so that each build (triggered by changes in the source) will have a unique number and the source control system should label the source so that the code that created that build can be identified. This is my solution to updating the version number when using NAnt, Draco.Net and SourceGear Vault.

The build process consists of the following:

Draco.Net monitors the project folder in the SourceGear Vault repository.
A developer checks in some changes which causes Vault to update the folder version number (based on the changeset).
Draco.Net detects the changes and gets the latest version of the project to a temporary build folder
The NAnt .build file (part of the project) is started by Draco.Net
Within NAnt a custom function gets the folder version from Vault and sets the revision part of the version number based on it
The project folder in Vault is labeled with the build revision number
The assembly is compiled using the version number by overriding the AssemblyInfo.cs file (using the standard <adminfo> task)
The NUnit tests are run and reports produced, MSI files created etc.
Everything is pretty straightforward using the standard functions of NAnt, Draco.Net and the SourceGear Vault NAnt Tasks other than step #5 which required a custom NAnt function to return the folder tree version number. The code for this is below which I added to the Vault NAnt Tasks project (I'm not sure yet of the process for feeding this into the project as an update):


using NAnt.Core;
using NAnt.Core.Attributes;
using VaultClientNetLib;
using VaultClientOperationsLib;
using VaultLib;

namespace NAnt.Contrib.Tasks.SourceGearVault
{
    /// <summary>
    /// Vault functions for NAnt
    /// </summary>
    /// <example>
    /// <code>
    /// <![CDATA[<property name="build.version.revision" value="${vault::getversion(vault.url, vault.repository, vault.path, vault.username, vault.password)}"/>
    ///   ]]></code>
    /// </example>
    [FunctionSet("vault", "Vault")]
    public class VaultFunctions : NAnt.Core.FunctionSetBase
    {
        public VaultFunctions(Project project, PropertyDictionary properties) : base(project, properties) { }
        
        #region getversion
        /// <summary>
        /// Gets the version of the object at the path specified.
        /// </summary>
        /// <param name="url">SourceGear Vault url.</param>
        /// <param name="repository">Name of repository.</param>
        /// <param name="path">Path to object.</param>
        /// <param name="username">Username.</param>
        /// <param name="password">Password.</param>
        /// <returns></returns>
        [Function("getversion")]
        public static string GetVersion(string url, string repository, string path, string username, string password)
        {
            string version = string.Empty;

            ClientInstance clientInstance = VaultFunctions.Login(url, VaultConnection.AccessLevelType.Client, username, password);

            VaultRepositoryInfo    currentRepository = new VaultRepositoryInfo();

            try 
            {
                if (VaultFunctions.SelectRepository( clientInstance, repository, username ))
                {
                    string normalizedPath = RepositoryPath.NormalizeFolder( path );
                    VaultClientTreeObject vaultClientTreeObject = clientInstance.TreeCache.Repository.Root.FindTreeObjectRecursive( path );
                    version = vaultClientTreeObject.Version.ToString();
                }            
            }
            finally
            {
                clientInstance.Logout();
            }

            return version;
        }
        #endregion
    
        #region Support functions.

        /// <summary>
        /// Login to Vault.
        /// </summary>
        /// <param name="url">URL.</param>
        /// <param name="accessLevel">Access level.</param>
        /// <param name="username">Username.</param>
        /// <param name="password">Password.</param>
        /// <returns></returns>
        public static ClientInstance Login(string url, VaultConnection.AccessLevelType accessLevel, string username, string password)
        {
            ClientInstance clientInstance = new ClientInstance();

            if( url.Length != 0 && username.Length != 0 )
            {
                clientInstance.Init( accessLevel );
                if (url.ToLower().EndsWith("/VaultService") == false)
                    url += "/VaultService";
                if (url.ToLower().StartsWith("http://") == false && url.ToLower().StartsWith("https://") == false)
                    url = "http://" + url;

                clientInstance.Login( url, username, password );
                if( clientInstance.ConnectionStateType != ConnectionStateType.Connected )
                {
                    throw new BuildException( "Login to Vault repository failed." );
                }
            }

            return clientInstance;
        }

        /// <summary>
        /// Selects the repository.
        /// </summary>
        /// <param name="clientInstance">Client instance.</param>
        /// <param name="repositoryName">Name of the repository.</param>
        /// <param name="username">Username.</param>
        /// <returns></returns>
        public static bool SelectRepository( ClientInstance clientInstance, string repositoryName, string username )
        {
            if( clientInstance.ConnectionStateType != ConnectionStateType.Connected )
            {
                return false;
            }

            VaultRepositoryInfo[] vaultRepositories = null;
            clientInstance.ListRepositories( ref vaultRepositories );

            foreach( VaultRepositoryInfo vaultRepositoryInfo in vaultRepositories )
            {
                if( vaultRepositoryInfo.RepName.ToLower() == repositoryName.ToLower() )
                {
                    VaultRepositoryInfo currentRepository = vaultRepositoryInfo;
                    clientInstance.SetActiveRepositoryID( currentRepository.RepID, username, currentRepository.UniqueRepID, true, true );
                    
                    return true;
                }
            }

            return false;
        }

        #endregion
    }
}

This is called from the NAnt build script


<?xml version="1.0"?>

<project name="NUnitAspInjector" default="build" basedir=".">

 <!-- set by Draco.Net for automated builds -->

 <property name="draco" value="false"/>

 <property name="build.id" value="0"/>

 <!-- project properties -->

 <property name="project.name" value="NUnitAspInjector"/>

 <!-- base project version for this trunk / branch -->

 <property name="build.version.major" value="1"/>

 <property name="build.version.minor" value="0"/>

 <property name="build.version.build" value="5000"/>

 <property name="build.version.revision" value="0"/>

 <!-- if automated build then set revision number based on version in vault -->

 <if test="${draco}">

  <!-- vault parameters (set in Draco.Net project properties) -->

  <!--  <property name="vault.url"  value="http://localhost"/>  <property name="vault.repository" value="default"/>  <property name="vault.path"  value="$/NUnitAspInjector/trunk"/>  <property name="vault.username"  value="build"/>  <property name="vault.password"  value="password"/>   -->

  <!-- set revision number based on tree version from vault -->

  <property name="build.version.revision" value="${vault::getversion(vault.url, vault.repository, vault.path, vault.username, vault.password)}"/>

  <!-- label the build -->

  <!-- in case a build is forced, this avoids a duplicate label error -->

  <vaultdeletelabel    url="${vault.url}"    username="${vault.username}"    password="${vault.password}"    repository="${vault.repository}"    path="${vault.path}"    labelstring="build ${build.version.revision}"/>

  <!-- apply the label to the source folder -->

  <vaultlabel    url="${vault.url}"    username="${vault.username}"    password="${vault.password}"    repository="${vault.repository}"    path="${vault.path}"    labelstring="build ${build.version.revision}"/>

 </if>

 <!-- set the full build version based on major.minor.build.revision -->

 <property name="build.version" value="${build.version.major}.${build.version.minor}.${build.version.build}.${build.version.revision}"/>

 <!-- output directory -->

 <property name="build.outdir" value="C:\working\buildoutput\${project.name}\${build.version}"/>

 <!-- target .NET framework and corresponding compiler directives -->

 <property name="nant.settings.currentframework" value="net-1.1"/>

 <property name="csc.define" value="NET11"/>

 <target name="build">

  <echo message="building: ${project.name} ${build.version}"/>

  <echo message="build id: ${build.id}" if="${draco}"/>

  <!-- build the solution in debugh mode so that the test are created -->

  <!-- this is a shortcut until a proper test project is created -->

  <solution solutionfile="src\NUnitAspInjector.sln" configuration="debug">

   <webmap>

    <map url="http://localhost/NUnitAspInjector.Sample/" path="src\NUnitAspInjector.Sample\"/>

   </webmap>

  </solution>

  <!-- make a folder for the assembly -->

  <mkdir dir="bin"/>

  <!-- if automated build then create assembly version info -->

  <if test="${draco}">

   <attrib file="src\NUnitAspInjector\AssemblyInfo.cs" readonly="false"/>

   <asminfo output="src\NUnitAspInjector\AssemblyInfo.cs" language="CSharp">

    <imports>

     <import name="System" />

     <import name="System.Reflection" />

     <import name="System.EnterpriseServices" />

     <import name="System.Runtime.InteropServices" />

    </imports>

    <attributes>

     <attribute type="ApplicationNameAttribute" value="${project.name}" />

     <attribute type="AssemblyProductAttribute" value="${project.name}" />

     <attribute type="AssemblyTitleAttribute" value="InteSoft ${project.name} ${build.version.major}.${build.version.minor} for .NET 1.1" />

     <attribute type="AssemblyDescriptionAttribute" value="${project.name} for ASP.NET" />

     <attribute type="AssemblyCompanyAttribute" value="InteSoft IT Ltd." />

     <attribute type="AssemblyCopyrightAttribute" value="Copyright &#x00A9; 2005 InteSoft IT Ltd." />

     <!-- the version stays at x.x.x.0 for binding -->

     <attribute type="AssemblyVersionAttribute" value="${build.version.major}.${build.version.minor}.${build.version.build}.0" />

     <!-- the information versions show the revision number -->

     <attribute type="AssemblyFileVersionAttribute" value="${build.version}" />

     <attribute type="AssemblyInformationalVersionAttribute" value="${build.version}" />

     <attribute type="AssemblyDelaySignAttribute" value="false" />

     <attribute type="AssemblyKeyFileAttribute" value="" />

     <attribute type="AssemblyKeyNameAttribute" value="" />

     <attribute type="CLSCompliantAttribute" value="true" />

     <attribute type="ComVisibleAttribute" value="false" />

    </attributes>

    <references>

     <include name="System.EnterpriseServices.dll" />

    </references>

   </asminfo>

  </if>

  <!-- now build the proper release version of our component -->

  <csc target="library" output="bin\NUnitAspInjector.dll" debug="false" define="${csc.define};" optimize="true" rebuild="true">

   <sources basedir="src\NUnitAspInjector">

    <include name="AssemblyInfo.cs"/>

    <include name="Gdi32.cs"/>

    <include name="Handler.cs"/>

    <include name="Html2Image.cs"/>

    <include name="InjectorModule.cs"/>

    <include name="InjectorWebFormTestCase.cs"/>

    <include name="RenderAssertion.cs"/>

    <include name="TestHostAttribute.cs"/>

    <include name="TestInjectorAttribute.cs"/>

    <include name="TestRenderAttribute.cs"/>

    <include name="TestServer.cs"/>

    <include name="cassini\ByteParser.cs"/>

    <include name="cassini\ByteString.cs"/>

    <include name="cassini\Connection.cs"/>

    <include name="cassini\Host.cs"/>

    <include name="cassini\Messages.cs"/>

    <include name="cassini\Request.cs"/>

    <include name="cassini\Server.cs"/>

   </sources>

   <resources prefix="NUnit.Extensions.Asp" basedir="src\NUnitAspInjector">

    <include name="Html2Image.resx"/>

   </resources>

   <references>

    <include asis="true" name="System.Web.dll"/>

    <include asis="true" name="System.Web.Services.dll"/>

    <include asis="true" name="System.Runtime.Remoting.dll"/>

    <include asis="true" name="System.XML.dll"/>

    <include asis="true" name="System.Windows.Forms.dll"/>

    <include asis="true" name="System.Drawing.dll"/>

    <include asis="true" name="lib\AForge.Imaging.dll"/>

    <include asis="true" name="lib\AForge.Math.dll"/>

    <include asis="true" name="lib\nmock.dll"/>

    <include asis="true" name="lib\nunit.framework.dll"/>

    <include asis="true" name="lib\NUnitAsp.dll"/>

    <include asis="true" name="lib\Interop.SHDocVw.dll"/>

    <include asis="true" name="lib\AxInterop.SHDocVw.dll"/>

    <include asis="true" name="lib\Microsoft.mshtml.dll"/>

   </references>

  </csc>

  <!-- copy this release version to the test app -->

  <copy file="bin\NUnitAspInjector.dll" todir="src\NUnitAspInjector.Sample\bin" overwrite="true"/>

  <!-- run the NUnit tests -->

  <nunit2 failonerror="true">

   <formatter type="Xml" usefile="true" extension=".xml" outputdir="${build.outdir}"/>

   <test assemblyname="src\NUnitAspInjector.Sample\bin\NUnitAspInjector.Sample.dll" appconfig="src\NUnitAspInjector.Sample\web.config">

    <categories>

     <exclude name="IIS"/>

    </categories>

   </test>

  </nunit2>

  <!-- create NUnit report -->

  <nunit2report out="NUnitReport.htm" todir="${build.outdir}" opendesc="true">

   <fileset>

    <include name="${build.outdir}\NUnitAspInjector.Sample.dll-results.xml"/>

   </fileset>

  </nunit2report>

  <!-- copy this release version to the output folder -->

  <copy file="bin\NUnitAspInjector.dll" todir="${build.outdir}" overwrite="true"/>

 </target>

</project>

The key line is this:


<property name="build.version.revision" value="${vault::getversion(vault.url, vault.repository, vault.path, vault.username, vault.password)}"/>

The vault url, repository, path, username and password are passed into the NAnt build script from the Draco.Net project properties and the version and labeling parts of the script only run when the script is invoked on the build server (so developers can run the script locally without creating rogue labels).

Incidentally, I did start by using a revision number based on the difference between the project start date and the current date/time using a variation of another script:


<!-- set the revision number based on a build startdate -->

<property name="build.startdate" value="01 Oct 2005"/>

<script language="C#">

 <imports>

  <import namespace="System.Globalization"/>

  <import namespace="System.Threading"/>

 </imports>

 <code>

 <![CDATA[

  public static void ScriptMain(Project project)

  {

   DateTime start = Convert.ToDateTime(project.Properties["build.startdate"]);   

   Calendar calendar = Thread.CurrentThread.CurrentCulture.Calendar;   

   int months = ((calendar.GetYear(DateTime.Today) - calendar.GetYear(start)) * 12) + calendar.GetMonth(DateTime.Today) - calendar.GetMonth(start);   

   int day = DateTime.Now.Day;   

   int revision = (months * 100) + day;   

   project.Properties["build.version.revision"] = revision.ToString();  

  }  

 ]]>

 </code>

</script>

However, I like the fact that the build number only changes when the source changes (due to checkins) rather than incrementing simply due to time. Also, with Vault, the version number of the folder ends up matching the build label applied to it which just seems neat. I like neat :)

That's it. Hope someone finds this of some use!

 


Tags:   , , ,
Categories:   .NET | Build
Actions:   E-mail | del.icio.us | Permalink | Comments (0) | Comment RSSRSS comment feed

Windows 2003 SP1 .NET Encoding Issue

Tuesday, 11 October 2005 16:31 by Simon

We recently came across an issue with one of our components (ASPRedirector.NET) where a customer who had installed Windows 2003 SP1 started getting errors on their website. The errors only happened if they used a certain encoding in the globalisation settings:


<globalization requestEncoding="iso-8859-15" responseEncoding="iso-8859-15"/>

The error being thrown was:

NotImplementedException: The method or operation is not implemented.
System.Web.CodePageNoBestFitEncoding.GetCharCount(Byte[] bytes, Int32 index, Int32 count)

If another encoding was used then everything worked fine (but they wanted to use this particular encoding)

Our component needed to use the encoder / decoder objects returned from Response.ContentEncoding in order to decode the bytes passed into a filter to look at the page content and encode them again so that any changes were output in the chosen encoding.

Creating a simple filter highlighted the problem:

public class Filter : Stream
 {
  private Encoder encoder;
  private Decoder decoder;
  private Stream baseStream;

  public Filter(HttpContext context)
  {
   Encoding encoding = context.Response.ContentEncoding;
   this.encoder = encoding.GetEncoder();
   this.decoder = encoding.GetDecoder();
   this.baseStream = context.Response.Filter;
  }

  public override bool CanRead
  {
   get { return false; }
  }

  public override bool CanWrite
  {
   get { return true; }
  }

  public override bool CanSeek
  {
   get { return false; }
  }

  public override void Close()
  {
   baseStream.Close();
  }

  public override void Flush()
  {
   baseStream.Flush();
  }

  public override long Length
  {
   get { throw new NotSupportedException(); }
  }

  public override long Seek(long offset, SeekOrigin origin)
  {
   throw new NotSupportedException();
  }

  public override void SetLength(long value)
  {
   throw new NotSupportedException();
  }

  public override long Position
  {
   get { throw new NotSupportedException(); }
   set { throw new NotSupportedException(); }
  }

  public override int Read(byte[] buffer, int offset, int count)
  {
   throw new NotSupportedException();
  }

  public override void Write(byte[] buffer, int offset, int count)
  {
   // get number of characters that the byte array will be decoded into
   int charCount = this.decoder.GetCharCount(buffer, offset, count);
   // create character array to hold them
   char[] chars = new char[charCount];
   // decode byte array into characters
   int charCountDecoded = decoder.GetChars(buffer, offset, count, chars, 0);

   // we now have characters and can process them
   // converting to a string should be the readable content
   string content = new string(chars);

   // get number of bytes that the character array will be encoded into
   int byteCount = encoder.GetByteCount(chars, 0, charCountDecoded, true);
   // create byte array to hold them
   byte[] bytes = new byte[byteCount];
   // encode character array into bytes
   int byteCountEncoded = encoder.GetBytes(chars, 0, charCountDecoded, bytes, 0, true);

   // write bytes to output stream - this should be exactly what came in
   baseStream.Write(bytes, 0, byteCountEncoded);
  }
 }

This fails if the responseEncoding is set to "iso-8859-15" after Windows 2003 SP1 has been applied but works before SP1 and if a different encoding is used (the default "utf-8" for instance).

So what's the problem? I found a lone post in a newsgroup that explained it:

SP1 of Windows Server introduced a new "feature" in the GlobalizationConfig class called EnableBestFitResponseEncoding.

Effect is that when you set anything other than UTF8 in web.config, e.g. <globalization requestEncoding="iso-8859-1" responseEncoding="iso-8859-1" />, many functions will break (including HttpUtility.URLDecode) with a MethodNotImplementedExecption in the new internal class CodePageNoBestFitEncoding.

Analyzing with the reflector reveals, that new code was added to the HttpResponse.ContentEncoding property:

                  if (!this._encoding.Equals(Encoding.UTF8))
                  {
                        string text1 = this._encoding.GetType().FullName;
                        if ((config1 == null) || !config1.EnableBestFitResponseEncoding)
                        {
                              if (text1 == "System.Text.MLangCodePageEncoding")
                              {
                                    this._encoding = Encoding.GetEncoding("mlang");
                              }
                              else if ((text1 == "System.Text.CodePageEncoding") || (text1 == "System.Text.Latin1Encoding"))
                              {
                                    int num1 = this._encoding.CodePage;
                                    this._encoding = new CodePageNoBestFitEncoding(num1);
                              }
                        }

Assuming that EnableBestFitResponseEncoding is false by default, this property will return a new instance of CodePageNoBestFitEncoding when you set a responseEncoding="iso-8859-1" in web.config.

Many of the normal Encoding methods in CodePageNoBestFitEncoding will throw the MethodNotImplemented exception which explains the above mentioned bahavior.

Some additional analysis reveals that there is a new attribute for <globalization > called "enableBestFitResponseEncoding". After setting this attribute to "true", everthing works again as normal with a responseEncoding other than UTF8.

A search in Microsoft's site for "enableBestFitResponseEncoding" returns 0 hits. Thank you Microsoft!

 


Tags:   , ,
Categories:   .NET
Actions:   E-mail | del.icio.us | Permalink | Comments (0) | Comment RSSRSS comment feed

NUnitAspInjector Project

Sunday, 9 October 2005 16:29 by Simon

This is a helper library to make unit testing with NUnit, NUnitAsp and NMock easier. It builds on some of the ideas here and also adds some new features such as the ability to create unit tests that match the rendered output of a page or control against a base-line image. TDD for HTML and CSS !

I still have to do a complete write-up on how to use it but for those that like to download and explore, here it is:

http://www.intesoft.net/NUnitAspInjector/

Tags:   , ,
Categories:   .NET
Actions:   E-mail | del.icio.us | Permalink | Comments (0) | Comment RSSRSS comment feed