type holyshared = Engineer<mixed>

技術的なことなど色々

Rustでlcovのレポートファイルをマージする

f:id:holyshared:20161207124650p:plain

これは Rust Advent Calendar 2016 (2) の 14日目の記事です。

lcovが出力するレポートのパーサー&マージャーをRustと実装しました。

lcovはgcovの拡張版みたいなもので、gcovの出力ファイルから、HTML形式のようなコードカバレッジレポートを出力するツールです。

lcovは出力の過程で、処理しやすいようにgcovの出力ファイルから独自のレポート形式に変換します。

そのレポートをパース&マージできるようにしたものが、今回実装したライブラリになります。

レポートのパース

lcovのレポートはデータをレコードで表現した単純なテキスト形式のファイルです。
各レコードは次のようになっています。
原文はこちらを参考してください。

No. レコード 説明
1 TN:<test name> テスト名
2 SF:<absolute path to the source file> ソースファイルのパス(絶対パス)
3 DA:<line number>,<execution count>[,<checksum>] 左から行番号、実行された回数、該当行のコードのMD5
4 FN:<line number of function> 関数の開始行
5 FNDA:<execution count>,<function name> 左から関数の実行された回数、関数名
6 FNF:<number of functions found> 見つかった関数の数
7 FNH:<number of function hit> 実行された関数の数
8 LH:<number of lines with an execution> 見つかった行の数
9 LF:<number of instrumented lines> 実行された行の数
10 BRDA:<line number>,<block number>,<branch number>,<taken> 左から行番号、ブロック番号、分岐番号、実行された回数
11 BRF:<number of branches found> 見つかった分岐の数
12 BRH:<number of branches hit> 実行された分岐の数
13 end_of_record 終了を示すレコード、ソースコード単位で出力される

レポートをパースするには、次のようにファイルからパーサーを生成して、1レコードずつパースします。

extern crate lcov_parser;

use lcov_parser:: { LCOVParser, LCOVRecord, FromFile };

fn main() {
    let mut parser = LCOVParser::from_file("../../../fixture/report.lcov").unwrap();

    loop {
        match parser.next().expect("parse the report") {
            None => { break; },
            Some(record) => match record {
                LCOVRecord::SourceFile(file_name) => println!("File: {}", file_name),
                LCOVRecord::EndOfRecord => println!("Finish"),
                _ => { continue; }
            }
        }
    }
}

レポートのマージ

さて本題のレポートマージです。
レポートをマージする際に、気をつけなければならいない点はマージするレポートのバージョンです。

マージしたいレポートが同じバージョンのソフトウェアのレポート場合、マージすることは可能ですが、違う場合は基本的にマージすることができません。

なぜマージできないかというと、どのバージョンの時、コードカバレッジはどの程度だったか?が知りたいわけで、異なるバージョンのレポートをマージすると、正しいレポートにならないためです。

結果
v1+ v1 v1
v1+ v2 マージできない(バージョンが違う)

この為、マージする際にレポートのチェックサムを利用します。
チェックサムは、DAレコードに情報として含まれています。(lcovのchecksumオプションを使用した場合)

このチェックサム値はソースコードの特定の行のMD5値になっており、同じバージョンのソフトウェアの場合、 値が同値になります。

下記の例だと、fixture.cの6行目のMD5値がPF4Rz2r7RTliO9u6bZ7h6gという意味になります。

SF:/Users/holyshared/Documents/projects/lcov-parser/tests/fixtures/merge/fixture.c
......
......
......
......
DA:6,2,PF4Rz2r7RTliO9u6bZ7h6g

このようなチェックを行いつつ、マージしてくれる関数も実装しています。
save_asを使用することで、出力ファイルを指定できます。

extern crate lcov_parser;

use lcov_parser:: { merge_files };

fn main() {
    let trace_files = [
        "../../../tests/fixtures/fixture1.info",
        "../../../tests/fixtures/fixture2.info"
    ];
    let _ = match merge_files(&trace_files) {
        Ok(report) => report.save_as("/tmp/merged_report.info"),
        Err(err) => panic!(err)
    };
}

まとめ

最初は、パーサーだけ実装する予定でしたが、lcovのPerlのコードを読んでいて、ついでだからマージもできるようにするかと思い実装してみました。 既存のプロダクトのコードの移植していく作業は、普段つかっているものの理解力があがるので、勉強になりますね。

Golangで五目並べ作った

そういえば、Golangでコード書いたことないや、と思って作ってみました。

Golangはコード読んだことあるけど、書いたことがなかったので、作り始める前に、 Golangチュートリアルを半分くらいやったり、出勤時間にパッケージのAPI調べてたしました。

https://github.com/holyshared/go-gobang

五目並べ

ゲーム仕様

できるだけ簡単な方が作りやすいと思ったので、五目並べの仕様は次の通りにしました。

  • ボードは25 * 25のサイズ
  • プレイヤーは白/黒の石を選べる
  • プレイヤーはNPCのプレイヤーと対戦する
  • プレイヤーが先行
  • 再対戦できる
  • フロント部分は頑張らない(Golangの学習が目的なので)

NPCのAI

NPCのAIを実装したのですが、「負けないようにするにはどうするか」という観点で実装してみました。 AIの思考パターンは次のようにしました。

  1. 自分の石が4つ揃っている場合、5つにする(絶対に勝つ為)
  2. 相手の石が4つ揃っていて、かつ隣接するマスの片方が空いてる場合、邪魔をする(絶対に揃えさせない)
  3. 相手の石が3つ揃っていて、かつ隣接するマスの両方が空いてる場合、邪魔をする(次の自分のターンで絶対に揃えさせない)
  4. 揃いそうなやつを増やす
    1. 自分の石が3つ揃っている場合、揃える為の残り2つのマスの空きからランダムに選ぶ
    2. 自分の石が2つ揃っている場合、揃える為の残り3つのマスの空きからランダムに選ぶ
    3. 自分の石が1つ揃っている場合、揃える為の残り4つのマスの空きからランダムに選ぶ
  5. どれもない場合、空いてるマスを選ぶ

これだけでもそこそこ強くなりました。

感想

設計どうするか、かなり悩みました。 マスが空いてるか、空いてないかの状態どうやって表現しようかとか(RustだったらOptionで表現するのになー)、 パッケージ分けようとして、パッケージ間でimportし合ってビルドがうまくいかなかったり....。

言語仕様がシンプルな分、ちょっと型の表現力が弱いイメージがやっぱりありますね。
そのかわり、学習コストが低いのは良かったです。

先週、みんなのGo言語買ったので、読んだ後に自分のコードもう一回読んでみます。

Hacklangのパッケージのテストコードを、型チェックかかるようにしたかったが断念した話

f:id:holyshared:20160305171858p:plain

タイトルの通りHacklang用のパッケージを開発していて、テストコードも型のチェックしたかったので、 できるか試してみました。

今、自分がパッケージ開発で使用しているテストツールは下記の通りです。

この構成だと、ソースはHackで書いて、テスト自体はPHPのエコシステムを使う感じになります。
これはこれでいいのですが、テストコードをpartialモードにする必要があり、テストコード内の型の矛盾などが、静的解析でチェックできない場合があります。

その場合、テストコードの型矛盾などの問題は、ランタイム時にようやく判明する可能性が高くなります。
極力そうゆうはことにはしたくないので、じゃあテストコードもstrictで書けばよくね?ってことになります。

Hacklangでテストコード書くには

基本的にテストコードをHackで書くには、次の方法があると思います。

  1. 既存のPHPのテスティングフレームワークを使用しつつ、テストコードをHackで書く
  2. Hacklang用のテスティングフレームワークを利用する

1の場合は既存のPHPテスティングフレームワークはそのままで、テストコードだけHackで書く方法です。
この場合、PHPのコードで、型の解決ができない部分がどうしてもでてきます。
じゃあその部分はどうするかというと、型の定義ファイルを作成して補います。

Hackのモードでdeclモードというものがあって、クラスや関数のシグニチャをファイルに書くと、型チェックの際に定義ファイルを参考に型のチェックを行ってくれます。
これでテストコードも型のチェックの恩恵を受けれるようになります。

2の場合は、そのままんまの意味で、HackUnitなどのHack用のテスティングフレームワークを利用します。

この場合、素直にHackでテストコード書けばいいので手間がかかならいです。

型の定義ファイル書くのだるいので、HackUnitで

自分は型の定義ファイル書きたくなかったので、HackUnitでテストコード書くことにしました。
機能が少なくて、学習コストはそんな高くなくさくっと導入できました。
ただ、型のチェックまだごまかしてるところが若干あります。

リリース後に気づいた問題

無事テストコードもHackで書けたのでリリースしまして、利用しているパッケージの方のバージョンあげようと思い、composer update実行し、型のチェックをかけたら、テストコードで型エラーがでるようになりました。

Unbound name: HackPack\HackUnit\Contract\Assert (an object type)

HackUnitのクラス名が解決できてないようで、なんでかなと思ったら、基本的に依存してるパッケージの開発用に使用するパッケージはインストールされないのを忘れていました

下のような場合、依存しているパッケージsome_packageのrequire-devで指定されているパッケージはインストールされないので、型のチェックが通りません。

.hhconfig
src
vendor
    /some_package < このパッケージのテストコードが型チェックに引っかかる。

なので、パッケージ開発している時はいいのですが、作ったパッケージを利用したさらに別のパッケージ作る場合は注意が必要です。 基本的に型をチェックをかけたくないコードのフィルタリングがhh_clientできないので、完全に詰んでる状態です。

flowtypeとかは、設定ファイルでその辺をコントロールできるのですが、何でhh_clientとはできないないのか謎ですね。

結果どうしたか

revertして、元に戻しました。
いまさら、revertして戻したくないなーと思ったのですが、いい解決方法がなかったので….
composerの設定でテストコードを除外して配布見たいなのができればいいのですが、できそうになかったし…

PHPのコードをHackのコードに変換する

PHPコードをHackのコードに置き換えるのは、最高に面倒くさいですよね。
なので、HHVMで提供されている、hackificatorを使用して、一気に変換できないかを検証してみました。

hackificatorPHPコードをHackに置き換える為のコマンドラインツールです。
hackificatorOCamlで実装されており、ASTベースでのコード変換を行いまます。(コード読む限り)

試した、HHVMのバージョンは3.12.0です。

置き換えのステップ

置き換えには2ステップ必要です。

  1. PHPのコードをHackのコードに置き換える
  2. Hackのコードをstrictモードにアップグレードする

PHPのコードをHackのコードに置き換える

下記のコマンドで、指定したディレクトリ内のPHPコードをHackのコードに置き換えます。

hackificator -thrift .

変換作業の仕様は試した感じでは下記のようでした。

  • ファイルのヘッダーを<?phpから<?hhに変える。
    • 変換した直後は、partialモードになる(ヘッダーにpartialコメントはつかない)
  • 変換時に型の矛盾などのエラーを検出した場合は変換しない。
  • 拡張子はPHPのまま。
  • タイプヒントの型の変換は行わない。
    • PHP7のreturn typeのselfをthisに置き換えたりしない。
  • declare(strict_types=1)があるファイルは変換できない。

Hackのコードをstrictモードにアップグレードする

下記のコマンドで、指定したディレクトリ内のHackコードをstrictモードにアップグレードします。

hackificator -upgrade .
  • アップグレードできるものは<?hh //strictにヘッダーを変える。
  • 型エラーが起きるものは変換しない(partialモード or declモードのまま)。
  • 拡張子はそのまま、.hhの場合は、.hhのまま、.phpの場合は.phpのまま。

まとめ

  • PHP7以降のコードベースでも、すんなりとはHackに移行できなさそう。
  • コードの変換はできるが、Hackらしいコードには変換できない。(当たり前だ)
  • 通常のPHPからHackへの移行は次のようになると思います。
    1. PHPからHackへコードを変える。
    2. 型の指定を変える、型の指定を追加する。
    3. 型のチェックを通す。
    4. strictモードへアップグレードする。(本来のHacklang)

Hacklang用にベンチマーク取れるライブラリを作った

f:id:holyshared:20160215130203p:plain

HacklangでのJITの検証とか、Vector、Setのパフォーマンスの計測がしたかったので作りました。
計測用のコードは下記のような感じになります。

namespace hhpack\performance\example;

require_once __DIR__ . '/../vendor/autoload.php';

use hhpack\performance as bench;

function main() : void
{

    bench\sync()->times(15)->run(() ==> {
        $stack = Vector {};

        for ($i = 0; $i < 200000; $i++) {
            $stack->add($i);
        }
    });

}
main();

hhpack\performance\sync関数はベンチマークオブジェクトを返します。
後は、runメソッドで計測したいコードをlamdaで指定します。

非同期処理は、hhpack\performance\syncの代わりにhhpack\performance\asyncを使用します。

namespace hhpack\performance\example;

require_once __DIR__ . '/../vendor/autoload.php';

use hhpack\performance as bench;

async function main() : Awaitable<void>
{
    await bench\async()->times(10)->run(async () ==> {
        // 非同期処理を書く
    });
}
\HH\Asio\join(main());

結果のレポートは標準出力に出ますが、Markdownでも出せるようにするつもりです。
作ってみて、JITが有効になってる時と、無効になってる時での処理速度の違いなど、いろいろ計測できて良いです。

github.com

typechecker-clientで型のカバレッジ取れるようにした

f:id:holyshared:20160114113035p:plain

型チェックの比率が知りたかったので、クライントのライブラリにAPIを新しく追加しました。 ファイル/ディレクトリ単位でデータを抜いて、整形することで見やすいフォーマットに整形して表示したり、 データを別の形式に変換したりすることができます。

簡単な使用方法

基本的に非同期な処理なので、awaitを使用して計算が終わるまで待つ必要があります。
具体的なコードは次のような感じになります。

async function coverage_select_main(string $cwd) : Awaitable<void>
{
    $client = new TypeCheckerClient($cwd);
    await $client->restart();

    $result = await $client->coverage();
    $files = $result->filter(($file) ==> $file instanceof File)
        ->filter(($file) ==> preg_match('/typechecker-client\/src/', $file->name()) === 1);

    echo 'Files:', PHP_EOL;
    foreach ($files as $file) {
        $formattedParsentage = sprintf('%6.2f%%', (float) $file->parsentage() * 100);
        echo $formattedParsentage, ' ', $file->name(), PHP_EOL;
    }
}
coverage_select_main(realpath(__DIR__ . '/../'));

どうやっているのか

HHVMにはhh_clientというコマンドラインプログラムがあります。
こいつを使用すると、型のチェックとカバレッジ計算ができます。

hh_clientには–coverageオプションがあり、実行時に指定するだけで型チェックの比率を確認できます。

hh_client check --coverage [対象ディレクトリ]

また、追加でjsonオプションを指定することで、チェック結果をjson形式で出力できます。

hh_client check --json --coverage [対象ディレクトリ]

typechecker-clientはこの出力されたjsonデータを使用しています。

リポジトリ

hhpackっていうorganizationで公開しているので、PRお待ちしております。

github.com

注意事項

HHVM 3.11.0で試す場合、hh_clientのバグで、プロセスが死なずに残る問題が発生しています。
これはもうすぐパッチがでるはずなので、しばらくまってください。

github.com

まあ、今月末くらいにLTSの3.12.0でるので、3.11.0のことなんてどうでもよくなると思います。

typesafetyっていうパッケージを作った

Screenshot

typesafetyというパッケージを作りました。
Hacklangでコードを書いていると頻繁に型のチェックをかけるのですが、チェックをするのにサーバーを再起動したりしないといけないのでとても面倒でした。
なので、サーバーの起動と型のチェックをまとめてできるようにしました。
特徴として、全てstrictモードで実装されているのと、UNSAFEコメントを使用したアンチパターンを使用していません。

使い方

composerを使用して、インストールします。

composer require hhpack/typesafety

後はコマンドを実行するだけで、型のチェックを行えます。

vendor/bin/typesafety [ROOT_DIRECTORY]

さらに、composer.jsonscriptsに実行できるようにすると楽になります。

{
    "scripts": {
        "check": "vendor/bin/typesafety"
    }
}

これで下記のように簡単に型チェックを行えるようになります。

composer check

CIでの型チェック

CIでテスト実行前に、型チェックをかけることにより、型安全な状態でテストを実行することができます。
型の補償がされていない状態でテストを実行しても、正常に終了する確率が低いので、事前に型のチェックを行うことでテスト時間を短縮できます。

travis-ciの場合は、yamlの設定ファイルに型のチェックを追加することで、実現できます。

script:
  - composer check
  - composer test

typesafetyをリリースするのに別途作ったパッケージ

  • hhpack/process - プロセスを扱うパッケージ、サブプロセスでコマンドを実行できたりする
  • hhpack/typechecker-client - 型チェッカーのクライアントライブラリ、サーバーの起動、停止などで使用する
  • hhpack/color - コンソールの出力をカラーリングするのに使用
  • hhpack/publisher - シンプルなPub/Subライブラリ、型のチェック結果を通知するのに使用