Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 18235] ファイルの概要

このコミットは、Go言語のリンカ (cmd/link) におけるテストデータの管理方法に関する重要な変更を導入しています。具体的には、テストで使用される「ゴールデンバイナリファイル」を、バイナリ形式からテキスト形式の「ヘックスダンプ」に置き換えるものです。これにより、バージョン管理システム上での差分(diff)がより人間にとって読みやすくなり、過去に発生したバイナリファイルの差分に関する問題(Debianバグ #716853)の再発を防ぐことを目的としています。

コミット

commit 9c1aa658bfb73018805fde0c6224a289aced0d03
Author: Russ Cox <rsc@golang.org>
Date:   Mon Jan 13 23:07:40 2014 -0500

    cmd/link: replace golden binary files with hex dumps
    
    The hex dumps will diff better, and I hope they will avoid
    a repeat of http://bugs.debian.org/716853.
    
    The CL will probably show the testdata diffs as "binary",
    but in fact the binary versions are being replaced by
    textual hex dumps (output of hexdump -C).
    
    R=iant
    CC=golang-codereviews
    https://golang.org/cl/51000044

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/9c1aa658bfb73018805fde0c6224a289aced0d03

元コミット内容

cmd/link: replace golden binary files with hex dumps

このコミットは、ゴールデンバイナリファイルをヘックスダンプに置き換えるものです。ヘックスダンプは差分がより見やすくなり、http://bugs.debian.org/716853 のような問題の再発を防ぐことが期待されます。この変更は、テストデータの差分が「バイナリ」として表示されるかもしれませんが、実際にはバイナリバージョンがテキスト形式のヘックスダンプ(hexdump -C の出力)に置き換えられています。

変更の背景

この変更の主な背景には、バージョン管理システム(特にGit)におけるバイナリファイルの扱いの難しさがあります。バイナリファイルは、内容が少しでも変更されるとファイル全体が異なるものとして認識され、差分ツールで具体的な変更箇所を視覚的に確認することが非常に困難です。これは、コードレビューの際に、テストデータの意図しない変更や、予期せぬ副作用を見落とすリスクを高めます。

コミットメッセージで具体的に言及されている http://bugs.debian.org/716853 は、Goコンパイラとリンカのテストスイートに関連するDebianのバグ報告です。このバグは、Goのテストスイートが特定のバイナリファイル(おそらくリンカの出力結果)を「ゴールデンファイル」として使用しており、これらのファイルがバージョン管理システム上で適切に扱われていなかったことに起因する可能性があります。バイナリファイルが変更されるたびに、Gitはファイル全体が変更されたと認識し、差分を効果的に表示できません。これにより、テストデータの変更がレビューアにとって不透明になり、問題の特定やデバッグが困難になるという課題がありました。

この問題を解決するため、Goチームはバイナリファイルを直接バージョン管理するのではなく、その内容をテキスト形式のヘックスダンプとして保存するというアプローチを採用しました。ヘックスダンプは、バイナリデータを16進数表記のテキストに変換したものであり、テキストファイルであるため、Gitなどの差分ツールで変更箇所を正確に追跡できるようになります。これにより、テストデータの変更がより透過的になり、コードレビューの品質向上とデバッグの効率化が図られます。

前提知識の解説

  • ゴールデンファイルテスト (Golden File Testing): ソフトウェアテストの手法の一つで、プログラムの出力(ファイル、データ構造、画像など)を事前に「正しい」とされる参照ファイル(ゴールデンファイル)と比較することで、プログラムの動作が期待通りであることを検証します。リンカのテストにおいては、特定のソースコードをコンパイル・リンクして生成された実行可能バイナリが、事前に生成された正しいバイナリ(ゴールデンバイナリ)と一致するかどうかを検証するのに使われます。

  • ヘックスダンプ (Hex Dump): バイナリファイルの内容を16進数(Hexadecimal)形式で表示したものです。通常、各バイトが2桁の16進数で表現され、行の先頭にはオフセット(アドレス)、行の末尾には対応するASCII文字(表示可能な場合)が併記されます。hexdump -C コマンドは、この形式で出力する一般的なUnixコマンドです。 例:

    00000000  48 65 6c 6c 6f 2c 20 57  6f 72 6c 64 21 0a        |Hello, World!.|
    

    ヘックスダンプはテキスト形式であるため、バージョン管理システムで差分を比較する際に、変更されたバイト列がどの位置でどのように変化したかを視覚的に確認しやすくなります。

  • Gitの差分 (Git Diff): Gitは、ファイルの変更履歴を追跡するためのバージョン管理システムです。git diff コマンドは、異なるバージョン間のファイルの変更点(差分)を表示します。テキストファイルの場合、変更された行や文字が明確に表示されますが、バイナリファイルの場合、Gitは通常「Binary files ... differ」と表示するだけで、具体的な変更内容は示しません。これが、本コミットが解決しようとしている問題点です。

  • Go言語のリンカ (cmd/link): Go言語のツールチェインの一部であり、コンパイルされたオブジェクトファイルやライブラリを結合して、実行可能なバイナリファイルを生成する役割を担います。リンカは、プログラムの実行に必要なすべてのコードとデータを配置し、アドレスを解決し、最終的な実行可能形式を作成します。リンカのテストは、生成されるバイナリが特定のプラットフォームやアーキテクチャで正しく動作することを確認するために重要です。

技術的詳細

このコミットの核心は、バイナリデータをテキスト形式のヘックスダンプに変換し、それをパースして元のバイナリデータに戻すためのユーティリティ関数を導入した点にあります。これにより、リンカのテストにおいて、生成されたバイナリとゴールデンファイルを比較する際に、ゴールデンファイルをヘックスダンプ形式で保存できるようになります。

具体的には、以下の新しい関数が src/cmd/link/hex_test.go に追加されました。

  1. parseHexdump(text string) ([]byte, error): この関数は、hexdump -C または Plan 9 の xd -b コマンドの出力形式であるヘックスダンプのテキストを受け取り、元のバイナリデータ([]byte)にパースして返します。

    • 各行を読み込み、| 以降のテキストダンプ部分を削除します。
    • 行をスペースで分割し、16進数表記のバイト値を抽出します。
    • 行の先頭にあるアドレス(オフセット)を解析し、必要に応じて出力バイトスライスを拡張します。
    • 抽出した16進数バイト値を byte 型に変換し、出力スライスに追加します。
    • * で示される「all zeros block omitted」(すべてゼロのブロックが省略されている)行を適切に処理し、元のデータが復元されるようにします。
    • パース中にエラーが発生した場合(不正なフォーマットなど)はエラーを返します。
  2. mustParseHexdumpFile(t *testing.T, file string) []byte: parseHexdump のラッパー関数で、テストヘルパーとして機能します。指定されたファイルからヘックスダンプを読み込み、parseHexdump を呼び出してデータをパースします。エラーが発生した場合は、t.Fatal を呼び出してテストを失敗させます。

  3. hexdump(data []byte) string: この関数は、与えられたバイナリデータ []byteencoding/hex.Dump を使用してヘックスダンプ形式の文字列に変換します。さらに、連続するゼロバイトのブロックを * で省略する処理(regexp.MustCompile を使用)を行い、hexdump -C の出力に近い形式にします。これにより、生成されるヘックスダンプが冗長にならず、可読性が向上します。

これらの関数が導入されたことで、src/cmd/link/prog_test.gocheckGolden 関数が変更されました。

  • 以前は ioutil.ReadFile(name) を使ってゴールデンバイナリファイルを直接読み込んでいましたが、この変更により mustParseHexdumpFile(t, name) を使用してヘックスダンプファイルからバイナリデータを復元するようになりました。
  • また、saveMismatch 定数が追加され、true の場合、テストで不一致が検出された際に、元のデータとヘックスダンプ形式のデータを一時ファイル(.raw.hex 拡張子)として保存する機能が追加されました。これはデバッグを容易にするためのものです。

このアプローチにより、リンカのテストデータ(src/cmd/link/testdata/ 以下のバイナリファイル)は、実際のバイナリファイルではなく、そのヘックスダンプ表現としてGitリポジトリに保存されることになります。これにより、これらのファイルが変更された場合でも、Gitの差分ツールで変更内容をテキストとして確認できるようになり、コードレビューの効率と正確性が向上します。

コアとなるコードの変更箇所

src/cmd/link/hex_test.go (新規ファイル)

// Copyright 2014 The Go Authors.  All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
	"encoding/hex"
	"fmt"
	"io/ioutil"
	"regexp"
	"strconv"
	"strings"
	"testing"
)

// mustParseHexdumpFile returns a block of data generated by
// parsing the hex dump in the named file.
// If the file cannot be read or does not contain a valid hex dump,
// mustParseHexdumpFile calls t.Fatal.
func mustParseHexdumpFile(t *testing.T, file string) []byte {
	hex, err := ioutil.ReadFile(file)
	if err != nil {
		t.Fatal(err)
	}
	data, err := parseHexdump(string(hex))
	if err != nil {
		t.Fatal(err)
	}
	return data
}

// parseHexdump parses the hex dump in text, which should be the
// output of "hexdump -C" or Plan 9's "xd -b",
// and returns the original data used to produce the dump.
// It is meant to enable storing golden binary files as text, so that
// changes to the golden files can be seen during code reviews.
func parseHexdump(text string) ([]byte, error) {
	var out []byte
	for _, line := range strings.Split(text, "\n") {
		if i := strings.Index(line, "|"); i >= 0 { // remove text dump
			line = line[:i]
		}
		f := strings.Fields(line)
		if len(f) > 1+16 {
			return nil, fmt.Errorf("parsing hex dump: too many fields on line %q", line)
		}
		if len(f) == 0 || len(f) == 1 && f[0] == "*" { // all zeros block omitted
			continue
		}
		addr64, err := strconv.ParseUint(f[0], 16, 0)
		if err != nil {
			return nil, fmt.Errorf("parsing hex dump: invalid address %q", f[0])
		}
		addr := int(addr64)
		if len(out) < addr {
			out = append(out, make([]byte, addr-len(out))...)
		}
		for _, x := range f[1:] {
			val, err := strconv.ParseUint(x, 16, 8)
			if err != nil {
				return nil, fmt.Errorf("parsing hexdump: invalid hex byte %q", x)
			}
			out = append(out, byte(val))
		}
	}
	return out, nil
}

func hexdump(data []byte) string {
	text := hex.Dump(data) + fmt.Sprintf("%08x\n", len(data))
	text = regexp.MustCompile(`\n([0-9a-f]+(\s+00){16}.*?\n)+`).ReplaceAllString(text, "\n*\n")
	return text
}

src/cmd/link/prog_test.go (変更箇所)

--- a/src/cmd/link/prog_test.go
+++ b/src/cmd/link/prog_test.go
@@ -136,15 +136,17 @@ func cloneSection(sect *Section) *Section {
 	return t
 }
 
+const saveMismatch = true
+
 // checkGolden checks that data matches the named file.
 // If not, it reports the error to the test.
 func checkGolden(t *testing.T, data []byte, name string) {
-	golden, err := ioutil.ReadFile(name)
-	if err != nil {
-		t.Errorf("%s: %v", name, err)
-		return
-	}
+	golden := mustParseHexdumpFile(t, name)
 	if !bytes.Equal(data, golden) {
+		if saveMismatch {
+			ioutil.WriteFile(name+".raw", data, 0666)
+			ioutil.WriteFile(name+".hex", []byte(hexdump(data)), 0666)
+		}
 		// TODO(rsc): A better diff would be nice, as needed.
 		i := 0
 		for i < len(data) && i < len(golden) && data[i] == golden[i] {

src/cmd/link/testdata/ 以下のバイナリファイル

以下のファイルがバイナリからヘックスダンプ形式に変換されました。

  • src/cmd/link/testdata/link.hello.darwin.amd64
  • src/cmd/link/testdata/macho.amd64.exit9
  • src/cmd/link/testdata/macho.amd64.hello
  • src/cmd/link/testdata/macho.amd64.helloro

これらのファイルは、Gitの差分では「Binary files ... differ」と表示されますが、実際にはテキスト形式のヘックスダンプに置き換えられています。

コアとなるコードの解説

src/cmd/link/hex_test.go

このファイルは、バイナリデータとヘックスダンプ形式の相互変換を行うためのユーティリティ関数を提供します。

  • mustParseHexdumpFile(t *testing.T, file string) []byte: テストコードから呼び出されることを想定したヘルパー関数です。指定されたファイルパスからヘックスダンプのテキストを読み込み、それを parseHexdump 関数に渡してバイナリデータに変換します。ファイル読み込みやパース中にエラーが発生した場合は、t.Fatal を呼び出してテストを即座に終了させます。これにより、テストコードがエラーハンドリングのロジックを重複して記述する必要がなくなります。

  • parseHexdump(text string) ([]byte, error): この関数は、hexdump -C コマンドの出力のような、標準的なヘックスダンプ形式のテキストを解析し、元のバイナリデータ []byte を復元します。

    • strings.Split(text, "\n") でテキストを行ごとに分割します。
    • if i := strings.Index(line, "|"); i >= 0 { line = line[:i] } の部分で、ヘックスダンプの右側にあるASCII文字の表示部分(| で区切られている)を削除し、純粋な16進数データ部分のみを処理対象とします。
    • strings.Fields(line) で行を単語(16進数バイト値)に分割します。
    • if len(f) == 0 || len(f) == 1 && f[0] == "*" は、hexdump が連続するゼロバイトのブロックを * で省略する際の処理です。この行をスキップすることで、元のデータが正しく復元されます。
    • strconv.ParseUint(f[0], 16, 0) で行の先頭にあるアドレス(オフセット)をパースします。
    • if len(out) < addr { out = append(out, make([]byte, addr-len(out))...) } は、アドレスに基づいて出力バイトスライス out のサイズを調整し、必要に応じてゼロで埋めます。これにより、データが正しいオフセットに配置されます。
    • for _, x := range f[1:] ループで、各16進数バイト値(x)を strconv.ParseUint(x, 16, 8) でパースし、byte(val) でバイト型に変換して out スライスに追加します。
  • hexdump(data []byte) string: この関数は、バイナリデータ []byte を受け取り、それをヘックスダンプ形式の文字列に変換します。

    • hex.Dump(data) は、Go標準ライブラリの encoding/hex パッケージが提供する関数で、バイナリデータを標準的なヘックスダンプ形式の文字列に変換します。
    • fmt.Sprintf("%08x\n", len(data)) は、データの全長を16進数で表示する行を追加します。これは hexdump -C の出力の最後に現れる形式に似ています。
    • regexp.MustCompile(...).ReplaceAllString(text, "\n*\n") の部分は、連続するゼロバイトの行を * に置き換える正規表現処理です。これにより、生成されるヘックスダンプがコンパクトになり、可読性が向上します。この正規表現は、アドレスと16個のゼロバイトが続く行のパターンを検出します。

src/cmd/link/prog_test.go

このファイルは、リンカのテストスイートの一部であり、生成されたバイナリが期待されるゴールデンファイルと一致するかどうかを検証する checkGolden 関数を含んでいます。

  • const saveMismatch = true: この定数は、テストで生成されたバイナリがゴールデンファイルと一致しなかった場合に、デバッグのために不一致のデータをファイルに保存するかどうかを制御します。true に設定されているため、不一致時には .raw.hex 拡張子を持つファイルが生成されます。

  • func checkGolden(t *testing.T, data []byte, name string): この関数は、リンカが生成したバイナリデータ data と、期待されるゴールデンファイル name を比較します。

    • 変更前は ioutil.ReadFile(name) を直接使用してゴールデンバイナリを読み込んでいましたが、変更後は golden := mustParseHexdumpFile(t, name) を使用するようになりました。これにより、ゴールデンファイルがヘックスダンプ形式で保存されていても、テスト時には元のバイナリデータに復元されて比較されるようになります。
    • if saveMismatch { ... } ブロックは、datagolden が一致しなかった場合に実行されます。
      • ioutil.WriteFile(name+".raw", data, 0666) は、リンカが生成した実際のバイナリデータを .raw 拡張子で保存します。
      • ioutil.WriteFile(name+".hex", []byte(hexdump(data)), 0666) は、リンカが生成したバイナリデータをヘックスダンプ形式に変換し、.hex 拡張子で保存します。 これらのファイルは、テストが失敗した際に、何が期待され、何が実際に生成されたのかを比較・分析するのに役立ちます。

この一連の変更により、Goリンカのテストにおけるゴールデンファイルの管理が大幅に改善され、バイナリファイルの変更がより透過的になり、開発者によるレビューとデバッグが容易になりました。

関連リンク

参考にした情報源リンク