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

[インデックス 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.gogoコマンドを呼び出すようにする」ことです。これにより、「複数ファイルテストや引数付きテストの実行が可能になる」と説明されています。

変更の背景

Go言語の初期のテストフレームワークは、個々のテストケースを実行するために、コンパイラ(gc)やリンカ(ld)といった低レベルのツールを直接呼び出していました。これは、シンプルな単一ファイルテストには機能しましたが、以下のような課題がありました。

  1. 複数ファイルテストの困難さ: 複数のGoソースファイルにまたがるテストケース(例えば、テスト対象のコードが複数のファイルに分割されている場合や、テストヘルパー関数が別のファイルに定義されている場合)を効率的に扱うことが困難でした。低レベルツールを直接操作する場合、依存関係の解決やビルドプロセスの管理が複雑になります。
  2. 引数付きテストの非対応: テスト対象のプログラムにコマンドライン引数を渡して動作を検証するようなシナリオに対応していませんでした。これは、より現実的なアプリケーションのテストを行う上で制約となります。
  3. テストスクリプトの複雑性: 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 buildgo 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関数が、テストファイルのコンパイルとリンクを明示的に制御していました。

  1. ソースコードの読み込みと一時ディレクトリへの書き込み: テスト対象のGoソースコードを読み込み、一時ディレクトリに書き出します。
  2. コンパイル: exec.Command("go", "tool", gc, ...) を使用して、gcコンパイラを直接呼び出し、Goソースファイルをオブジェクトファイルにコンパイルしていました。
  3. リンク: コンパイルが成功した後、exec.Command("go", "tool", ld, ...) を使用して、ldリンカを直接呼び出し、オブジェクトファイルを実行可能ファイルにリンクしていました。
  4. 実行: 最後に、生成された実行可能ファイルを直接起動していました。

このアプローチは、各ステップを細かく制御できる反面、Goモジュールやパッケージの依存関係解決、複数ファイルのビルド、コマンドライン引数の処理といった複雑なタスクをrun.go自身が管理する必要がありました。

変更後のアプローチ: 変更後は、goコマンドの高レベルな抽象化を利用することで、これらの複雑なタスクをgoコマンドに委譲しています。

  1. runcmdヘルパー関数の導入:
    • runcmdという新しい内部ヘルパー関数が導入されました。この関数は、任意のコマンドとその引数を受け取り、exec.Commandを使用して実行します。
    • 標準出力と標準エラー出力をキャプチャし、コマンドの実行ディレクトリ(cmd.Dir)を適切に設定します(一時ディレクトリまたは現在の作業ディレクトリ)。
    • 環境変数GOOSGOARCHを明示的に設定することで、クロスコンパイル環境でのテスト実行にも対応しています。
  2. 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プログラムも適切に処理できるため、複数ファイルテストのサポートが自然に実現されます。
  3. go buildの活用:
    • action"build"の場合、runcmd関数を使って go build コマンドを呼び出すようになりました。
    • go buildは、実行可能ファイルを生成しますが、実行はしません。
  4. go tool gcの継続利用(errorcheckおよびcompileアクション):
    • errorcheckアクション(コンパイルエラーのチェック)やcompileアクション(コンパイルのみ)では、引き続きgo tool gcが使用されています。これは、これらのアクションがコンパイルの特定の側面(エラーメッセージの解析など)に焦点を当てているため、低レベルのコンパイラツールを直接呼び出す方が適しているためと考えられます。
  5. test/testlibの変更:
    • test/testlibは、Goプロジェクトのテストスクリプトが利用するシェルヘルパー関数群です。このコミットでは、runシェル関数が変更され、コマンドライン引数から.goファイルを抽出し、それらをgo runコマンドに渡すロジックが追加されました。これにより、シェルスクリプトレベルでも複数ファイルテストのサポートが強化されています。
  6. エラーメッセージの改善:
    • errorCheck関数が改善され、エラーメッセージ内のファイルパスから一時ディレクトリのパスを削除し、より簡潔で読みやすい形式で表示されるようになりました。

この変更により、run.goはGoツールチェインのより高レベルな機能を利用するようになり、テスト実行ロジックが簡素化され、同時に機能が拡張されました。

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

このコミットの主要な変更は以下のファイルに集中しています。

  1. test/run.go: テスト実行ロジックの核心部分。
    • runcmdヘルパー関数の追加。
    • runアクションがgo runを使用するように変更。
    • buildアクションがgo buildを使用するように変更。
    • errorCheck関数のシグネチャ変更と、エラーメッセージの整形ロジックの追加。
  2. test/cmplxdivide.go: テストファイルのコメント変更。
    • // $G $D/$F.go $D/cmplxdivide1.go && $L $D/$F.$A && ./$A.out から // run cmplxdivide1.go へ変更。これは、新しいrun.go// runディレクティブの解釈方法に合わせたものです。
  3. 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.gocmplxdivide1.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 }

testlibrun関数は、シェルスクリプト内でGoファイルを処理するためのヘルパーです。変更前は単一のGoファイルと引数を想定していましたが、変更後はループを使って引数の中から.goで終わるファイルをすべて抽出し、gofiles変数に格納しています。そして、このgofiles$G(Goコンパイラまたはgo runコマンドを抽象化したもの)に渡すことで、複数ファイルのコンパイル・実行をサポートしています。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード(特にsrc/cmd/gosrc/cmd/compilesrc/cmd/linkのディレクトリ構造)
  • Go言語のコミット履歴とコードレビューシステム (Gerrit/CL): https://go.dev/cl/5753068
  • Go言語のIssue Tracker: https://go.dev/issue
  • Go言語のブログや技術記事(go rungo buildの内部動作に関するもの)