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

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

このコミットは、Go言語の標準ライブラリであるstrconvパッケージ(文字列と数値の変換を扱う)のテストを強化し、当時のGo言語の新しいテストフレームワークであるgotestへの移行を行ったものです。具体的には、数値変換関数(Atoi, Atouiなど)に対するより網羅的なテストケースが追加され、テストの実行方法がシェルスクリプトベースからGo言語ネイティブのgotestフレームワークへと変更されました。これにより、テストの記述と実行がよりGo言語のイディオムに沿った形になり、将来的なテストインフラの発展の基礎が築かれました。

コミット

commit 92a4fe1dd5e6db1b65ab837098ad90c312070166
Author: Russ Cox <rsc@golang.org>
Date:   Tue Nov 18 17:12:14 2008 -0800

    more tests for strconv; convert to gotest.
    
    R=r
    DELTA=219  (186 added, 32 deleted, 1 changed)
    OCL=19508
    CL=19523

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

https://github.com/golang/go/commit/92a4fe1dd5e6db1b65ab837098ad90c312070166

元コミット内容

more tests for strconv; convert to gotest.

変更の背景

このコミットが行われた2008年11月は、Go言語がまだ一般に公開される前の初期開発段階でした。当時のGo言語のテストは、シェルスクリプト(test.bashなど)と、Go言語で書かれたシンプルなテストヘルパー(src/lib/strconv/testing.goのようなファイル)を組み合わせて実行されていました。しかし、より大規模で複雑なプロジェクトに対応するためには、より堅牢でGo言語に統合されたテストフレームワークが必要とされていました。

このコミットの背景には、Go言語の標準的なテストツールであるgotest(現在のgo testコマンドの原型)の導入と成熟があります。gotestは、Go言語のソースコード内に直接テストを記述し、専用のコマンドで実行できる仕組みを提供することで、開発者がより簡単にテストを作成・実行できるようにすることを目指していました。

strconvパッケージは、文字列と数値の変換という、多くのアプリケーションで基本的な機能を提供する重要なパッケージです。そのため、このパッケージの正確性と堅牢性を保証することは非常に重要でした。既存のテストに加えて、特に数値のオーバーフローや不正な入力に対する挙動を検証するための、より詳細なテストケースが必要とされていました。

このコミットは、Go言語のテストインフラがシェルスクリプトベースの手動実行から、Go言語ネイティブの自動化されたフレームワークへと移行する過渡期における重要な一歩を示しています。

前提知識の解説

Go言語の初期のテスト慣習

Go言語の初期(2008年頃)には、現在のgo testコマンドのような洗練されたテストランナーはまだ存在しませんでした。テストはしばしばシェルスクリプト(例: test.bash)によってオーケストレーションされ、Go言語で書かれたテスト関数は、特定の命名規則(例: TestXxx)に従い、ブール値を返すことでテストの成功/失敗を示していました。テストの実行結果は、printlnfmt.printfで標準出力に出力され、シェルスクリプトがその出力を解析して全体のテスト結果を判断していました。

strconvパッケージ

strconvパッケージは、Go言語の標準ライブラリの一部であり、文字列と数値の相互変換機能を提供します。例えば、文字列"123"を整数123に変換したり、その逆を行ったりする際に使用されます。主な関数には以下のようなものがあります。

  • Atoi(s string) (i int, err os.Error): 文字列sを符号付き整数(int)に変換します。
  • Atoui(s string) (u uint, err os.Error): 文字列sを符号なし整数(uint)に変換します。
  • ParseInt(s string, base int, bitSize int) (i int64, err os.Error): 指定された基数(base)とビットサイズ(bitSize)で文字列をint64に変換します。
  • ParseUint(s string, base int, bitSize int) (u uint64, err os.Error): 指定された基数とビットサイズで文字列をuint64に変換します。

エラーハンドリング (os.Error, os.EINVAL, os.ERANGE)

Go言語では、エラーは関数の戻り値として明示的に返されます。このコミットの時点では、エラーを表すためにos.Errorインターフェースが使用されていました(後にerrorインターフェースに置き換わります)。

  • nil: エラーがないことを示します。
  • os.EINVAL: 不正な引数(Invalid argument)エラーを示します。例えば、数値として解析できない文字列が入力された場合などです。
  • os.ERANGE: 範囲外(Out of range)エラーを示します。例えば、変換結果が対象の数値型の最大値や最小値を超えてしまう場合などです。

数値型とビットサイズ

Go言語には、様々なサイズの整数型があります。

  • int: 符号付き整数。プラットフォームに依存し、32ビットまたは64ビットのいずれかです。
  • uint: 符号なし整数。プラットフォームに依存し、32ビットまたは64ビットのいずれかです。
  • int32: 32ビット符号付き整数。
  • uint32: 32ビット符号なし整数。
  • int64: 64ビット符号付き整数。
  • uint64: 64ビット符号なし整数。

これらの型にはそれぞれ表現できる数値の範囲が決まっており、その範囲を超える値を変換しようとするとos.ERANGEエラーが発生します。

  • uint64の最大値: 1<<64 - 1 (約1.84 x 10^19)
  • int64の最大値: 1<<63 - 1 (約9.22 x 10^18)
  • int64の最小値: -1<<63 (約-9.22 x 10^18)
  • uint32の最大値: 1<<32 - 1 (約4.29 x 10^9)
  • int32の最大値: 1<<31 - 1 (約2.14 x 10^9)
  • int32の最小値: -1<<31 (約-2.14 x 10^9)

makeコマンド

makeは、プログラムのコンパイルやビルド、テストなどのタスクを自動化するためのツールです。Makefileというファイルに定義されたルールに基づいてコマンドを実行します。このコミットでは、run.bashスクリプト内でmake testというコマンドが呼び出されるように変更されており、テストの実行がmakeシステムに統合されたことを示しています。

技術的詳細

このコミットの主要な技術的変更点は以下の通りです。

  1. シェルスクリプトベースのテストの廃止:

    • src/lib/strconv/test.bashファイルが削除されました。これは、strconvパッケージのテストを実行するためのシェルスクリプトでした。このスクリプトは、make cleanmakemake test(コメントアウトされているが、おそらく以前は使用されていた)といったコマンドを実行し、Goプログラムのビルドとテストの実行をオーケストレーションしていました。
    • この削除は、テストの実行がシェルスクリプトに依存するのではなく、Go言語のネイティブなテストフレームワークに移行したことを意味します。
  2. Go言語ネイティブのテストフレームワーク (gotest) への移行:

    • src/lib/strconv/testing.goファイルが削除されました。このファイルは、Go言語のテストヘルパー関数や構造体を定義していたものと思われますが、gotestの導入により不要となりました。
    • 新しくsrc/lib/strconv/testatoi.goが追加されました。このファイルには、strconvパッケージのAtoiおよびAtoui関数に対する新しいテストケースがGo言語で直接記述されています。
    • テスト関数はexport func TestXxx() boolという形式で定義されており、これは当時のgotestが認識するテスト関数のシグネチャでした。これらの関数はブール値を返し、trueなら成功、falseなら失敗を示します。
  3. strconv関数のテスト強化:

    • testatoi.goには、Uint64Test, Int64Test, Uint32Test, Int32Testという構造体が定義され、それぞれstrconv.atoui64, strconv.atoi64, strconv.atoui, strconv.atoi関数のテストデータ(入力文字列、期待される出力値、期待されるエラー)を保持しています。
    • これらのテストデータには、通常の数値、ゼロ、負の数、最大値・最小値、オーバーフローする値、不正な形式の文字列(例: "012345"のような先頭にゼロがある数値、"12345x"のような非数値文字を含む文字列)など、様々なエッジケースが含まれています。
    • 特に、1<<64-1uint64の最大値)、1<<63-1int64の最大値)、-1<<63int64の最小値)といったビット演算子を用いた数値リテラルが使われており、Go言語の数値型の限界を正確にテストしようとしていることがわかります。
    • テストロジックは、各テストデータに対して対象の関数を呼び出し、結果(出力値とエラー)が期待値と一致するかどうかを比較します。不一致の場合にはfmt.printfで詳細なエラーメッセージを出力し、okフラグをfalseに設定します。
  4. プラットフォーム依存のテスト (IntSize1()関数):

    • testatoi.goにはIntSize1()というヘルパー関数が定義されています。この関数は、現在の実行環境のint型が32ビットか64ビットかを判定します。
    • tmp := 1; if tmp<<16<<16 == 0 { return 32; }というロジックは、1を32ビット左シフト(1<<32)した結果がゼロになるかどうかで判定しています。32ビット環境では1<<32はオーバーフローしてゼロになりますが、64ビット環境ではゼロになりません。
    • この関数を利用して、TestAtoui()TestAtoi()は、実行環境のintおよびuintのサイズに応じて、32ビット用のテストデータ(uint32tests, int32tests)または64ビット用のテストデータ(uint64tests, int64tests)のいずれかを選択して実行します。これにより、異なるアーキテクチャ上でも適切なテストが実行されるようになっています。
  5. run.bashの変更:

    • src/run.bashファイルが変更され、bash test.bashの呼び出しがmake testに置き換えられました。これは、strconvパッケージのテスト実行が、シェルスクリプトからmakeシステムを介したgotestの実行へと完全に移行したことを示しています。

これらの変更は、Go言語のテストインフラがより洗練され、自動化され、Go言語のイディオムに沿ったものへと進化していく過程を示しています。

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

src/lib/strconv/test.bash (削除)

--- a/src/lib/strconv/test.bash
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/bash
-# Copyright 2009 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.
-
-set -e
-set -x
-
-make clean
-make
-# make test
-# ./test
-# rm -f *.6 6.out test

src/lib/strconv/testatoi.go (新規追加)

// Copyright 2009 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.

package strconv
import (
	"os";
	"fmt";
	"strconv"
)

type Uint64Test struct {
	in string;
	out uint64;
	err *os.Error;
}

var uint64tests = []Uint64Test {
	Uint64Test{ "0", 0, nil },
	Uint64Test{ "1", 1, nil },
	Uint64Test{ "12345", 12345, nil },
	Uint64Test{ "012345", 0, os.EINVAL },
	Uint64Test{ "12345x", 0, os.EINVAL },
	Uint64Test{ "98765432100", 98765432100, nil },
	Uint64Test{ "18446744073709551615", 1<<64-1, nil },
	Uint64Test{ "18446744073709551616", 1<<64-1, os.ERANGE },
}

type Int64Test struct {
	in string;
	out int64;
	err *os.Error;
}

var int64tests = []Int64Test {
	Int64Test{ "0", 0, nil },
	Int64Test{ "-0", 0, nil },
	Int64Test{ "1", 1, nil },
	Int64Test{ "-1", -1, nil },
	Int64Test{ "12345", 12345, nil },
	Int64Test{ "-12345", -12345, nil },
	Int64Test{ "012345", 0, os.EINVAL },
	Int64Test{ "-012345", 0, os.EINVAL },
	Int64Test{ "12345x", 0, os.EINVAL },
	Int64Test{ "-12345x", 0, os.EINVAL },
	Int64Test{ "98765432100", 98765432100, nil },
	Int64Test{ "-98765432100", -98765432100, nil },
	Int64Test{ "9223372036854775807", 1<<63-1, nil },
	Int64Test{ "-9223372036854775807", -(1<<63-1), nil },
	Int64Test{ "9223372036854775808", 1<<63-1, os.ERANGE },
	Int64Test{ "-9223372036854775808", -1<<63, nil },
	Int64Test{ "9223372036854775809", 1<<63-1, os.ERANGE },
	Int64Test{ "-9223372036854775809", -1<<63, os.ERANGE },
}

type Uint32Test struct {
	in string;
	out uint32;
	err *os.Error;
}

var uint32tests = []Uint32Test {
	Uint32Test{ "0", 0, nil },
	Uint32Test{ "1", 1, nil },
	Uint32Test{ "12345", 12345, nil },
	Uint32Test{ "012345", 0, os.EINVAL },
	Uint32Test{ "12345x", 0, os.EINVAL },
	Uint32Test{ "987654321", 987654321, nil },
	Uint32Test{ "4294967295", 1<<32-1, nil },
	Uint32Test{ "4294967296", 1<<32-1, os.ERANGE },
}

type Int32Test struct {
	in string;
	out int32;
	err *os.Error;
}

var int32tests = []Int32Test {
	Int32Test{ "0", 0, nil },
	Int32Test{ "-0", 0, nil },
	Int32Test{ "1", 1, nil },
	Int32Test{ "-1", -1, nil },
	Int32Test{ "12345", 12345, nil },
	Int32Test{ "-12345", -12345, nil },
	Int32Test{ "012345", 0, os.EINVAL },
	Int32Test{ "-012345", 0, os.EINVAL },
	Int32Test{ "12345x", 0, os.EINVAL },
	Int32Test{ "-12345x", 0, os.EINVAL },
	Int32Test{ "987654321", 987654321, nil },
	Int32Test{ "-987654321", -987654321, nil },
	Int32Test{ "2147483647", 1<<31-1, nil },
	Int32Test{ "-2147483647", -(1<<31-1), nil },
	Int32Test{ "2147483648", 1<<31-1, os.ERANGE },
	Int32Test{ "-2147483648", -1<<31, nil },
	Int32Test{ "2147483649", 1<<31-1, os.ERANGE },
	Int32Test{ "-2147483649", -1<<31, os.ERANGE },
}

export func TestAtoui64() bool {
	ok := true;
	for i := 0; i < len(uint64tests); i++ {
		t := &uint64tests[i];
		out, err := strconv.atoui64(t.in);
		if t.out != out || t.err != err {
			fmt.printf("strconv.atoui64(%v) = %v, %v want %v, %v\n",
				t.in, out, err, t.out, t.err);
			ok = false;
		}
	}
	return ok;
}

export func TestAtoi64() bool {
	ok := true;
	for i := 0; i < len(int64tests); i++ {
		t := &int64tests[i];
		out, err := strconv.atoi64(t.in);
		if t.out != out || t.err != err {
			fmt.printf("strconv.atoi64(%v) = %v, %v want %v, %v\n",
				t.in, out, err, t.out, t.err);
			ok = false;
		}
	}
	return ok;
}

func IntSize1() uint {
	tmp := 1;
	if tmp<<16<<16 == 0 {
		return 32;
	}
println("tmp<<32 = ", tmp<<32);
	return 64;
}

export func TestAtoui() bool {
	ok := true;
	switch IntSize1() {
	case 32:
		for i := 0; i < len(uint32tests); i++ {
			t := &uint32tests[i];
			out, err := strconv.atoui(t.in);
			if t.out != uint32(out) || t.err != err {
				fmt.printf("strconv.atoui(%v) = %v, %v want %v, %v\n",
					t.in, out, err, t.out, t.err);
				ok = false;
			}
		}
	case 64:
		for i := 0; i < len(uint64tests); i++ {
			t := &uint64tests[i];
			out, err := strconv.atoui(t.in);
			if t.out != uint64(out) || t.err != err {
				fmt.printf("strconv.atoui(%v) = %v, %v want %v, %v\n",
					t.in, out, err, t.out, t.err);
				ok = false;
			}
		}
	}
	return ok;
}

export func TestAtoi() bool {
	ok := true;
	switch IntSize1() {
	case 32:
		for i := 0; i < len(int32tests); i++ {
			t := &int32tests[i];
			out, err := strconv.atoi(t.in);
			if t.out != int32(out) || t.err != err {
				fmt.printf("strconv.atoi(%v) = %v, %v want %v, %v\n",
					t.in, out, err, t.out, t.err);
				ok = false;
			}
		}
	case 64:
		for i := 0; i < len(int64tests); i++ {
			t := &int64tests[i];
			out, err := strconv.atoi(t.in);
			if t.out != int64(out) || t.err != err {
				fmt.printf("strconv.atoi(%v) = %v, %v want %v, %v\n",
					t.in, out, err, t.out, t.err);
				ok = false;
			}
		}
	}
	return ok;
}

src/lib/strconv/testing.go (削除)

--- a/src/lib/strconv/testing.go
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright 2009 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.
-
-package testing
-
-export type Test struct {
-	name string;
-	f *() bool;
-}
-
-export func Main(tests *[]Test) {
-	ok := true;
-	for i := 0; i < len(tests); i++ {
-		ok1 := tests[i].f();
-		status := "FAIL";
-		if ok1 {
-			status = "PASS"
-		}
-		ok = ok && ok1;
-		println(status, tests[i].name);
-	}
-	if !ok {
-		sys.exit(1);
-	}
-}

src/run.bash (変更)

--- a/src/run.bash
+++ b/src/run.bash
@@ -14,7 +14,7 @@ xcd() {
 (xcd lib/strconv
 make clean
 time make
-bash test.bash
+make test
 ) || exit $?
 
 (xcd lib/reflect

コアとなるコードの解説

src/lib/strconv/testatoi.go

このファイルは、strconvパッケージの文字列から数値への変換関数(atoui64, atoi64, atoui, atoi)のテストケースを定義しています。

  1. テストデータ構造体:

    • Uint64Test, Int64Test, Uint32Test, Int32Testという構造体が定義されています。これらはそれぞれ、テスト対象の入力文字列(in)、期待される出力数値(out)、期待されるエラー(err)を保持します。
    • これらの構造体のスライス(例: uint64tests, int64tests)として、具体的なテストケースが初期化されています。各テストケースは、正常な変換、境界値、オーバーフロー、不正な入力など、様々なシナリオをカバーしています。
  2. テスト関数:

    • export func TestAtoui64() bool, export func TestAtoi64() bool, export func TestAtoui() bool, export func TestAtoi() boolという関数が定義されています。これらは当時のgotestフレームワークが自動的に発見して実行するテスト関数です。
    • 各テスト関数は、対応するテストデータスライスをループし、各テストケースに対してstrconvパッケージの変換関数を呼び出します。
    • 変換結果(outerr)が期待値(t.outt.err)と一致するかどうかを比較します。
    • もし結果が期待値と異なる場合、fmt.printfを使って詳細なエラーメッセージ(入力、実際の結果、期待される結果)を標準出力に出力し、テスト全体の成功を示すokフラグをfalseに設定します。
    • 最終的に、すべてのテストケースが成功した場合はtrueを、一つでも失敗した場合はfalseを返します。
  3. IntSize1()関数:

    • このヘルパー関数は、Goが実行されている環境のint型が32ビットか64ビットかを判定します。
    • tmp := 1; if tmp<<16<<16 == 0 { return 32; }というロジックは、1を32ビット左シフト(1 << 32)した結果がゼロになるかどうかをチェックしています。
      • 32ビットシステムでは、1 << 32はオーバーフローして結果がゼロになります。
      • 64ビットシステムでは、1 << 32はオーバーフローせず、2^32という値になります。
    • この判定結果に基づいて、TestAtoui()TestAtoi()は、32ビット環境ではuint32testsint32testsを、64ビット環境ではuint64testsint64testsを使用するように切り替えます。これにより、異なるアーキテクチャ上でも適切なサイズの数値変換テストが実行されるようになっています。

src/run.bash

このシェルスクリプトは、Go言語プロジェクト全体のビルドとテストの実行を管理するスクリプトの一部です。

  • 変更点として、strconvライブラリのディレクトリ(lib/strconv)に移動した後、以前はbash test.bashを実行していた箇所がmake testに置き換えられました。
  • これは、strconvパッケージのテスト実行が、シェルスクリプトによる直接的なテストスクリプトの呼び出しから、Makefileに定義されたtestターゲットを介したgotestの実行へと移行したことを明確に示しています。これにより、テストの実行がより標準化され、makeシステムに統合されました。

関連リンク

参考にした情報源リンク

  • Go言語の初期のテストフレームワークに関する議論やコミット履歴 (GitHubのGoリポジトリのコミット履歴を遡って確認)
  • Go言語のstrconvパッケージのソースコード (現在の実装と比較して、当時の実装の推測)
  • Go言語の数値型とビット演算に関する一般的な知識