[インデックス 1249] ファイルの概要
このコミットは、Go言語の標準ライブラリの一部である tabwriter
パッケージに対するものです。tabwriter
パッケージは、テキストデータを整形し、タブ区切りや特定の文字で区切られた列を自動的に揃える機能を提供します。これにより、ターミナル出力やログファイルなど、人間が読みやすい形式で表形式のデータを表示する際に非常に役立ちます。
具体的には、src/lib/tabwriter/tabwriter.go
が tabwriter
の主要なロジックを実装しており、src/lib/tabwriter/tabwriter_test.go
はその機能が正しく動作することを保証するためのテストコードを含んでいます。
コミット
commit 7cfa7eebf31126d7614281290d484874ad63413a
Author: Robert Griesemer <gri@golang.org>
Date: Tue Nov 25 14:06:59 2008 -0800
- fixed a problem with flushing
- added extra tests
R=r
DELTA=164 (107 added, 20 deleted, 37 changed)
OCL=20002
CL=20004
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/7cfa7eebf31126d7614281290d484874ad63413a
元コミット内容
- fixed a problem with flushing
- added extra tests
変更の背景
このコミットの主な背景は、tabwriter
パッケージにおける「フラッシング(flushing)」の問題の修正と、その機能の堅牢性を高めるためのテストの追加です。
tabwriter
は、入力されたテキストを内部バッファに保持し、タブや改行などの特定の区切り文字に基づいて列の幅を計算し、最終的な整形済み出力を生成します。このプロセスにおいて、バッファリングされたデータが適切にフラッシュ(出力)されない場合、以下のような問題が発生する可能性があります。
- 不完全な出力: データがバッファに残ったままで、期待される出力がすべて表示されない。
- 不正な整形: フラッシングのタイミングやロジックが不適切だと、列の揃えが崩れたり、余分なスペースや文字が挿入されたりする。
- デッドロックやリソースリーク: バッファが適切にクリアされないことで、メモリ使用量が増加したり、後続の書き込み処理がブロックされたりする。
このコミットは、特に最後のバッファされた行の扱いにおいて、フラッシングロジックに問題があったことを示唆しています。また、既存のテストだけではこれらのエッジケースや潜在的なバグを十分にカバーできていなかったため、より広範なテストケースを追加することで、将来的な回帰を防ぎ、コードの信頼性を向上させる目的がありました。
前提知識の解説
tabwriter
パッケージとは
tabwriter
パッケージは、Go言語でテキストの表形式データを整形するためのユーティリティです。主な機能は以下の通りです。
- 列の自動揃え: タブ (
\t
) で区切られた列を自動的に揃えます。各列の幅は、その列内で最も長い要素に合わせて動的に調整されます。 - パディング: 列の間に指定されたパディング文字(スペース、ドットなど)を挿入し、読みやすさを向上させます。
- アラインメント: 列のテキストを左揃えまたは右揃えに設定できます。
- バッファリング: 入力データを即座に出力するのではなく、内部バッファに蓄積し、改行 (
\n
) やFlush()
メソッドの呼び出しによって整形して出力します。これにより、後から列の幅を決定できるため、柔軟な整形が可能になります。
io.Writer
インターフェース
Go言語では、データの書き込み操作は io.Writer
インターフェースによって抽象化されています。tabwriter.Writer
はこの io.Writer
インターフェースを実装しており、Write
メソッドを通じてデータを受け取ります。これにより、tabwriter.Writer
は fmt.Fprintf
や io.Copy
など、io.Writer
を受け入れるあらゆる関数やメソッドと組み合わせて使用できます。
フラッシング (Flushing)
バッファリングされたI/Oにおいて、フラッシングとは、内部バッファに蓄積されたデータを最終的な出力先(ファイル、ネットワーク接続、標準出力など)に強制的に書き出す操作を指します。tabwriter
の場合、Flush()
メソッドを呼び出すことで、バッファ内のすべてのデータが整形されて出力されます。フラッシングが適切に行われないと、データが失われたり、不完全な出力になったりする可能性があります。
テスト駆動開発 (TDD) の原則
このコミットで「extra tests」が追加されたことは、テスト駆動開発(TDD)の原則と一致しています。TDDでは、バグを修正したり新機能を追加したりする前に、まずその問題や機能に対応するテストケースを作成します。テストが失敗することを確認した後、コードを修正または実装し、テストが成功することを確認します。これにより、コードの品質と信頼性が向上し、将来の変更による回帰を防ぐことができます。
技術的詳細
このコミットは、tabwriter
パッケージの tabwriter.go
と tabwriter_test.go
の両方に変更を加えています。
src/lib/tabwriter/tabwriter.go
の変更点
-
LastLine()
関数の削除:LastLine()
メソッドは、b.lines
の最後の要素(*array.IntArray
型)を返すヘルパー関数でした。この関数は削除され、その呼び出し箇所はb.Line(b.lines.Len() - 1)
という直接的な表現に置き換えられました。これは、コードの簡潔化またはインライン化によるわずかなパフォーマンス改善を意図している可能性があります。 -
utflen()
関数の削除:utflen()
関数は、UTF-8エンコードされたバイトスライス中の文字数をカウントするためのものでした。この関数は、コミットされたコードの他の場所で呼び出されていないため、デッドコードとして削除された可能性が高いです。tabwriter
の文字幅計算ロジックが変更されたか、この関数が不要になったことを示唆しています。 -
WriteLines()
メソッドのフラッシングロジックの修正:WriteLines
メソッドは、バッファリングされた行を実際に書き出す役割を担っています。このコミットでは、特に最後のバッファされた行の扱いが変更されました。 変更前は、常にb.Write0(Newline)
を呼び出して改行を出力していました。 変更後は、if i+1 == b.lines.Len()
という条件が追加されました。これは、現在処理している行がバッファ内の最後の行であるかどうかをチェックしています。- 最後の行の場合:
b.Write0(b.buf.a[pos : pos + b.width])
を呼び出し、バッファに残っているデータを直接書き出します。この場合、改行は追加されません。これは、最後の行の後に余分な改行が出力される問題を修正するため、または、最後の行が改行で終わらない場合に適切にフラッシュされるようにするためと考えられます。 - 最後の行ではない場合: 以前と同様に
b.Write0(Newline)
を呼び出して改行を出力します。 この変更は、「flushing problem」の核心部分であり、tabwriter
がバッファリングされたデータを正確かつ完全にフラッシュすることを保証します。
- 最後の行の場合:
-
Append()
メソッドのフラッシングロジックの修正:Append
メソッドは、入力データを処理し、セルや行の区切りを検出する役割を担っています。このメソッド内のフラッシングロジックも修正されました。 変更前は、if b.LastLine().Len() == 1
という条件で、最後の行に1つのセルしかない場合にFlush()
を呼び出していました。それ以外の場合はb.AddLine()
を呼び出して新しい行を追加していました。 変更後は、last := b.Line(b.lines.Len() - 1)
で最後の行を取得し、if last.Len() == 1
という条件で、前の行に1つのセルしかない場合にFlush()
を呼び出すように変更されました。そして、else
ブロック(b.AddLine()
を呼び出していた部分)が削除されました。 この変更は、\n
が検出された際に、直前の行が単一のセルで構成されている場合にのみ即座にフラッシュを行うようにロジックを簡素化しています。これにより、不必要なバッファリングを避け、より効率的かつ正確なフラッシングを実現しています。
src/lib/tabwriter/tabwriter_test.go
の変更点
-
Test1
関数のTest
へのリネーム: テスト関数Test1
がTest
にリネームされました。これは、Goのテストフレームワークにおける慣例に従った変更であり、単一のテストファイル内の主要なテスト関数であることを示唆しています。 -
Check
関数のエラーハンドリングの改善:Check
ヘルパー関数内で、io.WriteString
とw.Flush()
の呼び出しに対して、明示的なエラーチェックが追加されました。これにより、テスト中に書き込みやフラッシングでエラーが発生した場合に、より詳細なエラーメッセージが報告されるようになり、デバッグが容易になります。 -
広範なテストケースの追加と修正: 最も顕著な変更は、
Test
関数内に多数の新しいCheck
呼び出しが追加されたことです。これらの新しいテストケースは、以下のような様々なシナリオをカバーしています。- 空の入力、改行のみの入力。
- 単一の文字、単一のタブ。
- タブで区切られた複数のセルを持つ行。
- 行の末尾にタブがある場合、ない場合。
- 異なるパディング文字 (
.
, tabwidth
が0の場合の挙動。- 数値データや特殊文字を含む複雑な表形式データ。
- 特に、以前のテストではカバーされていなかった、最後の行のフラッシングに関するエッジケースや、タブと改行の組み合わせによる挙動が重点的にテストされています。
また、既存のテストケースも、期待される出力が修正され、より正確な結果を反映するように更新されています。例えば、以前のテストでは出力の最後に余分な改行が含まれていたものが、修正後の期待される出力では削除されています。これは、
tabwriter.go
のフラッシングロジックの修正と密接に関連しています。
これらの変更は、tabwriter
パッケージの堅牢性と信頼性を大幅に向上させることを目的としています。特に、フラッシングの問題を修正し、その修正が様々な入力に対して正しく機能することを広範なテストで検証しています。
コアとなるコードの変更箇所
src/lib/tabwriter/tabwriter.go
--- a/src/lib/tabwriter/tabwriter.go
+++ b/src/lib/tabwriter/tabwriter.go
@@ -138,11 +138,6 @@ func (b *Writer) Line(i int) *array.IntArray {
}
-func (b *Writer) LastLine() *array.IntArray {
- return b.lines.At(b.lines.Len() - 1).(*array.IntArray);
-}
-
-
// debugging support
func (b *Writer) Dump() {
pos := 0;
@@ -235,7 +230,17 @@ func (b *Writer) WriteLines(pos0 int, line0, line1 int) (pos int, err *os.Error)\
pos += w;
}
}
- err = b.Write0(Newline);
+
+ if i+1 == b.lines.Len() {
+ // last buffered line - we don't have a newline, so just write
+ // any outstanding buffered data
+ err = b.Write0(b.buf.a[pos : pos + b.width]);
+ pos += b.width;
+ b.width = 0;
+ } else {
+ // not the last line - write newline
+ err = b.Write0(Newline);
+ }
if err != nil {
goto exit;
}
@@ -246,18 +251,6 @@ exit:
}
-// TODO use utflen for correct formatting
-func utflen(buf *[]byte) int {
- n := 0;
- for i := 0; i < len(buf); i++ {
- if buf[i]&0xC0 != 0x80 {
- n++
- }
- }
- return n
-}
-
-
func (b *Writer) Format(pos0 int, line0, line1 int) (pos int, err *os.Error) {
pos = pos0;
column := b.widths.Len();
@@ -336,22 +329,21 @@ func (b *Writer) Append(buf *[]byte) {
i0 = i + 1; // exclude ch from (next) cell
// terminate cell
- b.LastLine().Push(b.width);
+ last := b.Line(b.lines.Len() - 1);
+ last.Push(b.width);
b.width = 0;
if ch == '\n' {
- if b.LastLine().Len() == 1 {
- // The last line has only one cell which does not have an
- // impact on the formatting of the following lines (the
+ b.AddLine();
+ if last.Len() == 1 {
+ // The previous line has only one cell which does not have
+ // an impact on the formatting of the following lines (the
// last cell per line is ignored by Format), thus we can
// flush the Writer contents.
err = b.Flush();
if err != nil {
return i0, err;
}
- } else {
- // We can't flush yet - just add a new line.
- b.AddLine();
}
}
}
src/lib/tabwriter/tabwriter_test.go
--- a/src/lib/tabwriter/tabwriter_test.go
+++ b/src/lib/tabwriter/tabwriter_test.go
@@ -49,7 +49,18 @@ func Check(t *testing.T, tabwidth, padding int, padchar byte, align_left bool, s
var w tabwriter.Writer;
w.Init(&b, tabwidth, padding, padchar, align_left);
- io.WriteString(&w, src);\n+\twritten, err := io.WriteString(&w, src);\n+\tif err != nil {\n+\t\tt.Errorf(\"--- src:\\n%s\\n--- write error: %v\\n\", src, err);\n+\t}\n+\tif written != len(src) {\n+\t\tt.Errorf(\"--- src:\\n%s\\n--- written = %d, len(src) = %d\\n\", src, written, len(src));\n+\t}\n+\n+\terr = w.Flush();\n+\tif err != nil {\n+\t\tt.Errorf(\"--- src:\\n%s\\n--- flush error: %v\\n\", src, err);\n+\t}\n
res := b.String();
if res != expected {
t.Errorf(\"--- src:\\n%s\\n--- got:\\n%s--- expected:\\n%s\", src, res, expected);
@@ -58,11 +69,95 @@ func Check(t *testing.T, tabwidth, padding int, padchar byte, align_left bool, s
}
-export func Test1(t *testing.T) {\n+export func Test(t *testing.T) {\n+\tCheck(\n+\t\tt, 8, 1, \'.\', true,\n+\t\t\"\",\n+\t\t\"\"\n+\t);\n+\n+\tCheck(\n+\t\tt, 8, 1, \'.\', true,\n+\t\t\"\\n\\n\\n\",\n+\t\t\"\\n\\n\\n\"\n+\t);\n+\n+\tCheck(\n+\t\tt, 8, 1, \'.\', true,\n+\t\t\"a\\nb\\nc\",\n+\t\t\"a\\nb\\nc\"\n+\t);\n+\n+\tCheck(\n+\t\tt, 8, 1, \'.\', true,\n+\t\t\"\\t\", // \'\\t\' terminates an empty cell on last line - nothing to print\n+\t\t\"\"\n+\t);\n+\n+\tCheck(\n+\t\tt, 8, 1, \'.\', false,\n+\t\t\"\\t\", // \'\\t\' terminates an empty cell on last line - nothing to print\n+\t\t\"\"\n+\t);\n+\n+\tCheck(\n+\t\tt, 8, 1, \'.\', true,\n+\t\t\"*\\t*\",\n+\t\t\"**\"\n+\t);\n+\n+\tCheck(\n+\t\tt, 8, 1, \'.\', true,\n+\t\t\"*\\t*\\n\",\n+\t\t\"*.......*\\n\"\n+\t);\n+\n+\tCheck(\n+\t\tt, 8, 1, \'.\', true,\n+\t\t\"*\\t*\\t\",\n+\t\t\"*.......*\"\n+\t);\n+\n+\tCheck(\n+\t\tt, 8, 1, \'.\', false,\n+\t\t\"*\\t*\\t\",\n+\t\t\".......**\"\n+\t);\n+\n+\tCheck(\n+\t\tt, 8, 1, \'.\', true,\n+\t\t\"\\t\\n\",\n+\t\t\"........\\n\"\n+\t);\n+\n+\tCheck(\n+\t\tt, 8, 1, \'.\', true,\n+\t\t\"a) foo\",\n+\t\t\"a) foo\"\n+\t);\n+\n \tCheck(\n \t\tt, 8, 1, \' \', true,\n-\t\t\"\\n\",\n-\t\t\"\\n\"\n+\t\t\"b) foo\\tbar\", // \"bar\" is not in any cell - not formatted, just flushed\n+\t\t\"b) foobar\"\n+\t);\n+\n+\tCheck(\n+\t\tt, 8, 1, \'.\', true,\n+\t\t\"c) foo\\tbar\\t\",\n+\t\t\"c) foo..bar\"\n+\t);\n+\n+\tCheck(\n+\t\tt, 8, 1, \'.\', true,\n+\t\t\"d) foo\\tbar\\n\",\n+\t\t\"d) foo..bar\\n\"\n+\t);\n+\n+\tCheck(\n+\t\tt, 8, 1, \'.\', true,\n+\t\t\"e) foo\\tbar\\t\\n\",\n+\t\t\"e) foo..bar.....\\n\"\n \t);\n \n \tCheck(\
コアとなるコードの解説
src/lib/tabwriter/tabwriter.go
の変更解説
-
LastLine()
およびutflen()
の削除:LastLine()
はb.Line(b.lines.Len() - 1)
と同等であり、直接的な呼び出しに置き換えることで、冗長な関数呼び出しを排除し、コードの可読性を維持しつつわずかな最適化を図っています。utflen()
は、提供された差分を見る限り、他の場所で呼び出されていないため、未使用のコードとして削除されたと考えられます。これはコードベースのクリーンアップと保守性の向上に貢献します。 -
WriteLines()
メソッドのフラッシングロジックの修正: この変更は、tabwriter
の「フラッシング問題」の核心を解決します。以前は、WriteLines
が処理する各行の終わりに無条件に改行 (Newline
) を出力していました。しかし、バッファリングされた最後の行の場合、その後に続くデータがないにもかかわらず改行が出力されると、余分な空行が生成されたり、出力が不完全になったりする問題がありました。 新しいロジックでは、if i+1 == b.lines.Len()
という条件で、現在処理中の行がバッファ内の最後の行であるかを厳密にチェックします。- もし最後の行であれば、改行を出力する代わりに、
b.buf.a[pos : pos + b.width]
を直接書き出すことで、バッファに残っている整形済みのデータをすべて出力します。これにより、最後の行の後に不要な改行が追加されることを防ぎ、出力の正確性を保証します。また、b.width = 0
とすることで、次の書き込みに備えて内部状態をリセットします。 - 最後の行でなければ、これまで通り改行を出力し、次の行の処理に備えます。
この修正により、
tabwriter
はバッファリングされたすべてのデータを、期待される形式で正確にフラッシュできるようになります。
- もし最後の行であれば、改行を出力する代わりに、
-
Append()
メソッドのフラッシングロジックの修正:Append
メソッド内の変更は、\n
(改行) が検出された際のフラッシングのタイミングをより正確に制御します。 変更前は、b.LastLine().Len() == 1
(現在の行に1つのセルしかない場合) にFlush()
を呼び出し、それ以外の場合はb.AddLine()
を呼び出して新しい行を追加していました。 変更後では、まずb.AddLine()
を呼び出して新しい行を追加し、その後にif last.Len() == 1
(前の行に1つのセルしかない場合) にFlush()
を呼び出すように変更されています。そして、else
ブロックが削除されています。 この修正の意図は、単一のセルで構成される行(例えば、見出しや単独のメッセージなど)が入力された場合、その行が後続の行の列幅計算に影響を与えないため、すぐにフラッシュして出力バッファをクリアできるという最適化です。これにより、tabwriter
はより効率的に動作し、不必要なバッファリングを避けることができます。また、b.AddLine()
を先に呼び出すことで、新しい行の準備が確実に行われます。
src/lib/tabwriter/tabwriter_test.go
の変更解説
-
Test1
からTest
へのリネームとエラーハンドリングの改善: テスト関数のリネームは、Goのテストの慣例に合わせたもので、機能的な変更はありません。Check
関数にio.WriteString
とw.Flush()
のエラーチェックが追加されたことは非常に重要です。これにより、テスト実行中に書き込みやフラッシングの操作でエラーが発生した場合、テストが単に失敗するだけでなく、具体的なエラーメッセージが報告されるようになります。これは、デバッグの効率を大幅に向上させ、問題の特定を容易にします。 -
広範なテストケースの追加と修正: このコミットのもう一つの主要な側面は、
tabwriter
の様々な挙動を検証するための新しいテストケースが大量に追加されたことです。これらのテストは、以下のようなシナリオを網羅しています。- エッジケースの網羅: 空文字列、連続する改行、単一のタブ、行の末尾のタブなど、以前のテストでは見落とされがちだったエッジケースが追加されています。これらは、
tabwriter
のフラッシングロジックがこれらの特殊な入力に対してどのように反応するかを検証するために不可欠です。 - フラッシングの正確性の検証: 特に、タブと改行の組み合わせ、および行の終端でのフラッシングの挙動に関するテストが強化されています。これにより、
tabwriter.go
で修正されたフラッシングロジックが、期待される出力を正確に生成することを確認します。 - 多様な入力と設定: 異なる
tabwidth
、padding
、padchar
、align_left
の設定で、様々なテキストパターン(数値、特殊文字、長い文字列など)がテストされています。これにより、tabwriter
が多様なユースケースで堅牢に機能することを確認します。 - 期待される出力の修正: 既存のテストケースの期待される出力が修正されているのは、
tabwriter.go
のフラッシングロジックの変更により、以前の出力がもはや正しくなくなったためです。これは、コードの変更がテストによって適切に検証され、テスト自体もコードの変更に合わせて更新されていることを示しています。
- エッジケースの網羅: 空文字列、連続する改行、単一のタブ、行の末尾のタブなど、以前のテストでは見落とされがちだったエッジケースが追加されています。これらは、
これらのテストの追加と改善は、tabwriter
パッケージの品質と信頼性を大幅に向上させ、将来の変更に対する安全網を提供します。
関連リンク
- Go言語の
text/tabwriter
パッケージのドキュメント: https://pkg.go.dev/text/tabwriter (現在のパッケージ名) - Go言語の
io
パッケージのドキュメント: https://pkg.go.dev/io
参考にした情報源リンク
- コミット情報:
/home/orange/Project/comemo/commit_data/1249.txt
- GitHubコミットページ: https://github.com/golang/go/commit/7cfa7eebf31126d7614281290d484874ad63413a
- Go言語の一般的な知識と標準ライブラリの構造。
- 差分(diff)の分析。