[インデックス 13367] ファイルの概要
このコミットは、Go言語の実験的なロケールパッケージ exp/locale/collate
に対して、Unicode Collation Algorithm (UCA) のテストファイルに基づいた回帰テストを追加するものです。具体的には、src/pkg/exp/locale/collate/regtest.go
という新しいテストファイルが追加され、UCAの公式テストデータを用いて collate
パッケージの文字列比較機能が正しく動作するかを検証します。
コミット
commit 77b1378c3e8b2669ae560c3a11c30e07f0a8cf8f
Author: Marcel van Lohuizen <mpvl@golang.org>
Date: Tue Jun 19 11:34:56 2012 -0700
exp/locale/collate: added regression test for collate package
based on UCA test files.
R=r
CC=golang-dev
https://golang.org/cl/6216056
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/77b1378c3e8b2669ae560c3a11c30e07f0a8cf8f
元コミット内容
exp/locale/collate: added regression test for collate package
based on UCA test files.
R=r
CC=golang-dev
https://golang.org/cl/6216056
変更の背景
このコミットの背景には、Go言語の exp/locale/collate
パッケージの品質と正確性を保証するという目的があります。collate
パッケージは、異なる言語や地域(ロケール)における文字列のソート順序を扱うためのものです。文字列のソートは、単なる文字コードの比較では不十分であり、言語固有のルール(例: ドイツ語の 'ä' は 'a' と 'e' の間に来る、スペイン語の 'ch' は一文字として扱われるなど)を考慮する必要があります。
Unicode Consortiumは、このような複雑な文字列比較の標準としてUnicode Collation Algorithm (UCA) を定義しています。UCAは、多言語環境での正確な文字列ソートを実現するための詳細なアルゴリズムを提供し、その実装がUCAの仕様に準拠しているかを検証するためのテストファイル(CollationTest.zip)も提供しています。
このコミットは、collate
パッケージがUCAの仕様に沿って正しく動作するかを確認するための回帰テストを導入することで、将来的な変更によって既存の正しい動作が損なわれないようにすることを目的としています。特に、Unicodeのバージョンアップに伴うUCAの変更にも対応できるよう、テストデータがUnicodeのバージョンと同期しているかどうかのチェックも含まれています。
前提知識の解説
ロケールと文字列照合(Collation)
ロケール(Locale): コンピュータプログラムがユーザーの言語、地域、文化的な慣習に合わせて動作するための設定の集合です。これには、日付と時刻のフォーマット、通貨表示、数値の区切り文字、そして文字列のソート順序などが含まれます。
文字列照合(Collation): 文字列を特定の順序で並べ替えるプロセスです。これは単に文字のUnicodeコードポイントを比較するだけでは不十分で、言語や文化に特有のルールを考慮する必要があります。例えば、英語では大文字と小文字が区別されないソート(case-insensitive sort)が一般的ですが、スウェーデン語では 'å', 'ä', 'ö' がアルファベットの最後に位置するなど、言語ごとに異なるルールが存在します。
Unicode Collation Algorithm (UCA)
Unicode Collation Algorithm (UCA): Unicode Consortiumによって定義された、多言語環境における文字列照合のための標準アルゴリズムです。UCAは、言語や文化に依存しない一貫したソート順序を提供することを目的としています。UCAは、文字列を比較する際に、文字の重み付け(Primary, Secondary, Tertiary, Quaternary)を用いて、アクセント、大文字・小文字、句読点などの違いを段階的に考慮します。
Default Unicode Collation Element Table (DUCET): UCAの核となるデータテーブルで、各Unicode文字がどのように照合されるべきかを示す「照合要素(Collation Element)」が定義されています。DUCETは、UCAのデフォルトの動作を規定し、特定のロケールに合わせたカスタマイズ(ロケール固有の照合ルール)の基盤となります。
回帰テスト(Regression Test)
回帰テスト(Regression Test): ソフトウェアの変更(バグ修正、新機能追加など)が、既存の機能に予期せぬ悪影響(回帰バグ)を与えていないことを確認するために実行されるテストです。このコミットで追加された回帰テストは、collate
パッケージの将来の変更が、UCAの仕様に準拠した文字列照合の正確性を損なわないことを保証するために重要です。
技術的詳細
このコミットで追加された regtest.go
は、Unicode Consortiumが提供するUCAのテストファイル CollationTest.zip
をダウンロードし、その内容を解析して collate
パッケージの Compare
および Key
メソッドの動作を検証します。
テストの基本的な流れは以下の通りです。
-
テストデータの取得:
http://www.unicode.org/Public/UCA/<unicode.Version>/CollationTest.zip
からUCAのテストファイルをダウンロードします。<unicode.Version>
はGoのunicode
パッケージがサポートするUnicodeのバージョンに置き換えられます。localFiles
フラグが設定されている場合、ローカルファイルシステムからテストデータを読み込むことも可能です(デバッグ用)。- ダウンロードしたZIPアーカイブを解凍し、個々のテストファイル(例:
CollationTest_*.txt
)を読み込みます。
-
テストデータの解析:
- 各テストファイルは、ソート済みの文字列ペアのリストを含んでいます。各行は
0009 0021; # ('\u0009') <CHARACTER TABULATION> [| | | 0201 025E]
のような形式です。 - セミコロンの前の部分は、Unicodeコードポイントの16進数表現で、これが比較対象の文字列を構成します。
#
の後の部分はコメントです。regtest.go
は正規表現^([\dA-F ]+);.*# (.*)\n?$
を用いて、各行から16進数表現の文字列とコメントを抽出します。- 抽出された16進数表現は、Goの文字列に変換されます。
- 各テストファイルは、ソート済みの文字列ペアのリストを含んでいます。各行は
-
照合テストの実行:
doTest
関数が各テストファイルに対して実行されます。collate.Root
をベースとしたcollate.Collator
インスタンスが作成されます。このインスタンスは、デフォルトでcollate.Tertiary
の強度(Primary, Secondary, Tertiaryの全てのレベルで比較を行う)を持ちます。- テストファイル内の文字列は、既にソートされた順序で並んでいます。したがって、隣接する文字列
prev
とs
を比較すると、collate.Compare(b, prev, s)
は-1
または0
を返すはずであり、collate.Compare(b, s, prev)
は1
または0
を返すはずです。 collate.Key
メソッドもテストされます。Key
メソッドは、文字列の照合キーを生成します。生成されたキーは、バイト列として比較可能であり、キーの比較結果が文字列の照合結果と一致することを確認します。NON_IGNOR
を含むテストファイルの場合、collate.Alternate
がcollate.AltNonIgnorable
に設定され、非無視文字(例: スペース、句読点)も照合に影響を与えるようにします。
-
エラーハンドリングと報告:
- テスト中にエラーが発生した場合、
fail
関数が呼び出され、エラーメッセージとエラーカウントが記録されます。 - エラーカウントが30を超えると、テストは強制終了されます。
- 全てのテストが成功した場合、"PASS" が出力されます。
- テスト中にエラーが発生した場合、
このテストは、Goの exp/locale/collate
パッケージがUnicode Collation Algorithmの複雑なルールにどれだけ正確に準拠しているかを、公式のテストデータを用いて厳密に検証するものです。
コアとなるコードの変更箇所
このコミットでは、src/pkg/exp/locale/collate/regtest.go
という新しいファイルが追加されています。
--- /dev/null
+++ b/src/pkg/exp/locale/collate/regtest.go
@@ -0,0 +1,180 @@
+// Copyright 2012 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// +build ignore
+
+package main
+
+import (
+ "archive/zip"
+ "bufio"
+ "bytes"
+ "exp/locale/collate"
+ "flag"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "net/http"
+ "os"
+ "path"
+ "regexp"
+ "strconv"
+ "strings"
+ "unicode"
+)
+
+// This regression test runs tests for the test files in CollationTest.zip
+// (taken from http://www.unicode.org/Public/UCA/<unicode.Version>/).
+//
+// The test files have the following form:
+// # header
+// 0009 0021; # ('\u0009') <CHARACTER TABULATION> [| | | 0201 025E]
+// 0009 003F; # ('\u0009') <CHARACTER TABULATION> [| | | 0201 0263]
+// 000A 0021; # ('\u000A') <LINE FEED (LF)>\t[| | | 0202 025E]
+// 000A 003F; # ('\u000A') <LINE FEED (LF)>\t[| | | 0202 0263]
+//
+// The part before the semicolon is the hex representation of a sequence
+// of runes. After the hash mark is a comment. The strings
+// represented by rune sequence are in the file in sorted order, as
+// defined by the DUCET.
+
+var url = flag.String("url",
+ "http://www.unicode.org/Public/UCA/"+unicode.Version+"/CollationTest.zip",
+ "URL of Unicode collation tests zip file")
+var localFiles = flag.Bool("local",
+ false,
+ "data files have been copied to the current directory; for debugging only")
+
+type Test struct {
+ name string
+ str []string
+ comment []string
+}
+
+var versionRe = regexp.MustCompile(`# UCA Version: (.*)\n?$`)
+var testRe = regexp.MustCompile(`^([\dA-F ]+);.*# (.*)\n?$`)
+
+func Error(e error) {
+ if e != nil {
+ log.Fatal(e)
+ }
+}
+
+func loadTestData() []Test {
+ if *localFiles {
+ pwd, _ := os.Getwd()
+ *url = "file://" + path.Join(pwd, path.Base(*url))
+ }
+ t := &http.Transport{}
+ t.RegisterProtocol("file", http.NewFileTransport(http.Dir("/")))
+ c := &http.Client{Transport: t}
+ resp, err := c.Get(*url)
+ Error(err)
+ if resp.StatusCode != 200 {
+ log.Fatalf(`bad GET status for "%s": %s`, *url, resp.Status)
+ }
+ f := resp.Body
+ buffer, err := ioutil.ReadAll(f)
+ f.Close()
+ Error(err)
+ archive, err := zip.NewReader(bytes.NewReader(buffer), int64(len(buffer)))
+ Error(err)
+ tests := []Test{}
+ for _, f := range archive.File {
+ // Skip the short versions, which are simply duplicates of the long versions.
+ if strings.Contains(f.Name, "SHORT") || f.FileInfo().IsDir() {
+ continue
+ }
+ ff, err := f.Open()
+ Error(err)
+ defer ff.Close()
+ input := bufio.NewReader(ff)
+ test := Test{name: path.Base(f.Name)}
+ for {
+ line, err := input.ReadString('\n')
+ if err != nil {
+ if err == io.EOF {
+ break
+ }
+ log.Fatal(err)
+ }
+ if len(line) <= 1 || line[0] == '#' {
+ if m := versionRe.FindStringSubmatch(line); m != nil {
+ if m[1] != unicode.Version {
+ log.Printf("warning:%s: version is %s; want %s", f.Name, m[1], unicode.Version)
+ }
+ }
+ continue
+ }
+ m := testRe.FindStringSubmatch(line)
+ if m == nil || len(m) < 3 {
+ log.Fatalf(`Failed to parse: "%s" result: %#v`, line, m)
+ }
+ str := ""
+ for _, split := range strings.Split(m[1], " ") {
+ r, err := strconv.ParseUint(split, 16, 64)
+ Error(err)
+ str += string(rune(r))
+ }
+ test.str = append(test.str, str)
+ test.comment = append(test.comment, m[2])
+ }
+ tests = append(tests, test)
+ }
+ return tests
+}
+
+var errorCount int
+
+func fail(t Test, pattern string, args ...interface{}) {
+ format := fmt.Sprintf("error:%s:%s", t.name, pattern)
+ log.Printf(format, args...)
+ errorCount++
+ if errorCount > 30 {
+ log.Fatal("too many errors")
+ }
+}
+
+func runes(b []byte) []rune {
+ return []rune(string(b))
+}
+
+func doTest(t Test) {
+ c := collate.Root
+ c.Strength = collate.Tertiary
+ b := &collate.Buffer{}
+ if strings.Contains(t.name, "NON_IGNOR") {
+ c.Alternate = collate.AltNonIgnorable
+ }
+
+ prev := []byte(t.str[0])
+ for i := 1; i < len(t.str); i++ {
+ s := []byte(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)
+ prev = s
+ continue
+ }
+ if r := c.Compare(b, prev, s); r == 1 {
+ fail(t, "%d: Compare(%.4X, %.4X) == %d; want -1 or 0", i, runes(prev), runes(s), r)
+ }
+ if r := c.Compare(b, s, prev); r == -1 {
+ fail(t, "%d: Compare(%.4X, %.4X) == %d; want 1 or 0", i, runes(s), runes(prev), r)
+ }
+ prev = s
+ }
+}
+
+func main() {
+ flag.Parse()
+ for _, test := range loadTestData() {
+ doTest(test)
+ }
+ if errorCount == 0 {
+ fmt.Println("PASS")
+ }
+}
コアとなるコードの解説
regtest.go
は、Goの標準的なテストフレームワーク testing
パッケージを使用せず、独立した実行可能なプログラムとして設計されています。これは、+build ignore
ディレクティブによって、通常のGoビルドプロセスから除外されるためです。このアプローチは、特定の外部リソース(この場合はUnicodeのテストファイル)に依存するテストや、より複雑なセットアップが必要なテストで採用されることがあります。
主要な関数とその役割は以下の通りです。
-
main()
:- プログラムのエントリポイントです。
- コマンドライン引数をパースします(
url
とlocalFiles
フラグ)。 loadTestData()
を呼び出して、Unicode Collation Algorithmのテストデータを読み込みます。- 読み込んだ各テストデータ (
Test
構造体) に対してdoTest()
を呼び出し、照合テストを実行します。 - 全てのテストが成功した場合、"PASS" を出力します。
-
loadTestData()
:- UCAのテストデータを含む
CollationTest.zip
ファイルをダウンロードまたはローカルから読み込みます。 http.Transport
とhttp.Client
を使用してHTTPリクエストを処理し、file
プロトコルもサポートすることでローカルファイルからの読み込みを可能にしています。- ZIPアーカイブを
zip.NewReader
で開き、各テストファイル(.txt
拡張子を持つファイル)を処理します。 - 各テストファイルの内容を
bufio.NewReader
で行ごとに読み込み、正規表現 (versionRe
,testRe
) を使って解析します。 - 解析された各行から、16進数表現のUnicodeコードポイントをGoの文字列に変換し、
Test
構造体のスライスとして返します。 - テストファイルのヘッダーに含まれるUCAバージョンと、Goの
unicode.Version
が一致しない場合は警告を出力します。
- UCAのテストデータを含む
-
doTest(t Test)
:- 個々のテストデータ (
Test
構造体) に対して照合テストを実行します。 collate.Root
を基にcollate.Collator
インスタンスc
を作成し、Strength
をcollate.Tertiary
に設定します。これにより、Primary, Secondary, Tertiaryの全てのレベルで比較が行われます。- テストファイル名に "NON_IGNOR" が含まれる場合、
c.Alternate
をcollate.AltNonIgnorable
に設定し、非無視文字(例: スペース、句読点)も照合に影響を与えるようにします。 - テストデータ内の文字列が既にソートされた順序で並んでいることを利用し、隣接する文字列ペア
prev
とs
を取得します。 c.Key(b, prev)
とc.Key(b, s)
を呼び出して照合キーを生成し、bytes.Compare
でキーを比較します。キーの比較結果が期待通り(prev
のキーがs
のキーより大きくない)であることを確認します。c.Compare(b, prev, s)
を呼び出して文字列を直接比較し、結果が期待通り(prev
がs
より大きくない)であることを確認します。c.Compare(b, s, prev)
を呼び出して逆順で比較し、結果が期待通り(s
がprev
より小さくない)であることを確認します。- 期待と異なる結果が出た場合、
fail()
関数を呼び出してエラーを記録します。
- 個々のテストデータ (
-
Error(e error)
:- エラーが発生した場合に
log.Fatal
を呼び出してプログラムを終了させるヘルパー関数です。
- エラーが発生した場合に
-
fail(t Test, pattern string, args ...interface{})
:- テスト失敗時にエラーメッセージをログに出力し、
errorCount
をインクリメントします。 - エラーが多すぎる場合(30個以上)は、テストを強制終了します。
- テスト失敗時にエラーメッセージをログに出力し、
このコードは、Goの exp/locale/collate
パッケージがUnicode Collation Algorithmの複雑な仕様に準拠していることを、外部の公式テストデータを用いて自動的に検証する堅牢な回帰テストシステムを構築しています。
関連リンク
- Unicode Collation Algorithm (UCA): http://www.unicode.org/reports/tr10/
- Default Unicode Collation Element Table (DUCET): UCAの仕様書内で参照されるデータテーブル。
- Goの
exp/locale/collate
パッケージのソースコード(当時のもの、現在はgolang.org/x/text/collate
に移行している可能性が高い)
参考にした情報源リンク
- Go Gerrit Change 6216056: https://golang.org/cl/6216056 (コミットメッセージに記載されているリンク)
- Unicode Collation Algorithm Test Files: http://www.unicode.org/Public/UCA/ (
regtest.go
内のURL) - Go言語の
unicode
パッケージのドキュメント:unicode.Version
の情報源。 - Go言語の
exp/locale/collate
パッケージのドキュメント(当時のもの)。 - Go言語の
archive/zip
,net/http
,regexp
,strconv
,strings
,bytes
パッケージのドキュメント。 - Go言語の
flag
,fmt
,io
,io/ioutil
,log
,os
,path
パッケージのドキュメント。 - Unicode Collation Algorithmに関する一般的な情報源(例: Wikipedia, 技術ブログなど)。
- 回帰テストに関する一般的なソフトウェアテストの知識。