[インデックス 15443] ファイルの概要
このコミットは、Go言語の標準ライブラリ os/exec
パッケージのテストコード (src/pkg/os/exec/exec_test.go
) に対する変更です。特に、Plan 9オペレーティングシステム上でのテストの互換性と正確性を向上させることを目的としています。Plan 9固有の挙動(終了ステータス文字列のフォーマット、ファイルディスクリプタの扱い、runtime.nanotime()
の実装詳細)に対応するための修正が含まれています。
コミット
commit fa625fb39a7547b295ac148bc000bb5884d01776
Author: Akshat Kumar <seed@mail.nanosouffle.net>
Date: Tue Feb 26 09:40:55 2013 -0800
os/exec: Pass tests on Plan 9
Adjust the exit status string for Plan 9.
Upon allocating >100 file descriptors, Plan 9
raises a warning. Moreover, the Go runtime for
32-bit version of Plan 9 keeps /dev/bintime
open for its implementation of runtime.nanotime().
This change accounts for these things in
TestExtraFiles.
R=rsc, rminnich, ality, bradfitz
CC=golang-dev
https://golang.org/cl/7363056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/fa625fb39a7547b295ac148bc000bb5884d01776
元コミット内容
このコミットの元の内容は以下の通りです。
os/exec: Pass tests on Plan 9
Adjust the exit status string for Plan 9.
Upon allocating >100 file descriptors, Plan 9
raises a warning. Moreover, the Go runtime for
32-bit version of Plan 9 keeps /dev/bintime
open for its implementation of runtime.nanotime().
This change accounts for these things in
TestExtraFiles.
変更の背景
この変更の背景には、Go言語のテストスイートがPlan 9オペレーティングシステム上で正しく動作しないという問題がありました。具体的には、以下のPlan 9固有の挙動がテストの失敗を引き起こしていました。
- 終了ステータス文字列のフォーマットの違い:
os/exec
パッケージがプロセス終了時に返すエラー文字列のフォーマットが、Plan 9では他のOS(Linux, macOS, Windowsなど)と異なっていました。Goのテストは特定のフォーマットの終了ステータス文字列を期待していましたが、Plan 9の出力がそれに合致しなかったため、TestExitStatus
が失敗していました。 - ファイルディスクリプタの警告: Plan 9では、プロセスが100を超えるファイルディスクリプタを割り当てると警告を発する挙動があります。
TestExtraFiles
テストは、意図的に多数のファイルディスクリプタを操作するため、この警告がテストのロジックに影響を与え、予期せぬ結果や失敗を引き起こす可能性がありました。 runtime.nanotime()
の実装と/dev/bintime
: 32ビット版のPlan 9におけるGoランタイムは、runtime.nanotime()
(高精度な時間計測関数)の実装のために/dev/bintime
というデバイスファイルを常に開いた状態に保ちます。これは、テストが予期する「開いているファイルディスクリプタの数」に影響を与え、特にTestExtraFiles
のようなファイルディスクリプタの数を厳密にチェックするテストで問題となりました。
これらのPlan 9固有の挙動に対応し、GoのテストスイートがPlan 9上でもパスするようにするために、このコミットが作成されました。
前提知識の解説
このコミットを理解するためには、以下の前提知識が必要です。
- Plan 9 from Bell Labs: ベル研究所が開発した分散オペレーティングシステムです。Unixの概念をさらに推し進め、すべてをファイルとして扱うという思想が徹底されています。ネットワーク透過性が高く、リソースの共有が容易なのが特徴です。Go言語は、その設計思想の一部をPlan 9から継承していると言われています。
- ファイルディスクリプタ (File Descriptor, FD): Unix系OSにおいて、プロセスが開いているファイルやソケット、パイプなどのI/Oリソースを識別するために用いられる非負の整数です。標準入力 (stdin) は0、標準出力 (stdout) は1、標準エラー出力 (stderr) は2が割り当てられるのが一般的です。
os/exec
パッケージ: Go言語の標準ライブラリで、外部コマンドを実行するための機能を提供します。プロセスの起動、標準入出力のリダイレクト、終了ステータスの取得などが可能です。ExitError
:os/exec
パッケージで外部コマンドの実行が失敗し、ゼロ以外の終了ステータスを返した場合に返されるエラー型です。このエラーには、プロセスの終了ステータスに関する情報が含まれます。runtime.GOOS
とruntime.GOARCH
: Go言語のruntime
パッケージで提供される定数で、それぞれ現在のビルドターゲットのオペレーティングシステム(例: "linux", "windows", "plan9")とアーキテクチャ(例: "amd64", "386")を示します。これらを使ってOSやアーキテクチャに依存するコードを書くことができます。/dev/bintime
(Plan 9): Plan 9における特殊なデバイスファイルの一つで、システムの時間情報を提供する役割を担います。Goのランタイムがnanotime()
の実装にこれを利用しているのは、Plan 9のシステムコールやデバイスの抽象化に合わせた設計のためです。os.Stderr.Fd()
:os
パッケージのStderr
(標準エラー出力)ファイルオブジェクトのファイルディスクリプタを取得するメソッドです。通常、これは2を返します。
技術的詳細
このコミットは、主に以下の技術的な側面でPlan 9の特殊性を扱っています。
-
終了ステータス文字列の差異への対応:
- 一般的なUnix系システムでは、
os/exec
が返すExitError
の文字列は"exit status N"
(Nは終了コード)のような形式です。 - しかし、Plan 9では、終了ステータス文字列がより詳細な情報(コマンドのベース名、プロセスID、終了コード)を含む
"exit status: 'command_base_name PID: N'"
のような形式になることがあります。 - このコミットでは、
runtime.GOOS
が"plan9"
の場合に、期待される終了ステータス文字列のフォーマットをPlan 9の形式に動的に調整することで、テストが正しくパスするようにしています。
- 一般的なUnix系システムでは、
-
ファイルディスクリプタのベースライン調整:
TestExtraFiles
は、プロセスが起動時に開いているファイルディスクリプタの数を基準として、追加で開かれたファイルディスクリプタをテストします。- 通常、Goプログラムが起動した直後には、標準入力(0)、標準出力(1)、標準エラー出力(2)の3つのファイルディスクリプタが開いています。そのため、テストでは
os.Stderr.Fd() + 1
(つまり3)からファイルディスクリプタのチェックを開始するのが一般的です。 - しかし、32ビット版のPlan 9では、
runtime.nanotime()
の実装のために/dev/bintime
が追加で開かれるため、起動時に開いているファイルディスクリプタの数が1つ増えます。 - このコミットでは、
basefds()
という新しいヘルパー関数を導入し、runtime.GOOS == "plan9" && runtime.GOARCH == "386"
の場合にベースとなるファイルディスクリプタの数を1つ増やすように調整しています。これにより、Plan 9 32ビット環境でのファイルディスクリプタのチェックが正確になります。 - Plan 9が100を超えるファイルディスクリプタで警告を出す挙動も、このベースライン調整とテストロジックの微調整によって間接的に考慮されています。テストは101までのファイルディスクリプタをチェックしますが、
basefds()
の調整により、Plan 9の特殊な初期状態が考慮され、警告がテストの失敗に直結しないようになっています。
-
CombinedOutput()
からRun()
と個別バッファへの変更:CombinedOutput()
はコマンドの標準出力と標準エラー出力を結合して返す便利なメソッドですが、テストのデバッグや特定のストリームの厳密なチェックには不向きな場合があります。- このコミットでは、
c.Stdout
とc.Stderr
にbytes.Buffer
を割り当ててc.Run()
を実行するように変更しています。これにより、標準出力と標準エラー出力を個別にキャプチャし、より詳細な検証やエラーメッセージの生成が可能になります。特に、t.Errorf
のメッセージにstderr
の内容を含めることで、テスト失敗時のデバッグ情報が豊富になっています。
-
ヘルパープロセスの出力ストリーム変更:
TestHelperProcess
内のread3
ケースでは、以前はos.Stderr.Write(bs)
でデータを書き出していましたが、これをos.Stdout.Write(bs)
に変更しています。これは、ヘルパープロセスがメインプロセスにデータを返す際に、標準エラー出力ではなく標準出力を使用するという一般的な慣習に合わせた変更です。これにより、メインプロセス側で標準出力を読み取ることで、ヘルパープロセスからのデータを受け取ることができます。
コアとなるコードの変更箇所
変更はすべて src/pkg/os/exec/exec_test.go
ファイル内で行われています。
-
import
文の追加:--- a/src/pkg/os/exec/exec_test.go +++ b/src/pkg/os/exec/exec_test.go @@ -14,6 +14,7 @@ import ( "net/http" "net/http/httptest" "os" + "path/filepath" "runtime" "strconv" "strings"
path/filepath
パッケージがインポートされました。これは、Plan 9の終了ステータス文字列を構築する際に、コマンドのベース名を取得するために使用されます。 -
TestExitStatus
関数の変更:--- a/src/pkg/os/exec/exec_test.go +++ b/src/pkg/os/exec/exec_test.go @@ -83,10 +84,16 @@ func TestNoExistBinary(t *testing.T) { func TestExitStatus(t *testing.T) { // Test that exit values are returned correctly - err := helperCommand("exit", "42").Run() + cmd := helperCommand("exit", "42") + err := cmd.Run() + want := "exit status 42" + switch runtime.GOOS { + case "plan9": + want = fmt.Sprintf("exit status: '%s %d: 42'", filepath.Base(cmd.Path), cmd.ProcessState.Pid()) + } if werr, ok := err.(*ExitError); ok { - if s, e := werr.Error(), "exit status 42"; s != e { - t.Errorf("from exit 42 got exit %q, want %q", s, e) + if s := werr.Error(); s != want { + t.Errorf("from exit 42 got exit %q, want %q", s, want) } } else { t.Fatalf("expected *ExitError from exit 42; got %T: %v", err, err)
want
変数が導入され、runtime.GOOS
が"plan9"
の場合にPlan 9固有の終了ステータス文字列が設定されるようになりました。 -
basefds()
関数の追加:--- a/src/pkg/os/exec/exec_test.go +++ b/src/pkg/os/exec/exec_test.go @@ -146,6 +153,20 @@ func TestPipes(t *testing.T) { var testedAlreadyLeaked = false +// basefds returns the number of expected file descriptors +// to be present in a process at start. +func basefds() uintptr { + n := os.Stderr.Fd() + 1 + + // Go runtime for 32-bit Plan 9 requires that /dev/bintime + // be kept open. + // See ../../runtime/time_plan9_386.c:/^runtime·nanotime + if runtime.GOOS == "plan9" && runtime.GOARCH == "386" { + n++ + } + return n +} + func TestExtraFiles(t *testing.T) { if runtime.GOOS == "windows" { t.Skip("no operating system support; skipping")
起動時に開いているファイルディスクリプタのベース数を計算する
basefds
関数が追加されました。Plan 9 32ビット環境では/dev/bintime
のために1つ多くなります。 -
TestExtraFiles
関数の変更:--- a/src/pkg/os/exec/exec_test.go +++ b/src/pkg/os/exec/exec_test.go @@ -155,7 +176,7 @@ func TestExtraFiles(t *testing.T) { // our environment. if !testedAlreadyLeaked { testedAlreadyLeaked = true - for fd := os.Stderr.Fd() + 1; fd <= 101; fd++ { + for fd := basefds(); fd <= 101; fd++ { err := os.NewFile(fd, "").Close() if err == nil { t.Logf("Something already leaked - closed fd %d", fd) @@ -209,13 +230,16 @@ func TestExtraFiles(t *testing.T) { } c := helperCommand("read3") + var stdout, stderr bytes.Buffer + c.Stdout = &stdout + c.Stderr = &stderr c.ExtraFiles = []*os.File{tf} - bs, err := c.CombinedOutput() + err = c.Run() if err != nil { - t.Fatalf("CombinedOutput: %v; output %q", err, bs) + t.Fatalf("Run: %v; stdout %q, stderr %q", err, stdout.Bytes(), stderr.Bytes()) } - if string(bs) != text { - t.Errorf("got %q; want %q", string(bs), text) + if stdout.String() != text { + t.Errorf("got stdout %q, stderr %q; want %q on stdout", stdout.String(), stderr.String(), text) } }
ファイルディスクリプタのチェックループが
basefds()
を使用するように変更されました。また、CombinedOutput()
がRun()
と個別のbytes.Buffer
に置き換えられ、エラーメッセージも詳細化されました。 -
TestHelperProcess
関数の変更 (caseread3
):--- a/src/pkg/os/exec/exec_test.go +++ b/src/pkg/os/exec/exec_test.go @@ -360,7 +384,7 @@ func TestHelperProcess(*testing.T) { \t\tdefault:\n \t\t\t// Now verify that there are no other open fds.\n \t\t\tvar files []*os.File\n -\t\t\t\tfor wantfd := os.Stderr.Fd() + 2; wantfd <= 100; wantfd++ { +\t\t\t\tfor wantfd := basefds() + 1; wantfd <= 100; wantfd++ { \t\t\t\tf, err := os.Open(os.Args[0]) \t\t\t\tif err != nil { \t\t\t\t\tfmt.Printf("error opening file with expected fd %d: %v", wantfd, err) @@ -384,7 +408,7 @@ func TestHelperProcess(*testing.T) { \t\t// what we do with fd3 as long as we refer to it;\n \t\t// closing it is the easy choice.\n \t\tfd3.Close()\n -\t\t\tos.Stderr.Write(bs) +\t\t\tos.Stdout.Write(bs) \tcase "exit": \t\tn, _ := strconv.Atoi(args[0]) \t\tos.Exit(n)
ヘルパープロセス内のファイルディスクリプタチェックも
basefds()
を使用するように変更され、os.Stderr.Write(bs)
がos.Stdout.Write(bs)
に修正されました。
コアとなるコードの解説
このコミットの核となる変更は、GoのテストがPlan 9オペレーティングシステムの特定の挙動に適応するように調整された点です。
-
TestExitStatus
の修正:filepath.Base(cmd.Path)
: 実行されたコマンドのパスからファイル名部分(例:/bin/ls
からls
)を抽出します。Plan 9の終了ステータス文字列にコマンド名が含まれるため、これを使用します。cmd.ProcessState.Pid()
: 実行されたプロセスのPID(プロセスID)を取得します。Plan 9の終了ステータス文字列にPIDが含まれるため、これを使用します。fmt.Sprintf("exit status: '%s %d: 42'", ...)
: Plan 9の終了ステータス文字列のフォーマットに合わせて、期待される文字列を動的に生成します。これにより、テストがPlan 9上でも正しい終了ステータスを認識できるようになります。
-
basefds()
関数の導入:- この関数は、Goプログラムが起動した時点で開いていると期待されるファイルディスクリプタの数を返します。
os.Stderr.Fd() + 1
は、標準入力(0)、標準出力(1)、標準エラー出力(2)の次のファイルディスクリプタ(通常は3)を指します。if runtime.GOOS == "plan9" && runtime.GOARCH == "386"
の条件は、32ビット版のPlan 9に特化した処理であることを示しています。この環境では、runtime.nanotime()
の実装のために/dev/bintime
が追加で開かれるため、n++
でベースラインを1つ増やしています。- この調整により、
TestExtraFiles
やTestHelperProcess
内のファイルディスクリプタのチェックが、Plan 9の特殊な初期状態を考慮して行われるようになり、誤った失敗を防ぎます。
-
TestExtraFiles
における出力キャプチャの改善:var stdout, stderr bytes.Buffer
とc.Stdout = &stdout
,c.Stderr = &stderr
は、外部コマンドの標準出力と標準エラー出力をそれぞれ別のメモリバッファにリダイレクトするための標準的なGoのパターンです。- これにより、
CombinedOutput()
のように両方を結合するのではなく、個別に内容を検査できるようになります。特に、t.Errorf
でstderr.String()
も出力することで、テストが失敗した際に、標準エラー出力に何かデバッグ情報や警告が出ていないかを確認しやすくなります。
-
TestHelperProcess
における出力ストリームの変更:os.Stderr.Write(bs)
からos.Stdout.Write(bs)
への変更は、ヘルパープロセスがテストデータや結果をメインプロセスに返す際の出力チャネルを標準エラー出力から標準出力へ切り替えるものです。これは、ヘルパープロセスが「結果」を返す場合は標準出力を使用し、「エラーやログ」を返す場合は標準エラー出力を使用するという、Unix系の一般的な慣習に沿った変更です。
これらの変更により、Goのos/exec
パッケージのテストがPlan 9という特定のOS環境においても、そのOSの特性を考慮した上で正確に動作するようになりました。
関連リンク
- Go言語の
os/exec
パッケージのドキュメント: https://pkg.go.dev/os/exec - Go言語の
runtime
パッケージのドキュメント: https://pkg.go.dev/runtime - Go言語の
path/filepath
パッケージのドキュメント: https://pkg.go.dev/path/filepath - Plan 9 from Bell Labs (Wikipedia): https://ja.wikipedia.org/wiki/Plan_9_from_Bell_Labs
- Go CL 7363056 (Gerrit Code Review): https://golang.org/cl/7363056 (コミットメッセージに記載されている元の変更リストへのリンク)
参考にした情報源リンク
- Go言語のソースコード (特に
src/runtime/time_plan9_386.c
): このファイルは、basefds()
関数のコメントで言及されている/dev/bintime
とruntime.nanotime()
の関係を理解する上で参考になります。 - Plan 9のドキュメントやコミュニティの議論: Plan 9のファイルディスクリプタの挙動や終了ステータス文字列のフォーマットに関する情報は、Plan 9の公式ドキュメントや関連するフォーラム、メーリングリストで確認できます。
- Go言語のテストに関する一般的なプラクティス:
bytes.Buffer
を使った出力のキャプチャや、ヘルパープロセスの利用方法は、Goのテストコードを書く上での一般的な知識として参考になります。