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

[インデックス 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言語自身で記述されたテストの方が、以下のような多くの利点をもたらします。

  1. 統合性: Go言語のビルドシステムやツールチェインとシームレスに連携します。
  2. 可搬性: シェルスクリプトはOSや環境に依存する場合がありますが、Goコードはクロスプラットフォームで動作します。
  3. 保守性: Go言語の型システムやコンパイラによるチェックの恩恵を受け、リファクタリングや変更が容易になります。
  4. 表現力: 複雑なテストロジックやデータ生成をGo言語の豊富なライブラリと構文で記述できます。
  5. 並列実行: 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の主要な機能と構造は以下の通りです。

  1. フラグの定義:

    • -root: テスト対象のGoファイルを探すルートディレクトリを指定します。デフォルトはGOROOTです。
    • -files: 特定のファイルをカンマ区切りで指定してテストを実行できます。
    • -n: テストに使用するゴルーチンの数を指定します。デフォルトはCPUの数です。
    • -verbose: 詳細モードを有効にします。
  2. gofmt関数:

    • gofmt(filename string, src *bytes.Buffer) error
    • 指定されたファイルの内容をgofmtでフォーマットし、結果をsrcバッファに書き込みます。
    • parse関数(gofmtの内部関数)でソースコードをAST(抽象構文木)にパースし、ast.SortImportsでインポートをソートした後、printer.Fprintでフォーマットされたコードを生成します。
  3. testFile関数:

    • testFile(t *testing.T, b1, b2 *bytes.Buffer, filename string)
    • 個々のGoファイルをテストするためのヘルパー関数です。
    • ファイルの内容を読み込み、構文エラーがないことを確認します(構文エラーのあるファイルはテスト対象から除外されます)。
    • 冪等性の検証:
      1. ファイルを一度gofmtでフォーマットし、結果をb1に保存します。
      2. b1の内容を再度gofmtでフォーマットし、結果をb2に保存します。
      3. b1b2の内容が完全に一致するかをbytes.Compareで比較します。一致しない場合、冪等性が破れているとしてエラーを報告します。
  4. testFiles関数:

    • testFiles(t *testing.T, filenames <-chan string, done chan<- int)
    • 複数のゴルーチンで並列にファイルテストを実行するための関数です。
    • filenamesチャネルからファイル名を受け取り、各ファイルに対してtestFileを呼び出します。
    • 処理が完了するとdoneチャネルにシグナルを送ります。
  5. genFilenames関数:

    • genFilenames(t *testing.T, filenames chan<- string)
    • テスト対象のGoファイル名を生成し、filenamesチャネルに送信します。
    • -filesフラグが指定されている場合は、そのファイルのみを処理します。
    • 指定されていない場合は、-rootディレクトリ以下のすべてのGoファイルをfilepath.Walkで再帰的に探索します。
  6. TestAll関数:

    • TestAll(t *testing.T)
    • このテストスイートのエントリポイントとなるテスト関数です。go testコマンドによって自動的に発見され実行されます。
    • testing.Short()が有効な場合はスキップされます(長時間かかるテストのため)。
    • genFilenamesゴルーチンを起動してファイル名の生成を開始します。
    • -nフラグで指定された数のtestFilesゴルーチンを起動し、並列でファイルテストを実行します。
    • すべてのtestFilesゴルーチンが完了するのを待ちます。

この新しいテストスイートは、Go言語の並行処理機能(ゴルーチンとチャネル)を積極的に活用しており、大規模なコードベースに対しても効率的にgofmtの冪等性テストを実行できるよう設計されています。

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

このコミットにおけるコアとなるコードの変更は、以下の2つのファイルの追加と削除です。

  1. src/cmd/gofmt/long_test.go の追加:

    • このファイルは、gofmtのテストロジックをGo言語で実装したものです。
    • 157行が追加されました。
    • Goのtestingパッケージを利用し、gofmtの冪等性を検証するテスト関数TestAllを含んでいます。
    • 並列処理(ゴルーチンとチャネル)を用いて、指定されたディレクトリ(デフォルトはGOROOT)内のすべてのGoファイルを効率的にテストします。
  2. 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/astgo/printergofmtのコア機能に関連するパッケージです。
  • フラグ変数: 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が実際にコードをフォーマットする際の主要なステップを模倣しています。parseast.SortImportsprinter.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言語の一般的な知識に基づいて解説を生成しました)。