PHPUnitのコードカバレッジHTMLでSJISやEUC-JPの日本語がスッ飛ぶのを何とかするまでの苦節

※先に種明かしをしておくと、PHP5.4以上で発生する現象です。

PHPUnitで`--coverage-html`オプションを指定するとテスト時に通ったコード、通らなかったコードをハイライト表示してくれる便利なコードカバレッジ機能ですが、UTF-8以外の日本語が含まれるソースに関しては以下のように日本語がスッ飛んでしまいます(SJISのソースです)。

![sjis.png](https://qiita-image-store.s3.amazonaws.com/0/17021/9e9774db-a5d5-0d4b-d800-14aa943b9283.png)

そして厄介なことに日本語のコメント行に至っては行ごとスッ飛んでしまうので、カバレッジのハイライトがずれてしまいます。

以下は同じソースのUTF-8バージョンです。

![utf8.png](https://qiita-image-store.s3.amazonaws.com/0/17021/90f8fb5c-d598-1487-a2df-80ed0eaec2c1.png)

ご覧のように本来5行目にあった`// テスト`というコメントがSJISバージョンでは行ごとなくなっているのでソースのハイライトがずれてしまっています。

まあUTF-8でソースを書けば何の問題も起こらないのですが、さまざまな事情により特定のエンコーディング縛りでソースを書かなければならない場合もあると思います。日本語直書きの判定ロジックをふんだんに盛り込んだ10年物のレガシーコードに毛を生やす場合とか。

※ちなみに自分の場合は、新規で書いたソースなんですが諸事情でSJISで書く必要があったのでこんなことになってます。

しかし、これではコードカバレッジが使い物にならないので何とかならないもんかと小一時間、いや大一時間ほどググってみたのですが驚くことに全く情報が見つからないという異常事態。考えられる可能性は3つ。

#### 1. SJISやEUC-JPで書かれるようなレガシーなシステム。= とてもテストできる形になってない。 = 考えるのをやめた。

#### 2. テストまではやってるけどコードカバレッジまでは見ない。もしくはカバー率の数字がある程度行ってればOK的なやつ。

#### 3. 自分、意識高い系男子なんで常にテストファーストです。(←やってない)

話が逸れました。

で、PHPUnitの巨大なソースなんて追いたくナイヨーとか思いながら仕方なく自分で調べたわけなんですが、そしたら予想外に早く問題の個所を見つけることができました。20～30分位かな。横着してググる事に固執したせいで倍以上の時間がかかったという始末です。

## 原因

問題の個所はココ(ComposerでPHPUnit4.xをインストールしたと仮定して)。

```
vendor
  ├─phpunit
  │  ├─php-code-coverage
  │  │  ├─src
  │  │  │  └─CodeCoverage
  │  │  │      ├─Report
  │  │  │      │  ├─HTML
  │  │  │      │  │  └─Renderer
  │  │  │      │  │      ├─File.php <==== コイツ☆
```

カバレッジの結果をHTMLに書き出す処理をしている所です。

```php:vendor/phpunit/php-code-coverage/src/CodeCoverage/Report/HTML/Renderer/File.php
protected function loadFile($file)
{
    $buffer              = file_get_contents($file);
    $tokens              = token_get_all($buffer);
    $result              = array('');
    $i                   = 0;
    $stringFlag          = false;
    $fileEndsWithNewLine = substr($buffer, -1) == "\n";
        .
        .
        .
```

`$file`がカバレッジするソースファイルで、それを`$buffer`に読み込んで[token_get_all](http://php.net/manual/ja/function.token-get-all.php)でバラしています。

もうちょい下の方に行くと、

```php:もうちょい下
list ($token, $value) = $token;

$value = str_replace(
    array("\t", ' '),
    array('&nbsp;&nbsp;&nbsp;&nbsp;', '&nbsp;'),
    htmlspecialchars($value)
);
```

バラしたトークンをぐるぐる回してる中で値部分を[htmlspecialchars](http://php.net/manual/ja/function.htmlspecialchars.php)にかけています。

原因はまさにここで、SJISやEUC-JPの文字列がこの`htmlspecialchars`後には空になっていました。理由は[コチラ(安心の徳丸ブランド)](http://blog.tokumaru.org/2011/11/php54htmlspecialchars.html)。

ちなみに自分がこの現象に遭遇した時に使用していたPHPのバージョンは超最新の5.6.3。

PHP5.4が出た頃にちょっとだけ騒がれた`htmlspecialchars`の仕様変更の影響を今頃になって実感しただけだったというだけの話でした。

なるほど、レガシーなコードで動くシステムはPHP5.1とか、行っても5.3程度だろうからこの現象に遭遇することがなかったと、つまりそういうわけですな。

そうだ。ググっても情報が見つからなかった原因に4つ目を追加しよう。

#### 4. 最新バージョンのPHPでSJIS使って自爆した上に世の中のPHPerに対し「おまえらテストテストって口だけかよっ」とか疑いの目を向けていた(反省)

日夜しっかりとPHPでテストしているみなさん、疑ってゴメンナサイ。

## 対策

懺悔はこんなところにして、同じような境遇にハマるかもしれない未来の子羊たちのためにどうやって解決したかをまとめておきます(__ただしPHP5.6以上。理由は後述します__)。

### 1. php.iniを以下のように編集

```ini:php.ini
; PHP's default character set is set to empty.
; http://php.net/default-charset
default_charset = "ISO-8859-1"
```

先に紹介した徳丸さんの記事によると、

> htmlspecialchars関数の第3引数には、処理対象文字列の文字エンコーディングを指定します。この指定をしない場合、従来（PHP5.3まで）はISO-8859-1とみなされていたのに対して、PHP5.4ではUTF-8とみなされるようになります。

ということなので、`default_charset`を"ISO-8859-1"にすることで`htmlspecialchars`がPHP5.4以前の時代と同じような挙動になるようにします。

__※`htmlspecialchars`の`encoding`が未指定の時に`default_charset`の値が使われるのはPHP5.6以降なので、PHP5.4～5.5の場合はこの方法では効果はありません。__

とりあえず、これで文字がスッ飛ぶことはなくなります。ただしこれだけでは不十分で、コードカバレッジレポートHTMLで文字化けします。そこで二の矢。

### 2. phpunit.xmlのコードカバレッジ出力設定を以下のようにする

```xml:phpunit.xml
<logging>
  <log type="coverage-html" target="./coverage" charset="Shift_JIS" />
</logging>
```

EUC-JPのソースの場合は`charset`をEUC-JPにすればおｋです。

`target`はどこでもいいんですが、重要なのは`charset`です。デフォルトではコードカバレッジレポートは`<meta charset="UTF-8">`で作成されてしまうので、ブラウザで見るとSJISやEUC-JPのソースの日本語が文字化けしてしまいますが、ここで出力HTMLのエンコーディングを指定することによって文字化けも回避することができます。

この2か所を設定すればPHPのバージョンを最新にしてもPHPUnitでSJISやEUC-JPのコードカバレッジで日本語が正常に表示できるようになります。

## て思うじゃん？

`phpunit.xml`での`charset`指定が有効なのはPHPUnit3.xまでで、PHPUnit4.xでは出力するHTMLのテンプレート部分でがっつりと

```
<meta charset="UTF-8">
```

とハードコーディングされていて`charset`の指定が入る余地がなくなっております。

ちなみに3.xだと

```
<meta charset="{{charset}}">
```

となっているので`charset`がちゃんと反映されます。あえてUTF-8と直書きにしたということはバグとかではなく今後の方針なんでしょうね・・・

というわけで、PHP5.6 + PHPUnit3.xまでの環境であれば上記の方法でSJISやEUC-JPの日本語がスッ飛ぶ事はなくなりますが、将来的にはSJISやEUC-JPのソースはコードカバレッジレポートで文字化けがデフォになりそうな雰囲気です。まあ、行自体がスッ飛ぶことがなくなればコードカバレッジの役割は一応果たせますが。

しかし、`charset`の指定は個々のファイルに対してでできるわけではないので、SJISとEUC-JPとUTF-8のソースが入り混じっている超絶カオスなシステムのテストを行った場合は結局文字化けが発生します。

そもそもPHP5.4とか5.5から動かせない場合もあるでしょう。

そこで・・・

## ライブラリのソースに直接介入(愚策)

PHPUnit4.xでSJISとかEUC-JPとかUTF-8のソースが混在していてもコードカバレッジレポートで一切文字化けが発生しないようにしよう、うん。(今こんな感じの顔:flushed:)

`vendor/phpunit/php-code-coverage/src/CodeCoverage/Report/HTML/Renderer/File.php`の`loadFile`メソッドに以下の1行を追加しましょう。

1行加えるだけだし、先っちょだけ的な感じで。

```php:vendor/phpunit/php-code-coverage/src/CodeCoverage/Report/HTML/Renderer/File.php
     protected function loadFile($file)
     {
         $buffer              = file_get_contents($file);
ｺﾚ ===>  $buffer              = mb_convert_encoding($buffer, 'UTF-8', mb_detect_encoding($buffer, mb_list_encodings()));
         $tokens              = token_get_all($buffer);
         $result              = array('');
         $i                   = 0;
         $stringFlag          = false;
         $fileEndsWithNewLine = substr($buffer, -1) == "\n";
             .
             .
             .
```

つまり、元のソースのエンコーディングが何であれコードカバレッジレポート用にHTML化する前にソース内容を全部UTF-8にしてしまえということです。

`composer update`を頻繁に行う人にとっては苦行ですね。はい。

とはいえ、これでどんなエンコーディングのソースが入り乱れたカオスシステムでも何でもドンと来いですよ(PHPUnitさんの個人的な意見です。個人的には関わりたくありませｹﾞﾎｹﾞﾎ)。

## まとめ

長文になった上に色々と無駄なことも書いてしまったのでまとめます。

### SJISやEUC-JPのソースでPHPUnitによるテストを行なった場合に、そのコードカバレッジレポートHTMLがどうなるか

|PHP|PHPUnit3.x|PHPUnit3.x + 対策|PHPUnit4.x|PHPUnit4.x + 対策|PHPUnit4.x + 魔改造
|:--|:--|:--|:--|:--|:--
|< 5.4|:smile:(たぶん)OK|:hushed:(たぶん)無意味|:worried:(たぶん)文字化け|:worried:(たぶん)文字化け|:laughing:(たぶん)こうかはばつぐんだ
|5.4|:tired_face:(たぶん)スッ飛ぶ|:tired_face:(たぶん)スッ飛ぶ|:tired_face:(たぶん)スッ飛ぶ|:tired_face:(たぶん)スッ飛ぶ|:laughing:(たぶん)こうかはばつぐんだ
|5.5|:tired_face:スッ飛ぶ|:tired_face:スッ飛ぶ|:tired_face:スッ飛ぶ|:tired_face:スッ飛ぶ|:laughing:こうかはばつぐんだ
|5.6|:tired_face:スッ飛ぶ|:smiley:一応解決|:tired_face:スッ飛ぶ|:worried:文字化け|:laughing:こうかはばつぐんだ

* `対策`とはこの記事の中盤で紹介したphp.iniとphpunit.xmlを設定する解決策のことです。
* `魔改造`とはPHPUnitのライブラリソースに直接手を加えることです。
* `文字化け`は行がずれるわけではないのでコードカバレッジの内容自体には支障はありません(たぶん)。
* `(たぶん)`とは、他の記事やドキュメントの仕様を見る限りでは問題なさそうだが実際に試したわけではないものです。

おう。魔改造ええんちゃう？(ご乱心)

## あとがき

動作確認はUbuntuのPHP5.5.19で少々、Windows7のPHP5.6.3でそれなりに行いました。

間違った情報を紹介しないよう有名どころの記事や公式ドキュメントなどは確認していますが、もし勘違いや誤字脱字等ありましたら色々とお手柔らかにお願いします。