[インデックス 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
)からの呼び出し方法の変更です。
tabify
関数のリファクタリングとdecorate
への改名:- 元の
tabify
関数は、文字列に最終的な改行を追加し、内部の改行の後にタブを追加する役割を持っていました。 - この関数は
decorate
に改名され、addFileLine
という新しいブール引数が追加されました。この引数がtrue
の場合、ファイル名と行番号のスタンプがメッセージの先頭に追加されるようになりました。
- 元の
decorate
関数内のruntime.Caller
の使用:decorate
関数内でruntime.Caller(3)
が呼び出されています。これは、decorate
関数がlog
関数から呼ばれ、さらにlog
関数がT.Log
などの公開関数から呼ばれるというコールスタックを考慮したものです。skip=3
は、decorate
、log
、そしてT.Log
などの呼び出し元(つまり、テストコード内でt.Log
が呼ばれた場所)の情報を取得することを意味します。runtime.Caller
が成功した場合、返されたファイルパスからstrings.LastIndex
を使って最後のパス区切り文字(/
または\
)以降の部分を抽出し、ファイル名のみを取得しています。これにより、絶対パスではなく、より簡潔なファイル名が表示されます。fmt.Sprintf("%s:%d: %s", file, line, s)
を使って、ファイル名:行番号: オリジナルメッセージ
の形式で文字列をフォーマットしています。
log
ヘルパー関数の導入:*testing.T
型にlog(s string)
という新しいプライベートヘルパーメソッドが追加されました。このメソッドは、引数として受け取った文字列s
をdecorate(s, true)
に渡して処理し、その結果をt.errors
に追加します。- この
log
関数は、常に同じスタック深度で呼び出されるように設計されており、runtime.Caller(3)
が常に正しい呼び出し元の情報を取得できるようにしています。
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 { ... }
ブロックが追加され、addFileLine
がtrue
の場合にファイル名と行番号のスタンプが生成されます。runtime.Caller(3)
:decorate
がlog
を介してT.Log
などの公開関数から呼ばれるため、呼び出し元のテストコードのファイルと行番号を取得するためにskip
引数に3
を指定しています。- ファイルパスの整形:
strings.LastIndex
を使って/
または\
を検索し、ファイル名のみを抽出しています。 fmt.Sprintf
によるフォーマット:ファイル名:行番号: オリジナルメッセージ
の形式で文字列を生成します。- 再帰呼び出しの変更: 複数行のメッセージの場合、2行目以降はファイル名と行番号を付加しないように
decorate(s[i+1:n], false)
とaddFileLine
をfalse
で再帰呼び出ししています。
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) }
- この関数は、引数
s
をdecorate(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言語
testing
パッケージのドキュメント: https://pkg.go.dev/testing - Go言語
runtime
パッケージのドキュメント: https://pkg.go.dev/runtime - Go言語
fmt
パッケージのドキュメント: https://pkg.go.dev/fmt - Go言語
strings
パッケージのドキュメント: https://pkg.go.dev/strings - このコミットのGo Gerrit Code Reviewページ: https://golang.org/cl/5376057
参考にした情報源リンク
申し訳ありませんが、このコミットが直接的に参照した外部の情報源リンクを特定することはできませんでした。しかし、この変更を理解する上で役立つGo言語の公式ドキュメントへのリンクを「関連リンク」セクションに記載しています。