[インデックス 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.gogetFile関数に、同じファイル名が複数回使用された場合に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.goillegalInputsと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)のレビューコメント(もしあれば)