Part Cover 紹介 その3 - Part Cover console から テストコード(NUnit)のカバレッジを取得する

Part Cover 紹介の第三弾です。以前の記事はこちら
Part Cover 紹介 その1 - Part Cover browser から テストコード(NUnit)のカバレッジを取得する - お だ のスペース
Part Cover 紹介 その2 - Part Cover browser から Windows アプリケーションのカバレッジを取得する - お だ のスペース


前回までは、GUI での実行について紹介しましたが今回からは、CUI で実行出来る Part Cover console について紹介していきます。
CUI で実行出来る利点は、何といっても自動化出来る事ですね!GUI だと自動化するのは難しいです。


Part Cover console の実行ファイルは、PartCover.exe になります。とりあえず、ヘルプを見てます。
ヘルプは、partcover.exe --help で確認出来ます。

ヘルプを見ていくと分かるのですが、全部見るのも面倒なので GUI で設定した値と CUI でのパラメータの表を以下に用意しました。

No. GUI (Part Cover browser) CUI (Part Cover console)
1 Executable File --target
2 Working Directory --target-work-dir
3 Working Arguments --target-args
4 Rules --include、--exclude

Rules に対応する パラメータが二つあるのは、GUI で Rules を指定する際に先頭に "+" か "-" を付ける事でカバレッジ取得の対象に含む/含まないを決めるのですが、
"+" を付けた場合(カバレッジ取得対象に含む)は、--include に "-" を付けた場合(カバレッジ取得対象から除く)は、--exclude に対応します。
ややこしいので実例で紹介します。

GUI 側では、

Executable File NUnit-Console.exe のパス
Working Directory テスト対象のアセンブリがあるディレクトリ
Working Arguments テスト対象のアセンブリ
Rules +[*Application1]*、-[*Application1]*.Form1、-[*Application1]*.Program

これを CUI 側で実行した物がこちらのイメージです。

--target NUnit-Console.exe のパス
--target-work-dir テスト対象のアセンブリがあるディレクトリ
--target-args テスト対象のアセンブリ
--include [*Application1]*
--exclude [*Application1]*.Form1
--exclude [*Application1]*.Program

但し、このままでは実行結果がそのままコンソールに出力されてしまうので、--output パラメータにて結果の出力先も指定しましょう。
このイメージをみて気付かれたかも知れませんがパラメータの指定方法は、-- (parameterName と value の区切りは、半角スペース)という形です。PartCover に付属しているマニュアルや、--help で確認したパラメータの指定方法とは異なります!
※マニュアルでは、--= の形式です。以前のバージョンではこの方法でしたが、最新のバージョンでは変更されたようです。*1


さてこれで Part Cover console の実行結果が取得出来ましたが、このままではただのXMLファイルなので読めません。。

<PartCoverReport date="2010-04-19T20:23:21.3850579+09:00">
 <File id="1" url="C:\Users\Shinsuke\Documents\Visual Studio 2010\Projects\WindowsFormsApplication1\WindowsFormsApplication1\CalcModel.cs" />
 <Assembly id="1" name="WindowsFormsApplication1" module="C:\Users\Shinsuke\AppData\Local\Temp\nunit20\ShadowCopyCache\4884_634073053982338777\Tests_3692996\assembly\dl3\871f7d9f\352f929e_98dcca01\WindowsFormsApplication1.EXE" domain="test-domain-TestProject.dll" domainIdx="1" />
 <Type asmref="1" name="WindowsFormsApplication1.Properties.Settings" flags="0">
  <Method name=".cctor" sig="void  ()" bodysize="22" flags="0" iflags="0" />
  <Method name="get_Default" sig="WindowsFormsApplication1.Properties.Settings  ()" bodysize="23" flags="0" iflags="0" />
  <Method name=".ctor" sig="void  ()" bodysize="8" flags="0" iflags="0" />
 </Type>
 <Type asmref="1" name="WindowsFormsApplication1.CalcModel" flags="0">
  <Method name="set_Arg2" sig="void  (int)" bodysize="9" flags="0" iflags="0">
   <pt visit="3" pos="0" len="8" />
  </Method>
  …省略
</PartCoverReport>

これを見やすく整形するために、Part Cover には xsltスタイルシートが付属しています。スタイルシートは、Part Cover のインストールしたディレクトリの \xslt に存在します。
※デフォルトでは、%PROGRAMFILES%\Gubka Bob\PartCover .NET 2.3\xslt
これを適用したいのですが、何故かそのまま適用しても正しく表示されません。というわけで、xslt ファイルを編集する必要があります。
スタイルシートは、アセンブリ単位(Report By Assembly.xslt)とクラス単位(Report By Class.xslt)の二つありますが、今回はクラス単位の方を適用します。

Report By Class.xslt の修正版

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxml="urn:schemas-microsoft-com:xslt">
<xsl:output method="html" indent="no"/>

<xsl:template match="PartCoverReport">

	<xsl:variable name="cov0style" select="'background:#FF4040;text-align:right;'"/>
	<xsl:variable name="cov20style" select="'background:#F06060;text-align:right;'"/>
	<xsl:variable name="cov40style" select="'background:#E78080;text-align:right;'"/>
	<xsl:variable name="cov60style" select="'background:#E0A0A0;text-align:right;'"/>
	<xsl:variable name="cov80style" select="'background:#D7B0B0;text-align:right;'"/>
	<xsl:variable name="cov100style" select="'background:#E0E0E0;text-align:right;'"/>
	
	<table style="border-collapse: collapse;">
		<tr style="font-weight:bold; background:whitesmoke;"><td colspan="2">Coverage by class</td></tr>
		
		<xsl:for-each select="Type">
			<tr>
				
				<xsl:element name="td">
					<xsl:attribute name="style">background:ghostwhite; padding: 5px  30px 5px  5px;</xsl:attribute>
					<xsl:value-of select="@name"/>
				</xsl:element>
				
				<xsl:variable name="codeSize" select="sum(./Method/pt/@len)+0"/>
				<xsl:variable name="coveredCodeSize" select="sum(./Method/pt[@visit>0]/@len)+0"/>
				
				<xsl:element name="td">
					<xsl:if test="$codeSize=0">
						<xsl:attribute name="style"><xsl:value-of select="$cov0style"/></xsl:attribute>
						0%
					</xsl:if>

					<xsl:if test="$codeSize &gt; 0">
						<xsl:variable name="coverage" select="ceiling(100 * $coveredCodeSize div $codeSize)"/>
						
						<xsl:if test="$coverage &gt;=  0 and $coverage &lt; 20"><xsl:attribute name="style"><xsl:value-of select="$cov20style"/></xsl:attribute></xsl:if>
						<xsl:if test="$coverage &gt;= 20 and $coverage &lt; 40"><xsl:attribute name="style"><xsl:value-of select="$cov40style"/></xsl:attribute></xsl:if>
						<xsl:if test="$coverage &gt;= 40 and $coverage &lt; 60"><xsl:attribute name="style"><xsl:value-of select="$cov60style"/></xsl:attribute></xsl:if>
						<xsl:if test="$coverage &gt;= 60 and $coverage &lt; 80"><xsl:attribute name="style"><xsl:value-of select="$cov80style"/></xsl:attribute></xsl:if>
						<xsl:if test="$coverage &gt;= 80"><xsl:attribute name="style"><xsl:value-of select="$cov100style"/></xsl:attribute></xsl:if>
						<xsl:value-of select="$coverage"/>%
					</xsl:if>
					
				</xsl:element>
			</tr>
		</xsl:for-each>
	</table>
	
</xsl:template>

</xsl:stylesheet>

ちなみに、変更箇所は、
1.<xsl:template match="/"> を <xsl:template match="PartCoverReport>
2.<xsl:for-each select="/PartCoverReport/type"> を <xsl:for-each select="Type">
3../method/code/pt〜 を ./Method/pt〜
に変更しています。
これを適用した結果が、こちら。

ただこれだとあまりにも寂しいので、スタイルシートを各人好みで拡張しましょう。
私はこんなスタイルシートを使用しています。

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:msxml="urn:schemas-microsoft-com:xslt">
<xsl:output method="html" indent="no"/>
<xsl:template match="PartCoverReport">
<html>
<body>
    <xsl:variable name="cov0style" select="'background:#ff0000;text-align:right;font-weight:bold;'"/>
    <xsl:variable name="cov20style" select="'background:#ff6600;text-align:right;font-weight:bold;'"/>
    <xsl:variable name="cov40style" select="'background:#ffcc00;text-align:right;font-weight:bold;'"/>
    <xsl:variable name="cov60style" select="'background:#cc9933;text-align:right;font-weight:bold;'"/>
    <xsl:variable name="cov80style" select="'background:#6699ff;text-align:right;font-weight:bold;'"/>
    <xsl:variable name="cov100style" select="'background:#00cc00;text-align:right;font-weight:bold;'"/>
    <script>
      function toggleDiv( imgId, divId )
      {
      eDiv = document.getElementById( divId );
      eImg = document.getElementById( imgId );

      if ( eDiv.style.display == "none" )
      {
      eDiv.style.display = "block";
      eImg.src = "./xsl/images/arrow_minus_small.gif";
      }
      else
      {
      eDiv.style.display = "none";
      eImg.src = "./xsl/images/arrow_plus_small.gif";
      }
      }
    </script>
    <table width="98%" cellspacing="0" cellpadding="2" border="0" class="section-table">

      <tr class="sectionheader">
        <td colspan="2">Part Cover - Coverage by class</td>
      </tr>

      <xsl:for-each select="Type">
        <tr>
          <xsl:variable name="className" select="@name"/>
          <td style="width: 15%; vertical-align: top;">
            <xsl:attribute name="class">section-data</xsl:attribute>
            <xsl:element name="input">
              <xsl:attribute name="type">image</xsl:attribute>
              <xsl:attribute name="onclick">
                javascript:toggleDiv('img<xsl:value-of select="$className"/>', 'divDetails<xsl:value-of select="$className"/>');
            </xsl:attribute>
              <xsl:attribute name="id">img<xsl:value-of select="$className"/></xsl:attribute>
              <xsl:attribute name="src">./xsl/images/arrow_plus_small.gif</xsl:attribute>
            </xsl:element>
            <xsl:value-of select="$className"/>
          </td>

          <xsl:variable name="codeSize" select="sum(./Method/pt/@len)+0"/>
          <xsl:variable name="coveredCodeSize" select="sum(./Method/pt[@visit>0]/@len)+0"/>

          <td style="width: 85%">
            <div>
              <xsl:if test="$codeSize=0">
                <xsl:attribute name="style"><xsl:value-of select="$cov0style"/></xsl:attribute>
                0%
              </xsl:if>

              <xsl:if test="$codeSize &gt; 0">
                <xsl:variable name="coverage" select="ceiling(100 * $coveredCodeSize div $codeSize)"/>
                <xsl:if test="$coverage = 0">
                  <xsl:attribute name="style"><xsl:value-of select="$cov0style"/></xsl:attribute>
                </xsl:if>
                <xsl:if test="$coverage &gt; 0 and $coverage &lt; 20">
                  <xsl:attribute name="style"><xsl:value-of select="$cov20style"/></xsl:attribute>
                </xsl:if>
                <xsl:if test="$coverage &gt;= 20 and $coverage &lt; 40">
                  <xsl:attribute name="style"><xsl:value-of select="$cov40style"/></xsl:attribute>
                </xsl:if>
                <xsl:if test="$coverage &gt;= 40 and $coverage &lt; 60">
                  <xsl:attribute name="style"><xsl:value-of select="$cov60style"/></xsl:attribute>
                </xsl:if>
                <xsl:if test="$coverage &gt;= 60 and $coverage &lt; 80">
                  <xsl:attribute name="style"><xsl:value-of select="$cov80style"/></xsl:attribute>
                </xsl:if>
                <xsl:if test="$coverage &gt;= 80">
                  <xsl:attribute name="style"><xsl:value-of select="$cov100style"/></xsl:attribute>
                </xsl:if>
                <xsl:value-of select="$coverage"/>%
              </xsl:if>
            </div>
            <div style="display:none">
              <xsl:attribute name="id">divDetails<xsl:value-of select="$className"/></xsl:attribute>
              <table border="0" cell-padding="6" cell-spacing="0" width="100%">
                <tr class="sectionheader">
                  <td style="width:80%;">Method name</td>
                  <td style="width:10%;">Code size</td>
                  <td style="width:10%;">Covered</td>
                </tr>
                <xsl:for-each  select="./method">
                  <xsl:variable name="methodCodeSize" select="sum(./code/pt/@len)+0"/>
                  <xsl:variable name="methodCoveredCodeSize" select="sum(./code/pt[@visit>0]/@len)+0"/>
                  <tr>
                    <xsl:if test="position() mod 2 = 0">
                      <xsl:attribute name="class">section-oddrow</xsl:attribute>
                    </xsl:if>
                    <td>
                      <xsl:value-of select="@name"/>
                    </td>
                    <td style="text-align: center;">
                      <xsl:value-of select="$methodCodeSize"/>
                    </td>
                    <td>
                      <xsl:if test="$methodCodeSize &gt; $methodCoveredCodeSize">
                        <xsl:attribute name="style"><xsl:value-of select="$cov0style"/>text-align: center;</xsl:attribute>
                      </xsl:if>
                      <xsl:if test="$methodCodeSize = $methodCoveredCodeSize">
                        <xsl:attribute name="style"><xsl:value-of select="$cov100style"/>text-align: center;</xsl:attribute>
                      </xsl:if>
                      <xsl:value-of select="$methodCoveredCodeSize"/>
                    </td>
                  </tr>
                </xsl:for-each>
              </table>
            </div>
          </td>
        </tr>
      </xsl:for-each>
    </table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>


これは、CCNET Welcome to CruiseControl.NET - CruiseControl.NET - Confluence に付属していたスタイルシートに少し手を入れた物だったと思います。
PartCover の結果ファイルには、他にもソースコードの行番号とかも持っているはずなので、ソースコードを表示して通っていない箇所をハイライトする等も頑張れば出来ると思います。

*1:1時間位ハマった…