[インデックス 12524] ファイルの概要
このコミットは、Go言語の標準ライブラリであるgo/parser
パッケージのテストコードの構造を改善するものです。具体的には、短く独立したテストケースをparser_test.go
およびerror_test.go
からshort_test.go
という新しいファイルに移動し、テストハーネスとして活用することで、テストの整理と保守性の向上を図っています。
変更されたファイルは以下の通りです。
src/pkg/go/parser/error_test.go
: 既存のテストヘルパー関数checkErrors
のシグネチャが変更され、テスト入力としてファイルだけでなく直接文字列も受け入れられるようになりました。また、getFile
関数に重複ファイル名チェックが追加されました。src/pkg/go/parser/parser_test.go
: 多数のテストケース(有効なプログラムと不正な入力)が削除され、それらをテストしていた関数も削除されました。これらのテストはshort_test.go
に移行されました。src/pkg/go/parser/short_test.go
: 新規作成されたファイルで、Goパーサーのテストに使用される短く独立した有効なプログラムと不正な入力の文字列リテラルが定義され、それらをテストする関数が実装されています。
コミット
commit 9b7b574edcff14d916215a72b7a9fc8bb82ab16e
Author: Robert Griesemer <gri@golang.org>
Date: Thu Mar 8 08:53:31 2012 -0800
go/parser: use test harness for short tests
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5782044
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9b7b574edcff14d916215a72b7a9fc8bb82ab16e
元コミット内容
go/parser: use test harness for short tests
R=golang-dev, rsc
CC=golang-dev
https://golang.org/cl/5782044
変更の背景
このコミットの主な背景は、Go言語のパーサー(go/parser
パッケージ)のテストコードの整理と効率化です。以前は、parser_test.go
やerror_test.go
といった既存のテストファイル内に、多数の短く独立したテストケース(有効なGoコードスニペットや意図的に不正なGoコードスニペット)が直接記述されていました。
このような構造では、テストケースが増えるにつれてファイルの肥大化や管理の複雑化を招く可能性があります。また、異なるテストファイル間で類似のテストロジックやデータが重複する可能性も考えられます。
このコミットは、これらの「短いテスト」を専用の「テストハーネス」に集約することで、以下の目的を達成しようとしています。
- テストコードのモジュール化と整理: 特定の種類のテスト(この場合は短いコードスニペットのパーステスト)を一つのファイルにまとめることで、コードベース全体のテスト構造をより明確にし、見通しを良くします。
- 保守性の向上: テストケースの追加や変更が、特定のファイル(
short_test.go
)に集中するため、関連する変更箇所を特定しやすくなります。 - 再利用性の促進:
short_test.go
で定義された有効/無効なコードスニペットのリストは、他のテスト関数や将来のテストで再利用しやすくなります。 - テスト実行の柔軟性: テストハーネスの導入により、特定の種類のテストのみを実行したり、異なる入力ソース(ファイルまたは文字列リテラル)でテストを実行したりする際の柔軟性が向上します。
要するに、この変更は、Goパーサーのテストスイートをより構造化され、管理しやすく、効率的なものにするためのリファクタリングの一環です。
前提知識の解説
このコミットを理解するためには、以下のGo言語およびソフトウェアテストに関する基本的な知識が必要です。
-
Go言語の
go/parser
パッケージ:- Go言語の標準ライブラリの一部であり、Goのソースコードを解析(パース)して抽象構文木(AST: Abstract Syntax Tree)を構築する機能を提供します。
- Goコンパイラ、
go fmt
(コードフォーマッタ)、go vet
(静的解析ツール)など、Goのツールチェインの多くの部分で基盤として利用されています。 ParseFile
関数は、指定されたソースコード(ファイルまたは文字列)をパースし、ASTを返します。パース中にエラーが発生した場合、そのエラーも返されます。token
パッケージと連携し、ソースコード内の各要素(トークン)の位置情報(行番号、列番号など)を管理します。token.FileSet
は、複数のファイルにまたがる位置情報を一元的に管理するための構造体です。
-
Go言語のテストフレームワーク (
testing
パッケージとgo test
コマンド):- Go言語には、標準で組み込みのテストフレームワークが提供されています。
testing
パッケージは、テスト関数を記述するための基本的な機能(*testing.T
型、t.Errorf
、t.Fatalf
など)を提供します。- テストファイルは通常、テスト対象のGoファイルと同じディレクトリに配置され、ファイル名が
_test.go
で終わる必要があります。 - テスト関数は
Test
で始まり、*testing.T
型の引数を一つ取ります(例:func TestMyFunction(t *testing.T)
)。 go test
コマンドを実行すると、カレントディレクトリおよびサブディレクトリ内のすべてのテストファイルが自動的に検出され、テスト関数が実行されます。
-
テストハーネス (Test Harness):
- ソフトウェアテストの文脈において、「テストハーネス」とは、テストの実行、管理、結果の報告を行うためのフレームワークや環境を指します。
- テスト対象のコードとテストコードを分離し、テストの自動化、再利用性、保守性を高める役割があります。
- このコミットでは、
short_test.go
という新しいファイルが、特定の種類のテスト(短いコードスニペットのパーステスト)のための「ハーネス」として機能しています。これにより、テストデータ(valids
、invalids
)とテストロジック(TestValid
、TestInvalid
)が明確に分離され、管理しやすくなっています。
-
interface{}
型 (Go言語の空インターフェース):- Go言語における
interface{}
は、任意の型の値を保持できる型です。これは、他の言語におけるObject
型やAny
型に似ています。 - このコミットでは、
checkErrors
関数がinput interface{}
を受け取るように変更されています。これにより、この関数はファイルパス(文字列)だけでなく、直接Goコードの文字列リテラルもテスト入力として受け取ることができるようになり、テストの柔軟性が向上しています。
- Go言語における
-
ioutil.ReadFile
(非推奨):- Go 1.16以降で非推奨となり、
os.ReadFile
に置き換えられました。ファイルの内容をバイトスライスとして読み込むための関数です。このコミットが作成された2012年当時はまだ現役でした。
- Go 1.16以降で非推奨となり、
これらの概念を理解することで、コミットがGoパーサーのテストスイートの構造をどのように改善しているかを深く把握できます。
技術的詳細
このコミットは、Goパーサーのテストスイートにおける「短いテスト」の管理方法を根本的に変更しています。その技術的詳細は以下の通りです。
-
short_test.go
の新規導入:- このコミットの最も重要な変更は、
src/pkg/go/parser/short_test.go
という新しいテストファイルが作成されたことです。 - このファイルは、Goパーサーが正しくパースできる短い有効なGoコードスニペットのリスト(
valids
変数)と、パースエラーを発生させるべき不正なGoコードスニペットのリスト(invalids
変数)を定義しています。 valids
とinvalids
は、それぞれstring
型のスライスとして定義されており、各要素はバッククォート文字列リテラル(raw string literal)でGoコードが記述されています。これにより、複数行のコードや特殊文字を含むコードもエスケープなしで記述できます。invalids
の各文字列には、期待されるエラーメッセージと位置を示すコメント(例:/* ERROR "expected 'package'" */
)が含まれています。これは、checkErrors
関数がエラーの検証を行う際に利用されます。TestValid
関数はvalids
スライス内の各コードスニペットをcheckErrors
関数に渡し、エラーが発生しないことを確認します。TestInvalid
関数はinvalids
スライス内の各コードスニペットをcheckErrors
関数に渡し、期待されるエラーが正確に検出されることを確認します。
- このコミットの最も重要な変更は、
-
checkErrors
関数の汎用化:src/pkg/go/parser/error_test.go
内のcheckErrors
関数のシグネチャが、func checkErrors(t *testing.T, filename string)
からfunc checkErrors(t *testing.T, filename string, input interface{})
に変更されました。- これにより、
checkErrors
関数は、ファイルパス(filename
)だけでなく、input
引数を通じて直接Goコードの文字列リテラルもテスト入力として受け取れるようになりました。 - 内部では、
readSource(filename, input)
というヘルパー関数が導入され、input
がnil
の場合はfilename
からファイルを読み込み、input
がnil
でない場合はinput
をソースとして使用するロジックが実装されています(コミット差分にはreadSource
の変更は直接含まれていませんが、そのように動作するように変更されたと推測されます)。 - この変更は、
short_test.go
で定義された文字列リテラルをcheckErrors
関数で直接テストするために不可欠です。
-
既存テストファイルからのテストケースの削除と移行:
src/pkg/go/parser/parser_test.go
から、illegalInputs
とvalidPrograms
という二つの大きな文字列スライスが削除されました。これらは、それぞれ不正なGoコードと有効なGoコードのテストケースを含んでいました。- これらのスライスを使用していた
TestParseIllegalInputs
とTestParseValidPrograms
関数も削除されました。 - これらのテストケースは、
short_test.go
のinvalids
とvalids
スライスに移行され、新しいテストハーネスを通じて実行されるようになりました。これにより、parser_test.go
のコード量が大幅に削減され、その役割がより明確になりました。
-
getFile
関数の堅牢化:src/pkg/go/parser/error_test.go
内のgetFile
関数に、fset.Iterate
ループ内で同じfilename
が複数回見つかった場合にpanic
するチェックが追加されました。- これは、テストデータの整合性を保証し、予期せぬ重複によってテストが誤動作するのを防ぐための防御的なプログラミングです。
-
テスト関数のリネーム:
parser_test.go
内のTestParse3
がTestParse
に、TestParse4
がTestParseDir
にリネームされました。これは、関数の名前がその役割をより正確に反映するようにするための、セマンティックな改善です。
これらの変更により、Goパーサーのテストスイートは、より整理され、モジュール化され、保守しやすくなりました。特に、短いコードスニペットのテストが専用の場所で管理されるようになったことで、テストの追加や変更が容易になり、テストコード全体の品質が向上しています。
コアとなるコードの変更箇所
このコミットにおけるコアとなるコードの変更箇所は以下の3つのファイルにまたがっています。
-
src/pkg/go/parser/error_test.go
getFile
関数に、同じファイル名が複数回使用された場合にpanic
するチェックが追加されました。--- a/src/pkg/go/parser/error_test.go +++ b/src/pkg/go/parser/error_test.go @@ -34,11 +34,14 @@ import ( const testdata = "testdata" +// getFile assumes that each filename occurs at most once func getFile(filename string) (file *token.File) { fset.Iterate(func(f *token.File) bool { if f.Name() == filename { + if file != nil { + panic(filename + " used multiple times") + } file = f - return false // end iteration } return true }) @@ -127,8 +130,8 @@ func compareErrors(t *testing.T, expected map[token.Pos]string, found scanner.Er } -func checkErrors(t *testing.T, filename string) { - src, err := ioutil.ReadFile(filename) +func checkErrors(t *testing.T, filename string, input interface{}) { + src, err := readSource(filename, input) if err != nil { t.Error(err) return @@ -157,7 +160,7 @@ func TestErrors(t *testing.T) { for _, fi := range list { name := fi.Name() if !fi.IsDir() && !strings.HasPrefix(name, ".") && strings.HasSuffix(name, ".src") { - checkErrors(t, filepath.Join(testdata, name)) + checkErrors(t, filepath.Join(testdata, name), nil) } } }
checkErrors
関数のシグネチャが変更され、input interface{}
引数が追加されました。これにより、ファイルパスだけでなく、直接文字列リテラルをテスト入力として受け取れるようになりました。
-
src/pkg/go/parser/parser_test.go
illegalInputs
とvalidPrograms
という、多数のテストケースを含む大きな文字列スライスが削除されました。- これらのスライスを使用していた
TestParseIllegalInputs
とTestParseValidPrograms
関数も削除されました。 TestParse3
がTestParse
に、TestParse4
がTestParseDir
にリネームされました。TestParseExpr
関数が、削除されたvalidPrograms
の代わりに、新しく導入されたvalids
(short_test.go
で定義)を使用するように変更されました。--- a/src/pkg/go/parser/parser_test.go +++ b/src/pkg/go/parser/parser_test.go @@ -14,87 +14,14 @@ import ( var fset = token.NewFileSet() -var illegalInputs = []interface{}{ - nil, - 3.14, - []byte(nil), - "foo!", - `package p; func f() { if /* should have condition */ {} };`, - `package p; func f() { if ; /* should have condition */ {} };`, - `package p; func f() { if f(); /* should have condition */ {} };`, - `package p; const c; /* should have constant value */`, - `package p; func f() { if _ = range x; true {} };`, - `package p; func f() { switch _ = range x; true {} };`, - `package p; func f() { for _ = range x ; ; {} };`, - `package p; func f() { for ; ; _ = range x {} };`, - `package p; func f() { for ; _ = range x ; {} };`, - `package p; func f() { switch t = t.(type) {} };`, - `package p; func f() { switch t, t = t.(type) {} };`, - `package p; func f() { switch t = t.(type), t {} };`, - `package p; var a = [1]int; /* illegal expression */`, - `package p; var a = [...]int; /* illegal expression */`, - `package p; var a = struct{} /* illegal expression */`, - `package p; var a = func(); /* illegal expression */`, - `package p; var a = interface{} /* illegal expression */`, - `package p; var a = []int /* illegal expression */`, - `package p; var a = map[int]int /* illegal expression */`, - `package p; var a = chan int; /* illegal expression */`, - `package p; var a = []int{[]int}; /* illegal expression */`, - `package p; var a = ([]int); /* illegal expression */`, - `package p; var a = a[[]int:[]int]; /* illegal expression */`, - `package p; var a = <- chan int; /* illegal expression */`, - `package p; func f() { select { case _ <- chan int: } };`, -} - -func TestParseIllegalInputs(t *testing.T) { - for _, src := range illegalInputs { - _, err := ParseFile(fset, "", src, 0) - if err == nil { - t.Errorf("ParseFile(%v) should have failed", src) - } - } -} - -var validPrograms = []string{ - "package p\\n", - `package p;`, - `package p; import "fmt"; func f() { fmt.Println("Hello, World!") };`, - `package p; func f() { if f(T{}) {} };`, - `package p; func f() { _ = (<-chan int)(x) };`, - `package p; func f() { _ = (<-chan <-chan int)(x) };`, - `package p; func f(func() func() func());`, - `package p; func f(...T);`, - `package p; func f(float, ...int);`, - `package p; func f(x int, a ...int) { f(0, a...); f(1, a...,) };`, - `package p; func f(int,) {};`, - `package p; func f(...int,) {};`, - `package p; func f(x ...int,) {};`, - `package p; type T []int; var a []bool; func f() { if a[T{42}[0]] {} };`, - `package p; type T []int; func g(int) bool { return true }; func f() { if g(T{42}[0]) {} };`, - `package p; type T []int; func f() { for _ = range []int{T{42}[0]} {} };`, - `package p; var a = T{{1, 2}, {3, 4}}`, - `package p; func f() { select { case <- c: case c <- d: case c <- <- d: case <-c <- d: } };`, - `package p; func f() { select { case x := (<-c): } };`, - `package p; func f() { if ; true {} };`, - `package p; func f() { switch ; {} };`, - `package p; func f() { for _ = range "foo" + "bar" {} };`, -} - -func TestParseValidPrograms(t *testing.T) { - for _, src := range validPrograms { - _, err := ParseFile(fset, "", src, SpuriousErrors) - if err != nil { - t.Errorf("ParseFile(%q): %v", src, err) - } - } -} - var validFiles = []string{ "parser.go", "parser_test.go", + "error_test.go", + "short_test.go", } -func TestParse3(t *testing.T) { +func TestParse(t *testing.T) { for _, filename := range validFiles { _, err := ParseFile(fset, filename, nil, DeclarationErrors) if err != nil { @@ -116,7 +43,7 @@ func nameFilter(filename string) bool { func dirFilter(f os.FileInfo) bool { return nameFilter(f.Name()) } -func TestParse4(t *testing.T) { +func TestParseDir(t *testing.T) { path := "." pkgs, err := ParseDir(fset, path, dirFilter, 0) if err != nil { @@ -158,7 +85,7 @@ func TestParseExpr(t *testing.T) { // it must not crash - for _, src := range validPrograms { + for _, src := range valids { ParseExpr(src) } }
-
src/pkg/go/parser/short_test.go
(新規ファイル)valids
とinvalids
という、それぞれ有効なGoコードと不正なGoコードの文字列リテラルを含むスライスが定義されました。TestValid
関数とTestInvalid
関数が実装され、それぞれvalids
とinvalids
の各要素をcheckErrors
関数に渡し、パース結果を検証します。--- /dev/null +++ b/src/pkg/go/parser/short_test.go @@ -0,0 +1,75 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains test cases for short valid and invalid programs. + +package parser + +import "testing" + +var valids = []string{ + "package p\\n", + `package p;`, + `package p; import "fmt"; func f() { fmt.Println("Hello, World!") };`, + `package p; func f() { if f(T{}) {} };`, + `package p; func f() { _ = (<-chan int)(x) };`, + `package p; func f() { _ = (<-chan <-chan int)(x) };`, + `package p; func f(func() func() func());`, + `package p; func f(...T);`, + `package p; func f(float, ...int);`, + `package p; func f(x int, a ...int) { f(0, a...); f(1, a...,) };`, + `package p; func f(int,) {};`, + `package p; func f(...int,) {};`, + `package p; func f(x ...int,) {};`, + `package p; type T []int; var a []bool; func f() { if a[T{42}[0]] {} };`, + `package p; type T []int; func g(int) bool { return true }; func f() { if g(T{42}[0]) {} };`, + `package p; type T []int; func f() { for _ = range []int{T{42}[0]} {} };`, + `package p; var a = T{{1, 2}, {3, 4}}`, + `package p; func f() { select { case <- c: case c <- d: case c <- <- d: case <-c <- d: } };`, + `package p; func f() { select { case x := (<-c): } };`, + `package p; func f() { if ; true {} };`, + `package p; func f() { switch ; {} };`, + `package p; func f() { for _ = range "foo" + "bar" {} };`, +} + +func TestValid(t *testing.T) { + for _, src := range valids { + checkErrors(t, src, src) + } +} + +var invalids = []string{ + `foo /* ERROR "expected 'package'" */ !`, + `package p; func f() { if { /* ERROR "expected operand" */ } };`, + `package p; func f() { if ; { /* ERROR "expected operand" */ } };`, + `package p; func f() { if f(); { /* ERROR "expected operand" */ } };`, + `package p; const c; /* ERROR "expected '='" */`, + `package p; func f() { if _ /* ERROR "expected condition" */ = range x; true {} };`, + `package p; func f() { switch _ /* ERROR "expected condition" */ = range x; true {} };`, + `package p; func f() { for _ = range x ; /* ERROR "expected '{'" */ ; {} };`, + `package p; func f() { for ; ; _ = range /* ERROR "expected operand" */ x {} };`, + `package p; func f() { for ; _ /* ERROR "expected condition" */ = range x ; {} };`, + `package p; func f() { switch t /* ERROR "expected condition" */ = t.(type) {} };`, + `package p; func f() { switch t /* ERROR "expected condition" */ , t = t.(type) {} };`, + `package p; func f() { switch t /* ERROR "expected condition" */ = t.(type), t {} };`, + `package p; var a = [ /* ERROR "expected expression" */ 1]int;`, + `package p; var a = [ /* ERROR "expected expression" */ ...]int;`, + `package p; var a = struct /* ERROR "expected expression" */ {}`, + `package p; var a = func /* ERROR "expected expression" */ ();`, + `package p; var a = interface /* ERROR "expected expression" */ {}`,\ + `package p; var a = [ /* ERROR "expected expression" */ ]int`, + `package p; var a = map /* ERROR "expected expression" */ [int]int`, + `package p; var a = chan /* ERROR "expected expression" */ int;`, + `package p; var a = []int{[ /* ERROR "expected expression" */ ]int};`, + `package p; var a = ( /* ERROR "expected expression" */ []int);`, + `package p; var a = a[[ /* ERROR "expected expression" */ ]int:[]int];`, + `package p; var a = <- /* ERROR "expected expression" */ chan int;`, + `package p; func f() { select { case _ <- chan /* ERROR "expected expression" */ int: } };`, +} + +func TestInvalid(t *testing.T) { + for _, src := range invalids { + checkErrors(t, src, src) + } +}
これらの変更は、Goパーサーのテストコードをよりモジュール化し、保守しやすくするための重要なステップです。
コアとなるコードの解説
このコミットの核となる変更は、Goパーサーのテストにおける「短いコードスニペット」の扱い方を体系化した点にあります。
-
short_test.go
の役割:- この新しいファイルは、Goパーサーのテストにおける「テストハーネス」として機能します。
valids
スライスには、Goパーサーがエラーなくパースできることが期待される、様々な有効なGoコードの断片が文字列リテラルとして集められています。これには、基本的なパッケージ宣言から、複雑な型宣言、関数シグネチャ、select
文のバリエーションなどが含まれます。invalids
スライスには、Goパーサーが特定の構文エラーを検出することが期待される、意図的に不正なGoコードの断片が文字列リテラルとして集められています。各不正なコードには、期待されるエラーメッセージと、そのエラーが発生するおおよその位置を示す/* ERROR "..." */
形式のコメントが付加されています。これは、テストが期待通りのエラーを正確に報告しているかを検証するために重要です。TestValid
とTestInvalid
関数は、これらのスライスをイテレートし、各コードスニペットをcheckErrors
関数に渡してパースを試みます。TestValid
はエラーがないことを、TestInvalid
は期待されるエラーが検出されることを検証します。
-
checkErrors
関数の柔軟性:error_test.go
内のcheckErrors
関数がinput interface{}
引数を受け入れるように変更されたことは、このテストハーネスの実現に不可欠です。- 以前はファイルからのみソースコードを読み込んでいましたが、この変更により、
short_test.go
で定義された文字列リテラルを直接テスト入力として使用できるようになりました。 - これにより、テストのセットアップが簡素化され、ディスクI/Oを伴わないインメモリでのテストが可能になり、テストの実行速度が向上する可能性があります。また、テストケースをファイルとして管理するオーバーヘッドがなくなります。
-
テストの分離と重複排除:
parser_test.go
からillegalInputs
とvalidPrograms
が削除され、それらをテストしていた関数もなくなったことで、parser_test.go
はより高レベルなパーステストやファイルベースのテストに集中できるようになりました。- これにより、テストコードの重複が排除され、各テストファイルの役割が明確になりました。例えば、短いコードスニペットのテストは
short_test.go
に、より大きなファイルやディレクトリのパーステストはparser_test.go
に残る、といった具合です。
このコミットは、Goパーサーのテストスイートをより構造化し、保守しやすく、効率的なものにするための典型的なリファクタリングパターンを示しています。テストデータをテストロジックから分離し、共通のテストヘルパー関数を汎用化することで、テストコード全体の品質と管理性が向上しています。
関連リンク
- Go言語公式ドキュメント: https://go.dev/doc/
go/parser
パッケージドキュメント: https://pkg.go.dev/go/parsertesting
パッケージドキュメント: https://pkg.go.dev/testing- Go Code Review (Gerrit) の変更リスト: https://golang.org/cl/5782044
- このリンクは、Goプロジェクトが当時使用していたGerritベースのコードレビューシステムへのリンクです。コミットメッセージに記載されている
https://golang.org/cl/5782044
がこれに該当します。
- このリンクは、Goプロジェクトが当時使用していたGerritベースのコードレビューシステムへのリンクです。コミットメッセージに記載されている
参考にした情報源リンク
- Go言語の公式ドキュメントおよびパッケージドキュメント
- Go言語のテストに関する一般的なプラクティスやチュートリアル
- Go言語のソースコードリポジトリ(特に
go/parser
パッケージのテストディレクトリ) - Gerritの変更リスト(CL)のレビューコメント(もしあれば)