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

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

このコミットは、Go言語の実験的なロケール照合(collation)パッケージ exp/locale/collate における回帰テストの修正に関するものです。具体的には、サロゲート文字(surrogate characters)を含むテストケースを無視するロジックを追加し、GoのUTF-8処理の変更によって発生したテストの失敗を解消しています。

コミット

commit c61a185f35fd58a200df7eebc2138af1b52a0c5e
Author: Marcel van Lohuizen <mpvl@golang.org>
Date:   Fri Aug 24 15:56:07 2012 +0200

    exp/locale/collate: add code to ignore tests with (unpaired) surrogates.
    In the regtest data, surrogates are assigned primary weights based on
    the surrogate code point value.  Go now converts surrogates to FFFD, however,
    meaning that the primary weight is based on this code point instead.
    This change drops tests with surrogates and lets the tests pass.
    
    R=r
    CC=golang-dev
    https://golang.org/cl/6461100

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

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

元コミット内容

exp/locale/collate: add code to ignore tests with (unpaired) surrogates. (exp/locale/collate: (対になっていない)サロゲート文字を含むテストを無視するコードを追加)

In the regtest data, surrogates are assigned primary weights based on the surrogate code point value. Go now converts surrogates to FFFD, however, meaning that the primary weight is based on this code point instead. This change drops tests with surrogates and lets the tests pass. (回帰テストデータでは、サロゲート文字にはそのコードポイント値に基づいてプライマリウェイトが割り当てられています。しかし、Goは現在サロゲート文字をU+FFFDに変換するため、プライマリウェイトはこのコードポイントに基づいて決定されます。この変更は、サロゲート文字を含むテストを破棄し、テストがパスするようにします。)

変更の背景

このコミットの背景には、Go言語の文字列処理、特にUTF-8エンコーディングとUnicodeサロゲート文字の扱いに関する変更があります。

Go言語の標準ライブラリ、特にunicode/utf8パッケージは、不正なUTF-8シーケンスや単独のサロゲートコードポイントを、Unicodeの置換文字であるU+FFFD () に変換するようになりました。これは、不正な入力に対する堅牢な処理を保証するための標準的な振る舞いです。

しかし、exp/locale/collateパッケージの回帰テストデータ(CollationTest.zipなど)は、国際化(I18N)における文字列の照合(ソート順の決定)を検証するために、特定のUnicodeコードポイント、特にサロゲート文字の扱いについて、Goの新しい振る舞いとは異なる期待値を持っていました。

具体的には、テストデータではサロゲート文字(U+D800からU+DFFFの範囲)が、そのコードポイント値に基づいて照合のプライマリウェイト(ソート順を決定する主要な要素)を持つと想定されていました。しかし、Goがこれらのサロゲート文字をU+FFFDに変換すると、照合のプライマリウェイトはU+FFFDのそれに変更されてしまいます。この不一致が原因で、サロゲート文字を含むテストケースが失敗するようになりました。

このコミットは、このテストの失敗を解決するために、サロゲート文字を含むテストケースを回帰テストの対象から除外することで、テストスイートが再びパスするようにすることを目的としています。これは、GoのUTF-8処理の変更にテストを適応させるための暫定的な、あるいは実用的な解決策と言えます。

前提知識の解説

このコミットを理解するためには、以下の概念について基本的な知識が必要です。

  1. UnicodeとUTF-8:

    • Unicode: 世界中の文字を統一的に扱うための文字コード標準です。各文字には一意の「コードポイント」(例: U+0041は'A')が割り当てられています。
    • UTF-8: Unicodeコードポイントをバイト列にエンコードするための可変長エンコーディング方式です。ASCII文字は1バイト、日本語の文字などは3バイト以上で表現されます。Go言語の文字列は内部的にUTF-8でエンコードされています。
  2. サロゲート文字(Surrogate Characters):

    • Unicodeには、U+D800からU+DFFFまでの範囲に「サロゲート」と呼ばれる特別なコードポイントがあります。これらは、UTF-16エンコーディングにおいて、基本多言語面(BMP: Basic Multilingual Plane, U+0000からU+FFFFまでの範囲)外の文字(例: 絵文字、歴史的な文字など)を表現するために、2つのサロゲートコードポイントを組み合わせて「サロゲートペア」として使用されます。
    • 単独のサロゲート(Unpaired Surrogates): サロゲートペアを構成しない単独のサロゲートコードポイントは、Unicodeの仕様上、不正な文字と見なされます。これらは有効なUnicodeスカラー値ではありません。
  3. Unicode置換文字(Replacement Character U+FFFD):

    • U+FFFD () は、Unicodeにおいて、エンコーディングエラーや不正な文字シーケンスを検出した際に、その文字の代わりに表示される特別な文字です。Goのunicode/utf8パッケージは、不正なUTF-8バイトシーケンスや単独のサロゲートコードポイントをデコードする際に、このU+FFFDを返します。
  4. 照合(Collation):

    • 照合とは、文字列を特定の言語や文化の規則に従ってソート(並べ替え)したり、比較したりするプロセスです。単純なコードポイント順のソートとは異なり、アクセント記号の有無、大文字小文字、特定の文字の組み合わせ(例: ドイツ語のßとss)などを考慮に入れるため、非常に複雑です。
    • 照合要素(Collation Elements)とウェイト(Weights): 照合は通常、文字列を「照合要素」のシーケンスに分解し、各要素にプライマリ、セカンダリ、ターシャリなどの「ウェイト」を割り当てることで行われます。プライマリウェイトは最も粗いソート順を決定し、セカンダリ、ターシャリと続くにつれてより細かいソート順を決定します。
  5. 回帰テスト(Regression Test):

    • ソフトウェア開発において、既存の機能が新しい変更によって壊れていないことを確認するために実行されるテストです。このコミットでは、exp/locale/collateパッケージの既存の照合ロジックが、GoのUTF-8処理の変更後も正しく機能するかを検証するテストが対象となっています。

技術的詳細

このコミットの技術的な核心は、Go言語のunicode/utf8パッケージのValidRune関数と、照合テストデータの不整合をどのように扱うかという点にあります。

Goのunicode/utf8パッケージは、UTF-8エンコーディングされたバイト列をUnicodeのrune(GoにおけるUnicodeコードポイントの型)にデコードする機能を提供します。このパッケージの重要な側面は、不正なUTF-8シーケンスや単独のサロゲートコードポイントを検出した場合に、それらをU+FFFDに変換して返すという堅牢な振る舞いです。

一方、exp/locale/collateパッケージの回帰テストは、Unicode Collation Algorithm (UCA) のテストデータ(おそらくCollationTest.zipのような標準的なデータセット)に基づいています。このテストデータは、サロゲート文字が特定の照合ウェイトを持つことを期待しています。しかし、Goがこれらのサロゲート文字をU+FFFDに変換すると、照合エンジンはU+FFFDのウェイトを使用して比較を行うため、テストデータが期待する結果と一致しなくなります。

具体的には、utf8.ValidRune(r rune)関数は、引数rが有効なUnicodeスカラー値である場合にtrueを返します。単独のサロゲートコードポイント(U+D800からU+DFFF)は有効なUnicodeスカラー値ではないため、ValidRunefalseを返します。

このコミットでは、このValidRuneの特性を利用して、テストデータから読み込んだ文字が有効なUnicodeスカラー値であるかどうかをチェックします。もし、読み込んだ文字の中にValidRunefalseを返すもの(すなわち、単独のサロゲート文字)が含まれていれば、そのテストケース全体を無効と判断し、テストの実行対象から除外します。

これにより、Goの現在のUTF-8処理の振る舞いと、古いテストデータの期待値との間の不整合によって発生していたテストの失敗を回避しています。これは、テストデータ自体を修正するのではなく、テストの実行ロジック側で特定のテストケースをスキップするというアプローチです。

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

変更はsrc/pkg/exp/locale/collate/regtest.goファイルに集中しています。

--- a/src/pkg/exp/locale/collate/regtest.go
+++ b/src/pkg/exp/locale/collate/regtest.go
@@ -24,6 +24,7 @@ import (
  	"strconv"
  	"strings"
  	"unicode"
+	"unicode/utf8"
  )
  
  // This regression test runs tests for the test files in CollationTest.zip
@@ -53,7 +54,7 @@ var localFiles = flag.Bool("local",
  
  type Test struct {
  	name    string
-	str     []string
+	str     [][]byte
  	comment []string
  }
  
@@ -186,14 +187,23 @@ func loadTestData() []Test {
  		if m == nil || len(m) < 3 {
  			log.Fatalf(`Failed to parse: "%s" result: %#v`, line, m)
  		}
-		str := ""
+		str := []byte{}
+		// In the regression test data (unpaired) surrogates are assigned a weight
+		// corresponding to their code point value.  However, utf8.DecodeRune,
+		// which is used to compute the implicit weight, assigns FFFD to surrogates.
+		// We therefore skip tests with surrogates.  This skips about 35 entries
+		// per test.
+		valid := true
  		for _, split := range strings.Split(m[1], " ") {
  			r, err := strconv.ParseUint(split, 16, 64)
  			Error(err)
-			str += string(rune(r))
+			valid = valid && utf8.ValidRune(rune(r))
+			str = append(str, string(rune(r))...)
+		}
+		if valid {
+			test.str = append(test.str, str)
+			test.comment = append(test.comment, m[2])
  		}
-		test.str = append(test.str, str)
-		test.comment = append(test.comment, m[2])
  	}
  	tests = append(tests, test)
  }
@@ -227,13 +237,13 @@ func doTest(t Test) {
  		c.Alternate = collate.AltNonIgnorable
  	}
  
-	prev := []byte(t.str[0])
+	prev := t.str[0]
  	for i := 1; i < len(t.str); i++ {
-		s := []byte(t.str[i])
+		s := t.str[i]
  		ka := c.Key(b, prev)
  		kb := c.Key(b, s)
  		if r := bytes.Compare(ka, kb); r == 1 {
-			fail(t, "%d: Key(%.4X) < Key(%.4X) (%X < %X) == %d; want -1 or 0", i, runes(prev), runes(s), ka, kb, r)
+			fail(t, "%d: Key(%.4X) < Key(%.4X) (%X < %X) == %d; want -1 or 0", i, []rune(string(prev)), []rune(string(s)), ka, kb, r)
  			prev = s
  			continue
  		}

コアとなるコードの解説

  1. unicode/utf8パッケージのインポート:

    • import "unicode/utf8"が追加されました。これは、サロゲート文字の有効性をチェックするためにutf8.ValidRune関数を使用するためです。
  2. Test構造体のstrフィールドの型変更:

    • type Test struct { ... str []string ... } から type Test struct { ... str [][]byte ... } に変更されました。
    • これは、テストデータとして文字列(string)の配列を保持するのではなく、バイトスライス([]byte)の配列を保持するように変更されたことを意味します。Goではstringは不変のバイトスライスとして扱われるため、この変更はより低レベルで直接的なバイト操作を意図している可能性があります。照合キーの生成にはバイトスライスが直接渡されるため、この変更はより自然なデータ表現と言えます。
  3. loadTestData()関数内の変更:

    • str変数の初期化: str := ""str := []byte{} に変更されました。これは、文字列を構築する代わりにバイトスライスを構築することを示しています。
    • validフラグの導入: valid := true というブール型のフラグが導入されました。このフラグは、現在のテストケースが有効なUnicodeコードポイントのみで構成されているかどうかを追跡するために使用されます。
    • サロゲート文字のチェックとスキップロジック:
      for _, split := range strings.Split(m[1], " ") {
          r, err := strconv.ParseUint(split, 16, 64)
          Error(err)
          valid = valid && utf8.ValidRune(rune(r)) // ここが重要
          str = append(str, string(rune(r))...)
      }
      if valid { // ここも重要
          test.str = append(test.str, str)
          test.comment = append(test.comment, m[2])
      }
      
      • テストデータから16進数で表現されたUnicodeコードポイントrをパースした後、utf8.ValidRune(rune(r))が呼び出されます。
      • ValidRuneは、rが有効なUnicodeスカラー値(つまり、単独のサロゲートではない)であればtrueを返します。
      • valid = valid && utf8.ValidRune(rune(r))という行は、ループ内で一度でもValidRunefalseを返した場合(つまり、単独のサロゲート文字が見つかった場合)、validフラグがfalseになり、それ以降trueに戻ることはありません。
      • ループの終了後、if valid { ... }ブロックによって、validフラグがtrueのままの場合にのみ、現在のテストケース(test.strtest.comment)がtestsスライスに追加されます。これにより、サロゲート文字を含むテストケースはtestsスライスに追加されず、結果としてテストが実行されなくなります。
  4. doTest()関数内の変更:

    • prev := []byte(t.str[0])prev := t.str[0] に、s := []byte(t.str[i])s := t.str[i] に変更されました。これは、Test.strの型が[][]byteに変更されたことに伴う、不要になった型変換の削除です。
    • fail関数呼び出し内の引数変更:
      • runes(prev)runes(s)[]rune(string(prev))[]rune(string(s)) に変更されました。
      • これは、prevs[]byte型になったため、runesヘルパー関数(おそらく[]runeを引数に取る)に渡す前に、string()で文字列に変換し、さらに[]rune()でruneスライスに変換する必要があるためです。

これらの変更により、GoのUTF-8処理の振る舞いと整合性の取れないテストケースが自動的にスキップされ、テストスイートが正常に機能するようになります。

関連リンク

参考にした情報源リンク

  • コミットメッセージと差分(上記に記載)
  • Go言語の公式ドキュメント(unicode/utf8パッケージに関する情報)
  • Unicode標準の関連ドキュメント(サロゲート文字、U+FFFDに関する情報)
  • 一般的なソフトウェア開発における回帰テストの概念
  • 国際化(I18N)における照合の概念
  • Go言語のGerritコードレビューシステム(golang.org/cl/6461100