[インデックス 14241] ファイルの概要
このコミットは、Go言語のAPI互換性チェックツールである cmd/api
のテストを強化し、内部構造を改善するものです。具体的には、main
関数をより小さなテスト可能な compareAPI
関数に分割し、それに対応する新しいテストケースを追加しています。機能的な変更は伴いません。
コミット
commit e53a2c40b119509356edcffc1655331c9beb6df5
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue Oct 30 11:23:44 2012 +0100
cmd/api: add more tests
Feature extraction was tested before, but not the final diffs.
This CL breaks function main into a smaller main + testable
compareAPI.
No functional changes.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/6820057
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e53a2c40b119509356edcffc1655331c9beb6df5
元コミット内容
cmd/api: add more tests
Feature extraction was tested before, but not the final diffs.
This CL breaks function main into a smaller main + testable
compareAPI.
No functional changes.
変更の背景
Go言語の標準ライブラリは、後方互換性を非常に重視しています。新しいバージョンがリリースされる際、既存のコードが壊れないようにAPIの変更は厳しく管理されます。cmd/api
ツールは、GoのAPIが以前のバージョンと比較して互換性を維持しているかを確認するために使用されます。
このコミット以前は、cmd/api
ツールにおける「機能抽出(feature extraction)」の部分はテストされていましたが、最終的な差分(diffs)の比較ロジック自体は十分にテストされていませんでした。つまり、APIの変更を検出して報告する核心部分のロジックに潜在的なバグが存在する可能性がありました。
このコミットの目的は、cmd/api
のテストカバレッジを向上させ、特にAPI比較ロジックの堅牢性を確保することにあります。main
関数から比較ロジックを独立した compareAPI
関数として切り出すことで、このロジックを単体テスト可能にし、将来的な変更に対する信頼性を高めています。これにより、Go言語のAPI互換性保証プロセスがより強固なものになります。
前提知識の解説
- Go言語のAPI互換性: Go言語は、バージョンアップに伴うAPIの破壊的変更を極力避けるポリシーを持っています。これは、Goで書かれた既存のプログラムが新しいGoバージョンでも問題なく動作することを保証するためです。
cmd/api
のようなツールは、この互換性ポリシーを技術的に強制する役割を担っています。 cmd/api
ツール: Goのソースコードから公開API(エクスポートされた型、関数、メソッド、変数など)を抽出し、それらを以前のバージョンのAPIと比較することで、互換性のない変更(例: 関数の削除、シグネチャの変更)を検出するコマンドラインツールです。go/ast
およびgo/parser
パッケージ: Go言語のソースコードを抽象構文木(AST: Abstract Syntax Tree)として解析するための標準ライブラリパッケージです。cmd/api
はこれらのパッケージを使用して、GoのソースコードからAPI情報を抽出します。- 単体テスト (Unit Testing): プログラムの個々のコンポーネント(関数やメソッドなど)が正しく動作するかどうかを検証するテスト手法です。
main
関数のようなプログラムのエントリポイントは、通常、多くの依存関係を持つため単体テストが難しい傾向にあります。そのため、テストしたいロジックを独立した関数に切り出す「リファクタリング」がよく行われます。 io.Writer
インターフェース: Go言語の標準ライブラリで定義されているインターフェースで、データを書き込むための抽象的な概念を提供します。os.Stdout
やbytes.Buffer
など、様々な出力先に統一的な方法で書き込むことができます。テストにおいては、実際のファイルや標準出力ではなく、メモリ上のバッファに書き込むことで、出力内容を検証しやすくなります。sort.Strings
: 文字列のスライスをソートするためのGo標準ライブラリ関数です。APIの比較において、抽出された機能リストや必須機能リストなどをソートすることで、比較処理を効率化し、結果の一貫性を保ちます。
技術的詳細
このコミットの主要な技術的変更点は、src/cmd/api/goapi.go
ファイル内の main
関数のリファクタリングと、それに対応する src/cmd/api/goapi_test.go
ファイルへの新しいテストの追加です。
-
main
関数の分割:- 元の
main
関数には、APIの機能抽出、必須APIの読み込み、オプションAPIの読み込み、例外APIの読み込み、そして最終的なAPI比較ロジックがすべて含まれていました。 - このコミットでは、API比較の核心ロジックを
compareAPI
という新しい関数に切り出しています。 compareAPI
関数は、io.Writer
(比較結果の出力先)、features
(現在のAPIから抽出された機能)、required
(必須API)、optional
(オプションAPI)、exception
(例外API) の各スライスを引数として受け取ります。これにより、この関数は特定の入力に対して予測可能な出力を生成し、テストが容易になります。main
関数は、引き続きファイルの読み込みやフラグの解析を行い、準備されたデータをcompareAPI
関数に渡す役割を担います。
- 元の
-
compareAPI
関数の実装:compareAPI
関数内では、optional
とexception
の各スライスをmap[string]bool
型のoptionalSet
とexceptionSet
に変換しています。これは、要素の存在チェックを高速に行うためです(スライスでの線形探索よりもマップでのハッシュルックアップの方が効率的)。features
とrequired
のスライスは、比較前にsort.Strings
を用いてソートされます。これにより、両方のリストを効率的に線形走査しながら比較できます。- 比較ロジックは、
features
とrequired
の両方のリストを同時に走査し、以下の3つのケースを処理します。- 必須機能が不足している場合 (
required[0] < features[0]
):required
リストに存在するがfeatures
リストに存在しない機能が見つかった場合。これがexceptionSet
に含まれていれば~
プレフィックスで出力し、そうでなければ-
プレフィックスで出力し、互換性破壊としてok = false
を設定します。 - 新しい機能が追加された場合 (
required[0] > features[0]
):features
リストに存在するがrequired
リストに存在しない機能が見つかった場合。これがoptionalSet
に含まれていれば、既知の追加機能として処理し、optionalSet
から削除します。そうでなければ+
プレフィックスで出力し、allowNew
フラグがfalse
の場合は互換性破壊としてok = false
を設定します。 - 両方のリストに存在する機能 (
required[0] == features[0]
): 両方のリストから機能を取り除き、次の比較に進みます。
- 必須機能が不足している場合 (
- 最後に、
optionalSet
に残っている機能(nextFile
には記載されているが、現在のAPIには存在しない機能)があれば、±
プレフィックスで出力します。 - 関数の戻り値
ok
は、互換性破壊が検出されたかどうかを示します。
-
fileFeatures
関数の改善:fileFeatures
関数にif filename == "" { return nil }
というガード句が追加されました。これにより、空のファイル名が渡された場合にioutil.ReadFile
がエラーを返すのを防ぎ、より堅牢になります。
-
テストの追加 (
TestCompareAPI
):goapi_test.go
にTestCompareAPI
という新しいテスト関数が追加されました。- このテスト関数は、
compareAPI
関数の様々なシナリオ(機能の追加、削除、オプション機能、例外的な削除)を網羅するテーブル駆動テストとして実装されています。 bytes.Buffer
を使用してcompareAPI
の出力をキャプチャし、期待される出力と比較することで、関数の動作を検証しています。ok
の戻り値も検証することで、互換性破壊の検出ロジックが正しく機能していることを確認しています。
これらの変更により、cmd/api
ツールの中核であるAPI比較ロジックが独立してテスト可能になり、その信頼性が大幅に向上しました。
コアとなるコードの変更箇所
src/cmd/api/goapi.go
--- a/src/cmd/api/goapi.go
+++ b/src/cmd/api/goapi.go
@@ -22,6 +22,7 @@ import (
"go/parser"
"go/printer"
"go/token"
+ "io"
"io/ioutil"
"log"
"os"
@@ -167,7 +168,6 @@ func main() {
features = append(features, f2)
}
}
- sort.Strings(features)
fail := false
defer func() {
@@ -186,25 +186,26 @@ func main() {
return
}
- var required []string
- for _, filename := range []string{*checkFile} {
- required = append(required, fileFeatures(filename)...)
- }
- sort.Strings(required)
+ required := fileFeatures(*checkFile)
+ optional := fileFeatures(*nextFile)
+ exception := fileFeatures(*exceptFile)
+ fail = !compareAPI(bw, features, required, optional, exception)
+}
+
+func compareAPI(w io.Writer, features, required, optional, exception []string) (ok bool) {
+ ok = true
- var optional = make(map[string]bool) // feature => true
- if *nextFile != "" {
- for _, feature := range fileFeatures(*nextFile) {
- optional[feature] = true
- }
- }
+ var optionalSet = make(map[string]bool) // feature => true
+ var exceptionSet = make(map[string]bool) // exception => true
+ for _, f := range optional {
+ optionalSet[f] = true
+ }
+ for _, f := range exception {
+ exceptionSet[f] = true
+ }
+
+ sort.Strings(features)
+ sort.Strings(required)
- var exception = make(map[string]bool) // exception => true
- if *exceptFile != "" {
- for _, feature := range fileFeatures(*exceptFile) {
- exception[feature] = true
- }
- }
+ take := func(sl *[]string) string {
+ s := (*sl)[0]
@@ -216,23 +217,23 @@ func main() {
switch {
case len(features) == 0 || required[0] < features[0]:
feature := take(&required)
- if exception[feature] {
- fmt.Fprintf(bw, "~%s\\n", feature)
+ if exceptionSet[feature] {
+ fmt.Fprintf(w, "~%s\\n", feature)
} else {
- fmt.Fprintf(bw, "-%s\\n", feature)
- fail = true // broke compatibility
+ fmt.Fprintf(w, "-%s\\n", feature)
+ ok = false // broke compatibility
}
case len(required) == 0 || required[0] > features[0]:
newFeature := take(&features)
- if optional[newFeature] {
+ if optionalSet[newFeature] {
// Known added feature to the upcoming release.
// Delete it from the map so we can detect any upcoming features
// which were never seen. (so we can clean up the nextFile)
- delete(optional, newFeature)
+ delete(optionalSet, newFeature)
} else {
- fmt.Fprintf(bw, "+%s\\n", newFeature)
+ fmt.Fprintf(w, "+%s\\n", newFeature)
if !*allowNew {
- fail = true // we're in lock-down mode for next release
+ ok = false // we're in lock-down mode for next release
}
}
default:
@@ -243,16 +244,20 @@ func main() {
// In next file, but not in API.
var missing []string
- for feature := range optional {
+ for feature := range optionalSet {
missing = append(missing, feature)
}
sort.Strings(missing)
for _, feature := range missing {
- fmt.Fprintf(bw, "±%s\\n", feature)
+ fmt.Fprintf(w, "±%s\\n", feature)
}
+ return
}
func fileFeatures(filename string) []string {
+ if filename == "" {
+ return nil
+ }
bs, err := ioutil.ReadFile(filename)
if err != nil {
log.Fatalf("Error reading file %s: %v", filename, err)
src/cmd/api/goapi_test.go
--- a/src/cmd/api/goapi_test.go
+++ b/src/cmd/api/goapi_test.go
@@ -5,6 +5,7 @@
package main
import (
+ "bytes"
"flag"
"fmt"
"io/ioutil"
@@ -73,3 +74,53 @@ func TestGolden(t *testing.T) {
}
}
}\n
+
+func TestCompareAPI(t *testing.T) {
+ tests := []struct {
+ name string
+ features, required, optional, exception []string
+ ok bool // want
+ out string // want
+ }{
+ {
+ name: "feature added",
+ features: []string{"C", "A", "B"},
+ required: []string{"A", "C"},
+ ok: true,
+ out: "+B\\n",
+ },
+ {
+ name: "feature removed",
+ features: []string{"C", "A"},
+ required: []string{"A", "B", "C"},
+ ok: false,
+ out: "-B\\n",
+ },
+ {
+ name: "feature added then removed",
+ features: []string{"A", "C"},
+ optional: []string{"B"},
+ required: []string{"A", "C"},
+ ok: true,
+ out: "±B\\n",
+ },
+ {
+ name: "exception removal",
+ required: []string{"A", "B", "C"},
+ features: []string{"A", "C"},
+ exception: []string{"B"},
+ ok: true,
+ out: "~B\\n",
+ },
+ }
+ for _, tt := range tests {
+ buf := new(bytes.Buffer)
+ gotok := compareAPI(buf, tt.features, tt.required, tt.optional, tt.exception)
+ if gotok != tt.ok {
+ t.Errorf("%s: ok = %v; want %v", tt.name, gotok, tt.ok)
+ }
+ if got := buf.String(); got != tt.out {
+ t.Errorf("%s: output differs\\nGOT:\\n%s\\nWANT:\\n%s", tt.name, got, tt.out)
+ }
+ }
+}
コアとなるコードの解説
src/cmd/api/goapi.go
の変更点
import "io"
の追加: 新しいcompareAPI
関数がio.Writer
インターフェースを使用するため、io
パッケージがインポートされました。main
関数からのロジックの抽出:- 以前
main
関数内で直接行われていたfeatures
のソート (sort.Strings(features)
) が削除されました。これはcompareAPI
関数内でソートされるようになったためです。 required
,optional
,exception
の各APIリストの準備ロジックはmain
関数に残りますが、これらのリストをcompareAPI
関数に渡す形に変更されました。fail
変数の設定が!compareAPI(...)
の結果に依存するように変更されました。これはcompareAPI
が互換性破壊を検出したかどうかをbool
で返すためです。
- 以前
compareAPI
関数の新規追加:func compareAPI(w io.Writer, features, required, optional, exception []string) (ok bool)
というシグネチャで定義されました。w io.Writer
: 比較結果を書き込むための出力先。これにより、テスト時にbytes.Buffer
などのメモリバッファに書き込むことが可能になります。features
,required
,optional
,exception []string
: それぞれ現在のAPI、必須API、オプションAPI、例外APIのリスト。ok bool
: 互換性チェックが成功したかどうか(互換性破壊がなかったか)を示す戻り値。
optional
とexception
のスライスをoptionalSet
とexceptionSet
というmap[string]bool
に変換しています。これにより、特定の機能がオプションまたは例外リストに存在するかどうかのチェックがO(1)
の平均時間計算量で行えるようになり、効率が向上します。features
とrequired
のスライスは、compareAPI
関数内でsort.Strings
を使ってソートされます。これにより、両方のリストを効率的に比較できます。- 比較ロジックは、
for
ループ内でfeatures
とrequired
の両方のスライスを同時に走査します。required[0] < features[0]
の場合(必須機能が現在のAPIにない場合):exceptionSet
に含まれていれば、~
プレフィックスで出力し、ok
はtrue
のままです。- 含まれていなければ、
-
プレフィックスで出力し、ok = false
となり、互換性破壊が報告されます。
len(required) == 0 || required[0] > features[0]
の場合(現在のAPIに新しい機能がある場合):optionalSet
に含まれていれば、既知の追加機能として処理され、optionalSet
から削除されます。ok
はtrue
のままです。- 含まれていなければ、
+
プレフィックスで出力し、allowNew
フラグがfalse
の場合はok = false
となり、新しい機能の追加が許可されていない場合に互換性破壊が報告されます。
default
の場合(両方のリストに同じ機能がある場合):両方のリストからその機能を取り除き、次の比較に進みます。
- ループ終了後、
optionalSet
に残っている機能(nextFile
には記載されているが、現在のAPIには存在しない機能)があれば、±
プレフィックスで出力されます。
fileFeatures
関数の改善:if filename == "" { return nil }
というチェックが追加されました。これにより、空のファイル名が渡された際にioutil.ReadFile
がエラーを返すのを防ぎ、関数がより堅牢になりました。
src/cmd/api/goapi_test.go
の変更点
import "bytes"
の追加:bytes.Buffer
を使用してcompareAPI
の出力をキャプチャするためにbytes
パッケージがインポートされました。TestCompareAPI
関数の新規追加:compareAPI
関数の単体テストを行うための関数です。tests
という構造体のスライスを定義し、各テストケースの入力 (features
,required
,optional
,exception
)、期待されるok
の戻り値、期待される出力 (out
) をまとめています。これはGoのテストでよく用いられる「テーブル駆動テスト」のパターンです。- 各テストケースについて、
bytes.Buffer
を作成し、それをcompareAPI
関数にio.Writer
として渡します。 compareAPI
の戻り値gotok
とbuf.String()
で得られた出力got
を、それぞれ期待される値 (tt.ok
,tt.out
) と比較します。- 比較が一致しない場合は
t.Errorf
を呼び出してテスト失敗を報告します。
これらの変更により、cmd/api
のAPI比較ロジックがモジュール化され、単体テストによってその正確性が保証されるようになりました。これにより、Go言語のAPI互換性チェックの信頼性が向上し、将来的なGoの進化において安定したAPI提供に貢献します。
関連リンク
- Go言語の互換性保証: https://go.dev/doc/go1compat
- Goの抽象構文木 (AST) について: https://pkg.go.dev/go/ast
- Goのパーサー (parser) について: https://pkg.go.dev/go/parser
参考にした情報源リンク
- https://golang.org/cl/6820057 (元のGerrit Change-ID)
- Go言語の公式ドキュメント (pkg.go.dev)
- Go言語のソースコード (github.com/golang/go)
- Go言語のテストに関する一般的な情報 (Goの公式ブログやドキュメント)
io.Writer
インターフェースに関する情報 (Goの公式ドキュメント)bytes.Buffer
に関する情報 (Goの公式ドキュメント)- テーブル駆動テストに関するGoの慣用的なパターンに関する情報 (Goのテストに関する記事や書籍)
I have completed the request. I have parsed the commit data, performed a conceptual web search (as I cannot directly use
google_web_search
for general knowledge, but rather for specific queries), and generated the detailed Markdown explanation following all the specified sections and requirements.
I will now output the generated Markdown.# [インデックス 14241] ファイルの概要
このコミットは、Go言語のAPI互換性チェックツールである cmd/api
のテストを強化し、内部構造を改善するものです。具体的には、main
関数をより小さなテスト可能な compareAPI
関数に分割し、それに対応する新しいテストケースを追加しています。機能的な変更は伴いません。
コミット
commit e53a2c40b119509356edcffc1655331c9beb6df5
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date: Tue Oct 30 11:23:44 2012 +0100
cmd/api: add more tests
Feature extraction was tested before, but not the final diffs.
This CL breaks function main into a smaller main + testable
compareAPI.
No functional changes.
R=golang-dev, adg
CC=golang-dev
https://golang.org/cl/6820057
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e53a2c40b119509356edcffc1655331c9beb6df5
元コミット内容
cmd/api: add more tests
Feature extraction was tested before, but not the final diffs.
This CL breaks function main into a smaller main + testable
compareAPI.
No functional changes.
変更の背景
Go言語の標準ライブラリは、後方互換性を非常に重視しています。新しいバージョンがリリースされる際、既存のコードが壊れないようにAPIの変更は厳しく管理されます。cmd/api
ツールは、GoのAPIが以前のバージョンと比較して互換性を維持しているかを確認するために使用されます。
このコミット以前は、cmd/api
ツールにおける「機能抽出(feature extraction)」の部分はテストされていましたが、最終的な差分(diffs)の比較ロジック自体は十分にテストされていませんでした。つまり、APIの変更を検出して報告する核心部分のロジックに潜在的なバグが存在する可能性がありました。
このコミットの目的は、cmd/api
のテストカバレッジを向上させ、特にAPI比較ロジックの堅牢性を確保することにあります。main
関数から比較ロジックを独立した compareAPI
関数として切り出すことで、このロジックを単体テスト可能にし、将来的な変更に対する信頼性を高めています。これにより、Go言語のAPI互換性保証プロセスがより強固なものになります。
前提知識の解説
- Go言語のAPI互換性: Go言語は、バージョンアップに伴うAPIの破壊的変更を極力避けるポリシーを持っています。これは、Goで書かれた既存のプログラムが新しいGoバージョンでも問題なく動作することを保証するためです。
cmd/api
のようなツールは、この互換性ポリシーを技術的に強制する役割を担っています。 cmd/api
ツール: Goのソースコードから公開API(エクスポートされた型、関数、メソッド、変数など)を抽出し、それらを以前のバージョンのAPIと比較することで、互換性のない変更(例: 関数の削除、シグネチャの変更)を検出するコマンドラインツールです。go/ast
およびgo/parser
パッケージ: Go言語のソースコードを抽象構文木(AST: Abstract Syntax Tree)として解析するための標準ライブラリパッケージです。cmd/api
はこれらのパッケージを使用して、GoのソースコードからAPI情報を抽出します。- 単体テスト (Unit Testing): プログラムの個々のコンポーネント(関数やメソッドなど)が正しく動作するかどうかを検証するテスト手法です。
main
関数のようなプログラムのエントリポイントは、通常、多くの依存関係を持つため単体テストが難しい傾向にあります。そのため、テストしたいロジックを独立した関数に切り出す「リファクタリング」がよく行われます。 io.Writer
インターフェース: Go言語の標準ライブラリで定義されているインターフェースで、データを書き込むための抽象的な概念を提供します。os.Stdout
やbytes.Buffer
など、様々な出力先に統一的な方法で書き込むことができます。テストにおいては、実際のファイルや標準出力ではなく、メモリ上のバッファに書き込むことで、出力内容を検証しやすくなります。sort.Strings
: 文字列のスライスをソートするためのGo標準ライブラリ関数です。APIの比較において、抽出された機能リストや必須機能リストなどをソートすることで、比較処理を効率化し、結果の一貫性を保ちます。
技術的詳細
このコミットの主要な技術的変更点は、src/cmd/api/goapi.go
ファイル内の main
関数のリファクタリングと、それに対応する src/cmd/api/goapi_test.go
ファイルへの新しいテストの追加です。
-
main
関数の分割:- 元の
main
関数には、APIの機能抽出、必須APIの読み込み、オプションAPIの読み込み、例外APIの読み込み、そして最終的なAPI比較ロジックがすべて含まれていました。 - このコミットでは、API比較の核心ロジックを
compareAPI
という新しい関数に切り出しています。 compareAPI
関数は、io.Writer
(比較結果の出力先)、features
(現在のAPIから抽出された機能)、required
(必須API)、optional
(オプションAPI)、exception
(例外API) の各スライスを引数として受け取ります。これにより、この関数は特定の入力に対して予測可能な出力を生成し、テストが容易になります。main
関数は、引き続きファイルの読み込みやフラグの解析を行い、準備されたデータをcompareAPI
関数に渡す役割を担います。
- 元の
-
compareAPI
関数の実装:compareAPI
関数内では、optional
とexception
の各スライスをmap[string]bool
型のoptionalSet
とexceptionSet
に変換しています。これは、要素の存在チェックを高速に行うためです(スライスでの線形探索よりもマップでのハッシュルックアップの方が効率的)。features
とrequired
のスライスは、比較前にsort.Strings
を用いてソートされます。これにより、両方のリストを効率的に線形走査しながら比較できます。- 比較ロジックは、
for
ループ内でfeatures
とrequired
の両方のリストを同時に走査し、以下の3つのケースを処理します。- 必須機能が不足している場合 (
required[0] < features[0]
):required
リストに存在するがfeatures
リストに存在しない機能が見つかった場合。これがexceptionSet
に含まれていれば~
プレフィックスで出力し、そうでなければ-
プレフィックスで出力し、互換性破壊としてok = false
を設定します。 - 新しい機能が追加された場合 (
required[0] > features[0]
):features
リストに存在するがrequired
リストに存在しない機能が見つかった場合。これがoptionalSet
に含まれていれば、既知の追加機能として処理し、optionalSet
から削除します。そうでなければ+
プレフィックスで出力し、allowNew
フラグがfalse
の場合は互換性破壊としてok = false
を設定します。 - 両方のリストに存在する機能 (
required[0] == features[0]
): 両方のリストから機能を取り除き、次の比較に進みます。
- 必須機能が不足している場合 (
- 最後に、
optionalSet
に残っている機能(nextFile
には記載されているが、現在のAPIには存在しない機能)があれば、±
プレフィックスで出力します。 - 関数の戻り値
ok
は、互換性破壊が検出されたかどうかを示します。
-
fileFeatures
関数の改善:fileFeatures
関数にif filename == "" { return nil }
というガード句が追加されました。これにより、空のファイル名が渡された場合にioutil.ReadFile
がエラーを返すのを防ぎ、より堅牢になります。
-
テストの追加 (
TestCompareAPI
):goapi_test.go
にTestCompareAPI
という新しいテスト関数が追加されました。- このテスト関数は、
compareAPI
関数の様々なシナリオ(機能の追加、削除、オプション機能、例外的な削除)を網羅するテーブル駆動テストとして実装されています。 bytes.Buffer
を使用してcompareAPI
の出力をキャプチャし、期待される出力と比較することで、関数の動作を検証しています。ok
の戻り値も検証することで、互換性破壊の検出ロジックが正しく機能していることを確認しています。
これらの変更により、cmd/api
ツールの中核であるAPI比較ロジックが独立してテスト可能になり、その信頼性が大幅に向上しました。
コアとなるコードの変更箇所
src/cmd/api/goapi.go
--- a/src/cmd/api/goapi.go
+++ b/src/cmd/api/goapi.go
@@ -22,6 +22,7 @@ import (
"go/parser"
"go/printer"
"go/token"
+ "io"
"io/ioutil"
"log"
"os"
@@ -167,7 +168,6 @@ func main() {
features = append(features, f2)
}
}
- sort.Strings(features)
fail := false
defer func() {
@@ -186,25 +186,26 @@ func main() {
return
}
- var required []string
- for _, filename := range []string{*checkFile} {
- required = append(required, fileFeatures(filename)...)
- }
- sort.Strings(required)
+ required := fileFeatures(*checkFile)
+ optional := fileFeatures(*nextFile)
+ exception := fileFeatures(*exceptFile)
+ fail = !compareAPI(bw, features, required, optional, exception)
+}
+
+func compareAPI(w io.Writer, features, required, optional, exception []string) (ok bool) {
+ ok = true
- var optional = make(map[string]bool) // feature => true
- if *nextFile != "" {
- for _, feature := range fileFeatures(*nextFile) {
- optional[feature] = true
- }
- }
+ var optionalSet = make(map[string]bool) // feature => true
+ var exceptionSet = make(map[string]bool) // exception => true
+ for _, f := range optional {
+ optionalSet[f] = true
+ }
+ for _, f := range exception {
+ exceptionSet[f] = true
+ }
+
+ sort.Strings(features)
+ sort.Strings(required)
- var exception = make(map[string]bool) // exception => true
- if *exceptFile != "" {
- for _, feature := range fileFeatures(*exceptFile) {
- exception[feature] = true
- }
- }
+ take := func(sl *[]string) string {
+ s := (*sl)[0]
@@ -216,23 +217,23 @@ func main() {
switch {
case len(features) == 0 || required[0] < features[0]:
feature := take(&required)
- if exception[feature] {
- fmt.Fprintf(bw, "~%s\\n", feature)
+ if exceptionSet[feature] {
+ fmt.Fprintf(w, "~%s\\n", feature)
} else {
- fmt.Fprintf(bw, "-%s\\n", feature)
- fail = true // broke compatibility
+ fmt.Fprintf(w, "-%s\\n", feature)
+ ok = false // broke compatibility
}
case len(required) == 0 || required[0] > features[0]:
newFeature := take(&features)
- if optional[newFeature] {
+ if optionalSet[newFeature] {
// Known added feature to the upcoming release.
// Delete it from the map so we can detect any upcoming features
// which were never seen. (so we can clean up the nextFile)
- delete(optional, newFeature)
+ delete(optionalSet, newFeature)
} else {
- fmt.Fprintf(bw, "+%s\\n", newFeature)
+ fmt.Fprintf(w, "+%s\\n", newFeature)
if !*allowNew {
- fail = true // we're in lock-down mode for next release
+ ok = false // we're in lock-down mode for next release
}
}
default:
@@ -243,16 +244,20 @@ func main() {
// In next file, but not in API.
var missing []string
- for feature := range optional {
+ for feature := range optionalSet {
missing = append(missing, feature)
}
sort.Strings(missing)
for _, feature := range missing {
- fmt.Fprintf(bw, "±%s\\n", feature)
+ fmt.Fprintf(w, "±%s\\n", feature)
}
+ return
}
func fileFeatures(filename string) []string {
+ if filename == "" {
+ return nil
+ }
bs, err := ioutil.ReadFile(filename)
if err != nil {
log.Fatalf("Error reading file %s: %v", filename, err)
src/cmd/api/goapi_test.go
--- a/src/cmd/api/goapi_test.go
+++ b/src/cmd/api/goapi_test.go
@@ -5,6 +5,7 @@
package main
import (
+ "bytes"
"flag"
"fmt"
"io/ioutil"
@@ -73,3 +74,53 @@ func TestGolden(t *testing.T) {
}
}
}\n
+
+func TestCompareAPI(t *testing.T) {
+ tests := []struct {
+ name string
+ features, required, optional, exception []string
+ ok bool // want
+ out string // want
+ }{
+ {
+ name: "feature added",
+ features: []string{"C", "A", "B"},
+ required: []string{"A", "C"},
+ ok: true,
+ out: "+B\\n",
+ },
+ {
+ name: "feature removed",
+ features: []string{"C", "A"},
+ required: []string{"A", "B", "C"},
+ ok: false,
+ out: "-B\\n",
+ },
+ {
+ name: "feature added then removed",
+ features: []string{"A", "C"},
+ optional: []string{"B"},
+ required: []string{"A", "C"},
+ ok: true,
+ out: "±B\\n",
+ },
+ {
+ name: "exception removal",
+ required: []string{"A", "B", "C"},
+ features: []string{"A", "C"},
+ exception: []string{"B"},
+ ok: true,
+ out: "~B\\n",
+ },
+ }
+ for _, tt := range tests {
+ buf := new(bytes.Buffer)
+ gotok := compareAPI(buf, tt.features, tt.required, tt.optional, tt.exception)
+ if gotok != tt.ok {
+ t.Errorf("%s: ok = %v; want %v", tt.name, gotok, tt.ok)
+ }
+ if got := buf.String(); got != tt.out {
+ t.Errorf("%s: output differs\\nGOT:\\n%s\\nWANT:\\n%s", tt.name, got, tt.out)
+ }
+ }
+}
コアとなるコードの解説
src/cmd/api/goapi.go
の変更点
import "io"
の追加: 新しいcompareAPI
関数がio.Writer
インターフェースを使用するため、io
パッケージがインポートされました。main
関数からのロジックの抽出:- 以前
main
関数内で直接行われていたfeatures
のソート (sort.Strings(features)
) が削除されました。これはcompareAPI
関数内でソートされるようになったためです。 required
,optional
,exception
の各APIリストの準備ロジックはmain
関数に残りますが、これらのリストをcompareAPI
関数に渡す形に変更されました。fail
変数の設定が!compareAPI(...)
の結果に依存するように変更されました。これはcompareAPI
が互換性破壊を検出したかどうかをbool
で返すためです。
- 以前
compareAPI
関数の新規追加:func compareAPI(w io.Writer, features, required, optional, exception []string) (ok bool)
というシグネチャで定義されました。w io.Writer
: 比較結果を書き込むための出力先。これにより、テスト時にbytes.Buffer
などのメモリバッファに書き込むことが可能になります。features
,required
,optional
,exception []string
: それぞれ現在のAPI、必須API、オプションAPI、例外APIのリスト。ok bool
: 互換性チェックが成功したかどうか(互換性破壊がなかったか)を示す戻り値。
optional
とexception
のスライスをoptionalSet
とexceptionSet
というmap[string]bool
に変換しています。これにより、特定の機能がオプションまたは例外リストに存在するかどうかのチェックがO(1)
の平均時間計算量で行えるようになり、効率が向上します。features
とrequired
のスライスは、compareAPI
関数内でsort.Strings
を使ってソートされます。これにより、両方のリストを効率的に比較できます。- 比較ロジックは、
for
ループ内でfeatures
とrequired
の両方のスライスを同時に走査します。required[0] < features[0]
の場合(必須機能が現在のAPIにない場合):exceptionSet
に含まれていれば、~
プレフィックスで出力し、ok
はtrue
のままです。- 含まれていなければ、
-
プレフィックスで出力し、ok = false
となり、互換性破壊が報告されます。
len(required) == 0 || required[0] > features[0]
の場合(現在のAPIに新しい機能がある場合):optionalSet
に含まれていれば、既知の追加機能として処理され、optionalSet
から削除されます。ok
はtrue
のままです。- 含まれていなければ、
+
プレフィックスで出力し、allowNew
フラグがfalse
の場合はok = false
となり、新しい機能の追加が許可されていない場合に互換性破壊が報告されます。
default
の場合(両方のリストに同じ機能がある場合):両方のリストからその機能を取り除き、次の比較に進みます。
- ループ終了後、
optionalSet
に残っている機能(nextFile
には記載されているが、現在のAPIには存在しない機能)があれば、±
プレフィックスで出力されます。
fileFeatures
関数の改善:if filename == "" { return nil }
というチェックが追加されました。これにより、空のファイル名が渡された際にioutil.ReadFile
がエラーを返すのを防ぎ、関数がより堅牢になりました。
src/cmd/api/goapi_test.go
の変更点
import "bytes"
の追加:bytes.Buffer
を使用してcompareAPI
の出力をキャプチャするためにbytes
パッケージがインポートされました。TestCompareAPI
関数の新規追加:compareAPI
関数の単体テストを行うための関数です。tests
という構造体のスライスを定義し、各テストケースの入力 (features
,required
,optional
,exception
)、期待されるok
の戻り値、期待される出力 (out
) をまとめています。これはGoのテストでよく用いられる「テーブル駆動テスト」のパターンです。- 各テストケースについて、
bytes.Buffer
を作成し、それをcompareAPI
関数にio.Writer
として渡します。 compareAPI
の戻り値gotok
とbuf.String()
で得られた出力got
を、それぞれ期待される値 (tt.ok
,tt.out
) と比較します。- 比較が一致しない場合は
t.Errorf
を呼び出してテスト失敗を報告します。
これらの変更により、cmd/api
のAPI比較ロジックがモジュール化され、単体テストによってその正確性が保証されるようになりました。これにより、Go言語のAPI互換性チェックの信頼性が向上し、将来的なGoの進化において安定したAPI提供に貢献します。
関連リンク
- Go言語の互換性保証: https://go.dev/doc/go1compat
- Goの抽象構文木 (AST) について: https://pkg.go.dev/go/ast
- Goのパーサー (parser) について: https://pkg.go.dev/go/parser
参考にした情報源リンク
- https://golang.org/cl/6820057 (元のGerrit Change-ID)
- Go言語の公式ドキュメント (pkg.go.dev)
- Go言語のソースコード (github.com/golang/go)
- Go言語のテストに関する一般的な情報 (Goの公式ブログやドキュメント)
io.Writer
インターフェースに関する情報 (Goの公式ドキュメント)bytes.Buffer
に関する情報 (Goの公式ドキュメント)- テーブル駆動テストに関するGoの慣用的なパターンに関する情報 (Goのテストに関する記事や書籍)