[インデックス 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
は文字列置換を行うための効率的なメカニズムを提供しますが、その内部実装は置換対象のパターンや置換後の文字列の特性に応じて、複数の異なるアルゴリズムを動的に選択します。これらのアルゴリズムが正しく選択され、期待通りに動作することを保証するためのテストは非常に重要です。また、Replacer
がio.Writer
に書き込む際に、io.Writer
側でエラーが発生した場合の挙動をテストすることも、堅牢なコードを保証するために不可欠です。
このコミットは、strings.Replacer
の堅牢性と正確性をさらに高めることを目的としています。
前提知識の解説
Go言語の strings
パッケージと Replacer
Go言語の標準ライブラリstrings
パッケージは、文字列操作のための多くのユーティリティ関数を提供します。その中でもReplacer
型は、複数の文字列置換ルールを効率的に適用するための構造体です。
Replacer
は、strings.NewReplacer(old1, new1, old2, new2, ...)
のように、old
とnew
のペアを可変長引数で受け取り、それらの置換ルールを内部に保持します。そして、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
ファイルに以下の主要な変更を加えています。
-
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"}, }
-
TestPickAlgorithm
の修正: このテスト関数は、strings.NewReplacer
が与えられた置換ルールに基づいて正しい内部アルゴリズムを選択するかどうかを検証します。変更前は関数内にテストケースがハードコードされていましたが、変更後は新しく定義されたalgorithmTestCases
変数を使用するように修正されました。これにより、テストケースの管理が容易になり、他のテストとの整合性が保たれます。 -
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
がそのエラーを適切に処理し、呼び出し元に伝えることをテストするために使用されます。 -
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) } } }
このテストは、
Replacer
がio.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"},
}
capitalLetters
とhtmlEscaper
は、このテストファイル内で既に定義されている*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.Replacer
のWriteString
メソッドが、基となる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.WriteString
がio.Writer
からのエラーを無視せず、正確に伝播させることを保証します。これは、Replacer
を使用するアプリケーションが、出力先の問題(例: ディスク容量不足、ネットワーク切断など)を適切に検出し、対応できるようにするために非常に重要です。
関連リンク
- Go Code Review 109170043: https://golang.org/cl/109170043
参考にした情報源リンク
- 特になし(提供されたコミット情報とGo言語の一般的な知識に基づいています)