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

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

このコミットは、Go言語のテストスイートにおけるテスト実行方法を簡素化し、標準出力の比較をより効率的に行うためのcmpoutという新しいテストディレクティブを導入するものです。これにより、テストファイルの先頭に記述されていた複雑なシェルコマンドが// cmpoutという簡潔な記述に置き換えられ、テストの可読性と保守性が向上しています。

コミット

commit e014cf0e545ca16abfd2a80d541750c6a3809082
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Fri Feb 24 13:17:26 2012 +1100

    test: add cmpout to testlib
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/5699060

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

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

元コミット内容

test: add cmpout to testlib

R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5699060

変更の背景

Go言語のテストスイートでは、特定のテストケースが生成する標準出力(stdout)を、期待される出力ファイルと比較することで、そのテストの正しさを検証するパターンが頻繁に用いられていました。これまでの方法では、各テストファイルの先頭に以下のようなシェルコマンドがコメントとして記述されていました。

// $G $D/$F.go && $L $F.$A && ./$A.out 2>&1 | cmp - $D/$F.out

このコマンドは、Goプログラムのコンパイル、リンク、実行、そしてその標準出力と期待される出力ファイルとの比較という一連の複雑な処理を記述しています。しかし、この記述は冗長であり、多くのテストファイルで繰り返し現れるため、テストコードの可読性を損ね、またテスト実行ロジックの変更があった場合に多数のファイルを修正する必要があるという保守性の問題がありました。

このコミットは、このような共通のテストパターンをcmpoutという新しいディレクティブとして抽象化し、testlibtest/run.goにそのロジックを組み込むことで、テストファイルの記述を簡素化し、テストインフラの保守性を向上させることを目的としています。

前提知識の解説

この変更を理解するためには、以下の概念について知っておく必要があります。

  1. Go言語のテストインフラ: Go言語のプロジェクトでは、testディレクトリ以下に様々なテストケースが配置されています。これらのテストは、Goの標準テストフレームワーク(go test)だけでなく、カスタムのテストスクリプトやユーティリティ(例: test/run.gotest/testlib)によって実行されることがあります。これは、コンパイラやランタイムの低レベルな挙動を検証するためなど、go testだけではカバーしきれない特殊なテスト要件に対応するためです。

  2. test/run.go: これはGo言語のテストスイート内で使用されるカスタムテストランナーの一つです。テストファイルの先頭に記述された特定のコメント行(ディレクティブ)を解析し、それに基づいてテストのコンパイル、実行、検証などのアクションを決定します。例えば、// run// compile// errorcheckといったディレクティブが存在します。

  3. test/testlib: これはシェルスクリプトの関数定義を含むファイルで、test/run.goなどのテストランナーから呼び出される共通のヘルパー関数を提供します。Goテストのコンパイル、リンク、実行、出力比較など、繰り返し使用されるシェルコマンドのロジックがここにカプセル化されています。

  4. シェルコマンドとリダイレクト:

    • $G, $D, $F, $L, $A: これらはシェル変数であり、Goのテストインフラ内で特定のパスやファイル名、アーキテクチャ情報などを表すために使用されます。
      • $G: Goコンパイラへのパス。
      • $D: テストファイルのディレクトリパス。
      • $F: テストファイルのベース名(拡張子なし)。
      • $L: Goリンカへのパス。
      • $A: 実行可能ファイルのアーキテクチャサフィックス(例: .exe)。
    • 2>&1: これはシェルにおけるファイルディスクリプタのリダイレクトです。2は標準エラー出力(stderr)、1は標準出力(stdout)を表します。2>&1は、標準エラー出力を標準出力にリダイレクトすることを意味します。これにより、プログラムの標準出力と標準エラー出力の両方が、後続のパイプ(|)に渡されます。
    • |: パイプ。左側のコマンドの標準出力を、右側のコマンドの標準入力に接続します。
    • cmp - file: cmpコマンドは2つのファイルをバイト単位で比較します。-は標準入力を意味します。したがって、cmp - $D/$F.outは、パイプで渡された入力(プログラムの標準出力と標準エラー出力)を、$D/$F.outというパスにある期待される出力ファイルと比較します。

技術的詳細

このコミットの技術的な核心は、Goのテストインフラにおけるテスト実行フローの抽象化と効率化にあります。

  1. cmpoutディレクティブの導入:

    • これまでテストファイルの先頭に直接記述されていた複雑なシェルコマンド($G $D/$F.go && $L $F.$A && ./$A.out 2>&1 | cmp - $D/$F.out)が、// cmpoutという簡潔なコメントに置き換えられました。
    • これにより、テストの意図がより明確になり、テストファイルの可読性が大幅に向上しました。
  2. test/run.goの変更:

    • test/run.go内のrun()関数に、新しいcmpoutケースが追加されました。
    • case "cmpout": action = "run" // the run case already looks for <dir>/<test>.out files fallthrough
    • このコードは、テストファイルのディレクティブがcmpoutである場合、内部的にaction変数を"run"に設定し、fallthroughキーワードによって次のcase "compile", "build", "run", "errorcheck":ブロックに処理を継続させます。
    • これは、cmpoutが本質的に「テストを実行し、その出力を比較する」というrunアクションの特殊なバリエーションであることを示しています。runアクションは既に<dir>/<test>.outファイルを探すロジックを含んでいるため、このfallthroughによって既存のrunロジックを再利用しつつ、cmpout固有の比較処理をtestlibに委譲する設計になっています。
  3. test/testlibの変更:

    • testlibファイルに新しいシェル関数cmpout()が追加されました。
    • cmpout() { $G $D/$F.go && $L $F.$A && ./$A.out 2>&1 | cmp - $D/$F.out }
    • この関数は、以前テストファイルの先頭に直接記述されていたシェルコマンドと全く同じロジックを含んでいます。
    • test/run.gocmpoutディレクティブを検出すると、最終的にこのcmpout()シェル関数が呼び出され、Goプログラムのコンパイル、リンク、実行、そしてその標準出力と期待される出力ファイル($D/$F.out)との比較が実行されます。

この変更により、テストの実行ロジックがtest/run.gotest/testlibに一元化され、テストファイルの記述が簡素化されました。将来的に出力比較のロジックに変更が必要になった場合でも、testlib内のcmpout()関数を修正するだけで済み、多数のテストファイルを個別に修正する必要がなくなります。

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

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

  1. test/run.go:

    --- a/test/run.go
    +++ b/test/run.go
    @@ -238,6 +238,9 @@ func (t *test) run() {
     	action = strings.TrimSpace(action)
     
     	switch action {
    +	case "cmpout":
    +		action = "run" // the run case already looks for <dir>/<test>.out files
    +		fallthrough
     	case "compile", "build", "run", "errorcheck":
     		t.action = action
     	default:
    
  2. test/testlib:

    --- a/test/testlib
    +++ b/test/testlib
    @@ -17,6 +17,10 @@ run() {
     	$G $D/$F.go && $L $F.$A && ./$A.out "$@"
     }
     
    +cmpout() {
    +	$G $D/$F.go && $L $F.$A && ./$A.out 2>&1 | cmp - $D/$F.out
    +}
    +
     errorcheck() {
     	errchk $G -e $D/$F.go
     }
    

また、以下の複数のテストファイルで、先頭のコメント行が従来の複雑なシェルコマンドから// cmpoutに置き換えられています。

  • test/deferprint.go
  • test/fixedbugs/bug328.go
  • test/fixedbugs/bug409.go
  • test/goprint.go
  • test/helloworld.go
  • test/ken/cplx0.go
  • test/ken/string.go
  • test/printbig.go

コアとなるコードの解説

test/run.goの変更

test/run.goの変更は、test構造体のrun()メソッド内で行われています。このメソッドは、テストファイルの先頭に記述されたディレクティブ(コメント行)を解析し、それに応じたアクションを実行します。

switch action {
case "cmpout":
	action = "run" // the run case already looks for <dir>/<test>.out files
	fallthrough
case "compile", "build", "run", "errorcheck":
	t.action = action
default:
	// ... (既存の処理)
}
  • case "cmpout":: 新しく追加されたケースです。テストファイルのディレクティブが"cmpout"である場合にこのブロックが実行されます。
  • action = "run": cmpoutディレクティブが検出された場合、内部的に実行されるアクションを"run"に設定します。これは、cmpoutが「プログラムを実行する」という基本的な動作を含むためです。
  • // the run case already looks for <dir>/<test>.out files: このコメントは、runアクションの既存のロジックが、テスト対象のGoプログラムの出力と比較するための期待される出力ファイル(例: test/deferprint.out)を自動的に探すことを示唆しています。
  • fallthrough: このキーワードは、現在のcaseブロックの処理が完了した後、次のcaseブロックの条件を評価せずに、そのまま次のcaseブロックの処理を実行することを指示します。これにより、cmpoutディレクティブはrunアクションのロジック(プログラムの実行など)を再利用しつつ、cmpout固有の出力比較ロジックはtestlibに委譲される形になります。

このfallthroughの利用は、Goのswitch文の強力な機能の一つであり、共通の処理を複数のケースで共有しつつ、特定のケースで追加の処理を行う場合に有効です。

test/testlibの変更

test/testlibはシェルスクリプトであり、Goテストの実行に必要な共通のシェル関数を定義しています。

cmpout() {
	$G $D/$F.go && $L $F.$A && ./$A.out 2>&1 | cmp - $D/$F.out
}
  • cmpout(): 新しく定義されたシェル関数です。
  • $G $D/$F.go: Goコンパイラ($G)を使用して、現在のテストファイル($D/$F.go)をコンパイルします。
  • &&: 論理AND演算子。左側のコマンドが成功した場合(終了コードが0の場合)にのみ、右側のコマンドを実行します。
  • $L $F.$A: Goリンカ($L)を使用して、コンパイルされたオブジェクトファイル($F.$A)をリンクし、実行可能ファイルを生成します。
  • ./$A.out: 生成された実行可能ファイルを実行します。
  • 2>&1: 実行可能ファイルの標準エラー出力(stderr)を標準出力(stdout)にリダイレクトします。これにより、プログラムのすべての出力(stdoutとstderr)がパイプに渡されます。
  • |: パイプ。左側のコマンド(プログラムの実行と出力リダイレクト)の標準出力を、右側のコマンド(cmp)の標準入力に渡します。
  • cmp - $D/$F.out: cmpコマンドは、標準入力(-)として受け取ったプログラムの出力と、期待される出力ファイル($D/$F.out)の内容を比較します。両者が一致すればcmpは成功し、そうでなければ失敗します。

このcmpout()関数は、Goテストインフラの外部から直接呼び出されることは少なく、主にtest/run.goのようなテストランナーによって、cmpoutディレクティブが指定されたテストに対して実行されます。これにより、テストの出力比較ロジックが一箇所に集約され、管理が容易になります。

関連リンク

参考にした情報源リンク

  • Go言語のソースコード(特にtestディレクトリ内のファイル)
  • シェルスクリプトのリダイレクトとパイプに関する一般的なドキュメント
  • cmpコマンドのmanページまたはドキュメント
  • Go言語のテストに関する公式ドキュメントやブログ記事(一般的なGoテストの概念理解のため)
  • Goのswitch文におけるfallthroughキーワードの挙動に関するドキュメント