1// Copyright 2015, Joe Tsai. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE.md file.
4
5package xflate
6
7import (
8	"bytes"
9	"compress/flate"
10	"io/ioutil"
11	"testing"
12
13	"github.com/dsnet/compress/internal/testutil"
14)
15
16func TestWriter(t *testing.T) {
17	br := bytes.Repeat
18	dh := testutil.MustDecodeHex
19
20	vectors := []struct {
21		desc   string        // Test description
22		conf   *WriterConfig // Input WriterConfig
23		input  []interface{} // Test input tokens (either flush mode or input string)
24		output []byte        // Expected output string
25	}{{
26		desc:  "empty stream",
27		input: []interface{}{},
28		output: dh("" +
29			"0d008705000048c82a51e8ff37dbf1", // Footer
30		),
31	}, {
32		desc:  "empty stream with empty chunk",
33		input: []interface{}{FlushSync},
34		output: dh("" +
35			"000000ffff000000ffff" + // Chunk0
36			"34c086050020916cb2a50bd20369da192deaff3bda05f8" + // Index0
37			"1dc08605002021ab44219b4aff7fd6de3bf8", // Footer
38		),
39	}, {
40		desc:  "empty stream with empty index",
41		input: []interface{}{FlushIndex},
42		output: dh("" +
43			"04c086050020191d53a1a508c9e8ff5bda7bf8" + // Index0
44			"15c08605002021ab44219ba2ff2f6bef5df8", // Footer
45		),
46	}, {
47		desc:  "empty stream with multiple empty chunks",
48		input: []interface{}{FlushFull, FlushFull, FlushFull},
49		output: dh("" +
50			"000000ffff" + // Chunk0
51			"000000ffff" + // Chunk1
52			"000000ffff" + // Chunk2
53			"148086058044655366e3817441ba205d504a83348c445ddcde7b6ffc" + // Index0
54			"15c08605002021ab44a103aaff2f6bef5df8", // Footer
55		),
56	}, {
57		desc:  "empty stream with multiple empty indexes",
58		input: []interface{}{FlushIndex, FlushIndex, FlushIndex},
59		output: dh("" +
60			"04c086050020191d53a1a508c9e8ff5bda7bf8" + // Index0
61			"3cc08605002019293a24a55464a585faff9bf600f8" + // Index1
62			"04c08605002019493a2494d050560afd7f4c7bfb" + // Index2
63			"25008705000048c82a51e880f4ff834df0", // Footer
64		),
65	}, {
66		desc:  "3k zeros, 1KiB chunks",
67		conf:  &WriterConfig{ChunkSize: 1 << 10},
68		input: []interface{}{br([]byte{0}, 3000)},
69		output: dh("" +
70			"621805a360148c5800000000ffff" + // Chunk0
71			"621805a360148c5800000000ffff" + // Chunk1
72			"621805a360140c3900000000ffff" + // Chunk2
73			"1c8086058044642b3bc9aa3464540784acea809055d99586dd5492446555a7b607fc" + // Index0
74			"0d008705000048c82a51c81ea1ff0f6cf2", // Footer
75		),
76	}, {
77		desc: "quick brown fox - manual chunking/indexing",
78		input: []interface{}{
79			"the quick", FlushSync, " brown fox", FlushFull, FlushFull, " jumped over the", FlushIndex, " lazy dog",
80		},
81		output: dh("" +
82			"2ac94855282ccd4cce06000000ffff52482aca2fcf5348cbaf00000000ffff" + // Chunk0
83			"000000ffff" + // Chunk1
84			"52c82acd2d484d51c82f4b2d5228c94805000000ffff" + // Chunk2
85			"2480860580446553762a0ad14211d207253b234546a1528ad4d3edbd0bfc" + // Index0
86			"52c849acaa5448c94f07000000ffff" + // Chunk3
87			"2c8086058044a281ec8611190d23b21221ca0851fdafbdf7de05fc" + // Index1
88			"1dc08605002021ab44219b52ff7fd6de3bf8", // Footer
89		),
90	}, {
91		desc:  "quick brown fox - automatic chunking/indexing",
92		conf:  &WriterConfig{ChunkSize: 4, IndexSize: 3},
93		input: []interface{}{"the quick brown fox jumped over the lazy dog"},
94		output: dh("" +
95			"2ac9485500000000ffff" + // Chunk0
96			"2a2ccd4c06000000ffff" + // Chunk1
97			"ca56482a02000000ffff" + // Chunk2
98			"2c8086058044655376c32a2b9999c9cc4c665691d04ea5a474747bef01fc" + // Index0
99			"ca2fcf5300000000ffff" + // Chunk3
100			"4acbaf5000000000ffff" + // Chunk4
101			"ca2acd2d00000000ffff" + // Chunk5
102			"04808605804445036537acb2929999cccc6466cb48112a45a193db7beffc" + // Index1
103			"4a4d51c807000000ffff" + // Chunk6
104			"2a4b2d5200000000ffff" + // Chunk7
105			"2ac9485500000000ffff" + // Chunk8
106			"04808605804445036537acb2929999cccc6466cb48112a45a193db7beffc" + // Index2
107			"ca49acaa04000000ffff" + // Chunk9
108			"5248c94f07000000ffff" + // Chunk10
109			"148086058084a261644b665632339399d9425629a44877b7f7de3bfc" + // Index3
110			"15c08605002021ab44a103aaff2f6bef5df8", // Footer
111		),
112	}}
113
114	for i, v := range vectors {
115		// Encode the test input.
116		var b, bb bytes.Buffer
117		xw, err := NewWriter(&b, v.conf)
118		if err != nil {
119			t.Errorf("test %d (%s), unexpected error: NewWriter() = %v", i, v.desc, err)
120		}
121		for _, tok := range v.input {
122			switch tok := tok.(type) {
123			case string:
124				bb.WriteString(tok)
125				if _, err := xw.Write([]byte(tok)); err != nil {
126					t.Errorf("test %d (%s), unexpected error: Write() = %v", i, v.desc, err)
127				}
128			case []byte:
129				bb.Write(tok)
130				if _, err := xw.Write(tok); err != nil {
131					t.Errorf("test %d (%s), unexpected error: Write() = %v", i, v.desc, err)
132				}
133			case FlushMode:
134				if err := xw.Flush(tok); err != nil {
135					t.Errorf("test %d (%s), unexpected error: Flush() = %v", i, v.desc, err)
136				}
137			default:
138				t.Fatalf("test %d (%s), unknown token: %v", i, v.desc, tok)
139			}
140		}
141		if err := xw.Close(); err != nil {
142			t.Errorf("test %d (%s), unexpected error: Close() = %v", i, v.desc, err)
143		}
144		if got, want, ok := testutil.BytesCompare(b.Bytes(), v.output); !ok {
145			t.Errorf("test %d (%s), mismatching bytes:\ngot  %s\nwant %s", i, v.desc, got, want)
146		}
147		if xw.OutputOffset != int64(b.Len()) {
148			t.Errorf("test %d (%s), output offset mismatch: got %d, want %d", i, v.desc, xw.OutputOffset, b.Len())
149		}
150		if xw.InputOffset != int64(bb.Len()) {
151			t.Errorf("test %d (%s), input offset mismatch: got %d, want %d", i, v.desc, xw.InputOffset, bb.Len())
152		}
153
154		// Verify that the output stream is DEFLATE compatible.
155		rd := bytes.NewReader(b.Bytes())
156		fr := flate.NewReader(rd)
157		buf, err := ioutil.ReadAll(fr)
158		if err != nil {
159			t.Errorf("test %d (%s), unexpected error: ReadAll() = %v", i, v.desc, err)
160		}
161		if got, want, ok := testutil.BytesCompare(buf, bb.Bytes()); !ok {
162			t.Errorf("test %d (%s), mismatching bytes:\ngot  %s\nwant %s", i, v.desc, got, want)
163		}
164		if rd.Len() > 0 {
165			t.Errorf("test %d (%s), not all bytes consumed: %d > 0", i, v.desc, rd.Len())
166		}
167	}
168}
169
170func TestWriterReset(t *testing.T) {
171	// Test bad Writer config.
172	if _, err := NewWriter(ioutil.Discard, &WriterConfig{Level: -431}); err == nil {
173		t.Fatalf("unexpected success: NewWriter()")
174	}
175
176	// Test Writer for idempotent Close.
177	xw := new(Writer)
178	xw.Reset(ioutil.Discard)
179	if _, err := xw.Write([]byte("hello, world!")); err != nil {
180		t.Fatalf("unexpected error: Write() = %v", err)
181	}
182	if err := xw.Close(); err != nil {
183		t.Fatalf("unexpected error: Close() = %v", err)
184	}
185	if err := xw.Close(); err != nil {
186		t.Fatalf("unexpected error: Close() = %v", err)
187	}
188	if _, err := xw.Write([]byte("hello, world!")); err != errClosed {
189		t.Fatalf("mismatching error: Write() = %v, want %v", err, errClosed)
190	}
191}
192
193// BenchmarkWriter benchmarks the overhead of the XFLATE format over DEFLATE.
194// Thus, it intentionally uses a very small chunk size with no compression.
195func BenchmarkWriter(b *testing.B) {
196	twain := testutil.MustLoadFile("../testdata/twain.txt")
197	bb := bytes.NewBuffer(make([]byte, 0, 2*len(twain)))
198	xw, _ := NewWriter(nil, &WriterConfig{Level: NoCompression, ChunkSize: 1 << 10})
199
200	b.ReportAllocs()
201	b.SetBytes(int64(len(twain)))
202	b.ResetTimer()
203
204	for i := 0; i < b.N; i++ {
205		bb.Reset()
206		xw.Reset(bb)
207		if _, err := xw.Write(twain); err != nil {
208			b.Fatalf("unexpected error: Write() = %v", err)
209		}
210		if err := xw.Close(); err != nil {
211			b.Fatalf("unexpected error: Close() = %v", err)
212		}
213	}
214}
215