[インデックス 12462] ファイルの概要
このコミットは、Go言語のテスト実行フレームワークにおける重要な改善を含んでいます。具体的には、test/run.go
スクリプトがテストのコンパイル、リンク、実行を行う際に、従来の直接的なツール呼び出し(go tool gc
, go tool ld
など)から、より高レベルなgo
コマンド(go run
, go build
など)を使用するように変更されています。これにより、複数ファイルにまたがるテストや、引数を必要とするテストの実行が容易になりました。
コミット
commit 105c5fa666fd29967a8818d3ccbc455722274496
Author: Russ Cox <rsc@golang.org>
Date: Wed Mar 7 01:54:39 2012 -0500
test: invoke go command in run.go
Lets us run multifile tests and tests with arguments.
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5753068
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/105c5fa666fd29967a8818d3ccbc455722274496
元コミット内容
このコミットの目的は、「run.go
でgo
コマンドを呼び出すようにする」ことです。これにより、「複数ファイルテストや引数付きテストの実行が可能になる」と説明されています。
変更の背景
Go言語の初期のテストフレームワークは、個々のテストケースを実行するために、コンパイラ(gc
)やリンカ(ld
)といった低レベルのツールを直接呼び出していました。これは、シンプルな単一ファイルテストには機能しましたが、以下のような課題がありました。
- 複数ファイルテストの困難さ: 複数のGoソースファイルにまたがるテストケース(例えば、テスト対象のコードが複数のファイルに分割されている場合や、テストヘルパー関数が別のファイルに定義されている場合)を効率的に扱うことが困難でした。低レベルツールを直接操作する場合、依存関係の解決やビルドプロセスの管理が複雑になります。
- 引数付きテストの非対応: テスト対象のプログラムにコマンドライン引数を渡して動作を検証するようなシナリオに対応していませんでした。これは、より現実的なアプリケーションのテストを行う上で制約となります。
- テストスクリプトの複雑性:
run.go
スクリプト自体が、コンパイル、リンク、実行といった一連のプロセスを詳細に管理する必要があり、コードが複雑化していました。
これらの課題を解決し、Go言語のテストインフラストラクチャをより堅牢で柔軟なものにするために、この変更が導入されました。go
コマンドは、Goツールチェインのフロントエンドとして、コンパイル、リンク、実行といった一連の操作を抽象化し、開発者にとって使いやすいインターフェースを提供します。この抽象化を利用することで、テストスクリプトの記述が簡素化され、より複雑なテストシナリオへの対応が可能になります。
前提知識の解説
このコミットを理解するためには、以下のGo言語の基本的な概念とツールに関する知識が必要です。
- Goツールチェイン: Go言語の開発環境は、コンパイラ、リンカ、フォーマッタ、テストランナーなど、様々なツールで構成されています。これらはまとめて「Goツールチェイン」と呼ばれます。
go
コマンド: Goツールチェインの主要なコマンドラインインターフェースです。go build
(プログラムのビルド)、go run
(プログラムのビルドと実行)、go test
(テストの実行)、go install
(パッケージのインストール)など、様々なサブコマンドを提供します。go tool
コマンド:go tool
は、Goツールチェインに含まれる低レベルのツール(例:gc
コンパイラ、ld
リンカ、asm
アセンブラなど)を直接呼び出すためのコマンドです。通常、開発者が直接使用することは少なく、go build
やgo run
のような高レベルコマンドの内部で利用されます。go tool gc
: Goコンパイラ(gc
はGo Compilerの略)。Goソースコードをオブジェクトファイルにコンパイルします。go tool ld
: Goリンカ(ld
はLinkerの略)。コンパイルされたオブジェクトファイルを結合し、実行可能ファイルを生成します。
test/run.go
: Goプロジェクトのテストスイート内で使用される内部スクリプトの一つで、個々のテストケースの実行ロジックを管理していました。このスクリプトは、テストファイルの解析、コンパイル、実行、そして結果の検証を担当します。- テストの実行方法: Go言語では、通常
go test
コマンドを使用してテストを実行します。しかし、Goプロジェクト自体のテスト(Goコンパイラやランタイムのテストなど)は、より低レベルなtest
ディレクトリ内のスクリプトによって管理されることがあります。このコミットが変更しているのは、まさにこの内部的なテスト実行ロジックです。 // run
ディレクティブ: Goのテストファイル(特にGoプロジェクトの内部テスト)では、ファイルの先頭に// run some_file.go
のようなコメントを記述することで、そのテストの実行方法や依存関係を指定する慣習があります。これは、test/run.go
スクリプトによって解釈されます。
技術的詳細
このコミットの技術的な核心は、test/run.go
スクリプトにおけるテスト実行フローの根本的な変更にあります。
変更前のアプローチ:
変更前は、test/run.go
内のrun
関数が、テストファイルのコンパイルとリンクを明示的に制御していました。
- ソースコードの読み込みと一時ディレクトリへの書き込み: テスト対象のGoソースコードを読み込み、一時ディレクトリに書き出します。
- コンパイル:
exec.Command("go", "tool", gc, ...)
を使用して、gc
コンパイラを直接呼び出し、Goソースファイルをオブジェクトファイルにコンパイルしていました。 - リンク: コンパイルが成功した後、
exec.Command("go", "tool", ld, ...)
を使用して、ld
リンカを直接呼び出し、オブジェクトファイルを実行可能ファイルにリンクしていました。 - 実行: 最後に、生成された実行可能ファイルを直接起動していました。
このアプローチは、各ステップを細かく制御できる反面、Goモジュールやパッケージの依存関係解決、複数ファイルのビルド、コマンドライン引数の処理といった複雑なタスクをrun.go
自身が管理する必要がありました。
変更後のアプローチ:
変更後は、go
コマンドの高レベルな抽象化を利用することで、これらの複雑なタスクをgo
コマンドに委譲しています。
runcmd
ヘルパー関数の導入:runcmd
という新しい内部ヘルパー関数が導入されました。この関数は、任意のコマンドとその引数を受け取り、exec.Command
を使用して実行します。- 標準出力と標準エラー出力をキャプチャし、コマンドの実行ディレクトリ(
cmd.Dir
)を適切に設定します(一時ディレクトリまたは現在の作業ディレクトリ)。 - 環境変数
GOOS
とGOARCH
を明示的に設定することで、クロスコンパイル環境でのテスト実行にも対応しています。
go run
の活用:action
が"run"
の場合、runcmd
関数を使ってgo run
コマンドを呼び出すようになりました。runcmd(append([]string{"go", "run", t.goFileName()}, args...)...)
のように、go run
にテスト対象のGoファイル名と、テストファイルに指定された追加の引数(args
)を渡します。go run
は、指定されたGoソースファイルを自動的にコンパイル、リンクし、実行します。これにより、run.go
がコンパイルとリンクのステップを個別に管理する必要がなくなりました。また、go run
は複数ファイルのGoプログラムも適切に処理できるため、複数ファイルテストのサポートが自然に実現されます。
go build
の活用:action
が"build"
の場合、runcmd
関数を使ってgo build
コマンドを呼び出すようになりました。go build
は、実行可能ファイルを生成しますが、実行はしません。
go tool gc
の継続利用(errorcheck
およびcompile
アクション):errorcheck
アクション(コンパイルエラーのチェック)やcompile
アクション(コンパイルのみ)では、引き続きgo tool gc
が使用されています。これは、これらのアクションがコンパイルの特定の側面(エラーメッセージの解析など)に焦点を当てているため、低レベルのコンパイラツールを直接呼び出す方が適しているためと考えられます。
test/testlib
の変更:test/testlib
は、Goプロジェクトのテストスクリプトが利用するシェルヘルパー関数群です。このコミットでは、run
シェル関数が変更され、コマンドライン引数から.go
ファイルを抽出し、それらをgo run
コマンドに渡すロジックが追加されました。これにより、シェルスクリプトレベルでも複数ファイルテストのサポートが強化されています。
- エラーメッセージの改善:
errorCheck
関数が改善され、エラーメッセージ内のファイルパスから一時ディレクトリのパスを削除し、より簡潔で読みやすい形式で表示されるようになりました。
この変更により、run.go
はGoツールチェインのより高レベルな機能を利用するようになり、テスト実行ロジックが簡素化され、同時に機能が拡張されました。
コアとなるコードの変更箇所
このコミットの主要な変更は以下のファイルに集中しています。
test/run.go
: テスト実行ロジックの核心部分。runcmd
ヘルパー関数の追加。run
アクションがgo run
を使用するように変更。build
アクションがgo build
を使用するように変更。errorCheck
関数のシグネチャ変更と、エラーメッセージの整形ロジックの追加。
test/cmplxdivide.go
: テストファイルのコメント変更。// $G $D/$F.go $D/cmplxdivide1.go && $L $D/$F.$A && ./$A.out
から// run cmplxdivide1.go
へ変更。これは、新しいrun.go
の// run
ディレクティブの解釈方法に合わせたものです。
test/testlib
: シェルスクリプトヘルパー。run
シェル関数が、複数Goファイルを引数として受け取り、go run
に渡すように変更。
コアとなるコードの解説
test/run.go
の変更点
// 新しく追加されたヘルパー関数
var cwd, _ = os.Getwd() // 現在の作業ディレクトリを取得
func (t *test) run() {
// ... (既存のコード)
// アクション文字列の解析
var args []string
f := strings.Fields(action) // アクション文字列をスペースで分割
if len(f) > 0 {
action = f[0] // 最初の要素がアクション名
args = f[1:] // 残りの要素が引数
}
// ... (既存のコード)
// コマンド実行ヘルパー関数
useTmp := true // デフォルトでは一時ディレクトリを使用
runcmd := func(args ...string) ([]byte, error) {
cmd := exec.Command(args[0], args[1:]...)
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = &buf
if useTmp {
cmd.Dir = t.tempDir // 一時ディレクトリを設定
}
// GOOSとGOARCH環境変数を設定
cmd.Env = append(cmd.Env, "GOOS="+runtime.GOOS, "GOARCH="+runtime.GOARCH)
err := cmd.Run()
return buf.Bytes(), err
}
long := filepath.Join(cwd, t.goFileName()) // テストファイルの絶対パス
switch action {
default:
t.err = fmt.Errorf("unimplemented action %q", action)
case "errorcheck":
// エラーチェックは引き続き go tool gc を使用
out, _ := runcmd("go", "tool", gc, "-e", "-o", "a."+letter, long)
t.err = t.errorCheck(string(out), long, t.gofile) // errorCheckの引数が増えた
case "compile":
// コンパイルも引き続き go tool gc を使用
out, err := runcmd("go", "tool", gc, "-e", "-o", "a."+letter, long)
if err != nil {
t.err = fmt.Errorf("%s\n%s", err, out)
}
case "build":
// ビルドは go build を使用
out, err := runcmd("go", "build", "-o", "a.exe", long)
if err != nil {
t.err = fmt.Errorf("%s\n%s", err, out)
}
case "run":
// 実行は go run を使用
useTmp = false // go run は通常、現在のディレクトリで実行されるため、一時ディレクトリは使用しない
// go run にテストファイル名と追加の引数を渡す
out, err := runcmd(append([]string{"go", "run", t.goFileName()}, args...)...)
if err != nil {
t.err = fmt.Errorf("%s\n%s", err, out)
}
if string(out) != t.expectedOutput() {
t.err = fmt.Errorf("incorrect output\n%s", out)
}
}
}
// errorCheck 関数のシグネチャ変更
// full: フルパス, short: 短縮パス (例: test/cmplxdivide.go)
func (t *test) errorCheck(outStr string, full, short string) (err error) {
// ... (既存のコード)
// エラーメッセージ内のディレクトリ名を短縮パスに置換
for i := range out {
out[i] = strings.Replace(out[i], full, short, -1)
}
// ... (既存のコード)
}
test/cmplxdivide.go
の変更点
--- a/test/cmplxdivide.go
+++ b/test/cmplxdivide.go
@@ -1,4 +1,4 @@
-// $G $D/$F.go $D/cmplxdivide1.go && $L $D/$F.$A && ./$A.out
+// run cmplxdivide1.go
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
この変更は、test/run.go
が// run
ディレクティブを解釈し、それに続くファイル名をgo run
コマンドの引数として渡すようになったことを示しています。これにより、cmplxdivide.go
とcmplxdivide1.go
という2つのファイルが連携してテストされるようになります。
test/testlib
の変更点
--- a/test/testlib
+++ b/test/testlib
@@ -14,7 +14,21 @@ build() {
}
run() {
-\t$G $D/$F.go && $L $F.$A && ./$A.out \"$@\"\n+\tgofiles=\"\"\n+\tingo=true\n+\twhile $ingo; do\n+\t\tcase \"$1\" in\n+\t\t*.go)\n+\t\t\tgofiles=\"$gofiles $1\"\n+\t\t\tshift\n+\t\t\t;;\n+\t\t*)\n+\t\t\tingo=false\n+\t\t\t;;\n+\t\tesac\n+\tdone\n+\n+\t$G $D/$F.go \"$gofiles\" && $L $F.$A && ./$A.out \"$@\"\n }
testlib
のrun
関数は、シェルスクリプト内でGoファイルを処理するためのヘルパーです。変更前は単一のGoファイルと引数を想定していましたが、変更後はループを使って引数の中から.go
で終わるファイルをすべて抽出し、gofiles
変数に格納しています。そして、このgofiles
を$G
(Goコンパイラまたはgo run
コマンドを抽象化したもの)に渡すことで、複数ファイルのコンパイル・実行をサポートしています。
関連リンク
- Go言語の公式ドキュメント: https://go.dev/doc/
go
コマンドのドキュメント: https://go.dev/cmd/go/- Go言語のテストに関するドキュメント: https://go.dev/doc/code#testing
参考にした情報源リンク
- Go言語のソースコード(特に
src/cmd/go
やsrc/cmd/compile
、src/cmd/link
のディレクトリ構造) - Go言語のコミット履歴とコードレビューシステム (Gerrit/CL): https://go.dev/cl/5753068
- Go言語のIssue Tracker: https://go.dev/issue
- Go言語のブログや技術記事(
go run
やgo build
の内部動作に関するもの)