0. はじめに
カバレッジを測定するのにオープンソースの OpenCppCoverage を使いたい。ただ,脚注1によれば HTML レポートを出力すると日本語が文字化けするらしい。OpenCppCoverage はバイナリでも配布されてはいるが,まずは自分でソースコードからビルドできるようにする。そして文字化けを直したい。
1. 必要なもの
下記公式サイトの情報によると以下の環境が必要であるとのこと。
https://github.com/OpenCppCoverage/OpenCppCoverage/wiki/Building-OpenCppCoverage
- Visual Studio 2019 version 16.4.X
- Visual Studio 2019 ビルド用ツール C++ MFC,ATL(x86 および x64)
- Git(Git.exe に
PATH
が通っていること)
今回は Visual Studio 2022 Community Edition,Git 2.42.0.2 を使用した。
2. ソースコードの入手
適当な作業用ディレクトリで以下のコマンドを実行する。
git clone https://github.com/OpenCppCoverage/OpenCppCoverage.git
3. サードパーティー製ライブラリのインストール
git コマンドを実行すると OpenCppCoverage というディレクトリが作られるので,そこに移動して以下の PowerShell スクリプトを実行する。
powershell.exe -executionpolicy bypass -File InstallThirdPartyLibraries.ps1
4. 文字化け対策
いくつかのファイルに日本語環境では以下のように文字化けしてしまう文字が含まれている。当然,このままだとコンパイルエラーになる。
Plugin::CoverageData CreateRandomCoverageData()
{
Plugin::CoverageData coverageData{ L"Test�", 42 };
std::default_random_engine generator;
std::uniform_int_distribution<int> distribution(0, 1);
for (int moduleIndex = 0; moduleIndex < 100; ++moduleIndex)
{
if (distribution(generator))
{
auto& module = coverageData.AddModule(std::to_wstring(moduleIndex));
AddRandomFiles(module, generator, distribution);
}
}
coverageData.AddModule("鳧�").AddFile("鳧�").AddLine(0, true);
return coverageData;
}
ただ,これは西ヨーロッパ環境(Windows-1252)では問題ない。
Plugin::CoverageData CreateRandomCoverageData()
{
Plugin::CoverageData coverageData{ L"Testé", 42 };
std::default_random_engine generator;
std::uniform_int_distribution<int> distribution(0, 1);
for (int moduleIndex = 0; moduleIndex < 100; ++moduleIndex)
{
if (distribution(generator))
{
auto& module = coverageData.AddModule(std::to_wstring(moduleIndex));
AddRandomFiles(module, generator, distribution);
}
}
coverageData.AddModule("éèà").AddFile("éèà").AddLine(0, true);
return coverageData;
}
このようなファイルは上記の例を含めて下記の4個ある。
- ExporterTest\CoverageDataSerializerTest.cpp
- ToolsTest\ToolTest.cpp
- TestCoverageConsole\TestCoverageConsole.cpp
- CppCoverageTest\CodeCoverageRunnerTest.cpp
以下のプロジェクトでソースコードの文字セットを Windows-1252 に変更し,日本語環境では表示できないという警告 C4566 を無効にする。
- ExplorerTest
- ToolsTest
- TestCoverageConsole
- CppCoverageTest
Visual Studio での設定画面を以下に示す。構成 Release/Debug およびプラットフォーム x64/Win32 によって設定は別々なので注意すること。
ソースコードの文字コードを Windows-1252 に変更する設定は以下の通り。
本記事の初稿では文字化けするソースコードを UTF-8(BOM付き)で保存することで文字化けを回避していたが,必ずしも必要ではないソースコードの変更は避けたほうが好ましいと考えて,本稿の対策に改めた。
5. ビルド
ソリューションプラットフォーム x64,ソリューション構成 Release でビルドする。コマンドラインから実行するなら下記の通り。
msbuild /p:Configuration=Release;platform=x64
6. ようやくテスト
下記のプログラムのカバレッジを取ってみる。
サンプルプログラムのソースコードはコチラ
#include <stdio.h>
#include <stdlib.h>
typedef unsigned __int64 UINT64;
static UINT64 ATOUI64( char *s ) {
return _strtoui64( s, NULL, 10 );
}
int main( int argc, char *argv[] ) {
if( argc < 2 ) {
fprintf( stderr, "素数のチェック,合成数の場合は素因数分解を行います。\n" );
fprintf( stderr, "\n" );
fprintf( stderr, "TESTPRIME(.EXE) [自然数]\n" );
fprintf( stderr, "\n" );
fprintf( stderr, "素数なら 0,合成数なら 1 を返します。\n" );
return( -1 );
}
UINT64 n = ATOUI64( argv[1] );
if( n < 2 ) {
fprintf( stderr, "2 以上の自然数を与えて下さい!!\n" );
return( -1 );
}
UINT64 factor[64];
int count = 0;
UINT64 p = n;
while( p % 2 == 0 ) { p /= 2; factor[count++] = 2; }
while( p % 3 == 0 ) { p /= 3; factor[count++] = 3; }
while( p % 5 == 0 ) { p /= 5; factor[count++] = 5; }
for( UINT64 q = 7; q * q <= p; ) {
while( p % q == 0 ) { p /= q; factor[count++] = q; } q += 4;
while( p % q == 0 ) { p /= q; factor[count++] = q; } q += 2;
while( p % q == 0 ) { p /= q; factor[count++] = q; } q += 4;
while( p % q == 0 ) { p /= q; factor[count++] = q; } q += 2;
while( p % q == 0 ) { p /= q; factor[count++] = q; } q += 4;
while( p % q == 0 ) { p /= q; factor[count++] = q; } q += 6;
while( p % q == 0 ) { p /= q; factor[count++] = q; } q += 2;
while( p % q == 0 ) { p /= q; factor[count++] = q; } q += 6;
}
if( p > 1 ) factor[count++] = p;
if( count == 1 ) {
fprintf( stdout, "整数 %I64u は素数です。\n", n );
return( 0 );
} else {
fprintf( stdout, "整数 %I64u は合成数です。\n", n );
int i;
for( i = 0; i < count - 1; i++ )
fprintf( stdout, "%I64u,", factor[i] );
fprintf( stdout, "%I64u を素因数に持ちます。\n", factor[i] );
return( 1 );
}
}
上記のテストプログラムをコンパイルする。最適化は禁止 /Od
とし,デバッグ情報を有効 /Zi
とする。
cl /Od /Zi TESTPRIME.C
カバレッジを実行する。D:\WORK\SAMPLE
はソースコードの置いているフォルダである。相対パスが効かないのはちょっと面倒なので後で直したい。
OpenCppCoverage --sources D:\WORK\SAMPLE -- TESTPRIME.EXE 6347156972521
HTML レポートの結果を示す。実行された行は ■ で示され,未実行の行は ■ で示される。
$\color{magenta}{\Large \textsf{確かに日本語の文字化けが酷い!!}}$

ソースコードと HTML 出力のテキストを16進数ダンプして調べてみると,ソースコード中の 7bit を超える文字に C2 もしくは C3 が付加されるようだ。C++ での文字コード変換ってどうやっているんだろうな?
ソースコードとHTML出力の文字の対応 | ||||||||
---|---|---|---|---|---|---|---|---|
ソースコード(Shift JIS) | 91 | 66 | 90 | 94 | 82 | CC | 83 | 60 |
HTML出力(UTF-8) | C2 91 | 66 | C2 90 | C2 94 | C2 82 | C3 8C | C2 83 | 60 |
7. 犯人追跡
脚注2を見ると,OpenCppCoverage ではソースコード中の各行に対して実行/非実行の情報を管理し,これらの情報を用いてレポート出力を行うようだ。出力形式には,HTML 形式のほかにもバイナリ形式など選択可能であり,HTML 形式以外ではソースコードは付与しない。すなわち HTML 形式で出力するところが怪しい。
という訳で,およそ犯人の目星をつけて探してみると,特に下記のメソッドにおいてソースコードの一行分 line
を読み出すところ std::getline(ifs, line)
が怪しい。
bool HtmlFileCoverageExporter::Export(
const Plugin::FileCoverage& fileCoverage,
std::wostream& output) const
{
auto filePath = fileCoverage.GetPath();
std::wifstream ifs{filePath.string()};
if (!ifs)
THROW(L"Cannot open file : " + filePath.wstring());
std::wstring line;
const Plugin::LineCoverage* previousLineCoverage = nullptr;
int styleChangesCount = 0;
int lineCount = 0;
for (int i = 1; std::getline(ifs, line); ++i)
{
auto lineCoverage = fileCoverage[i];
line = boost::spirit::classic::xml::encode(line);
if (AddLineCoverageColor(output, line, lineCoverage, previousLineCoverage))
++styleChangesCount;
++lineCount;
previousLineCoverage = lineCoverage;
}
AddEndStyleIfNeeded(output, previousLineCoverage);
output.flush();
return MustEnableCodePrettify(lineCount, styleChangesCount);
}
ここでソースコード(Shift JIS)からワイド文字列,すなわち UTF-16 への変換が行われているのかと思いきや,単にデータをゼロ拡張しているに過ぎないことが分かった。
ソースコードと読み込んだ std::wstring 文字列の対応 | ||||||||
---|---|---|---|---|---|---|---|---|
ソースコード(Shift JIS) | 91 | 66 | 90 | 94 | 82 | CC | 83 | 60 |
std::wstring 文字列 | 0091 | 0066 | 0090 | 0094 | 0082 | 008C | 0083 | 0060 |
8. 雑な解決案
ソースコードの文字コードが常にシステム規定値に従うのであれば,すなわち日本語環境下では常に Shift JIS ならば ifs.imbue(std::locale(""));
の一行を加えればよい。
bool HtmlFileCoverageExporter::Export(
const Plugin::FileCoverage& fileCoverage,
std::wostream& output) const
{
auto filePath = fileCoverage.GetPath();
std::wifstream ifs{filePath.string()};
if (!ifs)
THROW(L"Cannot open file : " + filePath.wstring());
+ ifs.imbue(std::locale("")); // システム規定値の文字コードに従うよう追加
std::wstring line;
const Plugin::LineCoverage* previousLineCoverage = nullptr;
int styleChangesCount = 0;
int lineCount = 0;
for (int i = 1; std::getline(ifs, line); ++i)
{
auto lineCoverage = fileCoverage[i];
line = boost::spirit::classic::xml::encode(line);
if (AddLineCoverageColor(output, line, lineCoverage, previousLineCoverage))
++styleChangesCount;
++lineCount;
previousLineCoverage = lineCoverage;
}
AddEndStyleIfNeeded(output, previousLineCoverage);
output.flush();
return MustEnableCodePrettify(lineCount, styleChangesCount);
}
こうすると確かに文字化けを防ぐことはできる。

よく見たら2020年5月の時点で GitHub の Pull Request に上がっていた。
https://github.com/OpenCppCoverage/OpenCppCoverage/pull/117
9. 今後の課題
- さすがにソースコードの文字コードがシステム規定値(日本語環境下では Shift JIS)ばかりという人は最近では少ないと思う。UTF-8 派の人のほうが多いかもしれない。ソースコードの文字コードを自動判別させるのは流石にシンドイので,まずは BOM を見て判別させ,BOM が無い場合のみシステム規定値に従うようにする。そうで無ければ,コマンドラインオプションで文字コードを指定できるようにしたい。
- タブ幅をコマンドラインオプションで指定できるようにしたい。ちなみに筆者はハードタブでスペース4文字幅派である。上記の例ではタブを4文字幅にするために HTML ファイルと同時に出力されるスタイルシートを編集して,下記の
tab-size: 4;
の行を追加した。third-party\google-code-prettify\prettify-CppCoverage.csspre.prettyprint { tab-size: 4; padding: 2px; border: 1px solid #888 }
10. 次回
これらの課題が次回で解決します。