[インデックス 11708] ファイルの概要
このコミットは、Go言語の公式フォーマッタであるgofmt
のテストメカニズムに関するものです。具体的には、これまでシェルスクリプト(test.sh
)で行われていたテストを、Go言語の標準テストフレームワーク(go test
)を利用したGoコード(long_test.go
)に置き換える変更が行われました。これにより、テストの実行、保守、拡張がよりGo言語のエコシステムに統合され、効率的かつ堅牢になることが期待されます。
コミット
- コミットハッシュ:
467f8751f91516fa13c85f1604212d5974190ec4
- 作者: Robert Griesemer gri@golang.org
- コミット日時: 2012年2月8日(水)08:47:02 -0800
- コミットメッセージ:
gofmt: replace defunct test.sh with a go test R=r, rsc CC=golang-dev https://golang.org/cl/5639053
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/467f8751f91516fa13c85f1604212d5974190ec4
元コミット内容
commit 467f8751f91516fa13c85f1604212d5974190ec4
Author: Robert Griesemer <gri@golang.org>
Date: Wed Feb 8 08:47:02 2012 -0800
gofmt: replace defunct test.sh with a go test
R=r, rsc
CC=golang-dev
https://golang.org/cl/5639053
---
src/cmd/gofmt/long_test.go | 157 ++++++++++++++++++++++++++++++++++++++++++\n
src/cmd/gofmt/test.sh | 168 ---------------------------------------------\n
2 files changed, 157 insertions(+), 168 deletions(-)\n
変更の背景
このコミットの主な背景は、gofmt
のテストスイートの近代化とGo言語エコシステムへの統合です。コミットメッセージにある「defunct test.sh
」(機能停止したtest.sh
)という記述から、既存のシェルスクリプトによるテストが何らかの理由で適切に機能していなかったか、あるいは保守が困難になっていたことが示唆されます。
Go言語は、その設計思想の中心に「シンプルさ」と「効率性」を置いており、テストに関してもgo test
コマンドとtesting
パッケージという強力な標準ツールを提供しています。プロジェクトが成熟し、Go言語の機能が充実するにつれて、シェルスクリプトのような外部ツールに依存するテストよりも、Go言語自身で記述されたテストの方が、以下のような多くの利点をもたらします。
- 統合性: Go言語のビルドシステムやツールチェインとシームレスに連携します。
- 可搬性: シェルスクリプトはOSや環境に依存する場合がありますが、Goコードはクロスプラットフォームで動作します。
- 保守性: Go言語の型システムやコンパイラによるチェックの恩恵を受け、リファクタリングや変更が容易になります。
- 表現力: 複雑なテストロジックやデータ生成をGo言語の豊富なライブラリと構文で記述できます。
- 並列実行:
go test
はテストの並列実行をサポートしており、テスト時間の短縮に貢献します。
このような背景から、gofmt
のテストをGo言語ネイティブの形式に移行することは、プロジェクト全体の品質と開発効率を向上させるための自然な流れであったと考えられます。
前提知識の解説
gofmt
gofmt
は、Go言語のソースコードを標準的なスタイルに自動的にフォーマットするツールです。Go言語のコードベース全体で一貫したコーディングスタイルを強制することで、コードの可読性を高め、レビュープロセスを簡素化し、開発者がスタイルに関する議論に時間を費やすのを防ぐことを目的としています。gofmt
はGo言語のツールチェインの不可欠な部分であり、多くのGo開発者にとって日常的に使用されるツールです。
Go言語のtesting
パッケージとgo test
コマンド
Go言語には、ユニットテスト、ベンチマークテスト、サンプルテストをサポートするための組み込みのtesting
パッケージとgo test
コマンドが用意されています。
testing
パッケージ: テスト関数を定義するための基本的な機能(例:*testing.T
型、t.Error()
,t.Errorf()
,t.Fatal()
,t.Fatalf()
など)を提供します。テスト関数はTestXxx(*testing.T)
というシグネチャを持ち、_test.go
というサフィックスを持つファイルに配置されます。go test
コマンド:testing
パッケージで記述されたテストを実行するためのコマンドです。テストの発見、コンパイル、実行、結果の表示を自動的に行います。-v
(詳細出力)、-run
(特定のテストの実行)、-cpu
(並列実行するCPU数)、-short
(短時間テストの実行)などの便利なフラグを提供します。
シェルスクリプトによるテストの限界
シェルスクリプトは、シンプルなコマンドの実行やファイル操作には非常に便利ですが、複雑なロジック、エラーハンドリング、並列処理、大規模なテストデータの管理などにおいては限界があります。特に、Go言語のようなコンパイル型言語のコードをテストする場合、シェルスクリプトでは以下のような課題が生じることがあります。
- エラーハンドリングの複雑さ: シェルスクリプトでの堅牢なエラーハンドリングは記述が複雑になりがちです。
- 可読性と保守性: 複雑なシェルスクリプトは可読性が低く、他の開発者が理解し、保守するのが難しい場合があります。
- 環境依存性: 使用するシェル(bash, zshなど)やOSのユーティリティ(find, grepなど)のバージョンによって挙動が異なる可能性があります。
- テスト結果の解析: テスト結果を構造化された形式で取得し、解析するのが困難です。
- 並列実行の困難さ: 複数のテストを効率的に並列実行するための組み込みメカニズムがありません。
これらの理由から、Go言語プロジェクトでは、テストの大部分をGo言語自身で記述することが推奨されます。
技術的詳細
このコミットでは、gofmt
のテストをシェルスクリプトからGo言語のテストに移行するために、src/cmd/gofmt/long_test.go
という新しいファイルが追加されました。このファイルは、gofmt
の主要な機能であるコードフォーマットの「冪等性(idempotency)」を検証することに焦点を当てています。冪等性とは、同じ入力を複数回適用しても、常に同じ出力が得られるという性質です。gofmt
の場合、一度フォーマットされたコードを再度gofmt
にかけても、そのコードは変化しないべきである、ということを意味します。
long_test.go
の主要な機能と構造は以下の通りです。
-
フラグの定義:
-root
: テスト対象のGoファイルを探すルートディレクトリを指定します。デフォルトはGOROOT
です。-files
: 特定のファイルをカンマ区切りで指定してテストを実行できます。-n
: テストに使用するゴルーチンの数を指定します。デフォルトはCPUの数です。-verbose
: 詳細モードを有効にします。
-
gofmt
関数:gofmt(filename string, src *bytes.Buffer) error
- 指定されたファイルの内容を
gofmt
でフォーマットし、結果をsrc
バッファに書き込みます。 parse
関数(gofmt
の内部関数)でソースコードをAST(抽象構文木)にパースし、ast.SortImports
でインポートをソートした後、printer.Fprint
でフォーマットされたコードを生成します。
-
testFile
関数:testFile(t *testing.T, b1, b2 *bytes.Buffer, filename string)
- 個々のGoファイルをテストするためのヘルパー関数です。
- ファイルの内容を読み込み、構文エラーがないことを確認します(構文エラーのあるファイルはテスト対象から除外されます)。
- 冪等性の検証:
- ファイルを一度
gofmt
でフォーマットし、結果をb1
に保存します。 b1
の内容を再度gofmt
でフォーマットし、結果をb2
に保存します。b1
とb2
の内容が完全に一致するかをbytes.Compare
で比較します。一致しない場合、冪等性が破れているとしてエラーを報告します。
- ファイルを一度
-
testFiles
関数:testFiles(t *testing.T, filenames <-chan string, done chan<- int)
- 複数のゴルーチンで並列にファイルテストを実行するための関数です。
filenames
チャネルからファイル名を受け取り、各ファイルに対してtestFile
を呼び出します。- 処理が完了すると
done
チャネルにシグナルを送ります。
-
genFilenames
関数:genFilenames(t *testing.T, filenames chan<- string)
- テスト対象のGoファイル名を生成し、
filenames
チャネルに送信します。 -files
フラグが指定されている場合は、そのファイルのみを処理します。- 指定されていない場合は、
-root
ディレクトリ以下のすべてのGoファイルをfilepath.Walk
で再帰的に探索します。
-
TestAll
関数:TestAll(t *testing.T)
- このテストスイートのエントリポイントとなるテスト関数です。
go test
コマンドによって自動的に発見され実行されます。 testing.Short()
が有効な場合はスキップされます(長時間かかるテストのため)。genFilenames
ゴルーチンを起動してファイル名の生成を開始します。-n
フラグで指定された数のtestFiles
ゴルーチンを起動し、並列でファイルテストを実行します。- すべての
testFiles
ゴルーチンが完了するのを待ちます。
この新しいテストスイートは、Go言語の並行処理機能(ゴルーチンとチャネル)を積極的に活用しており、大規模なコードベースに対しても効率的にgofmt
の冪等性テストを実行できるよう設計されています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更は、以下の2つのファイルの追加と削除です。
-
src/cmd/gofmt/long_test.go
の追加:- このファイルは、
gofmt
のテストロジックをGo言語で実装したものです。 157
行が追加されました。- Goの
testing
パッケージを利用し、gofmt
の冪等性を検証するテスト関数TestAll
を含んでいます。 - 並列処理(ゴルーチンとチャネル)を用いて、指定されたディレクトリ(デフォルトは
GOROOT
)内のすべてのGoファイルを効率的にテストします。
- このファイルは、
-
src/cmd/gofmt/test.sh
の削除:- このファイルは、これまで
gofmt
のテストに使用されていたシェルスクリプトです。 168
行が削除されました。- このシェルスクリプトは、
gofmt
のサイレントモード、冪等性、有効性(コンパイル可能か)をテストしていましたが、Go言語ネイティブのテストに置き換えられました。
- このファイルは、これまで
コアとなるコードの解説
src/cmd/gofmt/long_test.go
このファイルは、Go言語の標準テストフレームワークに則って記述されています。
- パッケージ宣言:
package main
gofmt
コマンドと同じパッケージに属しているため、gofmt
の内部関数や変数にアクセスできます。
- インポート:
bytes
,flag
,fmt
,go/ast
,go/printer
,io
,os
,path/filepath
,runtime
,strings
,testing
- テストに必要な標準ライブラリがインポートされています。特に
go/ast
とgo/printer
はgofmt
のコア機能に関連するパッケージです。
- テストに必要な標準ライブラリがインポートされています。特に
- フラグ変数:
root
,files
,ngo
,verbose
,nfiles
go test
コマンド実行時に引数として渡せるオプションを定義しています。
gofmt
関数:func gofmt(filename string, src *bytes.Buffer) error { f, _, err := parse(filename, src.Bytes(), false) // gofmtの内部関数parseを呼び出し if err != nil { return err } ast.SortImports(fset, f) // インポートのソート src.Reset() return (&printer.Config{printerMode, *tabWidth}).Fprint(src, fset, f) // フォーマット結果をsrcに書き込み }
- この関数は、
gofmt
が実際にコードをフォーマットする際の主要なステップを模倣しています。parse
、ast.SortImports
、printer.Fprint
といったgofmt
の内部ロジックを直接利用することで、実際のgofmt
の挙動を正確にテストしています。
- この関数は、
testFile
関数:func testFile(t *testing.T, b1, b2 *bytes.Buffer, filename string) { // ... ファイル読み込み、構文エラーチェック ... // 1回目のgofmt if err = gofmt(filename, b1); err != nil { t.Errorf("1st gofmt failed: %v", err) return } // 結果をコピー b2.Reset() b2.Write(b1.Bytes()) // 2回目のgofmt if err = gofmt(filename, b2); err != nil { t.Errorf("2nd gofmt failed: %v", err) return } // 1回目と2回目の結果が同一か比較(冪等性の検証) if bytes.Compare(b1.Bytes(), b2.Bytes()) != 0 { t.Errorf("%s: not idempotent", filename) } }
gofmt
の冪等性を検証する核心部分です。同じ入力に対して2回gofmt
を適用し、その結果が同じであることを確認します。これはgofmt
の重要な特性であり、このテストによってその保証が維持されます。
TestAll
関数:func TestAll(t *testing.T) { if testing.Short() { return // -shortフラグが指定された場合はスキップ } // ... ゴルーチンとチャネルを使った並列テスト実行のセットアップ ... // ファイル名生成ゴルーチン filenames := make(chan string, 32) go genFilenames(t, filenames) // テスト実行ゴルーチンを複数起動 done := make(chan int) for i := 0; i < *ngo; i++ { go testFiles(t, filenames, done) } // 全てのテストゴルーチンの完了を待つ for i := 0; i < *ngo; i++ { <-done } }
go test
コマンドが実行するメインのテスト関数です。testing.Short()
によるテストのスキップ、並列テストのためのゴルーチンとチャネルのセットアップ、そしてすべてのテストゴルーチンの完了を待つロジックが含まれています。これにより、大規模なGoコードベースに対しても効率的にテストを実行できます。
src/cmd/gofmt/test.sh
削除されたシェルスクリプトは、以下のような機能を持っていました。
eval $(go tool make --no-print-directory -f ../../Make.inc go-env)
: Goの環境変数を設定。apply1
,applydot
,apply
:find
コマンドとgrep
コマンドを組み合わせてGoファイルを探し、gofmt
を適用。silent
:gofmt
をサイレントモードで実行し、エラーがないか確認。idempotent
:gofmt
を複数回実行し、結果が同じであることをcmp -s
で確認(GoテストのtestFile
関数と同じ目的)。valid
:gofmt
でフォーマットされたファイルがGC
(Goコンパイラ)でコンパイル可能か確認(この部分はコメントアウトされ、無効化されていた)。runtest
,runtests
: 上記のテスト関数を呼び出す。
このシェルスクリプトは、Go言語のテストフレームワークが成熟する前の過渡期に、テストを実行するための実用的な手段として機能していたと考えられます。しかし、Go言語ネイティブのテストに置き換えられることで、前述のシェルスクリプトの限界が解消され、より堅牢で保守性の高いテスト環境が実現されました。
関連リンク
- Go CL (Change List) へのリンク: https://golang.org/cl/5639053
参考にした情報源リンク
- 特になし(コミットメッセージと差分、Go言語の一般的な知識に基づいて解説を生成しました)。