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

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

このコミットは、Go言語の標準ライブラリstringsパッケージ内のreplace_test.goファイルに対する変更です。具体的には、strings.Replacerの内部アルゴリズム選択と、WriteStringメソッドのエラーハンドリングに関するテストが追加・修正されています。

コミット

commit bcda286d34e0e1af5c9fb580869b73503d691b5c
Author: Dave Cheney <dave@cheney.net>
Date:   Wed Jun 25 03:06:07 2014 +1000

    strings: additional tests
    
    This CL re-applies the tests added in CL 101330053 and subsequently rolled back in CL 102610043.
    
    The original author of this change was Rui Ueyama <ruiu@google.com>
    
    LGTM=r, ruiu
    R=ruiu, r
    CC=golang-codereviews
    https://golang.org/cl/109170043

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

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

元コミット内容

strings: additional tests

This CL re-applies the tests added in CL 101330053 and subsequently rolled back in CL 102610043.

The original author of this change was Rui Ueyama <ruiu@google.com>

LGTM=r, ruiu
R=ruiu, r
CC=golang-codereviews
https://golang.org/cl/109170043

変更の背景

このコミットの背景には、過去に一度追加されたテストが何らかの理由でロールバック(取り消し)され、その後再び適用されるという経緯があります。コミットメッセージによると、これらのテストは元々 CL 101330053 で追加されましたが、CL 102610043 でロールバックされました。そして、このコミット bcda286d34e0e1af5c9fb580869b73503d691b5c (CL 109170043) で再適用されています。

通常、テストがロールバックされるのは、そのテスト自体に問題があったり、テストが依存するコードに未解決の問題があったり、あるいはテストが特定の環境で不安定になったりする場合です。今回の再適用は、それらの問題が解決されたか、あるいはテストの重要性が再認識されたことを示唆しています。

strings.Replacerは文字列置換を行うための効率的なメカニズムを提供しますが、その内部実装は置換対象のパターンや置換後の文字列の特性に応じて、複数の異なるアルゴリズムを動的に選択します。これらのアルゴリズムが正しく選択され、期待通りに動作することを保証するためのテストは非常に重要です。また、Replacerio.Writerに書き込む際に、io.Writer側でエラーが発生した場合の挙動をテストすることも、堅牢なコードを保証するために不可欠です。

このコミットは、strings.Replacerの堅牢性と正確性をさらに高めることを目的としています。

前提知識の解説

Go言語の strings パッケージと Replacer

Go言語の標準ライブラリstringsパッケージは、文字列操作のための多くのユーティリティ関数を提供します。その中でもReplacer型は、複数の文字列置換ルールを効率的に適用するための構造体です。

Replacerは、strings.NewReplacer(old1, new1, old2, new2, ...) のように、oldnewのペアを可変長引数で受け取り、それらの置換ルールを内部に保持します。そして、Replace(s string) メソッドで文字列全体に対して置換を実行したり、WriteString(w io.Writer, s string) メソッドで指定されたio.Writerに置換結果を書き込んだりすることができます。

Replacerの内部では、置換ルールの特性(例: 置換対象の文字列がすべて1バイト文字か、置換ルールが1つだけか、など)に基づいて、最適な置換アルゴリズムが動的に選択されます。これにより、パフォーマンスが最適化されます。主な内部アルゴリズムには以下のようなものがあります。

  • *strings.byteReplacer: すべての置換対象が1バイト文字の場合に利用される、バイト単位での高速な置換アルゴリズム。
  • *strings.byteStringReplacer: 1バイト文字と複数バイト文字が混在する場合や、特定の条件で利用されるアルゴリズム。
  • *strings.singleStringReplacer: 置換ルールが1つだけの場合に利用される、シンプルな置換アルゴリズム。
  • *strings.genericReplacer: 上記のいずれにも当てはまらない、より複雑な置換ルールの場合に利用される汎用的なアルゴリズム。これは通常、トライ木(Trie)などのデータ構造を用いて効率的な検索と置換を行います。

Go言語の io.Writer インターフェース

io.WriterはGo言語の標準ライブラリioパッケージで定義されているインターフェースです。データを書き込むための抽象的な概念を表現します。

type Writer interface {
    Write(p []byte) (n int, err error)
}

このインターフェースは、Writeという単一のメソッドを持ちます。Writeメソッドはバイトスライスpを受け取り、実際に書き込まれたバイト数nと、発生したエラーerrを返します。

ファイル、ネットワーク接続、標準出力など、Go言語における多くの出力先はio.Writerインターフェースを実装しています。これにより、異なる出力先に対して統一的な方法でデータを書き込むことが可能になります。

Replacer.WriteStringメソッドは、このio.Writerインターフェースを利用して、置換結果を任意の出力先に直接書き込むことができます。この際、io.Writerの実装側で書き込みエラーが発生する可能性があり、Replacer.WriteStringはそのエラーを適切に呼び出し元に伝える必要があります。

技術的詳細

このコミットは、src/pkg/strings/replace_test.goファイルに以下の主要な変更を加えています。

  1. algorithmTestCases 変数の導入: TestPickAlgorithm関数内でローカルに定義されていたテストケースのスライスが、algorithmTestCasesというグローバル(パッケージスコープ)変数として切り出されました。これにより、同じテストケースを複数のテスト関数で再利用できるようになります。このスライスは、*Replacerのインスタンスと、そのReplacerが内部で選択すると期待されるアルゴリズムの型名(文字列)のペアを保持しています。

    var algorithmTestCases = []struct {
    	r    *Replacer
    	want string
    }{
    	{capitalLetters, "*strings.byteReplacer"},
    	{htmlEscaper, "*strings.byteStringReplacer"},
    	{NewReplacer("12", "123"), "*strings.singleStringReplacer"},
    	{NewReplacer("1", "12"), "*strings.byteStringReplacer"},
    	{NewReplacer("", "X"), "*strings.genericReplacer"},
    	{NewReplacer("a", "1", "b", "12", "cde", "123"), "*strings.genericReplacer"},
    }
    
  2. TestPickAlgorithm の修正: このテスト関数は、strings.NewReplacerが与えられた置換ルールに基づいて正しい内部アルゴリズムを選択するかどうかを検証します。変更前は関数内にテストケースがハードコードされていましたが、変更後は新しく定義されたalgorithmTestCases変数を使用するように修正されました。これにより、テストケースの管理が容易になり、他のテストとの整合性が保たれます。

  3. errWriter 型の追加: io.Writerインターフェースを実装する新しい型errWriterが定義されました。この型は、Writeメソッドが常にエラー("unwritable"というメッセージを持つfmt.Errorf("unwritable"))を返すように実装されています。

    type errWriter struct{}
    
    func (errWriter) Write(p []byte) (n int, err error) {
    	return 0, fmt.Errorf("unwritable")
    }
    

    このerrWriterは、io.Writerが書き込みエラーを報告した場合に、Replacer.WriteStringがそのエラーを適切に処理し、呼び出し元に伝えることをテストするために使用されます。

  4. TestWriteStringError テスト関数の追加: この新しいテスト関数は、Replacer.WriteStringメソッドが、基となるio.Writerから受け取ったエラーを正しく返すことを検証します。 algorithmTestCasesの各Replacerインスタンスに対して、errWriterに文字列を書き込もうとします。そして、WriteStringが返したバイト数nが0であること、エラーがnilでないこと、そしてエラーメッセージが期待通り"unwritable"であることを確認します。

    func TestWriteStringError(t *testing.T) {
    	for i, tc := range algorithmTestCases {
    		n, err := tc.r.WriteString(errWriter{}, "abc")
    		if n != 0 || err == nil || err.Error() != "unwritable" {
    			t.Errorf("%d. WriteStringError = %d, %v, want 0, unwritable", i, n, err)
    		}
    	}
    }
    

    このテストは、Replacerio.Writerとのインタラクションにおいて堅牢であることを保証するために重要です。

これらの変更は、strings.Replacerの内部動作の正確性を保証し、特にエラー発生時の挙動を明確にテストすることで、ライブラリの信頼性を向上させます。

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

--- a/src/pkg/strings/replace_test.go
+++ b/src/pkg/strings/replace_test.go
@@ -308,20 +308,21 @@ func TestReplacer(t *testing.T) {
 	}
 }
 
+var algorithmTestCases = []struct {
+	r    *Replacer
+	want string
+}{
+	{capitalLetters, "*strings.byteReplacer"},
+	{htmlEscaper, "*strings.byteStringReplacer"},
+	{NewReplacer("12", "123"), "*strings.singleStringReplacer"},
+	{NewReplacer("1", "12"), "*strings.byteStringReplacer"},
+	{NewReplacer("", "X"), "*strings.genericReplacer"},
+	{NewReplacer("a", "1", "b", "12", "cde", "123"), "*strings.genericReplacer"},
+}
+
 // TestPickAlgorithm tests that NewReplacer picks the correct algorithm.
 func TestPickAlgorithm(t *testing.T) {
--	testCases := []struct {
--		r    *Replacer
--		want string
--	}{
--		{capitalLetters, "*strings.byteReplacer"},
--		{htmlEscaper, "*strings.byteStringReplacer"},
--		{NewReplacer("12", "123"), "*strings.singleStringReplacer"},
--		{NewReplacer("1", "12"), "*strings.byteStringReplacer"},
--		{NewReplacer("", "X"), "*strings.genericReplacer"},
--		{NewReplacer("a", "1", "b", "12", "cde", "123"), "*strings.genericReplacer"},
--	}
--	for i, tc := range testCases {
++	for i, tc := range algorithmTestCases {
 		got := fmt.Sprintf("%T", tc.r.Replacer())
 		if got != tc.want {
 			t.Errorf("%d. algorithm = %s, want %s", i, got, tc.want)
@@ -329,6 +330,23 @@ func TestPickAlgorithm(t *testing.T) {
 	}
 }
 
+type errWriter struct{}
+
+func (errWriter) Write(p []byte) (n int, err error) {
+	return 0, fmt.Errorf("unwritable")
+}
+
+// TestWriteStringError tests that WriteString returns an error
+// received from the underlying io.Writer.
+func TestWriteStringError(t *testing.T) {
+	for i, tc := range algorithmTestCases {
+		n, err := tc.r.WriteString(errWriter{}, "abc")
+		if n != 0 || err == nil || err.Error() != "unwritable" {
+			t.Errorf("%d. WriteStringError = %d, %v, want 0, unwritable", i, n, err)
+		}
+	}
+}
+
 // TestGenericTrieBuilding verifies the structure of the generated trie. There
 // is one node per line, and the key ending with the current line is in the
 // trie if it ends with a "+\".

コアとなるコードの解説

var algorithmTestCases の追加

このコードブロックは、strings.Replacerの内部アルゴリズム選択をテストするための共通のテストケースを定義しています。

var algorithmTestCases = []struct {
	r    *Replacer
	want string
}{
	{capitalLetters, "*strings.byteReplacer"},
	{htmlEscaper, "*strings.byteStringReplacer"},
	{NewReplacer("12", "123"), "*strings.singleStringReplacer"},
	{NewReplacer("1", "12"), "*strings.byteStringReplacer"},
	{NewReplacer("", "X"), "*strings.genericReplacer"},
	{NewReplacer("a", "1", "b", "12", "cde", "123"), "*strings.genericReplacer"},
}
  • capitalLettershtmlEscaperは、このテストファイル内で既に定義されている*Replacerのインスタンスです。これらはそれぞれ、すべて1バイト文字の置換と、HTMLエスケープのような複数バイト文字を含む置換の例として使われます。
  • NewReplacer("12", "123") は、単一の置換ルールを持つReplacerの例です。これは通常、*strings.singleStringReplacerアルゴリズムを選択します。
  • NewReplacer("1", "12") は、1バイト文字の置換ルールを持つReplacerの例です。これは*strings.byteStringReplacerアルゴリズムを選択する可能性があります。
  • NewReplacer("", "X") は、空文字列を置換対象とするReplacerの例です。空文字列の置換は特殊なケースであり、通常は*strings.genericReplacerアルゴリズムが選択されます。
  • NewReplacer("a", "1", "b", "12", "cde", "123") は、複数の置換ルールを持つReplacerの例です。これは最も汎用的な*strings.genericReplacerアルゴリズムを選択します。

このalgorithmTestCasesをグローバル変数として定義することで、TestPickAlgorithmだけでなく、後述のTestWriteStringErrorでも同じテストデータを利用できるようになり、テストコードの重複を減らし、保守性を向上させています。

TestPickAlgorithm の変更

-	testCases := []struct {
-		r    *Replacer
-		want string
-	}{
-		{capitalLetters, "*strings.byteReplacer"},
-		{htmlEscaper, "*strings.byteStringReplacer"},
-		{NewReplacer("12", "123"), "*strings.singleStringReplacer"},
-		{NewReplacer("1", "12"), "*strings.byteStringReplacer"},
-		{NewReplacer("", "X"), "*strings.genericReplacer"},
-		{NewReplacer("a", "1", "b", "12", "cde", "123"), "*strings.genericReplacer"},
-	}
-	for i, tc := range testCases {
+	for i, tc := range algorithmTestCases {

この変更は、TestPickAlgorithm関数が、関数内でローカルに定義されていたtestCasesスライスではなく、新しく追加されたパッケージスコープのalgorithmTestCases変数を使用するように修正されたことを示しています。これにより、テストケースの定義が一箇所に集約され、コードの重複が解消されました。テストのロジック自体は変更されていません。

type errWriter struct{} の追加

type errWriter struct{}

func (errWriter) Write(p []byte) (n int, err error) {
	return 0, fmt.Errorf("unwritable")
}

このコードブロックは、io.Writerインターフェースを実装するカスタム型errWriterを定義しています。この型のWriteメソッドは、常に0バイトを書き込んだことと、"unwritable"というエラーを返します。これは、io.Writerへの書き込みが失敗した場合のシナリオをシミュレートするために特別に設計されています。

TestWriteStringError の追加

// TestWriteStringError tests that WriteString returns an error
// received from the underlying io.Writer.
func TestWriteStringError(t *testing.T) {
	for i, tc := range algorithmTestCases {
		n, err := tc.r.WriteString(errWriter{}, "abc")
		if n != 0 || err == nil || err.Error() != "unwritable" {
			t.Errorf("%d. WriteStringError = %d, %v, want 0, unwritable", i, n, err)
		}
	}
}

この新しいテスト関数は、strings.ReplacerWriteStringメソッドが、基となるio.Writerから返されたエラーを適切に処理し、呼び出し元に伝播するかどうかを検証します。

  • for i, tc := range algorithmTestCases: algorithmTestCasesで定義されたすべてのReplacerインスタンスに対してテストを繰り返します。これにより、異なる内部アルゴリズムを持つReplacerがすべてエラーハンドリングを正しく行うことを確認します。
  • n, err := tc.r.WriteString(errWriter{}, "abc"): 各Replacerインスタンスtc.rに対して、常にエラーを返すerrWriterに文字列"abc"を書き込もうとします。
  • if n != 0 || err == nil || err.Error() != "unwritable":
    • n != 0: 書き込まれたバイト数が0であることを確認します。errWriterは常に0を返すため、WriteStringも0を返すはずです。
    • err == nil: エラーがnilでないことを確認します。errWriterは常にエラーを返すため、WriteStringもエラーを返すはずです。
    • err.Error() != "unwritable": 返されたエラーメッセージがerrWriterが返す"unwritable"と一致することを確認します。

このテストは、Replacer.WriteStringio.Writerからのエラーを無視せず、正確に伝播させることを保証します。これは、Replacerを使用するアプリケーションが、出力先の問題(例: ディスク容量不足、ネットワーク切断など)を適切に検出し、対応できるようにするために非常に重要です。

関連リンク

参考にした情報源リンク

  • 特になし(提供されたコミット情報とGo言語の一般的な知識に基づいています)