Lazy Diary @ Hatena Blog

PowerShell / Java / miscellaneous things about software development, Tips & Gochas. CC BY-SA 4.0/Apache License 2.0

ツールごとのC1カバレッジ取得方法比較

Java

ReportGeneratorでは、分岐カバー率50%を超える行は緑色で表示されてしまい、条件網羅を記録するJaCoCoのようなツールでは逆に表示が分かりにくいという問題があることが分かったので、JaCoCoはEclipseの画面をそのまま表示。

JaCoCoはバイトコード単位で分岐を記録しており、C1(分岐網羅)でなくC2(条件網羅)相当での記録になっているため、49行目が黄色表示となっているのが大きな違い。

JaCoCo OpenClover
f:id:satob:20211206010919p:plain f:id:satob:20211205031101p:plain

VB.NET

OpenCoverは23行目を真の場合しか実行していないことを正しく記録している一方、Coverletは23行目を100%実行済と記録してしまっている。35行目も同様。また、OpenCoverは50行目の条件分岐に偽のケースがあることを正しく認識している一方、Coverletはこちらも100%実行済と記録してしまっていることが分かる。AltCoverはOpenCoverと同じ。

Coverlet OpenCover AltCover
f:id:satob:20211202013142p:plain f:id:satob:20211204192147p:plain f:id:satob:20211217002629p:plain

OpenCloverでC1カバレッジが取れるか?

結論

JaCoCoでは行単位のC1カバレッジが取れないのだ、OpenCloverでは行単位のC1カバレッジが取れそう。C1が100%実行されていないのに100%と表示されてしまうといった問題もなさそう。

手順

手元の環境ではなぜかEclipseでOpenCloverを使う手順が上手く動かなかったので、ひとまずMavenから実行する方法で試してみた。

なお、Windows上のJavaではシステムプロパティuser.home環境変数%HOME%でなく%USERPROFILE%をもとに設定されるみたい。mavensettings.xml%HOME%\.m2でなく%USERPROFILE%\.m2以下に配置する。settings.xmlの内容は以下。

<?xml version="1.0" encoding="UTF-8"?>
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
  <pluginGroups>
    <pluginGroup>org.openclover</pluginGroup>
  </pluginGroups>
</settings>

コードおよび結果

テスト対象のクラスは以下。 Class for code coverage test · GitHub

得られたXMLのレポートファイルは以下。 OpenClover coverage result · GitHub

評価

結果、C1カバレッジを行単位で取得できた。Coverletのように取っていないはずのC1カバレッジを通っているよう報告するとか、if-elseifでマッチしなかったケースを無視するとかもなく、また1回しか実行していない行を2回実行していると報告することもない。 f:id:satob:20211205031053p:plain f:id:satob:20211205031101p:plain

OpenCoverでVB.NETのコードカバレッジが取れるか?

結論

OpenCover VB.NETとかでググってもぜんぜん事例が出てこないので心配になるが、OpenCoverでVB.NETのコードカバレッジも取れる。ちゃんと行単位でのC1 coverage(branch coverage, 分岐網羅)も取れる。Coverletと違って、C1が100%実行されていないのに100%と表示されてしまうといった問題もなく、便利に使えそう。

手順

手元の環境ではなぜかInstall-PackageでOpenCoverがインストールできなかったので、nugetのOpenCoverのページからOpenCoverのnupkgファイルをダウンロードしてきて、管理者権限のあるPowerShellコンソールから以下のコマンドでインストールした。

Install-Package OpenCover -Source .\

そのうえで、

satob.hatenablog.com

で作成したテスト対象のプログラムを

docs.microsoft.com

に記載の手順で作ったテスト用プロジェクトXUnit.OpenCover.Collectorから参照させる。またXUnit.OpenCover.slnを、作成し、XUnit.OpenCover.Collectorとテスト対象のプロジェクト両方を追加する。そのうえで

kuttsun.blogspot.com

に記載の「.NET Core + xUnit の場合」に記載の手順に従い、以下のようにコマンドを実行。-filterカバレッジ取得対象の名前空間を絞っておかないと、coverage.xmlが数MBのサイズになってしまう(関係ないDLLのカバレッジも取得してしまう模様)。

C:\Program` Files\PackageManagement\NuGet\Packages\OpenCover.4.7.1221\tools\OpenCover.Console.exe -register:user -target:"dotnet.exe" -targetargs:"test .\XUnit.OpenCover.Collector\XUnit.OpenCover.Collector.csproj" -filter:"+[myApp*]*" -oldstyle -output:"coverage.OpenCover.xml"

出力されたcoverage.xmlに対して

reportgenerator.exe -reports:.\coverage.OpenCover.xml -targetdir:OpenCover

を実行すると、OpenCoverフォルダ以下にHTMLファイルが出力される。

コードおよび結果

テストクラスの内容はCoverletのときに使ったものと同じ。得られたXMLのレポートファイルは以下。

OpenCover VB.NET coverage result · GitHub

評価

結果、VB.NETのプログラムのカバレージも取得できた。

f:id:satob:20211204192144p:plain f:id:satob:20211204192147p:plain

  • 31行目のC1は1 of 2 branches are coveredとなっており、行単位でbranch coveredが取得できていることを意味する。JaCoCoのように、Branches (C1 Coverage)が取れると言っておきながら、実際には条件網羅(condition coverage, C2)しか取れないということはない。
  • そのうえで、Coverletと異なり、23行目や35行目のC1(真のルートしか通っていない)が正しく1 of 2 branches are coveredとなっている。また、44行目からの条件分岐で通っていないケース(x=10, y=10のような)も50行目のC1に反映されている。
  • 25行目と37行目が、1回しか実行していないのに2 visitsになっているのはCoverletと同じ。

CoverletでVB.NETのコードカバレッジが取れるか?

結論

https://docs.microsoft.com/ja-jp/dotnet/core/testing/unit-testing-code-coverage では「Coverlet とは、C# 用のクロスプラットフォームのコード カバレッジ フレームワーク」と言っているが、実際にはCoverletでVB.NETのコードカバレッジも取れる。ちゃんと行単位でのC1 coverage(branch coverage, 分岐網羅)も取れる。ただし、VB.NETに限った話ではないかもしれないが、C1の計測のされかたや、実行回数のカウントに変な箇所が見うけられる。特にC1が100%されていないのに100%と表示されてしまうのは痛い。

手順

zenn.dev

に記載の手順で

github.com

に記載のプログラムをコンパイルできる状態にし、内容を少し編集した。そのうえで、

docs.microsoft.com

に記載の手順でmyApp.vbprojを参照させ、テストを実行・レポートを出力した。

コードおよび結果

テストクラスの内容は以下。

using Xunit;

namespace Xunit.Coverlet.Collector;

public class UnitTest1
{
    [Fact]
    public void Test1()
    {
        myApp.Program.Main(new string[] {});
    }
}

得られたXMLのレポートファイルは以下。 Coverlet VB.NET coverage result · GitHub

評価

結果、VB.NETのプログラムのカバレージも取得できた。

f:id:satob:20211202013132p:plain f:id:satob:20211202013142p:plain

特に、31行目のC1は1 of 2 branches are coveredとなっており、行単位でbranch coveredが取得できていることを意味する。JaCoCoのように、Branches (C1 Coverage)が取れると言っておきながら、実際には条件網羅(condition coverage, C2)しか取れないということはない。

ただし、結果にところどころ妙なところが見受けられる。

  • 23行目と35行目のC1は、真のルートしか通っていないため1 of 2 branches are coveredとなるべきところ、2 of 2 branches are coveredと表示されている。
    • これは、C1が100%になるようなテストケースを設定していないのに、レポート上ではC1が100%と表示されてしまっているわけで、テスト実施レベルの統一を目的にコードカバレッジを取得している場合は致命的……
  • 25行目と37行目は、1回しか実行していないのに2 visitsになっている。
  • 44行目からの条件分岐は、本当はx=10, y=10のようにどの条件にも該当しないケースがあるはずなのだが、それがC0にもC1にも反映されていない。

How to write records into .xlsx with OLEDB

satob.hatenablog.com satob.hatenablog.com

So, how should you write records into .xlsx files with OLEDB?

Requirement and Implementation

  • You want to record some data into a .xlsx file with relational database style. In other words, you want to use the .xlsx file as a lightweight database system that can use with out-of-the-box Windows feature.
  • A schema mapped into a .xlsx file, and a table mapped into a sheet in the .xlsx file.
  • If a .xlsx file or a sheet doesn't exist, it should be newly created automatically. If the file and the sheet already exist, data should be added to the sheet.
    • First, execute the CREATE TABLE statement with a suffix $ for the sheet name. If the book and the sheet already exist, this statement will be finished without error. If the book or the sheet doesn't exist, This statement throws OleDbException. Either way, no new sheet will be created with this operation, so you can test the existence of the sheet safely.
    • Second, execute the CREATE TABLE statement without a suffix $ for the sheet name if and only if the first CREATE TABLE statement throws OleDbException. It means the .xlsx file or the sheet doesn't exist, and the sheet (sheetname)1 will not be created with this operation.
  • Erroneous data should be invalidated with datatype constraints. If you put erroneous data in the sheet, some exceptions should be thrown.
    • You have to execute CREATE TABLE first even if the sheet already exists. If the CREATE TABLE has not been executed in a connection, the data type constraint in the sheet will not have an effect. Also, you have to suffix the sheet name with $ in the INSERT statement. Otherwise, it fails if the sheet already exists (it means, CREATE TABLE [sheetname$] was executed).
  • Sometimes you will want to show recorded data in some other format (ex. invoice or receipt).
    • You cannot specify the cell format with OLEDB. Even if you prepared the sheet with cells that have specified format, OLEDB will ignore those cells and use new rows to record data. So you have to have two sheets in a book: the first one (sheet M) stores recorded data and another one (sheet V) formats data.
    • If you don't want to touch raw data through the .xlsx file and want to show only the formatted sheet, it is preferable to hide sheet M. This script works regardless of whether the sheet is hidden or not. Also, it is preferable to point the focus to sheet V. The focus position on the sheet will not be changed with INSERT.
    • Also, you can protect sheet M and sheet V to avoid unintentional changes. OLEDB can insert records even if the sheet or cells are protected with passwords (confirmed with LibreOffice Calc 7.2.3.2).

Sample code

$fileName = "C:\tmp\SchemaName.xlsx"
$sheetName = "table"
$provider = "Provider=Microsoft.ACE.OLEDB.12.0"
$dataSource = "Data Source = $fileName"
$extend = "Extended Properties=Excel 12.0"
$checkExistenceSQL = "CREATE TABLE [${sheetName}$] (CHARVALUE CHAR(4), INTVALUE INTEGER)"
$ddlSQL            = "CREATE TABLE [$sheetName]    (CHARVALUE CHAR(4), INTVALUE INTEGER)"
$conn = New-Object System.Data.OleDb.OleDbConnection("$provider;$dataSource;$extend")
$sqlCommand = New-Object System.Data.OleDb.OleDbCommand
$sqlCommand.Connection = $conn
$conn.open()

try {
  try {
    # Check whether the file and the sheet already exist.
    $sqlCommand.CommandText = $checkExistenceSQL
    $sqlCommand.ExecuteNonQuery() > $null
  } catch {
    try {
      # Create them when they don't exist.
      $sqlCommand.CommandText = $ddlSQL
      $sqlCommand.ExecuteNonQuery() > $null
    } catch {
      throw $PSItem
    }
  }

  $insertSQL = "INSERT INTO [${sheetName}$] VALUES ('{0}', '{1}')"
  $sqlCommand.CommandText = ($insertSQL -F 'ABCD', '123')
  $sqlCommand.ExecuteNonQuery() > $null
} finally {
  # You have to close the book here.
  # Otherwise, the file will be stay locked with the PowerShell process.
  $conn.close()
}