gopwtのトレース

概要

power assertのgolang版のgopwtのトレース

github.com

どうやったか

使い方と全体ディレクトリ構造とコードをざっくり見た後に、インクリメンタルに動作させつつコードを書き写していった。

gopwtの使い方

main_test.go

func TestMain(m *testing.M) {
    flag.Parse()
    gopwt.Empower()
    os.Exit(m.Run())
}

func TestHoge(t *testing.T) {
    assert.OK(t, 2 == inc(2), "yeah!")
}

func inc(a int) int {
    return a + 1
}

上記を実行すると

--- FAIL: TestHoge (0.00s)
    assert.go:85: FAIL trace_gopwt_test.go:18
        assert.OK(t, 2 == inc(2), "yeah!")
                       |  |
                       |  3
                       false
        
        Assertion messages:
                - yeah!
        
        --- [int] inc(2)
        +++ [int] 2
        @@ -1,1 +1,1@@
        -3
        +2

のような形で出力される。

empowerをしないと下記のようになる

--- FAIL: TestHoge (0.00s)
--- FAIL: TestHoge (0.00s)
    assert.go:37: [FAIL Assertion] assert.OK(t, 2 == inc(2), "yeah!")
        
        Please call gopwt.Empower() in your TestMain(t * testing.M).It give you power.
        If you need more information, see http://github.com/ToQoz/gopwt
        
        AssertionMessage:
                - yeah!

どのように実現させているか?

empower内でやっていること

tmp dirにpackageを複製、調整し、 tmp dir上で exec.Command("go", "test") を実行している。 (package複製時に empower() は取り除いているので無限ループしない(取り除かないと無限ループするようなやり方で実現させているということ))

package複製時にやっていること

assert.OK を powered表示用のメソッド translatedassert.OK に書き換える。

その時に、translatedassertで使う色々な細かい情報を作っている。

translatedassert.OK のメソッドの定義

func OK(t *testing.T, e bool, messages []string, header, filename string, line int, origexpr string, termw int, expectedPos, gotPos int, pvPairs ...posValuePair)

実際に書き換えられたコード

translatedassert.OK(
    t, 
    2 == translatedassert.FRVInterface(translatedassert.MFCall("{path to tmp dir}}/trace_gopwt_test.go", 24, 912, translatedassert.RVOf(inc), translatedassert.RVOf(2))),
    []string{"yeah!"}, 
    `FAIL`,
    `trace_gopwt_test.go`, 
    24, 
    `assert.OK(t, 2 == inc(2), "yeah!")`, 
    236, 
    19, 
    14, 
    translatedassert.NewPosValuePair(14, 2, false, `2`), 
    translatedassert.NewPosValuePair(16, 2 == translatedassert.FRVInterface(translatedassert.MFCall("{path to tmp dir}}/trace_gopwt_test.go", 24, 912, translatedassert.RVOf(inc), translatedassert.RVOf(2))), true, `2 == inc(2)`),
    translatedassert.NewPosValuePair(19, translatedassert.FRVInterface(translatedassert.MFCall("{path to tmp dir}}/trace_gopwt_test.go", 24, 912, translatedassert.RVOf(inc), translatedassert.RVOf(2))), true, `inc(2)`), 
    translatedassert.NewPosValuePair(23, 2, false, `2`)
)

下記の4つがテスト用の式を分割した形で表示させるための大事な情報。 表示調整用のpositionのint値や、元の式のstring、式を評価した値などが作られている。

設計メモ

プロジェクトを分割すると、

  • テストをassert.OKの形式で書けるようにするところ/assert
  • power assert形式で、FAILした情報を見やすく表示してくれるところ /translatedassert
  • 元のテストファイルを解析し、必要な情報とともに assert.OKtranslatedassert.OK にpackageを書き換えるところ /translator

やってみてメモ

  • テストの出力部分をごにょごにょ調整しているだけだと思ったらとんでもなかった
  • goでgoのプログラムをメタ的に扱う機能が充実してそう

    • go fileをparseして一部書き換えるとかが割とカジュアルにできそう
  • ast nodeの再帰的な処理(子ノードや、ノードの種類に適応させた処理)が結構あって参考になりそう

  • typeのassertionやswitchでの処理がかなりあってgo的な書き方の参考になりそう
  • translatedassert向けのpkgはかなり大仕事でコード量が多いけど、 translator/internal 内に閉じ込めているので、プロジェクト全体の見通しが良い

    • /main.goからは translator.Translate()を呼び出して、作成したpkg上でexec.Command("go", "test") を実行するだけ
  • pkgを複製する処理の絡みでpathを色々調整する処理が結構複雑で理解が進みずらいところがあった

    • PackageContextにわかりやすい名前でメソッドを定義したら解りやすくなりそう?
  • PackageContextでのpkgを扱う処理を CopyPackage, ReadPackage, TypecheckPackage, RewritePackage に分割していて解りやすかった

    • まとめてやろうとしたらかなりカオスになってしまいそう
    • 複雑な大きな処理をどういった視点で分割するかの判断は難しそう
    • dependency pkgのcache周りの処理は理解しきれていない