Pull to refresh

Динамическое создание билд-плана, для анализа файлов .NET проекта, посредством FxCop

Reading time11 min
Views2.5K

Так получилось, что год назад, мне пришлось написать билд-план с использованием ant. Он предназначался для нашего небольшого веб-проекта, исполнялся на Hudson и должен был производить: компиляцию, прогон NUnit тестов, подсчет % покрытия кода тестам, поиск дублирующегося кода и выявление основных стилистических несоответствий в коде. Но это вступление, а далее поговорим, о написании билд-плана для анализа файлов проекта, посредством FxCop.

И так! Поехали!



Вводная



Как водится, я разбил билд-план на несколько составных частей:

  1. dbdeploy.build.xml — отвечает за создание тестовой базы данных и накат появляющихся скриптов
  2. fxcop.build.xml — отвечает за запуск анализа и обработки FxCop'ом файлов проекта и построения отчета о найденных проблемах
  3. main.build.xml — тут производятся основные действия по заполнению конфигов, автоматическому поиску sln файлов для их сборки
  4. ncover.build.xml — в этой части производится построение отчета, о покрытии кода тестами
  5. simian.build.xml — а тут производится построение отчета, о дублировании в коде
  6. tests.build.xml — ну а тут производится поиск всех NUnit тестов в папке проекта и их запуск


Такое модульное построение позволяет легко исключать отдельные части, разделенные по конкретной ответственности. Нам же с Вами, предстоит рассмотреть именно устройство fxcop.build.xml файла.

Приступим



Сначала я пробовал передавать, список подготовленных путей до анализируемых файлов, посредством командной строки, но как показала практика, это занятие муторное и долгое. И не очень надежное, так как при расширении проекта, нужно будет обновлять и список файлов для анализа. Тогда я стал искать способы динамического формирования списка файлов и передачи FxCop посредством Ant. Так как анализируемых файлов было не мало, нужна была именно автоматическая система поиска нужных файлов и передача их FxCop. Покопавшись в интернете и почитав мануал по командам Ant-Contrib и Ant, нашел, то что надо. Именно команда subant позволила достичь поставленной цели. Но об этом ниже!

Реализация


Рассмотрим устройство файла. В нем присутствуют несколько задач:

  • clean-fxcop-result-folder — очищает папку отчетов FxCop и удаляет динамически сформированных файл параметров для FxCop
  • run-fxcop — главная задача, которая производит запуск анализа файлов FxCop'ом
  • create-arguments — задача, которая обрабатывает пути к файлам, пригодным для анализа и записывает построчно в динамически формируемый файл суб билд-плана
  • write-head-part — производит запись заголовка в динамически формируемый файл суб билд-плана
  • write-footer-part — производит запись команд FxCop, завершающей список путей до анализируемых файлов


Далее рассмотрим основные команды Apache Ant для решения задачи.

basename — позволяет получить имя файла с расширением из полного пути.
loadfile — позволяет загрузить определенные данных из файла. В данном случае, таска используется для разбора proj файла .NET проекта.
subant — позволяет выполнить таску из другого билд файла, в данном случае из динамически сформированного для FxCop
propertyregex — позволяет выполнить выборку данных посредством заданного регулярного выражения на входной строке.
if — позволяет добавить в билд файл логику выполнения зависящую от логических выражений.

Теперь можно перейти к рассмотрению каждой из задач отдельно.

write-head-part


	<target name="write-head-part">	  
		<echo file="${dynafile.path}\${dynafilename}">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;project name="dyna-fxcop-build" default="run-fx-cop-report-creation" basedir="."&gt;
	&lt;target name="run-fx-cop-report-creation"&gt;
		&lt;exec executable="${fxcop.path}\FxCopCmd.exe" failonerror="false"&gt;</echo>
	</target>


В данной задаче, производится запись стандартного заголовка билд-плана, в файл находящийся по пути ${dynafile.path}\${dynafilename}, с использованием &lt; &gt;, для экранирования символов < >. А так же производится запись задачи exec для передачи необходимых параметров, приложению FxCop. Именно таким образом, передавая параметры, посредством arg, можно решить проблему длинного списка путей, до анализируемых файлов.

create-arguments


	<target name="create-arguments">
        //Вывод пути до файла на консоль для информирования
		<echo message="${item.file}"/>
        //Получение имени файла с расширением, которое записывается в свойство filename
		<basename property="filename" file="${item.file}"/>
		
        //Загрузка в свойство output.path строк содержащих <OutputPath> из файла csproj
		<loadfile srcfile="${item.file}" property="output.path">
                <filterchain>
                        <linecontains>
                                <contains value="&lt;OutputPath&gt;"/>
                        </linecontains>
                </filterchain>
        </loadfile>
		
        //Загрузка строк содержащих <OutputType> из файла csproj в свойство output.type
		<loadfile srcfile="${item.file}" property="output.type">
                <filterchain>
                        <linecontains>
                                <contains value="&lt;OutputType&gt;"/>
                        </linecontains>
                </filterchain>
        </loadfile>
		
        //Загрузка строк содержащих <AssemblyName> из файла csproj в свойство assembly.name 
		<loadfile srcfile="${item.file}" property="assembly.name">
                <filterchain>
                        <linecontains>
                                <contains value="&lt;AssemblyName&gt;"/>
                        </linecontains>
                </filterchain>
        </loadfile>
		
        //Выделение значения между открывающим и закрывающим тегом OutputPath и запись в output.path.info
		<propertyregex property="output.path.info" input="${output.path}"
					   regexp="&lt;OutputPath&gt;(.*?)&lt;/OutputPath&gt;" select="\1" />
		
        //Выделение значения между открывающим и закрывающим тегом OutputType и запись в output.type.info        
		<propertyregex property="output.type.info" input="${output.type}"
					   regexp="&lt;OutputType&gt;(.*?)&lt;/OutputType&gt;" select="\1" />
                       
		//Выделение значения между открывающим и закрывающим тегом AssemblyName и запись в assembly.name.info 			   
		<propertyregex property="assembly.name.info" input="${assembly.name}"
					   regexp="&lt;AssemblyName&gt;(.*?)&lt;/AssemblyName&gt;" select="\1" />
		
        //Получение пути до файла без имени файла
		<propertyregex property="item.path" input="${item.file}"
                       regexp="(.*)\\" select="\1" />

		<echo message="output.type.info = ${output.type.info}"/>
		<echo message="output.path = ${output.path}"/>
		
        //Формирование расширения файла в зависимости от значения в свойстве output.type.info
		<if>
			<contains string="WinExe" substring="${output.type.info}"/>
			<then>
				<property name="file.name.ext" value="${assembly.name.info}.exe"/>
			</then>
			<elseif>
				<contains string="Exe" substring="${output.type.info}"/>
				<then>
					<property name="file.name.ext" value="${assembly.name.info}.exe"/>
				</then>
			</elseif>
			<else>
				<property name="file.name.ext" value="${assembly.name.info}.dll"/>
			</else>
		</if>
		//Запись параметра <arg value=""/> с заполненным параметром value и записью данного значения в файл.
		<echo file="${dynafile.path}\${dynafilename}" append="true">				&lt;arg value="/f:${item.path}\${output.path.info}${file.name.ext}"/&gt;
		</echo>
	</target>


В данной задаче производится обработка файлов проекта, с расширением csproj. Из файла, выделяются данные тегов: OutputPath, OutputType и AssemblyName. Это необходимо для того, чтобы можно было не ориентироваться на название файла проекта (так как попадались такие файлы проектов в которых было изменено имя сборки). И в динамически создаваемый файл билд-плана, записываются строки arg для задачи exec, с указанием флага /f:.

write-footer-part


<target name="write-footer-part">
		<echo file="${dynafile.path}\${dynafilename}" append="true">				&lt;arg value="/r:${fxcop.path}\Rules"/&gt;
						&lt;arg value="/o:${fxcop.report.full.path}"/&gt;
		&lt;/exec&gt;
	&lt;/target&gt;
&lt;/project&gt;</echo>
	</target>


Эта задача производит запись заключительной части, динамически формируемого билд-плана, дописывая директивы FxCop, предназначенные для установки пути, до папки с правилами /r:${fxcop.path}\Rules и папки вывода отчета /o:${fxcop.report.full.path}. Так же производится запись закрывающих тегов для exec, traget и project.

run-fxcop


<target name="run-fxcop">	
        //Запись заголовка динамического билд-плана
		<antcall target="write-head-part"/>
        //Перевод файла в режим добавления данных в конец
		<echo file="${dynafile.path}\${dynafilename}" append="true">
		</echo>
		<var name="dll.names" value=""/>
        //Перебор всех csproj файлов в папке проекта, с передачей пути до файла, записанного в переменной item.file,
        //в задачу create-arguments
		<foreach  target="create-arguments" param="item.file" inheritall="true">
			<fileset dir="${basedir}" casesensitive="no">
			  <include name="**/*.csproj"/>
              //Исключаем все что находится в папке /obj/Debug/
			  <exclude name="**/obj/Debug/**.*"/>
			</fileset>
		</foreach>
        //Запись заключительной части динамического билд-плана
		<antcall target="write-footer-part"/>
        //Выполнение задачи из динамически созданного билд-плана.
		<subant target="run-fx-cop-report-creation">
			<fileset dir="${dynafile.path}" includes="${dynafilename}"/>
		</subant>
	</target>


Ну и самая главная задача билд-плана для FxCop, которая и производит динамическое создание, так сказать суб билд-плана. При помощи именно такого подхода и будет выполняться анализ файлов, проекта .NET. В данной задаче посредством write-head-part, записывается заголовок в файл, который создается по пути ${dynafile.path}\${dynafilename}. Далее производится перевод файла в режим добавления данных в конец, посредством команды echo с параметром append="true".

После этих действий, производится перебор файлов, с расширением csproj с использованием foreach. При этом, путь до файла, записывается в переменную item.file, которая определена посредством param="item.file". Ну и для того, чтобы ant не просматривал содержимое obj/Debug, используя инструкцию />, заносим в игнор.

Далее, при помощи write-footer-part, записывается заключительная часть динамически формируемого файла билда.

И теперь самое интересное! Так как мы, в динамическом билд-плане, создали задачу с именем run-fx-cop-report-creation, то теперь мы можем ее исполнить, посредством таски subant. В параметрах к subant, указанием путь, до динамически сформированного файла билд-плана, из которого и будет выполнена run-fx-cop-report-creation задача.

Заключение



Надеюсь что данный материал был интересен :) Спасибо за внимание!

Полный код xml билд-файла для FxCop
<?xml version="1.0" encoding="UTF-8"?>

<project name="fxcop-xxx-project" default="run-fxcop" basedir=".">
	<property name="dynafile.path" value="${basedir}"/>
	<property name="dynafilename" value="dynabuild.xml"/>
	<property name="fxcop.report.dir" value="${basedir}\FxCopReports"/>
	<property name="fxcop.report.full.path" value="${fxcop.report.dir}\fxcop.report.xml"/>
	
	<target name="clean-fxcop-result-folder">
		<echo message="Cleaning FxCop result report dir, and dynamic xml"/>
		<delete>
			<fileset dir="${fxcop.report.dir}" includes="**/*.*"/>
		</delete>
		<delete file="${dynafile.path}\${dynafilename}" failonerror="false"/>
	</target>
	
	<target name="run-fxcop">	
		<antcall target="write-head-part"/>
		<echo file="${dynafile.path}\${dynafilename}" append="true">
		</echo>
		<var name="dll.names" value=""/>
		<foreach  target="create-arguments" param="item.file" inheritall="true">
			<fileset dir="${basedir}" casesensitive="no">
			  <include name="**/*.csproj"/>
			  <exclude name="**/obj/Debug/**.*"/>
			</fileset>
		</foreach>
		<antcall target="write-footer-part"/>
		<subant target="run-fx-cop-report-creation">
			<fileset dir="${dynafile.path}" includes="${dynafilename}"/>
		</subant>
	</target>
	
	<target name="create-arguments">
		<echo message="${item.file}"/>
		<basename property="filename" file="${item.file}"/>
					   
		<loadfile srcfile="${item.file}" property="output.path">
                <filterchain>
                        <linecontains>
                                <contains value="&lt;OutputPath&gt;"/>
                        </linecontains>
                </filterchain>
        </loadfile>
		
		<loadfile srcfile="${item.file}" property="output.type">
                <filterchain>
                        <linecontains>
                                <contains value="&lt;OutputType&gt;"/>
                        </linecontains>
                </filterchain>
        </loadfile>
		
		<loadfile srcfile="${item.file}" property="assembly.name">
                <filterchain>
                        <linecontains>
                                <contains value="&lt;AssemblyName&gt;"/>
                        </linecontains>
                </filterchain>
        </loadfile>
		
		<propertyregex property="output.path.info" input="${output.path}"
					   regexp="&lt;OutputPath&gt;(.*?)&lt;/OutputPath&gt;" select="\1" />
					   
		<propertyregex property="output.type.info" input="${output.type}"
					   regexp="&lt;OutputType&gt;(.*?)&lt;/OutputType&gt;" select="\1" />
					   
		<propertyregex property="assembly.name.info" input="${assembly.name}"
					   regexp="&lt;AssemblyName&gt;(.*?)&lt;/AssemblyName&gt;" select="\1" />
		
		<propertyregex property="item.path" input="${item.file}"
                       regexp="(.*)\\" select="\1" />

		<echo message="output.type.info = ${output.type.info}"/>
		<echo message="output.path = ${output.path}"/>
		
		<if>
			<contains string="WinExe" substring="${output.type.info}"/>
			<then>
				<property name="file.name.ext" value="${assembly.name.info}.exe"/>
			</then>
			<elseif>
				<contains string="Exe" substring="${output.type.info}"/>
				<then>
					<property name="file.name.ext" value="${assembly.name.info}.exe"/>
				</then>
			</elseif>
			<else>
				<property name="file.name.ext" value="${assembly.name.info}.dll"/>
			</else>
		</if>
		
		<echo file="${dynafile.path}\${dynafilename}" append="true">				&lt;arg value="/f:${item.path}\${output.path.info}${file.name.ext}"/&gt;
		</echo>
	</target>
	
	
	<target name="write-head-part">	  
		<echo file="${dynafile.path}\${dynafilename}">&lt;?xml version="1.0" encoding="UTF-8"?&gt;
&lt;project name="dyna-fxcop-build" default="run-fx-cop-report-creation" basedir="."&gt;
	&lt;target name="run-fx-cop-report-creation"&gt;
		&lt;exec executable="${fxcop.path}\FxCopCmd.exe" failonerror="false"&gt;</echo>
	</target>
	
	<target name="write-footer-part">
		<echo file="${dynafile.path}\${dynafilename}" append="true">				&lt;arg value="/r:${fxcop.path}\Rules"/&gt;
						&lt;arg value="/o:${fxcop.report.full.path}"/&gt;
		&lt;/exec&gt;
	&lt;/target&gt;
&lt;/project&gt;</echo>
	</target>
</project>

Tags:
Hubs:
Total votes 16: ↑12 and ↓4+8
Comments0

Articles