Msbuild: преобразование одной itemgroup в другую itemgroup с другой структурой

#msbuild #mstest

#msbuild #mstest

Вопрос:

Возможно, я задаю здесь неправильный вопрос, и я открыт для этого, поэтому я немного расскажу о том, что я пытаюсь сделать. Я вызываю mstest через проект msbuild после динамического нахождения всех тестовых сборок. Я вызываю mstest отдельно для каждой тестовой сборки, чтобы результаты можно было импортировать в teamcity (мой сервер CI), как только они станут доступны, вместо того, чтобы ждать завершения всех из них, прежде чем показывать какой-либо прогресс в TC.

Проблема в том, что при этом выполняется по одному тесту за раз, а в сочетании с медленными затратами (даже на i7 quad открытие mstest занимает 3-5 секунд для каждого проекта) и множеством тестов выполнение тестов занимает несколько минут.

Используя задачу msbuild с BuildInParallel=true (и вызывая с параметром /m), можно создавать несколько проектов одновременно.

Итак, что я пытаюсь сделать, это

  • получаем список всех *.Tests.dll
  • Вызовите целевой объект ExecMsTest в том же проекте, параллельно, для каждой библиотеки dll

     <PropertyGroup>
            <MsTestExePath Condition="'$(MsTestExePath)'==''">C:Program Files (x86)Microsoft Visual Studio 10.0Common7IDEMSTest.exe</MsTestExePath>
            <MsTestSettingsPath Condition="'$(MsTestSettingsPath)'==''">Project.testsettings</MsTestSettingsPath>
    </PropertyGroup>
    <ItemGroup>
            <TestAssemblies Include="**bin***.Tests.dll" />
    </ItemGroup>
    
    <Target Name="RunTests">
            <Message Text="Found test assemblies: @(TestAssemblies)" />
    
            <MakeDir Directories="TestResults" />
    
            <MsBuild Projects="@(ProjectsToBuild)" Targets="ExecMsTest" BuildInParallel="True" />
    </Target>
    
    <Target Name="ExecMsTest">
            <Message Text="Running tests in $(TestAssembly)" />
            <!-- show TC progress -->
            <Message Text="##teamcity[progressMessage 'Running tests in $(TestAssembly).dll']" Importance="High" />
    
            <PropertyGroup>
                    <MsTestCommand>"$(MsTestExePath)" /testcontainer:"$(TestAssembly)" /resultsfile:"TestResults$(TestAssembly).trx" /testsettings:"$(MsTestSettingsPath)"</MsTestCommand>
            </PropertyGroup>
            <!-- Message Text="Exec: $(MsTestCommand)" / -->
            <Exec Command="$(MsTestCommand)" ContinueOnError="true" /> 
    
    <!-- import data to teamcity test results -->
            <Message Text="##teamcity[importData type='mstest' path='TestResults$(TestAssembly).trx']" />
            <Message Text="Tests complete from $(TestAssembly)" />
      

Однако это не совсем правильно. Вы можете видеть, что моя itemgroup называется TestAssemblies, но я передаю @(ProjectsToBuild) в mstest. Это связано с тем, что для задачи msbuild требуется группа элементов другого формата, что-то вроде:

     <ItemGroup>
            <ProjectsToBuild Include="Project.mstest.proj">
                    <Properties>TestAssembly=Project.UI.Tests</Properties>
            </ProjectsToBuild>
            <ProjectsToBuild Include="Project.mstest.proj">
                    <Properties>TestAssembly=Project.Model.Tests</Properties>
            </ProjectsToBuild>
    </ItemGroup>
  

Итак, в этом суть моего вопроса, предполагая, что я даже задаю правильные вещи: как мне преобразовать itemgroup TestAssemblies itemgroup во что-то похожее на itemgroup ProjectsToBuild?

На случай, если это не очевидно, имена элементов в TestAssemblies — это *.tests .Имена файлов Dll, хотя мне нужно, чтобы это имя было a внутри элемента, а имя элемента ProjectsToBuild должно быть файлом Project.mstest.proj (поскольку все они вызывают один и тот же файл).


Благодаря @Spider M9 это работает:

     <ItemGroup>
            <TestAssemblies Include="**bin***.Tests.dll" />
    </ItemGroup>

    <Target Name="RunTests">
            <Message Text="Found test assemblies: @(TestAssemblies)" />
            <ItemGroup>
                    <TestAssembliesToBuild Include="Project.mstest.proj">
                            <Properties>TestAssembly=%(TestAssemblies.FileName);FullPath=%(TestAssemblies.FullPath)</Properties>
                    </TestAssembliesToBuild>
            </ItemGroup>

            <MakeDir Directories="TestResults" />

            <MsBuild Projects="@(TestAssembliesToBuild)" Targets="ExecMsTest" BuildInParallel="True" />
    </Target>
  

При запуске msbuild в однопоточном режиме вся моя сборка (которая включает компиляцию, создание моментальных снимков приложения и базы данных, развертывание схемы для пары баз данных, которые используются в некоторых модульных тестах, и затем, наконец, запуск mstest) заняла около 930 минут. После этого изменения это заняло ~ 7m.

Однако, прежде чем получить ответ на этот вопрос, я просто попробовал запустить один экземпляр mstest, чтобы посмотреть, насколько это улучшит ситуацию, и это занимает около 4 миллионов (из которых запуск mstest занимает чуть более 1 минуты). Недостатком является то, что мне приходится ждать завершения всех тестов, прежде чем получать результаты, но, учитывая ошеломляющее улучшение с 6m до 1m, это вполне приемлемый компромисс.

Чтобы было понятно, единственное отличие заключается в том, что mstest запускается один раз, а не десятки раз, и, по-видимому, многозадачность также дает некоторую выгоду. Я запускаю это на Core i7-860 (4 физических ядра, 8 логических ядер), и я подозреваю, что количество ядер сильно повлияет на уровень улучшения этого изменения.

Вот мои новые RunTests:

     <Target Name="RunTests">
            <Message Text="Found test assemblies: @(TestAssemblies)" />

            <MakeDir Directories="TestResults" />

            <!-- this executes mstest once, and runs all assemblies at the same time. Faster, but no output to TC until they're all completed -->
            <PropertyGroup>
                    <MsTestCommand>"$(MsTestExePath)" @(TestAssemblies->'/testcontainer:"%(FullPath)"', ' ') /resultsfile:"TestResultsResults.trx" /testsettings:"$(MsTestSettingsPath)"</MsTestCommand>
            </PropertyGroup>

            <Message Text="##teamcity[progressMessage 'Running tests']" Importance="High" />
            <Message Text="Exec: $(MsTestCommand)" />
            <Exec Command="$(MsTestCommand)" ContinueOnError="true" />
            <Message Text="##teamcity[importData type='mstest' path='TestResultsResults.trx']" />
    </Target>
  

кроме того, вам нужен файл testsettings с: <Execution parallelTestCount="0"> (0 означает автоматическое определение, по умолчанию равно 1) и необходимо вызвать msbuild, используя /m параметр и / или <Msbuild BulidInParallel="true">

Ответ №1:

Попробуйте это:

 <ItemGroup>
   <TestAssemblies Include="**bin***.Tests.dll" />
   <TestAssembliesToBuild Include="Project.mstest.proj">
     <Properties>TestAssembly=%(TestAssemblies.FileName)</Properties>
   </TestAssembliesToBuild>
</ItemGroup>
<Message Text="'%(TestAssembliesToBuild.Properties)'" />
  

Комментарии:

1. Это навело меня на правильный путь. Я определил TestAssemblies как глобальную itemgroup, и TestAssembliestoBuild не работает, определенный в той же itemgroup, но он работает, если он находится внутри itemgroup, которая находится внутри задачи. После этого это отлично работает.