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

[インデックス 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.Stdoutbytes.Buffer など、様々な出力先に統一的な方法で書き込むことができます。テストにおいては、実際のファイルや標準出力ではなく、メモリ上のバッファに書き込むことで、出力内容を検証しやすくなります。
  • sort.Strings: 文字列のスライスをソートするためのGo標準ライブラリ関数です。APIの比較において、抽出された機能リストや必須機能リストなどをソートすることで、比較処理を効率化し、結果の一貫性を保ちます。

技術的詳細

このコミットの主要な技術的変更点は、src/cmd/api/goapi.go ファイル内の main 関数のリファクタリングと、それに対応する src/cmd/api/goapi_test.go ファイルへの新しいテストの追加です。

  1. main 関数の分割:

    • 元の main 関数には、APIの機能抽出、必須APIの読み込み、オプションAPIの読み込み、例外APIの読み込み、そして最終的なAPI比較ロジックがすべて含まれていました。
    • このコミットでは、API比較の核心ロジックを compareAPI という新しい関数に切り出しています。
    • compareAPI 関数は、io.Writer (比較結果の出力先)、features (現在のAPIから抽出された機能)、required (必須API)、optional (オプションAPI)、exception (例外API) の各スライスを引数として受け取ります。これにより、この関数は特定の入力に対して予測可能な出力を生成し、テストが容易になります。
    • main 関数は、引き続きファイルの読み込みやフラグの解析を行い、準備されたデータを compareAPI 関数に渡す役割を担います。
  2. compareAPI 関数の実装:

    • compareAPI 関数内では、optionalexception の各スライスを map[string]bool 型の optionalSetexceptionSet に変換しています。これは、要素の存在チェックを高速に行うためです(スライスでの線形探索よりもマップでのハッシュルックアップの方が効率的)。
    • featuresrequired のスライスは、比較前に sort.Strings を用いてソートされます。これにより、両方のリストを効率的に線形走査しながら比較できます。
    • 比較ロジックは、featuresrequired の両方のリストを同時に走査し、以下の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 は、互換性破壊が検出されたかどうかを示します。
  3. fileFeatures 関数の改善:

    • fileFeatures 関数に if filename == "" { return nil } というガード句が追加されました。これにより、空のファイル名が渡された場合に ioutil.ReadFile がエラーを返すのを防ぎ、より堅牢になります。
  4. テストの追加 (TestCompareAPI):

    • goapi_test.goTestCompareAPI という新しいテスト関数が追加されました。
    • このテスト関数は、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: 互換性チェックが成功したかどうか(互換性破壊がなかったか)を示す戻り値。
    • optionalexception のスライスを optionalSetexceptionSet という map[string]bool に変換しています。これにより、特定の機能がオプションまたは例外リストに存在するかどうかのチェックが O(1) の平均時間計算量で行えるようになり、効率が向上します。
    • featuresrequired のスライスは、compareAPI 関数内で sort.Strings を使ってソートされます。これにより、両方のリストを効率的に比較できます。
    • 比較ロジックは、for ループ内で featuresrequired の両方のスライスを同時に走査します。
      • required[0] < features[0] の場合(必須機能が現在のAPIにない場合):
        • exceptionSet に含まれていれば、~ プレフィックスで出力し、oktrue のままです。
        • 含まれていなければ、- プレフィックスで出力し、ok = false となり、互換性破壊が報告されます。
      • len(required) == 0 || required[0] > features[0] の場合(現在のAPIに新しい機能がある場合):
        • optionalSet に含まれていれば、既知の追加機能として処理され、optionalSet から削除されます。oktrue のままです。
        • 含まれていなければ、+ プレフィックスで出力し、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 の戻り値 gotokbuf.String() で得られた出力 got を、それぞれ期待される値 (tt.ok, tt.out) と比較します。
    • 比較が一致しない場合は t.Errorf を呼び出してテスト失敗を報告します。

これらの変更により、cmd/api のAPI比較ロジックがモジュール化され、単体テストによってその正確性が保証されるようになりました。これにより、Go言語のAPI互換性チェックの信頼性が向上し、将来的なGoの進化において安定したAPI提供に貢献します。

関連リンク

参考にした情報源リンク

  • 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.Stdoutbytes.Buffer など、様々な出力先に統一的な方法で書き込むことができます。テストにおいては、実際のファイルや標準出力ではなく、メモリ上のバッファに書き込むことで、出力内容を検証しやすくなります。
  • sort.Strings: 文字列のスライスをソートするためのGo標準ライブラリ関数です。APIの比較において、抽出された機能リストや必須機能リストなどをソートすることで、比較処理を効率化し、結果の一貫性を保ちます。

技術的詳細

このコミットの主要な技術的変更点は、src/cmd/api/goapi.go ファイル内の main 関数のリファクタリングと、それに対応する src/cmd/api/goapi_test.go ファイルへの新しいテストの追加です。

  1. main 関数の分割:

    • 元の main 関数には、APIの機能抽出、必須APIの読み込み、オプションAPIの読み込み、例外APIの読み込み、そして最終的なAPI比較ロジックがすべて含まれていました。
    • このコミットでは、API比較の核心ロジックを compareAPI という新しい関数に切り出しています。
    • compareAPI 関数は、io.Writer (比較結果の出力先)、features (現在のAPIから抽出された機能)、required (必須API)、optional (オプションAPI)、exception (例外API) の各スライスを引数として受け取ります。これにより、この関数は特定の入力に対して予測可能な出力を生成し、テストが容易になります。
    • main 関数は、引き続きファイルの読み込みやフラグの解析を行い、準備されたデータを compareAPI 関数に渡す役割を担います。
  2. compareAPI 関数の実装:

    • compareAPI 関数内では、optionalexception の各スライスを map[string]bool 型の optionalSetexceptionSet に変換しています。これは、要素の存在チェックを高速に行うためです(スライスでの線形探索よりもマップでのハッシュルックアップの方が効率的)。
    • featuresrequired のスライスは、比較前に sort.Strings を用いてソートされます。これにより、両方のリストを効率的に線形走査しながら比較できます。
    • 比較ロジックは、for ループ内で featuresrequired の両方のリストを同時に走査し、以下の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 は、互換性破壊が検出されたかどうかを示します。
  3. fileFeatures 関数の改善:

    • fileFeatures 関数に if filename == "" { return nil } というガード句が追加されました。これにより、空のファイル名が渡された場合に ioutil.ReadFile がエラーを返すのを防ぎ、より堅牢になります。
  4. テストの追加 (TestCompareAPI):

    • goapi_test.goTestCompareAPI という新しいテスト関数が追加されました。
    • このテスト関数は、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: 互換性チェックが成功したかどうか(互換性破壊がなかったか)を示す戻り値。
    • optionalexception のスライスを optionalSetexceptionSet という map[string]bool に変換しています。これにより、特定の機能がオプションまたは例外リストに存在するかどうかのチェックが O(1) の平均時間計算量で行えるようになり、効率が向上します。
    • featuresrequired のスライスは、compareAPI 関数内で sort.Strings を使ってソートされます。これにより、両方のリストを効率的に比較できます。
    • 比較ロジックは、for ループ内で featuresrequired の両方のスライスを同時に走査します。
      • required[0] < features[0] の場合(必須機能が現在のAPIにない場合):
        • exceptionSet に含まれていれば、~ プレフィックスで出力し、oktrue のままです。
        • 含まれていなければ、- プレフィックスで出力し、ok = false となり、互換性破壊が報告されます。
      • len(required) == 0 || required[0] > features[0] の場合(現在のAPIに新しい機能がある場合):
        • optionalSet に含まれていれば、既知の追加機能として処理され、optionalSet から削除されます。oktrue のままです。
        • 含まれていなければ、+ プレフィックスで出力し、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 の戻り値 gotokbuf.String() で得られた出力 got を、それぞれ期待される値 (tt.ok, tt.out) と比較します。
    • 比較が一致しない場合は t.Errorf を呼び出してテスト失敗を報告します。

これらの変更により、cmd/api のAPI比較ロジックがモジュール化され、単体テストによってその正確性が保証されるようになりました。これにより、Go言語のAPI互換性チェックの信頼性が向上し、将来的なGoの進化において安定したAPI提供に貢献します。

関連リンク

参考にした情報源リンク

  • 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のテストに関する記事や書籍)