type holyshared = Engineer<mixed>

PHP、Hack、Ruby、Javascript周りの技術ブログ

HackでDBのマイグレーションツールを作った

HackでDBのマイグレーションツールを作ってみた。
データベースの作成、削除、スキーマ変更の適用、適用の取り消しが一通りできます。

現在のバージョンでサポートしているRDBMSMySQLのみです。
また、マイグレーションSQLベースです、DSLはサポートしていません。

github.com

使い方

まずはじめに、JSON形式の設定ファイルをルートディレクトリのconfigの下に、database.jsonという名前でおきます。 開発環境、ステージング環境とかは自分で追加してください。

{
  "type": "sql",
  "path": "db/migrate",
  "enviroments": {
    "development": {
      "host": "localhost",
      "port": 3306,
      "name": "migrate",
      "user": { "ENV": "DB_USERNAME" },
      "password": { "ENV": "DB_PASSWORD" }
    }
  }
}

データベースの作成/削除する

create/dropコマンドで、データベースの作成、削除ができます。

データベースの作成

vendor/bin/migrate create

データベースの削除

vendor/bin/migrate drop

マイグレーションを適用する

genコマンドでマイグレーションファイルを作成して、適用する変更をSQLで書いたら、upコマンドで適用できます。 –toオプションで指定したところまで、変更を適用できます。

マイグレーションファイルの生成

vendor/bin/migrate gen create-users

マイグレーションの適用

vendor/bin/migrate up

or

vendor/bin/migrate up --to=20150824010439-create-users

マイグレーションの変更を戻す

downコマンドで適用したマイグレーションを適用前までに戻すことができます。 resetコマンドはすべての変更を元に戻します。

resetコマンドですべて戻らない場合、SQLファイルに漏れがないかを確認してください。

vendor/bin/migrate down 20150824010439-create-users
vendor/bin/migrate reset

今後について

–dry-runオプションを追加して、dry runできるようにしたり、DB作成時にCHARACTER SET、COLLATEを指定できるようにする予定です。
PostgreSQLのサポートは設計プランがまだできてないので、後回しになると思います。

プロフェッショナルSSL/TLSを読み終わった

ラムダノートさんから出ている、プロフェッショナルSSL/TLSを読み終わりました。 SSL/TLSを理解するのに、基本的に必要な情報が網羅されていて、とてもいい本だと思います。

プロフェッショナルSSL/TLS(紙書籍+電子書籍)www.lambdanote.com

よかった点

  1. 鍵交換、暗号化アルゴリズムについての解説があった。

    一度調べたものの、あまり自信がなかったので、再度理解するのに役に立ちました。
    これはXXXだから、鍵交換アルゴリズムはこれで、暗号化方式はこれのはず、ぐらいは記憶に定着した感があります。

    ある程度知っておかないと、CipherSuiteの設定確認する時、大変だと思います。

  2. MITM(中間者攻撃)の理解が進んだ、また実際にあった攻撃の事例が載っていた。

    Lucky13、POODLEなどの攻撃方法、受動的か能動的かで攻撃方法が異なるなど。
    これも、どの時期にこういう攻撃があったなどの事実関係で載っているので、歴史を学べてよかったです。

  3. CA(認証局)の事例が載っていた。

    CAがドメインの証明書を発行する際に、ドメインのチェックをこなっておらず、不正な証明書が取得できていた事例が載っており、発覚してからCAがどういう末路を辿るのかがよく理解できました。

    また、CAの調査も必要に感じました、普段あんまり気にしてなかったので、過去問題が起きてないかとかそういう調査するのはありだと思います。

  4. 公開鍵ピンニング(HPKP)、HSTSなどの防御方法

    攻撃から守る、または攻撃を受けにくくするにはどうするかも載っていました。

    特にHSTSあたりは、やりやすいので、HTTPS対応しないとけない人は、参考にするといいのではと思いました。
    max-ageを小さめにして、徐々に長くしていくと、リカバリしやすいなど、適用する際の参考手順が載っています。

まとめ

価格はちょっと高いですが、内容的には満足でした。
紙の方で読んで、会社の本棚に置き、電子版で読み直すとかががいいと思います。 3回以上読んだ方がいい内容です。

Hackで書いた自作のオプションパーサーをリファクタリングした

結構昔にHackでオプションパーサー書いていたのですが、しばらく面倒でメンテナンスを放置していました。 それをHHVM-3.21.0がリリースされてたタイミングで、メンテナンスしている他のパッケージと一緒にリファクタリングしました。

使いかたはこんな感じで、shortオプション、longオプション、オプションの説明、コールバック用のlambdaを指定していきます。 あとは使い方の設定を指定するして、parseメソッドに引数を指定するだけです。

<?hh //partial

use HHPack\Getopt as cli;
use HHPack\Getopt\App\{ ArgumentParser };

function main() : void {
    $help = false;
    $version = false;
    $fileName = 'test';

    $argParser = cli\app('example', '1.0.0')
        ->description("This cli application is example.\n\n")
        ->usage("  {app.name} [OPTIONS]\n\n")
        ->options([
            cli\on(['-h', '--help'], 'display help message', () ==> {
                $help = true;
            }),
            cli\on(['-v', '--version'], 'display version', () ==> {
                $version = true;
            }),
            cli\take_on(['-n', '--name'], 'file name', ($name) ==> {
                $fileName = $name;
            })
        ]);

    $args = $argParser->parse($argv);

    if ($help) {
        echo 'help on', PHP_EOL;
    }

    if ($version) {
        echo 'version on', PHP_EOL;
    }

    if ($fileName !== 'test') {
        echo 'name = ', $fileName, PHP_EOL;
    }
}
main();

CLIの場合、エントリポイントのソースはpartialモードを指定しないといけないので、(strictの場合はTOPレベルに実行可能なコードを書けない為)適当な関数にラップして、実行するのがいいと思います。

そうすると、型のチェックもやってくれるようになります。(ドキュメントに書いてある)

自分が真面目に書く場合は、strictモードに寄せたいので、アプリケーションのクラス書いて、runメソッドを実行みたいな薄い実装にすると思います。

https://github.com/hhpack/getopt/blob/master/example/app.hh

HHVM-3.21.0で色々チェックしてたけど、タイプチェッカーの性能が上がってて、古いバージョンでエラー出てなかった奴が出るようになったので、やっぱりバージョン上がるたびにチェックした方が良さそうです。

Hackの型のチェックのエラー内容をレビューコメントとして投稿できるようにした

Hackの型チェックをCI上で行っていたのですが、エラー内容を見るのにCI環境側までいちいち見に行かないといけないので、OCamlでレビューコメントとして投稿できるtypesafety-cliを作りました。

レビューコメントは下記のような感じになります。
https://github.com/hhpack/hackunit-docker-example/pull/3

このプログラムをHHVM + Composerと一緒にパッケージしたDockerイメージを作って、TravisCIでテストの自動化を行っています。(メンテナンスしているパッケージはだいたい置き換え済み)

Docker化したことにより、HHVMのリリースにもすぐに追従できるようになりました。

作った理由

もともと下記のものはHackで作っていました。

  • typechecker-client - hh_clientを扱えるクライアントパッケージ
  • typesafety - エラーメッセージをわかりやすい形に整形して出力するCLIパッケージ

これを機能拡張すればいいのですが、Hackで実装している為、HHVMのバージョンが上がるたびに「これ今動くんかな?」と気にする必要があるので、しばらくメンテナンス放置してると動かなくなる可能性があります。

メンテナとしては、ある程度放置していても動いてくれる方が楽なので、今回の場合はHackをやめて別実装にしました。

OCamlにした理由

最近、趣味でOCamlでコードを書いているのですが、書いていて気持ちが楽なのでOCamlにしています。
基本的にツール周りのコードは年々「雑に書きたい、しかし雑に書いてもそこそこ動いて欲しい」、と思うようになっており、OCamlはその辺があうと思っています。

OOPな言語で書くことが多くて、設計はすぐに思いつくのですが、Rubyですらも書くのはだるいと思ってしまうことが最近多いので、特にクラスの設計とか気にせず、コードを書き始められるのがOCamlのいいところですね。

言語ファイルをチェックできるgemを作った

Screen Shot

言語ファイルをチェックできるgem、i18n_checkerを作りました。

多言語対応をしていると、typoで翻訳されていなかったり、言語ファイルに追加漏れがあったりするので、簡単なチェックができるツールが欲しかったのが作った理由です。

サポートするファイル形式

このgemは下記の形式のファイルをソースとして、翻訳テキストが存在しているかチェックします。
テキストの抜き出しは、ASTを使用してノードから抽出しています。

使い方

使い方はRakefileにタスクを追加するだけです。
タスクのオプションは下記のオプションが使用できます。

オプション名 説明
source_paths ソースファイルのリスト
locale_file_paths 翻訳ファイルのリスト
reporter 検出結果を出力するレポーター
require 'i18n_checker/rake_task'

I18nChecker::RakeTask.new do |task|
  # haml templates, ruby sources
  task.source_paths = FileList['app/models/*', 'app/views/*']

  # locale file paths
  task.locale_file_paths = FileList['config/locales/*']
end

後は、locale_checkタスクを実行するだけで、翻訳テキストが見つからない参照を検出できます。

bundle exec rake locale_check

今後について

今回は翻訳テキストが参照しかチェックしてませんが、使用してない翻訳テキストも調べられるようにするつもりです。

今仕事でやっている案件が実証実験を結構やっていて、実験が終わった後のコードとか、設定が残っていて不要なものをどんどん削除していきたいのでツールで解決できるものは解決していきたです。

OUnitでOCamlライブラリのテストを書く

これはML Advent Calendar 2016の18日目の記事です。

OCamlのテストフレームワークOUnitについて解説します。
また、Oasisの簡単な説明も行います。

プロジェクトのセットアップ

OCamlのプロジェクトでは、だいたいの場合はOasisを使います。
Oasisはビルドの為の支援ツールで、ビルドに必要なMakefileやメタ情報を含んだファイルを設定から自動生成できます。

OasisOpamを使用して簡単にインストールできます。

opam install oasis

インストールが完了したら、設定ファイル_oasisを作成し、必要最低限の情報を記述します。

OASISFormat: 0.4
Name:        example                                        # プロジェクト名
Version:     0.1.0                                          # バージョン
Synopsis:    example                                        # ライブラリの説明
Authors:     Noritaka Horio <holy.shared.design@gmail.com> # 著作者
License:     MIT                                            # ライセンス
Plugins:     META (0.4), StdFiles (0.4), DevFiles (0.4)     # 使用するプラグイン
BuildTools: ocamlbuild                                      # ビルドツール

# ライブラリ名
Library example
  # ソースの場所
  Path:       src
  # ビルドするモジュール
  Modules: Example

もしくは、quickstartコマンドを使用します。
聞かれる質問に答えていくだけで、_oasisファイルを生成できます。

oasis quickstart

_oasisファイルが作成できたら、setupコマンドを実行します。

oasis setup

setupコマンドを実行すると、MakefileやAUTHORS.txt、README.mdなどのファイルが生成されているはずです。

Oasisプラグインで機能を追加できますが、試す分には下記の3つぐらいで十分です。

プラグイン 説明
META ライブラリ用のMETAファイルを生成する
StdFiles _oasisの定義から、README.txt, INSTALL.txt, AUTHORS.txtを生成する
DevFiles ビルドに必要な、Makefileを生成する(なくてもビルドはできます)

ライブラリの実装とテストコード

_oasisの設定で、ビルドするライブラリの指定をします。
ライブラリ名とソースの場所、そしてビルドするモジュールをカンマ区切りで列挙します。

# ライブラリ名
Library example
  # ソースの場所
  Path:       src
  # ビルドするモジュール
  Modules: Example

srcディレクトリを作成して、example.mlファイルを追加して、適当な関数を追加します。
ここでは、文字列を受け取って、文字列をそのまま返す関数を定義します。(解説の為の雑な関数です。)

let say msg = msg

次にこのモジュールのテストコードを書きます。
testsディレクトリを作成して、エントリポイントとなるtest.mlと、モジュール単体のテストコードexample_test.mlを作成します。

テストコードは、OUnitの独自演算子の>::、>:::を使用して記述します。(>:もあるが使うことはないと思う)

example_test.ml

Exampleモジュールのsay関数のテストは、受け取った引数をそのまま返すだけなのでこんな感じになります。

open OUnit2

(* say関数のテスト関数、テストの説明と関数でテストを表現する *)
let say_test =
  "echo string" >:: (fun _ -> assert_equal (Example.say "hello") "hello")

(* このテストモジュールのすべてのテストをまとめる *)
let tests =
  "all_tests" >::: [ say_test; ]

test.ml

エントリポイントではrun_test_tt_main関数でテストを実行するようにします。
ここで各テストジュールをまとめて、指定します。

open OUnit2

let all_tests = "all_tests" >::: [
  Example_test.tests
]

let () =
  run_test_tt_main all_tests;;

演算子の意味

>::

文字列とテスト関数より、新しいテストを返します。

let some_func1 = true

"test desc" >:: (fun ctx -> assert_bool "failed message" (some_func1 ()))

>:::

文字列とテストのリストより、新しいテストを返します。
複数のテストをグルーピングする為に使用します。

let some_func1 = true
let some_func2 = false

"all_tests" >::: [
    "test desc" >:: (fun ctx -> assert_bool "failed message" (some_func1 ()));
    "test desc" >:: (fun ctx -> assert_bool "failed message" (some_func2 ()))
]

テストコードのビルドとテストの実行

テストコードが追加できたら、_oasisファイルにテスト用の設定を追加します。

# テスト用のバイナリの名前
Executable test
 # テストコードの場所
  Path: tests
  # テストコードのエントリポイント
  MainIs: test.ml
  Build$: flag(tests)
  # ビルド結果のフォーマット(バイトコードにもできる)
  CompiledObject: native
  # 配布する必要がないので、false
  Install: false
  # 依存しているモジュールoUnitとテスト対象のライブラリを指定
  BuildDepends: oUnit, example

Test test
  Run$:               flag(tests)
  # テストコマンド
  Command:            $test
  # テストを実行するワーキングディレクトリ
  WorkingDirectory:   tests

テストを実行するには、configureでテスト用に設定を変えた後に、testでテストを実行できます。
これはテストコードを含んだ実行可能なバイナリを生成して、実行しています。

make configure CONFIGUREFLAGS=--enable-tests
make test

まとめ

ここで使用したファイルとかは、ここに適当に書いたOCamlのコードの中にあるので、実物を試したい人はご確認ください。 github.com

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のコードを読んでいて、ついでだからマージもできるようにするかと思い実装してみました。 既存のプロダクトのコードの移植していく作業は、普段つかっているものの理解力があがるので、勉強になりますね。