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

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

このコミットは、Go言語のテストスイートにおけるテスト実行方法の改善を目的としています。具体的には、testlibrunoutputコマンドを導入し、生成されたプログラムの実行をより効率的かつGoのツールチェインに統合された形で行えるようにしています。これにより、テストスクリプト内で直接シェルコマンドを記述する代わりに、testlibの抽象化された機能を利用できるようになり、テストコードの可読性と保守性が向上しています。

コミット

commit dda6d6aa7087f51a59bbe60d7b73d170c715ddd0
Author: Shenghou Ma <minux.ma@gmail.com>
Date:   Fri Apr 20 23:45:43 2012 +0800

    test: use testlib in a few more cases (part 2)
            Introduced "runoutput" cmd for running generated program
    
    R=golang-dev, iant, bradfitz, remyoudompheng
    CC=golang-dev
    https://golang.org/cl/5869049

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

https://github.com/golang/go/commit/dda6d6aa7087f51a59bbe60d7b73d170c715ddd0

元コミット内容

test: use testlib in a few more cases (part 2)
        Introduced "runoutput" cmd for running generated program

R=golang-dev, iant, bradfitz, remyoudompheng
CC=golang-dev
https://golang.org/cl/5869049

変更の背景

Go言語のテストフレームワークでは、テストの実行や検証のために様々なスクリプトやヘルパー関数が使用されていました。このコミット以前は、一部のテスト、特にプログラムを生成し、その生成されたプログラムを実行して出力を検証するようなテストでは、シェルスクリプトのコマンド(例: $G $D/$F.go && $L $F.$A && ./$A.out >tmp.go && $G tmp.go && $L tmp.$A && ./$A.out)がテストファイルの先頭に直接記述されていました。

このような直接的なシェルコマンドの記述は、以下のような問題を引き起こしていました。

  1. 重複と冗長性: 同様のテストパターンが多数存在する場合、同じようなシェルコマンドが繰り返し記述され、コードの重複が生じていました。
  2. 保守性の低下: シェルコマンドの構文は複雑になりがちで、変更が必要になった場合に複数のファイルを修正する必要があり、保守が困難でした。
  3. 可読性の低下: テストの意図がシェルコマンドの羅列の中に埋もれてしまい、テストコードの可読性が損なわれていました。
  4. プラットフォーム依存性: シェルコマンドはOSや環境に依存する可能性があり、クロスプラットフォームでのテスト実行に問題が生じる可能性がありました。

このコミットは、これらの問題を解決するために、testlibという既存のテストヘルパーライブラリにrunoutputという新しい抽象化されたコマンドを導入し、テストスクリプトから直接的なシェルコマンドを排除することを目指しています。これにより、テストの記述が簡潔になり、保守性が向上し、Goのテストインフラストラクチャとの統合が強化されます。

前提知識の解説

このコミットを理解するためには、以下のGo言語のテストに関する基本的な知識が必要です。

  • Go言語のテストフレームワーク: Goには標準でtestingパッケージが提供されており、go testコマンドを使ってテストを実行します。しかし、Goのプロジェクト、特にGo自身のテストスイートでは、より複雑なテストシナリオ(例: コンパイルエラーのチェック、ランタイムエラーのチェック、特定の出力の検証など)に対応するために、testlibのようなカスタムのテストヘルパーやスクリプトが使用されることがあります。
  • go runコマンド: go runコマンドは、Goのソースファイルをコンパイルし、その結果生成されたバイナリを実行するコマンドです。開発中に一時的にプログラムを実行する際によく使われます。
  • testlib: Goのテストスイート内で使用される内部的なヘルパーライブラリまたはスクリプトの集合体です。テストのセットアップ、実行、クリーンアップなどの共通のタスクを抽象化し、テストコードの記述を簡潔にする役割を担っています。このコミットでは、testlibがシェルスクリプトとして実装されている部分(test/testlibファイル)と、Goのコードとして実装されている部分(test/run.goなど)の両方に変更が加えられています。
  • tmp.goファイル: 多くのテストシナリオでは、テスト対象のGoプログラムが別のGoプログラムを生成し、その生成されたプログラムをさらに実行して結果を検証するというパターンがあります。この場合、一時的に生成されるGoソースコードを保存するためにtmp.goのような一時ファイルが使用されます。
  • $G, $D, $F, $L, $Aなどの変数: これらはGoのテストスクリプト内で使用されるシェル変数で、それぞれGoコンパイラ、テストファイルのディレクトリ、テストファイル名、Goリンカ、アーキテクチャなどを指すプレースホルダーです。このコミットでは、これらの変数を直接使用するシェルコマンドをrunoutputという抽象化されたコマンドに置き換えています。
  • test/run.go: Goのテストスイートの実行ロジックを管理するGoプログラムです。各テストケースの実行アクション(コンパイル、ビルド、実行、エラーチェックなど)を定義し、それに応じた処理を行います。
  • test/run: Goのテストスイートを実行するためのシェルスクリプトです。test/run.goプログラムを呼び出し、テストの実行環境をセットアップします。

技術的詳細

このコミットの技術的な核心は、Goのテストスイートにおける「プログラムの生成と実行」という共通のパターンを抽象化し、testlibrunoutputという新しいアクションを追加した点にあります。

  1. testlibへのrunoutput関数の追加 (test/testlib): test/testlibはシェルスクリプトとして実装されており、テストヘルパー関数を提供しています。このコミットでは、runoutput()という新しいシェル関数が追加されました。

    --- a/test/testlib
    +++ b/test/testlib
    @@ -13,6 +13,11 @@ build() {\n    $G $D/$F.go && $L $F.$A\n }\n 
    +runoutput() {\n+\tgo run "$D/$F.go" > tmp.go\n+\tgo run tmp.go\n+}\n+\n run() {\n    gofiles=""\n    ingo=true
    

    このrunoutput関数は、以下の2つのステップを実行します。

    • go run "$D/$F.go" > tmp.go: 現在のテストファイル($D/$F.go)をgo runで実行し、その標準出力をtmp.goというファイルにリダイレクトします。これは、テスト対象のGoプログラムが別のGoソースコードを生成するシナリオに対応しています。
    • go run tmp.go: tmp.goに書き出されたGoソースコードをgo runで実行します。これにより、生成されたプログラムが実行され、その結果がテストの検証対象となります。
  2. test/run.goにおけるrunoutputアクションのサポート: test/run.goは、Goのテストスイートのメインの実行ロジックを担うGoプログラムです。このファイルでは、test構造体のactionフィールドに"runoutput"という新しいアクションが追加され、run()メソッド内でこのアクションが処理されるようになりました。

    --- a/test/run.go
    +++ b/test/run.go
    @@ -172,7 +172,7 @@ type test struct {\n    donec       chan bool // closed when done\n 
    \tsrc    string\n-\taction string // "compile", "build", "run", "errorcheck", "skip"\n+\taction string // "compile", "build", "run", "errorcheck", "skip", "runoutput"\n 
    \ttempDir string\n    \terr     error\n@@ -251,7 +251,7 @@ func (t *test) run() {\n    case "cmpout":\n    \taction = "run" // the run case already looks for <dir>/<test>.out files\n    \tfallthrough\n-\tcase "compile", "build", "run", "errorcheck":\n+\tcase "compile", "build", "run", "errorcheck", "runoutput":\n    \tt.action = action\n    case "skip":\n    \tt.action = "skip"\n@@ -316,6 +316,26 @@ func (t *test) run() {\n    \tif string(out) != t.expectedOutput() {\n    \t\tt.err = fmt.Errorf("incorrect output\\n%s", out)\n    \t}\n+\n+\tcase "runoutput":\n+\t\tuseTmp = false\n+\t\tout, err := runcmd("go", "run", t.goFileName())\n+\t\tif err != nil {\n+\t\t\tt.err = fmt.Errorf("%s\\n%s", err, out)\n+\t\t}\n+\t\ttfile := filepath.Join(t.tempDir, "tmp__.go")\n+\t\terr = ioutil.WriteFile(tfile, out, 0666)\n+\t\tif err != nil {\n+\t\t\tt.err = fmt.Errorf("write tempfile:%s", err)\n+\t\t\treturn\n+\t\t}\n+\t\tout, err = runcmd("go", "run", tfile)\n+\t\tif err != nil {\n+\t\t\tt.err = fmt.Errorf("%s\\n%s", err, out)\n+\t\t}\n+\t\tif string(out) != t.expectedOutput() {\n+\t\t\tt.err = fmt.Errorf("incorrect output\\n%s", out)\n+\t\t}\n \t}\n }\n ```
    `runoutput`ケースでは、`go run`コマンドを2回実行しています。
    *   1回目は、元のテストファイル(`t.goFileName()`)を実行し、その出力を一時ファイル(`tmp__.go`)に書き込みます。
    *   2回目は、その一時ファイル(`tmp__.go`)を実行します。
    *   最終的に、2回目の実行結果が期待される出力(`t.expectedOutput()`)と一致するかどうかを検証します。
    
    
  3. テストファイルの簡素化 (test/64bit.go, test/chan/select5.go, test/crlf.go): 以前はテストファイルの先頭に直接記述されていた複雑なシェルコマンドが、単に// runoutputというコメントに置き換えられました。これは、test/run.goがテストファイルの最初の行を読み取り、それが// runoutputであれば、runoutputアクションを実行するように設定されているためです。

    --- a/test/64bit.go
    +++ b/test/64bit.go
    @@ -1,6 +1,4 @@
    -// $G $D/$F.go && $L $F.$A && ./$A.out >tmp.go &&
    -// $G tmp.go && $L tmp.$A && ./$A.out || echo BUG: 64bit
    -// rm -f tmp.go
    +// runoutput
    

    これにより、テストファイルの可読性が大幅に向上し、テストの意図がより明確になりました。

  4. クリーンアップの改善 (test/run): test/runシェルスクリプトでは、テスト実行後に一時ファイルtmp.goが確実に削除されるように、クリーンアップ処理が追加されました。

    --- a/test/run
    +++ b/test/run
    @@ -100,7 +100,7 @@ do
    \t\t\techo $i >>pass.out\n    \t\tfi\n    \t\techo $(awk 'NR==1{print $2}' "$TMP2FILE") $D/$F >>times.out\n-\t\trm -f $F.$A $A.out\n+\t\trm -f $F.$A $A.out tmp.go\n    \t) done\n    done | # clean up some stack noise\n    \tegrep -v '^(r[0-9a-z]+|[cfg]s)  +0x'  |
    

この変更により、Goのテストスイートはよりモジュール化され、テストの記述が簡潔になり、将来的なテストインフラストラクチャの拡張が容易になりました。

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

このコミットにおけるコアとなるコードの変更箇所は以下のファイルに集中しています。

  1. test/64bit.go, test/chan/select5.go, test/crlf.go: これらのテストファイルの先頭から、複雑なシェルコマンドが削除され、代わりに// runoutputというコメントが追加されました。

    例: test/64bit.go

    --- a/test/64bit.go
    +++ b/test/64bit.go
    @@ -1,6 +1,4 @@
    -// $G $D/$F.go && $L $F.$A && ./$A.out >tmp.go &&
    -// $G tmp.go && $L tmp.$A && ./$A.out || echo BUG: 64bit
    -// rm -f tmp.go
    +// runoutput
    
  2. test/run: テスト実行後のクリーンアップ処理にtmp.goの削除が追加されました。

    --- a/test/run
    +++ b/test/run
    @@ -100,7 +100,7 @@ do
    \t\t\techo $i >>pass.out\n    \t\tfi\n    \t\techo $(awk 'NR==1{print $2}' "$TMP2FILE") $D/$F >>times.out\n-\t\trm -f $F.$A $A.out\n+\t\trm -f $F.$A $A.out tmp.go\n    \t) done\n    done | # clean up some stack noise\n    \tegrep -v '^(r[0-9a-z]+|[cfg]s)  +0x'  |
    
  3. test/run.go: test構造体のactionフィールドに"runoutput"が追加され、run()メソッド内に"runoutput"ケースの処理ロジックが実装されました。

    --- a/test/run.go
    +++ b/test/run.go
    @@ -172,7 +172,7 @@ type test struct {\n    donec       chan bool // closed when done\n 
    \tsrc    string\n-\taction string // "compile", "build", "run", "errorcheck", "skip"\n+\taction string // "compile", "build", "run", "errorcheck", "skip", "runoutput"\n 
    \ttempDir string\n    \terr     error\n@@ -251,7 +251,7 @@ func (t *test) run() {\n    case "cmpout":\n    \taction = "run" // the run case already looks for <dir>/<test>.out files\n    \tfallthrough\n-\tcase "compile", "build", "run", "errorcheck":\n+\tcase "compile", "build", "run", "errorcheck", "runoutput":\n    \tt.action = action\n    case "skip":\n    \tt.action = "skip"\n@@ -316,6 +316,26 @@ func (t *test) run() {\n    \tif string(out) != t.expectedOutput() {\n    \t\tt.err = fmt.Errorf("incorrect output\\n%s", out)\n    \t}\n+\n+\tcase "runoutput":\n+\t\tuseTmp = false\n+\t\tout, err := runcmd("go", "run", t.goFileName())\n+\t\tif err != nil {\n+\t\t\tt.err = fmt.Errorf("%s\\n%s", err, out)\n+\t\t}\n+\t\ttfile := filepath.Join(t.tempDir, "tmp__.go")\n+\t\terr = ioutil.WriteFile(tfile, out, 0666)\n+\t\tif err != nil {\n+\t\t\tt.err = fmt.Errorf("write tempfile:%s", err)\n+\t\t\treturn\n+\t\t}\n+\t\tout, err = runcmd("go", "run", tfile)\n+\t\tif err != nil {\n+\t\t\tt.err = fmt.Errorf("%s\\n%s", err, out)\n+\t\t}\n+\t\tif string(out) != t.expectedOutput() {\n+\t\t\tt.err = fmt.Errorf("incorrect output\\n%s", out)\n+\t\t}\n \t}\n }\n ```
    
    
  4. test/testlib: runoutput()シェル関数が追加されました。

    --- a/test/testlib
    +++ b/test/testlib
    @@ -13,6 +13,11 @@ build() {\n    $G $D/$F.go && $L $F.$A\n }\n 
    +runoutput() {\n+\tgo run "$D/$F.go" > tmp.go\n+\tgo run tmp.go\n+}\n+\n run() {\n    gofiles=""\n    ingo=true
    

コアとなるコードの解説

test/64bit.go, test/chan/select5.go, test/crlf.go の変更

これらのファイルは、Goのテストスイートの一部であり、特定の言語機能やコンパイラの挙動をテストするために使用されます。変更前は、テストの実行ロジックがファイルの先頭にシェルコマンドとして直接記述されていました。例えば、test/64bit.goでは、Goプログラムをコンパイルし、実行し、その出力から一時的なGoファイルを生成し、さらにその一時ファイルをコンパイル・実行するという一連の複雑なステップが記述されていました。

変更後、これらの複雑なシェルコマンドは// runoutputという単一行のコメントに置き換えられました。これは、test/run.goがテストファイルの最初の行を解析し、このコメントが存在する場合に、新しく導入されたrunoutputアクションを実行するように設計されているためです。これにより、テストファイルの記述が大幅に簡素化され、テストの目的がより明確になりました。

test/run の変更

test/runは、Goのテストスイートを実行するためのトップレベルのシェルスクリプトです。このスクリプトは、テストの実行環境を設定し、test/run.goプログラムを呼び出して実際のテストを実行します。

変更点であるrm -f $F.$A $A.out tmp.goは、テスト実行後に生成される可能性のある一時ファイル(tmp.go)を確実に削除するためのものです。以前はtmp.goの削除が明示的に行われていなかったため、テストの失敗や中断時に一時ファイルが残り、ディスクスペースの消費や後続のテスト実行に影響を与える可能性がありました。この変更により、テスト環境のクリーンアップが改善され、テストの信頼性が向上します。

test/run.go の変更

test/run.goは、Goのテストスイートの実行エンジンです。各テストケースの実行方法を決定し、それに応じたGoコマンドを実行します。

  • actionフィールドへの"runoutput"の追加: test構造体内のactionフィールドは、テストがどのような種類のアクションを実行するかを定義します。以前は"compile", "build", "run", "errorcheck", "skip"などのアクションがありました。このコミットで"runoutput"が追加されたことにより、test/run.goは「プログラムを生成し、その生成されたプログラムを実行する」という新しいテストパターンを認識し、適切に処理できるようになりました。

  • run()メソッド内の"runoutput"ケースの実装: run()メソッドは、test構造体のactionフィールドに基づいて異なる処理を実行します。新しく追加された"runoutput"ケースは、以下の手順でテストを実行します。

    1. runcmd("go", "run", t.goFileName()): まず、元のテストファイル(t.goFileName())をgo runコマンドで実行します。この実行の出力は、通常、別のGoソースコードです。
    2. ioutil.WriteFile(tfile, out, 0666): 上記のgo runの出力を、一時ファイル(tmp__.go)に書き込みます。filepath.Join(t.tempDir, "tmp__.go")は、一時ファイルがテスト固有の一時ディレクトリ内に作成されることを保証します。
    3. runcmd("go", "run", tfile): 次に、書き込まれた一時ファイル(tmp__.go)をgo runコマンドで実行します。これが、テスト対象の「生成されたプログラム」の実際の実行です。
    4. if string(out) != t.expectedOutput(): 最後に、2回目のgo runの出力が、テストケースで定義された期待される出力(t.expectedOutput())と一致するかどうかを検証します。一致しない場合はエラーとして報告されます。

この実装により、test/run.goは、テストファイルに直接シェルコマンドを記述することなく、「プログラム生成と実行」のテストシナリオをGoのコード内で効率的に管理できるようになりました。エラーハンドリングも適切に行われ、実行中の問題が捕捉されます。

test/testlib の変更

test/testlibは、Goのテストスイート内で使用されるシェルスクリプトベースのヘルパー関数群です。

  • runoutput() シェル関数の追加: このコミットで追加されたrunoutput()関数は、test/run.go"runoutput"アクションと連携して機能します。このシェル関数は、go run "$D/$F.go" > tmp.gogo run tmp.goという2つのシェルコマンドをラップしています。 このシェル関数は、test/run.go// runoutputコメントを検出する前の、古いテスト実行メカニズムとの互換性や、特定のシェルスクリプトベースのテストシナリオで利用される可能性があります。しかし、このコミットの主な目的は、test/run.goを介してGoのコード内でrunoutputロジックを処理することにあります。test/testlib内のrunoutputは、Goのテストフレームワークが進化する過程で、一時的に存在したか、あるいは特定のレガシーなテストケースをサポートするために残された可能性があります。

全体として、これらの変更はGoのテストインフラストラクチャをより堅牢で、保守しやすく、Goのツールチェインと密接に統合されたものにするための重要なステップです。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード (特にtest/ディレクトリ): https://github.com/golang/go
  • Goのgo runコマンドに関するドキュメント: https://go.dev/cmd/go/#hdr-Run_main_package
  • Goのテストフレームワークの内部動作に関する一般的な知識
  • Gitのコミット履歴と差分表示
  • Goのコードレビューシステム (Gerrit) の変更リスト (CL) の概念
  • Goのテストスイートの進化に関する一般的な情報 (Web検索を通じて得られた情報)