理系学生日記

おまえはいつまで学生気分なのか

Golangでファイル差分をわかりやすく表示する

ファイルを出力する機能のユニットテストを書く場合に、期待値としてのファイルと生成したファイルを比較し、差分があれば当該差分をきれいに表示したい場合があります。

これはgo-diffを利用することで可能になります。

例えば、以下のような形です。テストが落ちた時にどこがおかしいのか読みやすいですね。

上記を実現しているテストコードはこんな形です。

import (
    "testing"
    // snip

    "github.com/sergi/go-diff/diffmatchpatch"
)
(略)
    for _, f := range fileNames {
        expectedFilePath := filepath.Join(expectedDir, f)
        expected, err := ioutil.ReadFile(expectedFilePath)
        if err != nil {
            t.Errorf("cannot read expetec file [%s]: %s", expectedFilePath, err)
            continue
        }

        actualPath := filepath.Join(dir, f)
        actual, err := ioutil.ReadFile(actualPath)
        if err != nil {
            t.Errorf("cannot read generated file [%s]: %s", actualPath, err)
            continue
        }

        if string(expected) != string(actual) {
            dmp := diffmatchpatch.New()
            a, b, c := dmp.DiffLinesToChars(string(expected), string(actual))
            diffs := dmp.DiffMain(a, b, false)
            diffs = dmp.DiffCharsToLines(diffs, c)
            t.Errorf("%sに差分があります\n%s", f, dmp.DiffPrettyText(diffs))
        }
    }
(略)

go-diff

上記で使っているgo-diffは、Googleのdiff-match-patchのGo用のportです。

詳細な使い方については人生を豊かにする文字列diff入門 | フューチャー技術ブログがわかりやすいでしょう。文字列の差分を取り、それを表示するアルゴリズムは奥深く、様々な粒度での差分検知の仕組みが提供されています。

diff-match-patchの差分検出アルゴリズムは、デフォルトだと文字レベルの粒度のようなのですが、さすがにそれだと細かすぎます。そのため、冒頭に示したコードでは以下の順で行レベルの差分を組み立てていきます。

  1. 1行1 Unicodeになるように変換 (DiffLinesToChars)
  2. 差分を取る (DiffMain)
  3. 差分情報を行レベルに再構成 (DiffCharsToLines)

最後に、その差分をわかりやすく色付けして表示します (DiffPrettyText)

このあたりは、diff-match-patchのWikiの以下のページを参照ください。