type holyshared = Engineer<mixed>

技術的なことなど色々

ppx_inline_testでモジュール内にテストコードを書く

これはML Advent Calendar 2017の記事です。
枠が空いていて、誰も書いてなかったので書きました。

OCamlのテスト用のツールで、構文拡張を使用したppx_inline_testがあります。
今まで、OUnitを使っていたのですが試しに使用してみました。

使用するのに必要なライブラリは下記の通りです。

  • ppx_jane - Jane Streetが公開している構文拡張ライブラリ群
    • ほんとは必要ないパッケージがあるので、使いたくありませんでした(ビルドが面倒だったので使いました。)
  • jbuilder - Jane Streetが公開しているOCamlのビルドツール

サンプルのプロジェクトは下記のリポジトリに用意してあります。
ppx_inline_test_with_jbuilder

OPAM用のファイルを用意する

ルートディレクトリにライブラリ用のOPAMファイルを用意し、 build、build-testにビルド用のコマンドを追加します。

そのほかの項目はメタ情報を参照して、必要な物を追加してください。

opam-version: "1.2"
name: "example"
version: "0.1.0"
authors: ["YOUR NAME"]

build: [
  ["jbuilder" "build" "-p" name "-j" jobs]
]

build-test: [
  ["jbuilder" "runtest" "-p" name]
]

depends: [
  "jbuilder" {build}
]
available: [ ocaml-version >= "4.06.0" ]

テストコードを含んだライブラリの用意

ライブラリの用意

src/exampleディレクトリを作成して、example1.mlというファイル名でライブラリのコードをおきます。

ここでは、say関数を定義して、その関数のテストコードを用意してます。
このテストはわざと落ちるようにしています。

またtest_unitはテストがパスした場合は()、失敗した場合は例外(exn)を投げる必要があります。

let say () = "hello"

let%test_module _ = (module struct
  let%test_unit "say hello" =
    assert (say () = "world")
end)

ライブラリのビルド設定

ビルド用のファイルjbuildを用意します。

library_flags-linkallを指定します、これは指定しないと、テストモジュールがリンクされないです。

また、preprocessppx_jane, ppx_driver.runnerを指定して、テストコードの構文拡張を処理できるようにします。

(jbuild_version 1)

(library (
  (public_name example.example1)
  (name example_example1)
  (library_flags -linkall)
  (preprocess (pps (ppx_jane ppx_driver.runner)))
))

テストランナーの用意

テストランナーのコード追加

testディレクトを作成して、テストランナーのコードを用意します。
ファイル名はtest_runner.mlにします。

テストランナーのソースには下記のコードを追加します。

Ppx_inline_test_lib.Runtime.exit ()

テストのランナーのビルド設定

まずテスト対象のライブラリをまとめたライブラリをビルドできるようにします。
そして、executableでテストランナーの依存ライブラリに加えます。

その後に、aliasruntestを追加します。
test_runner.exeのサブコマンドでinline-test-runnerを指定し、次にテスト対象のライブラリ名を指定します。

(library (
  (name example_all)
  (libraries (example.example1))
  (library_flags -linkall)
  (preprocess (pps (ppx_jane ppx_driver.runner)))
))

(executable (
  (name test_runner)
  (libraries (ppx_inline_test.runner.lib example_all))
))

(alias
  ((name runtest)
  (deps (test_runner.exe))
  (action (run ${<} inline-test-runner example_example1))))

(jbuild_version 1)

実際にテスト実行する際に、実行されるコマンドは下記の通りです。
ライブラリ名は、パブリックな名前だと実行できませんでした。

_build/default/test/test_runner.exe inline-test-runner example_example1

テストの実行

各手順が終われば、下記のコマンドを実行するとテストが実行でき、テストが落ちるはずです。

jbuilder runtest

落ちたのが確認できた場合、今度はライブラリのコードを修正して、テストが通ることを確認してみてください。

感想

テストが実行できるようになるまで、結構時間がかかりました。
特に-linkallフラグを指定していなくて、テストが実行できなかったのにハマりました。

また、ppx_assertを使いたかったのですが、テストモジュールに別途必要なモジュールをopenしないとコンパイルが通らなくて、若干面倒でした。

モジュールにテストコードを直接かけるのはいいですが、OUnitの方がわかりやすいかなーといいう印象です。

Typesafetyの0.10.0をリリースした

Typesafetyの0.10.0をリリースし、下記の変更を加えました。

  1. OASISからjbuilderへの移行
  2. タイプチェックがパスした時も、レビューコメントを残せるようにした

OASISからjbuilderへの移行

ビルドツールをOASISからjbuilderへ変更しました。
移行作業自体は1日で終わり、案外楽に行えました。

手順は下記の通りです。

  1. OASISの設定を残しつつ、各ライブラリのビルド設定を行う。
  2. openしているモジュール名をリネームする。
  3. jbuilderで全てのライブラリのビルド、実行ファイルがコンパイルできることを確認する。
  4. テストファイルもライブラリと同じく、jbuilderコンパイル & 実行できるようにする。
  5. OASISで必要だったファイルを削除する。
  6. *.opamファイルを変更する。

移行後にテスト自体がこけるようになり、そこだけ時間がかかりました

テストコードはテスト用にレポートのフィクスチャを用意して、生成したレポート結果と比較するようなテストコードだったのですが、テストの実行ファイルが生成される場所が変わったため、フィクスチャファイルが読めなくなっていました。

テストの実行ファイルは_build/default/testsに生成され、フィクスチャファイルはtests/fixturesに置いていたので、相対パスで読めなくなったのが原因です。

解決方法としてはjbuildにフィクスチャファイルをコピーする設定を追加しました。
prognを追加して、ファイルをコピーする設定にしました。

パスは_build/default/testsからの相対パスで指定し、ディレクトリごと_build/default/testsにコピーします。

(alias (
  (name runtest)
  (deps (test.exe))
  (action (
    progn
      (run cp -R ../../../examples .)
      (run cp -R ../../../tests/fixtures .)
      (run ${<})
  ))
))

タイプチェックがパスした時も、レビューコメントを残せるように

今まではタイプエラーがある時のみ、結果をPRのコメントとして投稿していましたが、エラーがなくてもコメントを投稿するようにしました。

理由としては、タイプエラーがドッグフーディング中にほとんど発生しない為、正しく動いているのか判断できないことが多かったからです。

タイプエラーがない時にもコメントを残すことで、PR出した人もタイプエラーがないことをCIのログを見て確認する必要もないですし、正常に機能していることを確認することができます。

エラーがある時だけコメントを残したい場合は、--skip-passedを使用することで、スキップできます。

typesafety --review --skip-passed

移行後の感想

事前にjbuilderをある程度試していたので、思ったり簡単に移行することができました。
またタイプエラーがない時にも、コメントを残すようになったので、ほとんどマージなどもGitHubで完結できるようになったのは良かったです。

次は後もう少しでOCamlの4.06.0が出そうなので、そのチェックが必要です。

jbuilderでビルドに必要なファイルを生成する

jbuilderでビルドに必要なファイルを生成する方法を調べました。
なんで調べたかというと、atdgenjsonのパーサーのコードを出力したかったからです。

パッケージのインストール

jbuilderatdgenをインストールします。

opam install atdgen jbuilder

ruleをjbuildファイルに追加する

次のようなJSONスキーマファイルがある場合、jbuildファイルにruleを追加するだけでOKです。
スキーマファイルはuser.atdします。(拡張子はなんでもいいはず)

スキーマファイル

type user = {
  name: string;
  description: string;
}

jbuildファイル

targetsに生成されるファイルを指定して、depsに依存しているファイルを指定します。
また、actionで実行するコマンドを指定します。

パラメータ

パラメータ 説明
\${@} targetsで指定したファイルリスト
\${<} depsで指定したファイルリスト
\${bin:} プログラム名、PATHなどからプログラムを検索する

他にもあるので、variables-expansionを参照してください。

JSONパーサーの生成

JSONパーサーを生成するruleは下記の通りです。

(rule (
  (targets (user_j.ml user_j.mli))
  (deps (user.atd))
  (action (run ${bin:atdgen} -j ${<}))
))

実際に発行されるコマンドは下記の通りです。

atdgen -j user.atd 

型の定義ファイルの生成

型の定義ファイルの生成するruleは下記の通りです。

(rule (
  (targets (user_t.ml user_t.mli))
  (deps (user.atd))
  (action (run ${bin:atdgen} -t ${<}))
))

実際に発行されるコマンドは下記の通りです。

atdgen -t user.atd 

jbuildファイルのlibrariesを追加する

jbuildファイルのlibrary設定のlibrariesにatdgenを指定します。

(library (
  (public_name user)
  (name user)
  (libraries (atdgen))
))

また、user.mlファイルには生成されたコードを利用するようにしてみます。
User_tは生成される予定のモジュールです。

let make ~name ~description =
  User_t.({ name=name; description=description })

ビルドできるか試す

ビルドコマンドでビルドできるか試します。

jbuilder build

Unbound moduleなどのコンパイルエラーが出なければ問題ないはずです。
生成されたコードは_buildのサブディレクトリにオブジェクトファイルなどと一緒に出力されるようで、 コードの管理からは外れていて問題ないようです。

実際に試したコードは下記にあります。
example_jbuilder_with_atdgen

typesafety_cliのビルドツールの変更計画

CI上でテスト前の型チェックのエラーをGitHubのPRのコメントとして投稿するツールtypesafety-cliのビルドツールを変えようとしています。

具体的にはOASISからjbuilderに移行しようとしています。
開発当初はOASISで良かったのですが、以下の理由で辛く感じるようになってきました。

  1. 内部的なライブラリのビルド設定を1つのファイルに書く必要がある。
  2. 新しくライブラリを追加する際に_oasisを別途編集しなければならない。
  3. _oasisメタデータから、opam用のファイルをopam2oasisで生成するが面倒である。
  4. _oasisと.opamファイルの内容重複しているものが多い。

jbuilderはファイルをビルド単位で分割できる

下記の2つに関してjbuilderはファイルを分割できます。
またライブラリを分割したくなっても、ビルド用のファイルを追加するだけなので、変更の差分が基本的に追加だけになりそうである。

  1. 内部的なライブラリのビルド設定を1つのファイルに書く必要がある。
  2. 新しくライブラリを追加する際に_oasisを別途編集しなければならない。

.opamファイル一つでいい

OASISの場合、_oasisファイルと.opamファイルが必要になりますが、jbuilderの場合は.opamファイルだけで良いので、 リリース作業もOASISと比べて楽になりそうである。

自動化すればいんですが、あんまり自動化のための作業を増やしくたくないです。

  1. _oasisメタデータから、opam用のファイルをopam2oasisで生成するのが面倒である。
  2. _oasisと.opamファイルの内容重複しているものが多い。

OCamlのビルドツール

OCamlのビルドツールはjbuilderjengaocamlbuildOMakeOASISなどいろいろありますが、 これ使っておけばOK見たいなのがあるといいですね。

今だとOMakejbuilderになりそうですが。
OASIS使用しているプロジェクト減っている気がします。

Hackのコード自動生成できるようにした

毎回エディタからファイル作成したり、名前空間指定したりするのが面倒なので、コードを生成できるようにした。
コードの生成というか、ファイル生成に近いです。

github.com

使い方

名前空間ディレクトリのペアに対応させるジェネレータをマッピングする設定ファイルを書いて、 オートロードで読み込めるようにするだけで、あとはコマンド実行するだけでクラスファイルを出力してくれる。

設定ファイルを用意する

<?hh //strict

namespace MyPackage\Generators;

use HHPack\Codegen\Cli\{ GeneratorProvider };
use HHPack\Codegen\HackUnit\{ TestClassGenerator };
use HHPack\Codegen\Project\{ PackageClassGenerator };
use function HHPack\Codegen\Cli\{ namespace_of, library, library_test };

final class Generators implements GeneratorProvider {

  // Your package namespace
  const string PACKAGE_NAMESPACE = 'MyPackage';
  const string PACKAGE_TEST_NAMESPACE = 'MyPackage\Test';

  public function generators(): Iterator<Pair<GenerateType, ClassFileGenerator>> {
    // Link package namespace to generator
    yield library(
      namespace_of(static::PACKAGE_NAMESPACE, 'src')
        ->map(PackageClassGenerator::class));

    // Link package test namespace to generator
    yield library_test(
      namespace_of(static::PACKAGE_TEST_NAMESPACE, 'test')
        ->map(TestClassGenerator::class));
  }
}

hh_autoload.jsonに設定を追加

次にhh_autoload.jsonのdevRootsにパスを設定します。
下記の設定だと、configディレクトリが読みこみ対象になります。

{
  "roots": "src",
  "devRoots": "config"
}

hhvm_autoloadが必要なので、インストールしてください。

github.com

これだけでコードが生成できるようになります。

コードの生成

パッケージのクラスを生成する

生成するタイプとクラス名を指定すると、設定ファイルで指定したディレクトリにファイルを生成できます。
ファイルにはあらかじめ名前空間が設定されるので、指定する手間が省けます。

クラスファイル

libを指定すると、クラスファイルを生成します。

vendor/bin/codegen lib Foo\LibClass

テストクラスファイル

testを指定すると、テスト用のクラスファイルを生成します。

vendor/bin/codegen test Foo\LibClassTest

独自ジェネレータ

ClassFileGeneratableインターフェースを実装すれば独自のジェネレータを定義できます。
下記は、プレーンなクラスファイルを生成するジェネレータです。

どういうファイルが生成可能かはhack-codegenを参考にしてください。
shapeを定義したり、trait定義できたり、結構いろんなものが生成できます。

github.com

<?hh //strict

namespace MyPackage\Generators;

use HHPack\Codegen\{GenerateClass, ClassFileGeneratable};
use Facebook\HackCodegen\{ICodegenFactory, CodegenFile, CodegenClass};

final class CustomGenerator implements ClassFileGeneratable {

  public function __construct(private ICodegenFactory $cg) {}

  public static function from(ICodegenFactory $factory): this {
    return new self($factory);
  }

  public function generate(GenerateClass $class): CodegenFile {
    // ファイルを生成するコードを書く
    return $this->cg
      ->codegenFile($class->fileName())
      ->setIsStrict(true)
      ->setNamespace($class->belongsNamespace());
  }

  private function classOf(string $className): CodegenClass {
    return $this->cg->codegenClass($className)->setIsFinal(true);
  }
}

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回以上読んだ方がいい内容です。