[インデックス 10550] ファイルの概要
このコミットは、Go言語の標準ライブラリ encoding/asn1
パッケージ内のテストコード asn1_test.go
における、OpenBSD環境でのテスト失敗を修正するものです。具体的には、time.Parse
関数がタイムゾーン情報をどのように扱うか、そして reflect.DeepEqual
がその結果を厳密に比較することによって生じる問題に対処しています。
コミット
- コミットハッシュ:
e812db35581d257fb2d3518509898fc22bdd2d48
- Author: Russ Cox rsc@golang.org
- Date: Wed Nov 30 13:36:25 2011 -0500
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/e812db35581d257fb2d3518509898fc22bdd2d48
元コミット内容
encoding/asn1: fix test on OpenBSD
time.Parse uses time.Local if it has the right zone offset,
otherwise it calls time.FixedZone. The test's use of reflect.DeepEqual
meant that the test expected time.FixedZone always, failing
when the local time zone really would have used -0700 for
that time. The fix is to format the time to display only the
pieces we intend to test.
R=golang-dev, agl, iant
CC=golang-dev
https://golang.org/cl/5437088
変更の背景
このコミットは、Go言語の encoding/asn1
パッケージのテストがOpenBSD環境で失敗するという問題に対応するために行われました。問題の根源は、time.Parse
関数の挙動と、テストにおける reflect.DeepEqual
の使用方法にありました。
time.Parse
関数は、与えられた時刻文字列を解析する際に、その文字列が特定のタイムゾーンオフセット(例: -0700
)を含んでいる場合、そのオフセットがシステムのローカルタイムゾーン(time.Local
)と一致するかどうかを内部的に確認します。もし一致すれば time.Local
を使用し、一致しない場合は time.FixedZone
を呼び出して固定のタイムゾーンを作成します。
テストコードでは、parseUTCTime
関数(内部で time.Parse
を使用)の出力と期待される結果を reflect.DeepEqual
で比較していました。reflect.DeepEqual
は非常に厳密な比較を行うため、time.Time
構造体の内部表現(特にタイムゾーン情報)が完全に一致しないと false
を返します。
OpenBSD環境では、特定の時刻(例: -0700
オフセットを持つ時刻)が、システムのローカルタイムゾーンと一致する状況が発生しました。この場合、time.Parse
は time.Local
を使用して time.Time
オブジェクトを生成します。しかし、テストが期待していたのは、常に time.FixedZone
によって生成された time.Time
オブジェクトでした。この内部的なタイムゾーン表現の違いが reflect.DeepEqual
によって検出され、テストが失敗していました。
この問題は、テストがタイムゾーンの「名前」や内部表現ではなく、時刻の「値」そのもの(年、月、日、時、分、秒、オフセット)を比較すべきであるという認識の欠如に起因していました。
前提知識の解説
1. ASN.1 (Abstract Syntax Notation One)
ASN.1は、データ構造を記述するための標準的な記法であり、通信プロトコルやデータストレージにおいて、異なるシステム間でデータを交換する際に使用されます。encoding/asn1
パッケージは、Go言語でASN.1形式のデータをエンコードおよびデコードするための機能を提供します。
2. UTCTime (Universal Time Coordinated Time)
ASN.1では、時刻情報を表現するためのいくつかの型が定義されており、その一つが UTCTime
です。UTCTime
は、協定世界時(UTC)を基準とした時刻を表し、通常は YYMMDDhhmmssZ
または YYMMDDhhmmss+hhmm
の形式で表現されます。このコミットで問題となっているのは、この UTCTime
の解析とテストです。
3. Go言語の time
パッケージ
Go言語の time
パッケージは、時刻と日付を扱うための機能を提供します。
time.Time
構造体: 時刻と日付を表す基本的な型です。内部的には、特定の時点(エポックからの経過時間)と、その時刻がどのタイムゾーンで解釈されるべきかという情報(ロケーション)を持っています。time.Parse(layout, value string) (Time, error)
: 指定されたレイアウト文字列に基づいて時刻文字列を解析し、time.Time
オブジェクトを返します。この関数は、解析時にタイムゾーン情報が存在する場合、その情報をtime.Time
オブジェクトのロケーションとして設定します。time.Local
: システムのローカルタイムゾーンを表す*time.Location
型の変数です。time.FixedZone(name string, offset int) *time.Location
: 指定された名前とUTCからのオフセット(秒単位)を持つ固定のタイムゾーンを作成します。例えば、-0700
オフセットはtime.FixedZone("", -7*3600)
のように表現できます。time.Time.Format(layout string) string
:time.Time
オブジェクトを指定されたレイアウト文字列に基づいてフォーマットし、文字列として返します。この関数は、time.Parse
とは逆の操作を行います。
4. Go言語の reflect
パッケージ
Go言語の reflect
パッケージは、実行時にプログラムの型情報を検査したり、値を操作したりするための機能を提供します。
reflect.DeepEqual(x, y interface{}) bool
: 2つの引数x
とy
が「深く」等しいかどうかを報告します。これは、配列、構造体、マップ、スライスなどの複合型に対して、その要素が再帰的に等しいかどうかを比較します。time.Time
の場合、その内部構造(エポックからの経過時間だけでなく、タイムゾーンのロケーション情報なども含む)が完全に一致しないとtrue
を返しません。
技術的詳細
このコミットの技術的な核心は、time.Time
オブジェクトの比較における reflect.DeepEqual
の厳密性と、time.Parse
がタイムゾーンを解決する際の挙動の組み合わせにあります。
time.Time
構造体は、単に時刻の瞬間(Unixエポックからのナノ秒数)だけでなく、その時刻がどのタイムゾーン(*time.Location
)に属しているかという情報も保持しています。time.Location
オブジェクトは、タイムゾーンの名前(例: "America/Los_Angeles" や "UTC")や、夏時間規則などの詳細な情報を含んでいます。
time.Parse
が時刻文字列を解析する際、例えば 20111130133625-0700
のようなオフセット情報を含む文字列の場合、Goの time
パッケージは次のようなロジックで time.Location
を決定します。
- まず、指定されたオフセットがシステムのローカルタイムゾーン (
time.Local
) の現在のオフセットと一致するかどうかを試みます。 - もし一致すれば、
time.Local
をそのtime.Time
オブジェクトのロケーションとして設定します。 - 一致しない場合、またはオフセット情報が明示的に指定されているがローカルタイムゾーンとは異なる場合、
time.FixedZone
を使用して、そのオフセットに対応する匿名(名前なし)の固定タイムゾーンを作成し、それをロケーションとして設定します。
問題は、reflect.DeepEqual
が time.Time
オブジェクトを比較する際に、この time.Location
オブジェクトの内部表現まで厳密に比較してしまう点にありました。
- テストが期待していたのは、常に
time.FixedZone
によって作成されたtime.Location
を持つtime.Time
オブジェクトでした。これは、テストデータが特定の固定オフセット(例:-0700
)を持つ時刻文字列を扱っていたためです。 - しかし、OpenBSD環境では、テスト実行時のシステムのローカルタイムゾーンが、たまたまテストデータ内のオフセット(例:
-0700
)と一致する状況が発生しました。この場合、time.Parse
はtime.Local
を使用してtime.Time
オブジェクトを生成します。 - 結果として、
time.Local
によって生成されたtime.Time
オブジェクトと、time.FixedZone
によって生成されたtime.Time
オブジェクトは、同じ時刻の瞬間を表していても、内部のtime.Location
オブジェクトが異なるため、reflect.DeepEqual
はfalse
を返してしまい、テストが失敗しました。
この問題を解決するために、コミットでは reflect.DeepEqual
を使用する代わりに、time.Time.Format
メソッドを用いて時刻を特定のフォーマット文字列で文字列化し、その文字列同士を比較するというアプローチが取られました。
新しいフォーマット文字列 Jan _2 15:04:05 -0700 2006
は、Go言語の time
パッケージのレイアウト文字列の特殊な形式です。このレイアウトは、年、月、日、時、分、秒、そしてUTCからの数値オフセット(例: -0700
)を含みますが、タイムゾーンの「名前」(例: "PST" や "America/Los_Angeles")は含みません。これにより、time.Local
と time.FixedZone
のどちらが使われたかに関わらず、時刻の「値」と「オフセット」のみが比較対象となり、内部的なタイムゾーン表現の違いによるテスト失敗が回避されます。
この修正は、テストの目的が時刻の厳密な内部表現ではなく、その時刻が表す瞬間とオフセットが正しいことを確認することであるという原則に基づいています。
コアとなるコードの変更箇所
--- a/src/pkg/encoding/asn1/asn1_test.go
+++ b/src/pkg/encoding/asn1/asn1_test.go
@@ -223,13 +223,21 @@ var utcTestData = []timeTest{\n func TestUTCTime(t *testing.T) {\n \tfor i, test := range utcTestData {\n \t\tret, err := parseUTCTime([]byte(test.in))\n-\t\tif (err == nil) != test.ok {\n-\t\t\tt.Errorf(\"#%d: Incorrect error result (did fail? %v, expected: %v)\", i, err == nil, test.ok)\n-\t\t}\n-\t\tif err == nil {\n-\t\t\tif !reflect.DeepEqual(test.out, ret) {\n-\t\t\t\tt.Errorf(\"#%d: Bad result: %v (expected %v)\", i, ret, test.out)\n+\t\tif err != nil {\n+\t\t\tif test.ok {\n+\t\t\t\tt.Errorf(\"#%d: parseUTCTime(%q) = error %v\", i, err)\n \t\t\t}\n+\t\t\tcontinue\n+\t\t}\n+\t\tif !test.ok {\n+\t\t\tt.Errorf(\"#%d: parseUTCTime(%q) succeeded, should have failed\", i)\n+\t\t\tcontinue\n+\t\t}\n+\t\tconst format = \"Jan _2 15:04:05 -0700 2006\" // ignore zone name, just offset\n+\t\thave := ret.Format(format)\n+\t\twant := test.out.Format(format)\n+\t\tif have != want {\n+\t\t\tt.Errorf(\"#%d: parseUTCTime(%q) = %s, want %s\", test.in, have, want)\n \t\t}\n \t}\n }\n```
## コアとなるコードの解説
変更は `src/pkg/encoding/asn1/asn1_test.go` ファイルの `TestUTCTime` 関数に集中しています。
1. **エラーハンドリングの改善**:
変更前は、`if (err == nil) != test.ok` という条件でエラーの有無と期待値を比較していました。これは少し読みにくい表現です。
変更後は、まず `if err != nil` でエラーが発生したかどうかを確認し、もしエラーが発生していて、かつテストが成功を期待している場合 (`test.ok` が `true`) にエラーを報告するように修正されました。
また、エラーが発生した場合、またはエラーが発生しなかったがテストが失敗を期待している場合 (`!test.ok`) には、`continue` を使って次のテストケースに進むようにロジックが整理されました。これにより、エラー処理がより明確になりました。
2. **`reflect.DeepEqual` から `time.Format` を用いた比較への変更**:
これがこのコミットの最も重要な変更点です。
変更前は、`if !reflect.DeepEqual(test.out, ret)` を使用して、`parseUTCTime` の結果 (`ret`) と期待される結果 (`test.out`) の `time.Time` オブジェクトを直接比較していました。前述の通り、これはタイムゾーンの内部表現の違いによってテストが失敗する原因となっていました。
変更後、以下の行が追加されました。
```go
const format = "Jan _2 15:04:05 -0700 2006" // ignore zone name, just offset
have := ret.Format(format)
want := test.out.Format(format)
if have != want {
t.Errorf("#%d: parseUTCTime(%q) = %s, want %s", test.in, have, want)
}
```
* `const format = "Jan _2 15:04:05 -0700 2006"`: これはGo言語の `time.Format` メソッドで使用される特殊なレイアウト文字列です。この文字列は、特定の参照時刻(2006年1月2日15時04分05秒、UTC-0700)を表現しており、`time.Time` オブジェクトをフォーマットする際のテンプレートとして機能します。このレイアウトは、タイムゾーンの「名前」(例: "PST")を含まず、数値オフセット(例: "-0700")のみを含むように設計されています。
* `have := ret.Format(format)`: `parseUTCTime` の結果である `ret` (time.Time オブジェクト) を、定義された `format` に従って文字列に変換します。
* `want := test.out.Format(format)`: 期待される結果である `test.out` (time.Time オブジェクト) を、同じ `format` に従って文字列に変換します。
* `if have != want`: 変換された2つの文字列を直接比較します。これにより、`time.Time` オブジェクトの内部的なタイムゾーン表現の違いを無視し、時刻の年、月、日、時、分、秒、および数値オフセットが一致するかどうかのみを比較するようになります。
この修正により、テストはタイムゾーンの内部的な詳細に依存することなく、時刻の「意味的な値」が正しく解析されていることを検証できるようになり、OpenBSD環境でのテスト失敗が解消されました。
## 関連リンク
* Go CL 5437088: [https://golang.org/cl/5437088](https://golang.org/cl/5437088)
## 参考にした情報源リンク
* Go言語の `time` パッケージ公式ドキュメント (内部知識に基づく)
* Go言語の `reflect` パッケージ公式ドキュメント (内部知識に基づく)
* ASN.1 および UTCTime の概念 (内部知識に基づく)