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

[インデックス 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 メソッドの動作を検証します。

テストの基本的な流れは以下の通りです。

  1. テストデータの取得:

    • http://www.unicode.org/Public/UCA/<unicode.Version>/CollationTest.zip からUCAのテストファイルをダウンロードします。<unicode.Version> はGoの unicode パッケージがサポートするUnicodeのバージョンに置き換えられます。
    • localFiles フラグが設定されている場合、ローカルファイルシステムからテストデータを読み込むことも可能です(デバッグ用)。
    • ダウンロードしたZIPアーカイブを解凍し、個々のテストファイル(例: CollationTest_*.txt)を読み込みます。
  2. テストデータの解析:

    • 各テストファイルは、ソート済みの文字列ペアのリストを含んでいます。各行は 0009 0021; # ('\u0009') <CHARACTER TABULATION> [| | | 0201 025E] のような形式です。
    • セミコロンの前の部分は、Unicodeコードポイントの16進数表現で、これが比較対象の文字列を構成します。
    • # の後の部分はコメントです。
    • regtest.go は正規表現 ^([\dA-F ]+);.*# (.*)\n?$ を用いて、各行から16進数表現の文字列とコメントを抽出します。
    • 抽出された16進数表現は、Goの文字列に変換されます。
  3. 照合テストの実行:

    • doTest 関数が各テストファイルに対して実行されます。
    • collate.Root をベースとした collate.Collator インスタンスが作成されます。このインスタンスは、デフォルトで collate.Tertiary の強度(Primary, Secondary, Tertiaryの全てのレベルで比較を行う)を持ちます。
    • テストファイル内の文字列は、既にソートされた順序で並んでいます。したがって、隣接する文字列 prevs を比較すると、collate.Compare(b, prev, s)-1 または 0 を返すはずであり、collate.Compare(b, s, prev)1 または 0 を返すはずです。
    • collate.Key メソッドもテストされます。Key メソッドは、文字列の照合キーを生成します。生成されたキーは、バイト列として比較可能であり、キーの比較結果が文字列の照合結果と一致することを確認します。
    • NON_IGNOR を含むテストファイルの場合、collate.Alternatecollate.AltNonIgnorable に設定され、非無視文字(例: スペース、句読点)も照合に影響を与えるようにします。
  4. エラーハンドリングと報告:

    • テスト中にエラーが発生した場合、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():

    • プログラムのエントリポイントです。
    • コマンドライン引数をパースします(urllocalFiles フラグ)。
    • loadTestData() を呼び出して、Unicode Collation Algorithmのテストデータを読み込みます。
    • 読み込んだ各テストデータ (Test 構造体) に対して doTest() を呼び出し、照合テストを実行します。
    • 全てのテストが成功した場合、"PASS" を出力します。
  • loadTestData():

    • UCAのテストデータを含む CollationTest.zip ファイルをダウンロードまたはローカルから読み込みます。
    • http.Transporthttp.Client を使用してHTTPリクエストを処理し、file プロトコルもサポートすることでローカルファイルからの読み込みを可能にしています。
    • ZIPアーカイブを zip.NewReader で開き、各テストファイル(.txt 拡張子を持つファイル)を処理します。
    • 各テストファイルの内容を bufio.NewReader で行ごとに読み込み、正規表現 (versionRe, testRe) を使って解析します。
    • 解析された各行から、16進数表現のUnicodeコードポイントをGoの文字列に変換し、Test 構造体のスライスとして返します。
    • テストファイルのヘッダーに含まれるUCAバージョンと、Goの unicode.Version が一致しない場合は警告を出力します。
  • doTest(t Test):

    • 個々のテストデータ (Test 構造体) に対して照合テストを実行します。
    • collate.Root を基に collate.Collator インスタンス c を作成し、Strengthcollate.Tertiary に設定します。これにより、Primary, Secondary, Tertiaryの全てのレベルで比較が行われます。
    • テストファイル名に "NON_IGNOR" が含まれる場合、c.Alternatecollate.AltNonIgnorable に設定し、非無視文字(例: スペース、句読点)も照合に影響を与えるようにします。
    • テストデータ内の文字列が既にソートされた順序で並んでいることを利用し、隣接する文字列ペア prevs を取得します。
    • c.Key(b, prev)c.Key(b, s) を呼び出して照合キーを生成し、bytes.Compare でキーを比較します。キーの比較結果が期待通り(prev のキーが s のキーより大きくない)であることを確認します。
    • c.Compare(b, prev, s) を呼び出して文字列を直接比較し、結果が期待通り(prevs より大きくない)であることを確認します。
    • c.Compare(b, s, prev) を呼び出して逆順で比較し、結果が期待通り(sprev より小さくない)であることを確認します。
    • 期待と異なる結果が出た場合、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, 技術ブログなど)。
  • 回帰テストに関する一般的なソフトウェアテストの知識。