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

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

このコミットは、Go言語の標準ライブラリであるtestingパッケージにおけるテスト結果の出力形式を改善するものです。具体的には、テスト失敗時のメッセージに、エラーが発生したファイル名と行番号(file:lineスタンプ)を付加する機能が追加されました。これにより、テストのデバッグがより容易になります。

コミット

commit 2c39ca08cd6bb94b31ac6e15b0da33b345b62170
Author: Rob Pike <r@golang.org>
Date:   Thu Nov 10 11:59:50 2011 -0800

    testing: add file:line stamps to messages.
    
    A single-line error looks like this:
    --- FAIL: foo_test.TestFoo (0.00 seconds)
            foo_test.go:123: Foo(8) = "10" want "100"
    
    A multi-line error looks like this:
    --- FAIL: foo_test.TestFoo (0.00 seconds)
            foo_test.go:456: Foo(88) = "100"
                    want "1000"
    
    R=rsc, bradfitz
    CC=golang-dev
    https://golang.org/cl/5376057

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

https://github.com/golang/go/commit/2c39ca08cd6bb94b31ac6e15b0da33b345b62170

元コミット内容

このコミットは、Goのtestingパッケージにおいて、テスト失敗時の出力メッセージにファイル名と行番号の情報を追加するものです。これにより、テストが失敗した際に、どのファイルのどの行でエラーが発生したのかを直接的に把握できるようになり、デバッグの効率が向上します。

コミットメッセージには、単一行のエラーと複数行のエラーの出力例が示されており、foo_test.go:123:foo_test.go:456:のようにファイル名と行番号がプレフィックスとして付加されていることがわかります。

変更の背景

Go言語のテストフレームワークはシンプルで使いやすいことで知られていますが、初期のバージョンではテスト失敗時の詳細な情報が不足しているという課題がありました。特に、エラーメッセージだけでは、コードベースのどこで問題が発生したのかを特定するのが難しい場合がありました。

この変更は、開発者がテスト失敗の原因を迅速に特定し、デバッグプロセスを効率化することを目的としています。ファイル名と行番号が直接出力されることで、IDEやエディタの機能と連携しやすくなり、エラー箇所へのジャンプが容易になります。これは、大規模なプロジェクトや複雑なテストケースにおいて特に有用です。

前提知識の解説

このコミットの変更内容を理解するためには、以下のGo言語の概念と標準ライブラリに関する知識が必要です。

  • Go言語のtestingパッケージ: Go言語に組み込まれているテストフレームワークです。go testコマンドによって実行され、Testプレフィックスを持つ関数をテストとして認識します。*testing.T型はテストの状態を管理し、Log, Logf, Error, Errorf, Fatal, Fatalfなどのメソッドを提供してテスト結果を報告します。
  • runtimeパッケージ: Goプログラムのランタイム環境とのインタフェースを提供するパッケージです。
    • runtime.Caller(skip int): この関数は、現在のゴルーチンのコールスタックに関する情報を報告します。skip引数は、スタックフレームをスキップする数を指定します。runtime.Caller(0)Caller自身の情報を返し、runtime.Caller(1)Callerを呼び出した関数の情報を返します。このコミットでは、テストヘルパー関数からT.Logなどが呼ばれた際の呼び出し元のファイルと行番号を取得するために使用されます。
  • fmtパッケージ: フォーマットされたI/Oを実装するパッケージです。
    • fmt.Sprintf(format string, a ...interface{}) string: フォーマット指定子に基づいて文字列を生成し、その結果を返します。C言語のsprintfに似ています。
  • stringsパッケージ: 文字列操作のためのユーティリティ関数を提供するパッケージです。
    • strings.LastIndex(s, substr string) int: 文字列s内でsubstrが最後に現れるインデックスを返します。見つからない場合は-1を返します。ファイルパスからファイル名のみを抽出するために使用されます。
  • ファイルパスの操作: Unix系システムでは/、Windows系システムでは\がパスの区切り文字として使用されます。strings.LastIndexを使ってこれらの区切り文字を検索し、ファイル名部分を抽出する一般的なパターンです。

技術的詳細

このコミットの主要な変更点は、テストメッセージにファイル名と行番号を追加するための新しいヘルパー関数decorateの導入と、既存のtesting.Tメソッド(Log, Logf, Error, Errorf, Fatal, Fatalf)からの呼び出し方法の変更です。

  1. tabify関数のリファクタリングとdecorateへの改名:
    • 元のtabify関数は、文字列に最終的な改行を追加し、内部の改行の後にタブを追加する役割を持っていました。
    • この関数はdecorateに改名され、addFileLineという新しいブール引数が追加されました。この引数がtrueの場合、ファイル名と行番号のスタンプがメッセージの先頭に追加されるようになりました。
  2. decorate関数内のruntime.Callerの使用:
    • decorate関数内でruntime.Caller(3)が呼び出されています。これは、decorate関数がlog関数から呼ばれ、さらにlog関数がT.Logなどの公開関数から呼ばれるというコールスタックを考慮したものです。skip=3は、decoratelog、そしてT.Logなどの呼び出し元(つまり、テストコード内でt.Logが呼ばれた場所)の情報を取得することを意味します。
    • runtime.Callerが成功した場合、返されたファイルパスからstrings.LastIndexを使って最後のパス区切り文字(/または\)以降の部分を抽出し、ファイル名のみを取得しています。これにより、絶対パスではなく、より簡潔なファイル名が表示されます。
    • fmt.Sprintf("%s:%d: %s", file, line, s)を使って、ファイル名:行番号: オリジナルメッセージの形式で文字列をフォーマットしています。
  3. logヘルパー関数の導入:
    • *testing.T型にlog(s string)という新しいプライベートヘルパーメソッドが追加されました。このメソッドは、引数として受け取った文字列sdecorate(s, true)に渡して処理し、その結果をt.errorsに追加します。
    • このlog関数は、常に同じスタック深度で呼び出されるように設計されており、runtime.Caller(3)が常に正しい呼び出し元の情報を取得できるようにしています。
  4. testing.Tメソッドの変更:
    • Log, Logf, Error, Errorf, Fatal, Fatalfといった既存の公開メソッドは、直接tabifyを呼び出す代わりに、新しく導入されたlogヘルパー関数を呼び出すように変更されました。
    • これにより、これらのメソッドが生成するすべてのテストメッセージに、自動的にファイル名と行番号のスタンプが付加されるようになりました。

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

src/pkg/testing/testing.go ファイルが変更されています。

--- a/src/pkg/testing/testing.go
+++ b/src/pkg/testing/testing.go
@@ -75,8 +75,25 @@ func Short() bool {
 	return *short
 }
 
-// Insert final newline if needed and tabs after internal newlines.
-func tabify(s string) string {
+// decorate inserts the a final newline if needed and indentation tabs for formatting.
+// If addFileLine is true, it also prefixes the string with the file and line of the call site.
+func decorate(s string, addFileLine bool) string {
+	if addFileLine {
+		_, file, line, ok := runtime.Caller(3) // decorate + log + public function.
+		if ok {
+			// Truncate file name at last file name separator.
+			if index := strings.LastIndex(file, "/"); index >= 0 {
+				file = file[index+1:]
+			} else if index = strings.LastIndex(file, "\\"); index >= 0 {
+				file = file[index+1:]
+			}
+		} else {
+			file = "???"
+			line = 1
+		}
+		s = fmt.Sprintf("%s:%d: %s", file, line, s)
+	}
+	s = "\t" + s // Every line is indented at least one tab.
 	n := len(s)
 	if n > 0 && s[n-1] != '\n' {
 		s += "\n"
@@ -84,7 +101,8 @@ func tabify(s string) string {
 	}
 	for i := 0; i < n-1; i++ { // -1 to avoid final newline
 		if s[i] == '\n' {
-			return s[0:i+1] + "\t" + tabify(s[i+1:n])
+			// Second and subsequent lines are indented an extra tab.
+			return s[0:i+1] + "\t" + decorate(s[i+1:n], false)
 		}
 	}
 	return s
@@ -116,37 +134,38 @@ func (t *T) FailNow() {
 	runtime.Goexit()
 }
 
+// log generates the output. It's always at the same stack depth.
+func (t *T) log(s string) { t.errors += decorate(s, true) }
+
 // Log formats its arguments using default formatting, analogous to Print(),
 // and records the text in the error log.
-func (t *T) Log(args ...interface{}) { t.errors += "\t" + tabify(fmt.Sprintln(args...)) }
+func (t *T) Log(args ...interface{}) { t.log(fmt.Sprintln(args...)) }
 
 // Logf formats its arguments according to the format, analogous to Printf(),
 // and records the text in the error log.
-func (t *T) Logf(format string, args ...interface{}) {
-	t.errors += "\t" + tabify(fmt.Sprintf(format, args...))
-}
+func (t *T) Logf(format string, args ...interface{}) { t.log(fmt.Sprintf(format, args...)) }
 
 // Error is equivalent to Log() followed by Fail().
 func (t *T) Error(args ...interface{}) {
-	t.Log(args...)
+	t.log(fmt.Sprintln(args...))
 	t.Fail()
 }
 
 // Errorf is equivalent to Logf() followed by Fail().
 func (t *T) Errorf(format string, args ...interface{}) {
-	t.Logf(format, args...)
+	t.log(fmt.Sprintf(format, args...))
 	t.Fail()
 }
 
 // Fatal is equivalent to Log() followed by FailNow().
 func (t *T) Fatal(args ...interface{}) {
-	t.Log(args...)
+	t.log(fmt.Sprintln(args...))
 	t.FailNow()
 }
 
 // Fatalf is equivalent to Logf() followed by FailNow().
 func (t *T) Fatalf(format string, args ...interface{}) {
-	t.Logf(format, args...)
+	t.log(fmt.Sprintf(format, args...))
 	t.FailNow()
 }
 

コアとなるコードの解説

decorate関数の変更

元のtabify関数がdecorateに改名され、addFileLineという新しいブール引数が追加されました。

// decorate inserts the a final newline if needed and indentation tabs for formatting.
// If addFileLine is true, it also prefixes the string with the file and line of the call site.
func decorate(s string, addFileLine bool) string {
	if addFileLine {
		_, file, line, ok := runtime.Caller(3) // decorate + log + public function.
		if ok {
			// Truncate file name at last file name separator.
			if index := strings.LastIndex(file, "/"); index >= 0 {
				file = file[index+1:]
			} else if index = strings.LastIndex(file, "\\"); index >= 0 {
				file = file[index+1:]
			}
		} else {
			file = "???"
			line = 1
		}
		s = fmt.Sprintf("%s:%d: %s", file, line, s)
	}
	s = "\t" + s // Every line is indented at least one tab.
	n := len(s)
	if n > 0 && s[n-1] != '\n' {
		s += "\n"
	}
	for i := 0; i < n-1; i++ { // -1 to avoid final newline
		if s[i] == '\n' {
			// Second and subsequent lines are indented an extra tab.
			return s[0:i+1] + "\t" + decorate(s[i+1:n], false)
		}
	}
	return s
}
  • if addFileLine { ... } ブロックが追加され、addFileLinetrueの場合にファイル名と行番号のスタンプが生成されます。
  • runtime.Caller(3): decoratelogを介してT.Logなどの公開関数から呼ばれるため、呼び出し元のテストコードのファイルと行番号を取得するためにskip引数に3を指定しています。
  • ファイルパスの整形: strings.LastIndexを使って/または\を検索し、ファイル名のみを抽出しています。
  • fmt.Sprintfによるフォーマット: ファイル名:行番号: オリジナルメッセージの形式で文字列を生成します。
  • 再帰呼び出しの変更: 複数行のメッセージの場合、2行目以降はファイル名と行番号を付加しないようにdecorate(s[i+1:n], false)addFileLinefalseで再帰呼び出ししています。

logヘルパー関数の追加

*testing.T型にlogというプライベートメソッドが追加されました。

// log generates the output. It's always at the same stack depth.
func (t *T) log(s string) { t.errors += decorate(s, true) }
  • この関数は、引数sdecorate(s, true)に渡し、その結果をt.errorsフィールドに追加します。
  • このlog関数を介することで、decorate関数が常に同じスタック深度(runtime.Caller(3)が意図した通りに動作する深度)で呼び出されることが保証されます。

testing.Tの公開メソッドの変更

Log, Logf, Error, Errorf, Fatal, Fatalfの各メソッドは、直接tabify(現在はdecorate)を呼び出す代わりに、新しく追加されたlogヘルパー関数を呼び出すように変更されました。

例: Logメソッドの変更

-func (t *T) Log(args ...interface{}) { t.errors += "\t" + tabify(fmt.Sprintln(args...)) }
+func (t *T) Log(args ...interface{}) { t.log(fmt.Sprintln(args...)) }

これにより、これらのメソッドが生成するすべてのテストメッセージに、自動的にファイル名と行番号のスタンプが付加されるようになりました。

関連リンク

参考にした情報源リンク

申し訳ありませんが、このコミットが直接的に参照した外部の情報源リンクを特定することはできませんでした。しかし、この変更を理解する上で役立つGo言語の公式ドキュメントへのリンクを「関連リンク」セクションに記載しています。