// 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 tabwriter import ( "bytes" "fmt" "io" "testing" ) type buffer struct { a []byte } func (b *buffer) init(n int) { b.a = make([]byte, 0, n) } func (b *buffer) clear() { b.a = b.a[0:0] } func (b *buffer) Write(buf []byte) (written int, err error) { n := len(b.a) m := len(buf) if n+m <= cap(b.a) { b.a = b.a[0 : n+m] for i := 0; i < m; i++ { b.a[n+i] = buf[i] } } else { panic("buffer.Write: buffer too small") } return len(buf), nil } func (b *buffer) String() string { return string(b.a) } func write(t *testing.T, testname string, w *Writer, src string) { t.Helper() written, err := io.WriteString(w, src) if err != nil { t.Errorf("--- test: %s\n--- src:\n%q\n--- write error: %v\n", testname, src, err) } if written != len(src) { t.Errorf("--- test: %s\n--- src:\n%q\n--- written = %d, len(src) = %d\n", testname, src, written, len(src)) } } func verify(t *testing.T, testname string, w *Writer, b *buffer, src, expected string) { t.Helper() err := w.Flush() if err != nil { t.Errorf("--- test: %s\n--- src:\n%q\n--- flush error: %v\n", testname, src, err) } res := b.String() if res != expected { t.Errorf("--- test: %s\n--- src:\n%q\n--- found:\n%q\n--- expected:\n%q\n", testname, src, res, expected) } } func check(t *testing.T, testname string, minwidth, tabwidth, padding int, padchar byte, flags uint, src, expected string) { t.Helper() var b buffer b.init(1000) var w Writer w.Init(&b, minwidth, tabwidth, padding, padchar, flags) // write all at once title := testname + " (written all at once)" b.clear() write(t, title, &w, src) verify(t, title, &w, &b, src, expected) // write byte-by-byte title = testname + " (written byte-by-byte)" b.clear() for i := 0; i < len(src); i++ { write(t, title, &w, src[i:i+1]) } verify(t, title, &w, &b, src, expected) // write using Fibonacci slice sizes title = testname + " (written in fibonacci slices)" b.clear() for i, d := 0, 0; i < len(src); { write(t, title, &w, src[i:i+d]) i, d = i+d, d+1 if i+d > len(src) { d = len(src) - i } } verify(t, title, &w, &b, src, expected) } var tests = []struct { testname string minwidth, tabwidth, padding int padchar byte flags uint src, expected string }{ { "1a", 8, 0, 1, '.', 0, "", "", }, { "1a debug", 8, 0, 1, '.', Debug, "", "", }, { "1b esc stripped", 8, 0, 1, '.', StripEscape, "\xff\xff", "", }, { "1b esc", 8, 0, 1, '.', 0, "\xff\xff", "\xff\xff", }, { "1c esc stripped", 8, 0, 1, '.', StripEscape, "\xff\t\xff", "\t", }, { "1c esc", 8, 0, 1, '.', 0, "\xff\t\xff", "\xff\t\xff", }, { "1d esc stripped", 8, 0, 1, '.', StripEscape, "\xff\"foo\t\n\tbar\"\xff", "\"foo\t\n\tbar\"", }, { "1d esc", 8, 0, 1, '.', 0, "\xff\"foo\t\n\tbar\"\xff", "\xff\"foo\t\n\tbar\"\xff", }, { "1e esc stripped", 8, 0, 1, '.', StripEscape, "abc\xff\tdef", // unterminated escape "abc\tdef", }, { "1e esc", 8, 0, 1, '.', 0, "abc\xff\tdef", // unterminated escape "abc\xff\tdef", }, { "2", 8, 0, 1, '.', 0, "\n\n\n", "\n\n\n", }, { "3", 8, 0, 1, '.', 0, "a\nb\nc", "a\nb\nc", }, { "4a", 8, 0, 1, '.', 0, "\t", // '\t' terminates an empty cell on last line - nothing to print "", }, { "4b", 8, 0, 1, '.', AlignRight, "\t", // '\t' terminates an empty cell on last line - nothing to print "", }, { "5", 8, 0, 1, '.', 0, "*\t*", "*.......*", }, { "5b", 8, 0, 1, '.', 0, "*\t*\n", "*.......*\n", }, { "5c", 8, 0, 1, '.', 0, "*\t*\t", "*.......*", }, { "5c debug", 8, 0, 1, '.', Debug, "*\t*\t", "*.......|*", }, { "5d", 8, 0, 1, '.', AlignRight, "*\t*\t", ".......**", }, { "6", 8, 0, 1, '.', 0, "\t\n", "........\n", }, { "7a", 8, 0, 1, '.', 0, "a) foo", "a) foo", }, { "7b", 8, 0, 1, ' ', 0, "b) foo\tbar", "b) foo bar", }, { "7c", 8, 0, 1, '.', 0, "c) foo\tbar\t", "c) foo..bar", }, { "7d", 8, 0, 1, '.', 0, "d) foo\tbar\n", "d) foo..bar\n", }, { "7e", 8, 0, 1, '.', 0, "e) foo\tbar\t\n", "e) foo..bar.....\n", }, { "7f", 8, 0, 1, '.', FilterHTML, "f) f<o\tbar\t\n", "f) f<o..bar.....\n", }, { "7g", 8, 0, 1, '.', FilterHTML, "g) f<o\tbar\t non-terminated entity &", "g) f<o..bar..... non-terminated entity &", }, { "7g debug", 8, 0, 1, '.', FilterHTML | Debug, "g) f<o\tbar\t non-terminated entity &", "g) f<o..|bar.....| non-terminated entity &", }, { "8", 8, 0, 1, '*', 0, "Hello, world!\n", "Hello, world!\n", }, { "9a", 1, 0, 0, '.', 0, "1\t2\t3\t4\n" + "11\t222\t3333\t44444\n", "1.2..3...4\n" + "11222333344444\n", }, { "9b", 1, 0, 0, '.', FilterHTML, "1\t2\t3\t4\n" + // \f inside HTML is ignored "11\t222\t3333\t44444\n", "1.2..3...4\n" + "11222333344444\n", }, { "9c", 1, 0, 0, '.', 0, "1\t2\t3\t4\f" + // \f causes a newline and flush "11\t222\t3333\t44444\n", "1234\n" + "11222333344444\n", }, { "9c debug", 1, 0, 0, '.', Debug, "1\t2\t3\t4\f" + // \f causes a newline and flush "11\t222\t3333\t44444\n", "1|2|3|4\n" + "---\n" + "11|222|3333|44444\n", }, { "10a", 5, 0, 0, '.', 0, "1\t2\t3\t4\n", "1....2....3....4\n", }, { "10b", 5, 0, 0, '.', 0, "1\t2\t3\t4\t\n", "1....2....3....4....\n", }, { "11", 8, 0, 1, '.', 0, "本\tb\tc\n" + "aa\t\u672c\u672c\u672c\tcccc\tddddd\n" + "aaa\tbbbb\n", "本......b.......c\n" + "aa......本本本..cccc....ddddd\n" + "aaa.....bbbb\n", }, { "12a", 8, 0, 1, ' ', AlignRight, "a\tè\tc\t\n" + "aa\tèèè\tcccc\tddddd\t\n" + "aaa\tèèèè\t\n", " a è c\n" + " aa èèè cccc ddddd\n" + " aaa èèèè\n", }, { "12b", 2, 0, 0, ' ', 0, "a\tb\tc\n" + "aa\tbbb\tcccc\n" + "aaa\tbbbb\n", "a b c\n" + "aa bbbcccc\n" + "aaabbbb\n", }, { "12c", 8, 0, 1, '_', 0, "a\tb\tc\n" + "aa\tbbb\tcccc\n" + "aaa\tbbbb\n", "a_______b_______c\n" + "aa______bbb_____cccc\n" + "aaa_____bbbb\n", }, { "13a", 4, 0, 1, '-', 0, "4444\t日本語\t22\t1\t333\n" + "999999999\t22\n" + "7\t22\n" + "\t\t\t88888888\n" + "\n" + "666666\t666666\t666666\t4444\n" + "1\t1\t999999999\t0000000000\n", "4444------日本語-22--1---333\n" + "999999999-22\n" + "7---------22\n" + "------------------88888888\n" + "\n" + "666666-666666-666666----4444\n" + "1------1------999999999-0000000000\n", }, { "13b", 4, 0, 3, '.', 0, "4444\t333\t22\t1\t333\n" + "999999999\t22\n" + "7\t22\n" + "\t\t\t88888888\n" + "\n" + "666666\t666666\t666666\t4444\n" + "1\t1\t999999999\t0000000000\n", "4444........333...22...1...333\n" + "999999999...22\n" + "7...........22\n" + "....................88888888\n" + "\n" + "666666...666666...666666......4444\n" + "1........1........999999999...0000000000\n", }, { "13c", 8, 8, 1, '\t', FilterHTML, "4444\t333\t22\t1\t333\n" + "999999999\t22\n" + "7\t22\n" + "\t\t\t88888888\n" + "\n" + "666666\t666666\t666666\t4444\n" + "1\t1\t999999999\t0000000000\n", "4444\t\t333\t22\t1\t333\n" + "999999999\t22\n" + "7\t\t22\n" + "\t\t\t\t88888888\n" + "\n" + "666666\t666666\t666666\t\t4444\n" + "1\t1\t999999999\t0000000000\n", }, { "14", 1, 0, 2, ' ', AlignRight, ".0\t.3\t2.4\t-5.1\t\n" + "23.0\t12345678.9\t2.4\t-989.4\t\n" + "5.1\t12.0\t2.4\t-7.0\t\n" + ".0\t0.0\t332.0\t8908.0\t\n" + ".0\t-.3\t456.4\t22.1\t\n" + ".0\t1.2\t44.4\t-13.3\t\t", " .0 .3 2.4 -5.1\n" + " 23.0 12345678.9 2.4 -989.4\n" + " 5.1 12.0 2.4 -7.0\n" + " .0 0.0 332.0 8908.0\n" + " .0 -.3 456.4 22.1\n" + " .0 1.2 44.4 -13.3", }, { "14 debug", 1, 0, 2, ' ', AlignRight | Debug, ".0\t.3\t2.4\t-5.1\t\n" + "23.0\t12345678.9\t2.4\t-989.4\t\n" + "5.1\t12.0\t2.4\t-7.0\t\n" + ".0\t0.0\t332.0\t8908.0\t\n" + ".0\t-.3\t456.4\t22.1\t\n" + ".0\t1.2\t44.4\t-13.3\t\t", " .0| .3| 2.4| -5.1|\n" + " 23.0| 12345678.9| 2.4| -989.4|\n" + " 5.1| 12.0| 2.4| -7.0|\n" + " .0| 0.0| 332.0| 8908.0|\n" + " .0| -.3| 456.4| 22.1|\n" + " .0| 1.2| 44.4| -13.3|", }, { "15a", 4, 0, 0, '.', 0, "a\t\tb", "a.......b", }, { "15b", 4, 0, 0, '.', DiscardEmptyColumns, "a\t\tb", // htabs - do not discard column "a.......b", }, { "15c", 4, 0, 0, '.', DiscardEmptyColumns, "a\v\vb", "a...b", }, { "15d", 4, 0, 0, '.', AlignRight | DiscardEmptyColumns, "a\v\vb", "...ab", }, { "16a", 100, 100, 0, '\t', 0, "a\tb\t\td\n" + "a\tb\t\td\te\n" + "a\n" + "a\tb\tc\td\n" + "a\tb\tc\td\te\n", "a\tb\t\td\n" + "a\tb\t\td\te\n" + "a\n" + "a\tb\tc\td\n" + "a\tb\tc\td\te\n", }, { "16b", 100, 100, 0, '\t', DiscardEmptyColumns, "a\vb\v\vd\n" + "a\vb\v\vd\ve\n" + "a\n" + "a\vb\vc\vd\n" + "a\vb\vc\vd\ve\n", "a\tb\td\n" + "a\tb\td\te\n" + "a\n" + "a\tb\tc\td\n" + "a\tb\tc\td\te\n", }, { "16b debug", 100, 100, 0, '\t', DiscardEmptyColumns | Debug, "a\vb\v\vd\n" + "a\vb\v\vd\ve\n" + "a\n" + "a\vb\vc\vd\n" + "a\vb\vc\vd\ve\n", "a\t|b\t||d\n" + "a\t|b\t||d\t|e\n" + "a\n" + "a\t|b\t|c\t|d\n" + "a\t|b\t|c\t|d\t|e\n", }, { "16c", 100, 100, 0, '\t', DiscardEmptyColumns, "a\tb\t\td\n" + // hard tabs - do not discard column "a\tb\t\td\te\n" + "a\n" + "a\tb\tc\td\n" + "a\tb\tc\td\te\n", "a\tb\t\td\n" + "a\tb\t\td\te\n" + "a\n" + "a\tb\tc\td\n" + "a\tb\tc\td\te\n", }, { "16c debug", 100, 100, 0, '\t', DiscardEmptyColumns | Debug, "a\tb\t\td\n" + // hard tabs - do not discard column "a\tb\t\td\te\n" + "a\n" + "a\tb\tc\td\n" + "a\tb\tc\td\te\n", "a\t|b\t|\t|d\n" + "a\t|b\t|\t|d\t|e\n" + "a\n" + "a\t|b\t|c\t|d\n" + "a\t|b\t|c\t|d\t|e\n", }, } func Test(t *testing.T) { for _, e := range tests { check(t, e.testname, e.minwidth, e.tabwidth, e.padding, e.padchar, e.flags, e.src, e.expected) } } type panicWriter struct{} func (panicWriter) Write([]byte) (int, error) { panic("cannot write") } func wantPanicString(t *testing.T, want string) { t.Helper() if e := recover(); e != nil { got, ok := e.(string) switch { case !ok: t.Errorf("got %v (%T), want panic string", e, e) case got != want: t.Errorf("wrong panic message: got %q, want %q", got, want) } } } func TestPanicDuringFlush(t *testing.T) { defer wantPanicString(t, "tabwriter: panic during Flush") var p panicWriter w := new(Writer) w.Init(p, 0, 0, 5, ' ', 0) io.WriteString(w, "a") w.Flush() t.Errorf("failed to panic during Flush") } func TestPanicDuringWrite(t *testing.T) { defer wantPanicString(t, "tabwriter: panic during Write") var p panicWriter w := new(Writer) w.Init(p, 0, 0, 5, ' ', 0) io.WriteString(w, "a\n\n") // the second \n triggers a call to w.Write and thus a panic t.Errorf("failed to panic during Write") } func BenchmarkTable(b *testing.B) { for _, w := range [...]int{1, 10, 100} { // Build a line with w cells. line := bytes.Repeat([]byte("a\t"), w) line = append(line, '\n') for _, h := range [...]int{10, 1000, 100000} { b.Run(fmt.Sprintf("%dx%d", w, h), func(b *testing.B) { b.Run("new", func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { w := NewWriter(io.Discard, 4, 4, 1, ' ', 0) // no particular reason for these settings // Write the line h times. for j := 0; j < h; j++ { w.Write(line) } w.Flush() } }) b.Run("reuse", func(b *testing.B) { b.ReportAllocs() w := NewWriter(io.Discard, 4, 4, 1, ' ', 0) // no particular reason for these settings for i := 0; i < b.N; i++ { // Write the line h times. for j := 0; j < h; j++ { w.Write(line) } w.Flush() } }) }) } } } func BenchmarkPyramid(b *testing.B) { for _, x := range [...]int{10, 100, 1000} { // Build a line with x cells. line := bytes.Repeat([]byte("a\t"), x) b.Run(fmt.Sprintf("%d", x), func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { w := NewWriter(io.Discard, 4, 4, 1, ' ', 0) // no particular reason for these settings // Write increasing prefixes of that line. for j := 0; j < x; j++ { w.Write(line[:j*2]) w.Write([]byte{'\n'}) } w.Flush() } }) } } func BenchmarkRagged(b *testing.B) { var lines [8][]byte for i, w := range [8]int{6, 2, 9, 5, 5, 7, 3, 8} { // Build a line with w cells. lines[i] = bytes.Repeat([]byte("a\t"), w) } for _, h := range [...]int{10, 100, 1000} { b.Run(fmt.Sprintf("%d", h), func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { w := NewWriter(io.Discard, 4, 4, 1, ' ', 0) // no particular reason for these settings // Write the lines in turn h times. for j := 0; j < h; j++ { w.Write(lines[j%len(lines)]) w.Write([]byte{'\n'}) } w.Flush() } }) } } const codeSnippet = ` some command foo # aligned barbaz # comments but mostly single cell lines ` func BenchmarkCode(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { w := NewWriter(io.Discard, 4, 4, 1, ' ', 0) // no particular reason for these settings // The code is small, so it's reasonable for the tabwriter user // to write it all at once, or buffer the writes. w.Write([]byte(codeSnippet)) w.Flush() } }