Click here to Skip to main content
15,891,828 members
Articles / Web Development / ASP.NET

Secrets for Setting Up Continuous Integration

Rate me:
Please Sign up or sign in to vote.
2.88/5 (7 votes)
23 Feb 2009CPOL5 min read 65.6K   54   41  
A few simple tips that should help when you are considering setting up CI
using System;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Text;
using Agile.Common;
using Agile.Common.UI;
using Agile.Genie.Descriptors;
using GeneratorCustomization;

namespace Agile.Genie.Generators
{
    /// <summary>
    /// Generates a NAnt build file for a given project file.
    /// </summary>
    [GUIDetails("Build File")]
    public class BuildFileGenerator : Generator
    {
        #region Constructors and Factories

        /// <summary>
        /// Constructor
        /// </summary>
        private BuildFileGenerator(FileInfo projectFile)
        {
            _projectFile = AgileFileInfo.Build(projectFile);
        }

        /// <summary>
        /// Instantiate a new Build file generator for the given solution file.
        /// </summary>
        /// <remarks>File type must be a project file, if it is not,
        /// null will be returned.
        /// <p>Will also return null if the project file is in the 'excluded' list.</p></remarks>
        /// <returns>Returns null if the file is not a project file, otherwise returns the instantiated generator.</returns>
        public static BuildFileGenerator Build(FileInfo projectFile)
        {
            if (!Files.IsTheRightFileType(projectFile, "csproj", "vbproj"))
                return null;
            if (!projectFile.Exists)
                return null;

            // If an exception occurs during instantiation, just return null
            try
            {
                var generator = new BuildFileGenerator(projectFile);
                if (generator.IsExcludedProject)
                    return null;
                return generator;
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
                return null;
            }
        }

        #endregion

        private readonly AgileFileInfo _projectFile;
        private VisualStudioProjectFile _visualStudioProjectFile;
        private DataTable serverTestingProjects;

        /// <summary>
        /// Checks if the project file is in the excluded projects list in the Generator Details Dataset.
        /// </summary>
        /// <remarks>Excluded projects are ones that we do not want to generate a build file for
        /// (and also do not want included in the master.build).
        /// </remarks>
        /// <returns>True if the project has been listed explicitly to be excluded.</returns>
        private bool IsExcludedProject
        {
            get
            {
                // Ignore all .Tests projects
                if (ProjectFile.Name.Contains(".Test"))
                    return true;

                foreach (DataRow row in GeneratorsData.All.Tables["ProjectsExcludedFromBuild"].Rows)
                {
                    if (ProjectFile.NameExcludingExtension == row["ProjectName"].ToString())
                        return true;
                }
                return false;
            }
        }

        /// <summary>
        /// Gets the project system file that we are generating the .build file for.
        /// </summary>
        public FileInfo ProjectFileInfo
        {
            get { return ProjectFile.FileInfo; }
        }

        /// <summary>
        /// Gets the AgileFileInfo object that encapsulates the project file.
        /// </summary>
        public AgileFileInfo ProjectFile
        {
            get { return _projectFile; }
        }

        /// <summary>
        /// Gets the visual studio project files.
        /// </summary>
        private VisualStudioProjectFile VisualStudioProjectFile
        {
            get
            {
                if (_visualStudioProjectFile == null)
                {
                    _visualStudioProjectFile = VisualStudioProjectFileFactory.Build(ProjectFileInfo);
                }
                return _visualStudioProjectFile;
            }
        }

        /// <summary>
        /// Returns the name of the output file that will be generated by the msbuild process
        /// </summary>
        private static string OutputFileName
        {
            get
            {
                // Don't append anything for Compact framework...doesn't matter if it overwrites (if the CF results overwrites the normal build results it means the normal build was successful)
                // Cruise Control can only grab one results file.
                return "buildResults.xml";
            }
        }


        /// <summary>
        /// Generates the build file core code
        /// </summary>
        /// <returns></returns>
        public string GenerateBuildFileCode()
        {
            string code =string.Format(@"
<project name=""{0}"" default=""go"" basedir=""."">
		<property name=""TestsBaseDirectory"" value=""{7}"" />
		<property name=""TestsAssemblyDirectory"" value=""${{TestsBaseDirectory}}\bin\Release"" />
		<property name=""RunDirectory"" value=""${{project::get-base-directory()}}\${{project::get-name()}}"" />
        <property name=""BuildResultsDirectory"" value=""${{directory::get-current-directory()}}\Build"" />
        <property name=""nant.onfailure"" value=""OnFailure"" />

	<!-- Go
		Runs both the build and test targets.
	  --> 
	<target name=""go"" depends=""build{6}, test, UpdateResults""/>
	
	<!-- Build
		Builds the project and test project (if there is one).
	 -->
{4}
{5}

	<!-- test
		Tests are compulsory - every project must have them. All test results are copied into
		C:\build\testresults (or whatever your RootDirectory is)
	  -->
	  
{1}
{2}
{3}
</project>
"
                    , FileName
                    , GenerateTestSection()
                    , GenerateCopyResults()
                    , GenerateOnFailure()
                    , GenerateBuildTarget()
                    , GenerateBuildTestTarget()
                    , (!VisualStudioProjectFile.IsUnitTested || !VisualStudioProjectFile.HasTestProject) ? string.Empty : ", buildTest"
                    , GetTestProjectFileDirectoryOnly()
                    );
            return code;
        }

        /// <summary>
        /// Gets the command line args for the msbuild execution.
        /// </summary>
        /// <returns></returns>
        private string GenerateBuildTarget()
        {
            // Set the command line details (for msbuild) differently for CF and normal projects. 
            string commandLine = VisualStudioProjectFile.IsCompactFramework 
                                     ? string.Format(@"{0}", FileName.Replace("Compact", "Compact") + ProjectFile.FileInfo.Extension) 
                                     : string.Format(@"{0}", string.Format("{0}{1}", FileName, ProjectFile.FileInfo.Extension));

            return GetBuildTarget("build", commandLine);
        }

        /// <summary>
        /// Gets the command line args for the msbuild execution.
        /// </summary>
        /// <returns></returns>
        private string GenerateBuildTestTarget()
        {
                // If it does not have a unit test project, dont create the 'buildTest' target
            if (!VisualStudioProjectFile.IsUnitTested || !VisualStudioProjectFile.HasTestProject)
                return string.Empty;

            // Default to building the test project (doing this ensures the test project gets built which triggers a build of the project itself if required)
            return GetBuildTarget("buildTest",  GetTestProjectFileLocation());

//            return GetBuildTarget("buildTest", string.Format(@"""..\${{project::get-name()}}.Test\{0}"
//                , string.Format("{0}.Test{1}", FileName, ProjectFile.FileInfo.Extension)));
        }

        /// <summary>
        /// Returns the directory that the test project file is in 
        /// </summary>
        private string GetTestProjectFileLocation()
        {
            FileInfo testProject = VisualStudioProjectFile.GetTestProjectFile();
            if (testProject == null || testProject.Directory == null)
                return string.Empty;

            // don't get the full path as build files get checked in and will likely be in a different dir on the build box and other developers machines.
            string testProjectName = testProject.Name;
            return string.Format(@"{0}\{1}", GetTestProjectFileDirectoryOnly(), testProjectName);
        }

        /// <summary>
        /// Returns the directory that the test project file is in 
        /// </summary>
        private string GetTestProjectFileDirectoryOnly()
        {
            FileInfo testProject = VisualStudioProjectFile.GetTestProjectFile();
            if (testProject == null || testProject.Directory == null)
                return string.Empty;

            // don't get the full path as build files get checked in and will likely be in a different dir on the build box and other developers machines.
            string testProjectDirectory = testProject.Directory.Name;
            return string.Format(@"..\{0}", testProjectDirectory);
        }

        private string GetBuildTarget(string targetName, string commandLineCode)
        {
            const string rootDriveEnvironmentalVariableName = "RootDrive";
            string platform = VisualStudioProjectFile.IsCompactFramework ? "AnyCPU" : "AnyCPU"; // need tobe different for CF?
            string stuffAtTheEnd =
                string.Format(
                    @" /verbosity:quiet /nologo /p:Configuration=Release;Platform={2} /logger:${{environment::get-variable('{1}')}}:\Build\CCLogger\ThoughtWorks.CruiseControl.MSBuild.dll;{0}""/>""/>"
                    , OutputFileName
                    , rootDriveEnvironmentalVariableName
                    , platform);

            return string.Format(@"
	<target name=""{0}"">
		<delete file=""{2}"" failonerror=""false""/>
		<echo message=""***  Building {1}...""/>
    	<exec 
			program=""msbuild"" 
			commandline=""{1}{3}
		<echo message=""***  Build of {1} Completed Successfully!""/>
	</target>
"
                , targetName
                , commandLineCode
                , OutputFileName
                , stuffAtTheEnd
                );
        }


        /// <summary>
        /// Generate the CopyResults target
        /// </summary>
        /// <returns></returns>
        private static string GenerateCopyResults()
        {
            return string.Format(@"
	<target name=""UpdateResults"">
		<mkdir dir=""${{BuildResultsDirectory}}"" failonerror=""false""/>
        <copy verbose=""true"" todir=""${{BuildResultsDirectory}}"" overwrite=""true"">
			<fileset basedir=""${{project::get-base-directory()}}"">
				<include name=""buildResults.xml""/>
			</fileset>
		</copy>
	</target>

	<target name=""CopyResults"">
		<mkdir dir=""${{BuildResultsDirectory}}\TestResults"" failonerror=""true""/>
		<copy verbose=""true"" todir=""${{BuildResultsDirectory}}\TestResults"">
			<fileset basedir=""${{TestsAssemblyDirectory}}"">
				<include name=""*results.xml""/>
			</fileset>
		</copy>
		<echo message=""copied test results from: ${{TestsAssemblyDirectory}}...TO --> ${{BuildResultsDirectory}}""></echo>
	</target>
");
        }

        /// <summary>
        /// Generate the OnFailure target
        /// </summary>
        /// <returns></returns>
        private string GenerateOnFailure()
        {
            var onFailure =
                new StringBuilder(
                    string.Format(
                        @"
    <target name=""OnFailure"" description=""Called when build fails"">
        <property name=""message"" value=""OnFailure""/>
        <call target=""CopyResults"" />
        
		<call target=""UpdateResults"" />
"
                        , OutputFileName));

            if (ProjectIsServerTesting(VisualStudioProjectFile.ProjectName))
            {
                onFailure.Append(
                    string.Format(
                        @"
		<mail 
          from=""isisclient@aus.fujixerox.com"" 
          tolist=""mark.wallis@aus.fujixerox.com; ali.shafai@aus.fujixerox.com; dave.wheeler@aus.fujixerox.com; craig.hunter@aus.fujixerox.com"" 
          subject=""Tests failed in ${{project::get-name()}}"" 
          mailhost=""ausnry-ex01.pilot.local""
          message=""Note: this mail is also generated when a compile error occurs."">
          <attachments>
              <include name=""${{TestsAssemblyDirectory}}\*Results.xml"" />
          </attachments>
       </mail>
"));
            }

            onFailure.Append(@"
    </target>
");
            return onFailure.ToString();
        }


        /// <summary>
        /// Returns true if the tests for the project test the server.
        /// </summary>
        /// <remarks>COPIED IN MasterBuildFileGenerator</remarks>
        /// <returns></returns>
        private bool ProjectIsServerTesting(string projectName)
        {
            if (serverTestingProjects == null)
                serverTestingProjects = GeneratorsData.GetDataTable("ServerTestingProjects");

            foreach (DataRow row in serverTestingProjects.Rows)
            {
                if (row["ProjectName"].ToString() == projectName)
                    return true;
            }

            return false;
        }


        /// <summary>
        /// Generates the test section of the build file
        /// </summary>
        /// <returns></returns>
        public string GenerateTestSection()
        {
            if (VisualStudioProjectFile.OutputType.EndsWith("Exe"))
                return GenerateTestsNotRun("TESTS NOT CURRENTLY RUN FOR PROJECTS WITH AN Exe Output");

            if (!VisualStudioProjectFile.IsUnitTested)
                return GenerateTestsNotRun("PROJECTS OF THIS TYPE ARE NOT UNIT TESTED!");

            if (!VisualStudioProjectFile.HasTestProject)
                return GenerateTestsNotRun("THIS PROJECT DOES NOT HAVE A .TEST PROJECT!");

            return GenerateTestSectionDetails();
        }

        /// <summary>
        /// Generates the test section of the build file for VS2003 projects
        /// </summary>
        /// <returns></returns>
        public string GenerateTestSectionDetails()
        {
            FileInfo testProjectFile = VisualStudioProjectFile.GetTestProjectFile();
            string code = string.Format(
                    @"    <target name=""test"">
    <echo message=""*******************************************************""/>
    <echo message="" RUNNING TEST SUITES                                  *""/>
    <echo message=""     - {0}\{1}.dll""/>
    <echo message=""                                                      *""/>
    <echo message=""*******************************************************""/>

        <nunit2 verbose=""true"" failonerror=""true"">
        <formatter type=""Plain"" />
        <formatter type=""Xml"" usefile=""true"" extension="".xml"" />
        <test assemblyname=""${{TestsAssemblyDirectory}}\{1}.dll""/>
    </nunit2>

	<call target=""CopyResults"" />
    <echo message=""""/>
    <echo message=""***  ${{project::get-name()}} SUCCESSFULLY BUILT AND TESTED!  ***""/>
    <echo message=""""/>
        
	</target>"
                    , GetTestProjectFileDirectoryOnly()
                    , testProjectFile.Name.Replace(".csproj", string.Empty).Replace(".vbproj", string.Empty)
                    );
            return code;
        }

        /// <summary>
        /// Generates the test section but just reports in NAnt that tests are not run.
        /// </summary>
        /// <returns></returns>
        public string GenerateTestsNotRun(string reason)
        {
            string code =
                string.Format(
                    @"
    <target name=""test"">
        <echo message=""*******************************************************""/>
        <echo message="" {0}      *""/>
        <echo message=""                                                      *""/>
        <echo message=""                   !!TESTS NOT RUN!!                  *""/>
        <echo message=""*******************************************************""/>        
    </target>",
                    reason);
            return code;
        }


        /// <summary>
        /// Gets the list of comma separated dependencies.
        /// </summary>
        /// <example>For Generators:
        /// <p>'Common, Descriptors'</p>
        /// </example>
        /// <returns>string containing comma separated list of the projects dependencies.</returns>
        public string GetCommaSeparatedDependencies()
        {
            var dependencies = new StringBuilder();
            try
            {
                foreach (VisualStudioProjectFile referencedAssembly in VisualStudioProjectFile.ReferencedProjects)
                {
                    if (!IsAnExcludedAssembly(referencedAssembly.ProjectAssemblyName))
                        dependencies.Append(string.Format(@", {0}", referencedAssembly.ProjectName));
                }
            }
            catch (Exception ex)
            {
                dependencies.Append(string.Format("    An Exception occured trying to get dependencies [{0}]",
                                                  ex.Message));
            }

            return Strings.RemoveFirstInstanceOf(",", dependencies.ToString()).Trim();
        }

        /// <summary>
        /// Determines if the assembly is in the list of assemblies that are not to be
        /// included in the build file dependency list.
        /// </summary>
        /// <remarks>This is important because we don't want to include system and third 
        /// party assemblies in our dependency list for build files (we only want to know
        /// which of OUR assemblies must be built first!)</remarks>
        /// <param name="assemblyName">Check if this assembly is to be excluded.</param>
        /// <returns>Returns true if it is to be excluded.</returns>
        public bool IsAnExcludedAssembly(string assemblyName)
        {
            foreach (DataRow excluded in GeneratorsData.GetDataRowsFor("BuildFileGeneratorExcludedDependencies"))
            {
                string excludeAssembliesStartingWith = excluded["StartingWith"].ToString();
                if (assemblyName.StartsWith(excludeAssembliesStartingWith))
                    return true;
            }
            return false;
        }

        #region Overrides

        /// <summary>
        /// Gets the string that is used as the project dependency 'heading'.
        /// </summary>
        /// <remarks>Declared as public static because the MasterBuildFileGenerator accesses the value.
        /// Don't normally like to do this but that generator uses BuildFile generated by this
        /// class to do it's generation work. However if it needs to get any other info, better
        /// to make a Base BuildFileGen class...</remarks>
        public static string ProjectDependencyString = "This Project is dependent on:";

        /// <summary>
        /// Gets the base directory for file generation.
        /// </summary>
        /// <remarks>i.e. The base directory that the file will be generated in.</remarks>
        public override DirectoryInfo FileGenerationDirectory
        {
            get { return ProjectFile.FileInfo.Directory; }
        }

        /// <summary>
        /// Gets the additional information that is required to save the generated file
        /// in the correct directory.
        /// e.g. "Northwind\SQL\StoredProcs"
        /// </summary>
        /// <remarks>i.e. The base directory that the file will be generated in.</remarks>
        protected override string FileGenerationDirectoryDetails
        {
            get { return "NOT USED FOR BUILD FILE GENERATOR, FOR THIS WE JUST USE THE DIR OF THE SLN FILE."; }
        }

        /// <summary>
        /// Gets the description of the purpose of the file.
        /// </summary>
        protected override string FilePurpose
        {
            get
            {
                return string.Format(@"{0} Build File
Compiles and tests the project in Release mode.
{1}
{2}"
                                     , FileName
                                     , ProjectDependencyString
                                     , GetCommaSeparatedDependencies());
            }
        }

        /// <summary>
        /// Gets a string containing additional notes about the files contents.
        /// </summary>
        protected override string FileNotes
        {
            get { return string.Empty; }
        }

        /// <summary>
        /// Gets the file extension of the file that is to be created.
        /// </summary>
        /// <example>.sql</example>
        public override string FileExtension
        {
            get { return ".build"; }
        }

        /// <summary>
        /// Gets the file name of the file that is to be created, not including the file extension.
        /// </summary>
        /// <example>fileThatIsBeingGenerated</example>
        public override string FileName
        {
            get { return ProjectFileInfo.Name.Replace(ProjectFileInfo.Extension, ""); }
        }

        /// <summary>
        /// Overrides the GeneratedStamp to put the comment tags in front.
        /// </summary>
        public override string GeneratedStamp
        {
            get { return "<!-- " + base.GeneratedStamp + " -->"; }
        }

        /// <summary>
        /// Generate all of the code.
        /// </summary>
        /// <returns></returns>
        public override string Generate()
        {
            try
            {
                string baseCode = base.Generate();
                var generatedCode = new StringBuilder(baseCode);
                generatedCode.Append(GenerateBuildFileCode());

                Debug.WriteLine(string.Format(@"Generated build file for {0}", VisualStudioProjectFile.ProjectName));
                return generatedCode.ToString();
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.Message);
                return new StringBuilder(
                    string.Format("<!-- An exception occurred whilst generating:- \r\n \r\n{0} -->", ex.Message)).
                    ToString();
            }
        }

        /// <summary>
        /// Generates the comments section for the file
        /// </summary>
        /// <returns></returns>
        public override string GenerateFileHeaderComments()
        {
            return string.Format(@"
<!-- {0}
-->
"
                                 , Strings.SplitMultiLineString(FilePurpose, " "));
        }

        /// <summary>
        /// Initialize all delegates for code generation. 
        /// i.e. set the strategy pattern handlers.
        /// </summary>
        protected override void InitializeDelegates()
        {
            // nothing to initialize for the build file generator.
        }

        #endregion

        #region Old code that was used to get the project file from sln file details. Now just using proj file directly 

        //        /// <summary>
        //        /// Gets the path of the project file from the solution file.
        //        /// NOTE: Assumes that the project and solution files have the same name
        //        /// (not including the extension) and the project is in the solution.
        //        /// </summary>
        //        /// <returns></returns>
        //        private string GetProjectFileFullPath()
        //        {
        //            string solutionText = Files.GetFileContents(ProjectFileInfo);
        //            string initialReference = string.Format(@"""{0}"","
        //                                                    , ProjectFile.NameExcludingExtension);
        //            int initialReferenceIndex = solutionText.ToLower().IndexOf(initialReference.ToLower());
        //            int pathStartIndex = solutionText.ToLower().IndexOf(ProjectFile.NameExcludingExtension.ToLower()
        //                                                                , initialReferenceIndex + initialReference.Length);
        //            int pathEndIndex = solutionText.IndexOf(@"""", pathStartIndex);
        //            string projectFileSubPath = solutionText.Substring(pathStartIndex, pathEndIndex - pathStartIndex);
        //            return Path.Combine(ProjectFileInfo.DirectoryName, projectFileSubPath);
        ////            FileInfo file = new FileInfo(Path.Combine(ProjectFileInfo.DirectoryName, projectFileSubPath));
        ////            return AgileFileInfo.Build(file);
        //        }
        //        /// <summary>
        //        /// Gets the path of the project file from the solution file.
        //        /// NOTE: Assumes that the project and solution files have the same name
        //        /// (not including the extension) and the project is in the solution.
        //        /// </summary>
        //        /// <returns></returns>
        //        private string GetProjectFileDirectory()
        //        {
        //            return
        //                Strings.RemoveFirstInstanceOf(ProjectFile.NameExcludingExtension + ".csproj", GetProjectFileFullPath());
        //        }

        #endregion

        /// <summary>
        /// Generate the copy configs section for projects that have config files.
        /// </summary>
        /// <returns></returns>
        [Obsolete]
        private string GenerateCopyConfigs()
        {
            //            if (!VisualStudioProjectFile.NeedsConfigFilesCopied)
            return string.Empty;
            //
            //            return
            //                @"
            //				<echo message=""
            //***  Copying Config files...from: ${directory::get-parent-directory(GrandParent)}\Build\Configs"" />
            //		<copy verbose=""true"" todir=""${RunDirectory}"" overwrite=""true"" failonerror=""true"">
            //			<fileset basedir=""${directory::get-parent-directory(GrandParent)}\Build\Configs"">
            //				<include name=""*configuration.config""/>
            //			</fileset>		
            //		</copy>
            //";
        }

    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) Peer Placements Pty Ltd
Australia Australia
I live in Sydney and have been a developer for almost a decade now. I have a passion for technology and a strong interest in discovering 'better, cleaner, faster' ways to get systems out the door because I believe software development takes too long. If I have an idea I want to realise it as quickly as possible...plus writing systems for someone else I want to deliver quickly so I can move onto the next interesting project!

Comments and Discussions