Monday, 22 April 2013

Building a wildcard containing asterisks in an MSBuild CreateItem

Suppose you want to use CreateItem to build a string containing a pattern, based on the value of a property being passed to your Target (say, to make an OpenCover Filter group).
If the Include attribute value includes an asterisk (even if escaped as hex) then a pattern match is attempted against the filesystem, and you end up with nothing in the group.
After several hours of messing about, I came up with a solution building the pattern with the word STAR instead of an asterisk, and using an inline C# Target to create a TaskItem with the correct text.
Here is the result, in a .proj file at the solution level and using the NuGet packages for OpenCover, NUnit.Runners and ReportGenerator.
<?xml version="1.0" encoding="utf-8"?>

<Project DefaultTargets="Clean" xmlns="http://schemas.microsoft.com/developer/msbuild/2003" ToolsVersion="4.0">

    <PropertyGroup>

        <OpenCoverVersion>4.5.1403</OpenCoverVersion>

        <NUnitVersion>2.6.2</NUnitVersion>

        <ReportGeneratorVersion>1.8.0.0</ReportGeneratorVersion>

    </PropertyGroup>

 

    <ItemGroup>

        <TestedNamespace Include="MyCode">

            <TestsNamespace>MyCode.Tests</TestsNamespace>

        </TestedNamespace>

        <TestedNamespace Include="MyWebStuff">

            <TestsNamespace>MyWebStuff.Tests</TestsNamespace>

        </TestedNamespace>

        <ReportFiles Include="@(TestedNamespace->'OpenCover\%(TestsNamespace).opencover.xml')"/>

    </ItemGroup>



    <PropertyGroup>

        <Configuration Condition="'$(Configuration)' == ''">Release</Configuration>

        <OpenCoverPath>$(MSBuildProjectDirectory)\packages\OpenCover.$(OpenCoverVersion)</OpenCoverPath>

        <OpenCoverMSBuildTasksPath>$(OpenCoverPath)\MSBuild</OpenCoverMSBuildTasksPath>

        <OpenCoverPath>$(MSBuildProjectDirectory)\packages\OpenCover.$(OpenCoverVersion)</OpenCoverPath>

        <OpenCoverMSBuildTasksPath>$(OpenCoverPath)\MSBuild</OpenCoverMSBuildTasksPath>

        <NUnitRunner>$(MSBuildProjectDirectory)\packages\NUnit.Runners.$(NUnitVersion)\tools\nunit-console-x86.exe</NUnitRunner>

        <ReportGenerator>$(MSBuildProjectDirectory)\packages\ReportGenerator.$(ReportGeneratorVersion)\ReportGenerator.exe</ReportGenerator>

    </PropertyGroup>

 

    <Import Project="$(OpenCoverMSBuildTasksPath)\OpenCover.targets"/>

    <UsingTask AssemblyFile="$(ReportGenerator)" TaskName="ReportGenerator"/>

 

    <UsingTask TaskName="TurnSTARintoActualStar" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll">

        <ParameterGroup>

            <Input ParameterType="Microsoft.Build.Framework.ITaskItem[]" Required="true" />

            <Result ParameterType="Microsoft.Build.Framework.ITaskItem[]" Output="true" />

        </ParameterGroup>

        <Task>

            <Code Type="Fragment" Language="cs">

                <![CDATA[

    if (Input.Length > 0)

    {

        Result = new TaskItem[Input.Length];

        for (int i = 0; i < Input.Length; i++)

        {

            ITaskItem item = Input[i];

            Result[i] = new TaskItem(item.GetMetadata("Identity").Replace("STAR", "*"));

        }

    }

]]>

            </Code>

        </Task>

    </UsingTask>

 

    <Target Name="Clean">

        <RemoveDir Directories="OpenCover" />

        <CallTarget Targets="Report"/>

    </Target>

 

    <Target Name="Report" DependsOnTargets="Build">

        <ReportGenerator

            TargetDirectory="OpenCover\Report"

            ReportTypes="Html"

            ReportFiles="@(ReportFiles)"

            />

    </Target>

    

    <Target Name="Build" Inputs="@(TestedNamespace)" Outputs="OpenCover\%(TestsNamespace).opencover.xml">

        <MakeDir Directories="OpenCover" />

 

        <CreateItem Include="+[%(TestedNamespace.Identity)STAR]STAR;-[%(TestedNamespace.TestsNamespace)]STAR;-[%(TestedNamespace.Identity)STAR]%(TestedNamespace.Identity).Properties.STAR">

            <Output TaskParameter="Include" ItemName="SafeFilters" />

        </CreateItem>

        

        <CreateItem Include="$(MSBuildProjectDirectory)\%(TestedNamespace.TestsNamespace)\bin\$(Configuration)\%(TestedNamespace.TestsNamespace).dll">

            <Output TaskParameter="Include" PropertyName="TestsAssemblyPath" />

        </CreateItem>

 

        <CreateItem Include='"$(TestsAssemblyPath)" /noshadow /xml:"OpenCover\%(TestedNamespace.TestsNamespace).nunit.xml"'>

            <Output TaskParameter="Include" PropertyName="NUnitArgs" />

        </CreateItem>

 

        <CreateItem Include="@(TestedNamespace->'OpenCover\%(TestsNamespace).opencover.xml')">

            <Output TaskParameter="Include" PropertyName="OpenCoverXml" />

        </CreateItem>

 

        <TurnSTARintoActualStar Input="@(SafeFilters)">

            <Output ItemName="Filters" TaskParameter="Result" />

        </TurnSTARintoActualStar>

 

        <OpenCover 

            ToolPath="$(OpenCoverPath)"

            Register="True" 

            Target="$(NUnitRunner)" 

            TargetArgs='$(NUnitArgs)'

            Output="$(OpenCoverXml)"

            CoverByTest="$(TestsAssemblyPath)"

            Filter="@(Filters)"/>

    </Target>

 

</Project>