1package toml
2
3import (
4	"bytes"
5	"errors"
6	"fmt"
7	"reflect"
8	"strings"
9	"testing"
10	"time"
11)
12
13type failingWriter struct {
14	failAt  int
15	written int
16	buffer  bytes.Buffer
17}
18
19func (f *failingWriter) Write(p []byte) (n int, err error) {
20	count := len(p)
21	toWrite := f.failAt - (count + f.written)
22	if toWrite < 0 {
23		toWrite = 0
24	}
25	if toWrite > count {
26		f.written += count
27		f.buffer.Write(p)
28		return count, nil
29	}
30
31	f.buffer.Write(p[:toWrite])
32	f.written = f.failAt
33	return toWrite, fmt.Errorf("failingWriter failed after writing %d bytes", f.written)
34}
35
36func assertErrorString(t *testing.T, expected string, err error) {
37	expectedErr := errors.New(expected)
38	if err == nil || err.Error() != expectedErr.Error() {
39		t.Errorf("expecting error %s, but got %s instead", expected, err)
40	}
41}
42
43func TestTreeWriteToEmptyTable(t *testing.T) {
44	doc := `[[empty-tables]]
45[[empty-tables]]`
46
47	toml, err := Load(doc)
48	if err != nil {
49		t.Fatal("Unexpected Load error:", err)
50	}
51	tomlString, err := toml.ToTomlString()
52	if err != nil {
53		t.Fatal("Unexpected ToTomlString error:", err)
54	}
55
56	expected := `
57[[empty-tables]]
58
59[[empty-tables]]
60`
61
62	if tomlString != expected {
63		t.Fatalf("Expected:\n%s\nGot:\n%s", expected, tomlString)
64	}
65}
66
67func TestTreeWriteToTomlString(t *testing.T) {
68	toml, err := Load(`name = { first = "Tom", last = "Preston-Werner" }
69points = { x = 1, y = 2 }`)
70
71	if err != nil {
72		t.Fatal("Unexpected error:", err)
73	}
74
75	tomlString, _ := toml.ToTomlString()
76	reparsedTree, err := Load(tomlString)
77
78	assertTree(t, reparsedTree, err, map[string]interface{}{
79		"name": map[string]interface{}{
80			"first": "Tom",
81			"last":  "Preston-Werner",
82		},
83		"points": map[string]interface{}{
84			"x": int64(1),
85			"y": int64(2),
86		},
87	})
88}
89
90func TestTreeWriteToTomlStringSimple(t *testing.T) {
91	tree, err := Load("[foo]\n\n[[foo.bar]]\na = 42\n\n[[foo.bar]]\na = 69\n")
92	if err != nil {
93		t.Errorf("Test failed to parse: %v", err)
94		return
95	}
96	result, err := tree.ToTomlString()
97	if err != nil {
98		t.Errorf("Unexpected error: %s", err)
99	}
100	expected := "\n[foo]\n\n  [[foo.bar]]\n    a = 42\n\n  [[foo.bar]]\n    a = 69\n"
101	if result != expected {
102		t.Errorf("Expected got '%s', expected '%s'", result, expected)
103	}
104}
105
106func TestTreeWriteToTomlStringKeysOrders(t *testing.T) {
107	for i := 0; i < 100; i++ {
108		tree, _ := Load(`
109		foobar = true
110		bar = "baz"
111		foo = 1
112		[qux]
113		  foo = 1
114		  bar = "baz2"`)
115
116		stringRepr, _ := tree.ToTomlString()
117
118		t.Log("Intermediate string representation:")
119		t.Log(stringRepr)
120
121		r := strings.NewReader(stringRepr)
122		toml, err := LoadReader(r)
123
124		if err != nil {
125			t.Fatal("Unexpected error:", err)
126		}
127
128		assertTree(t, toml, err, map[string]interface{}{
129			"foobar": true,
130			"bar":    "baz",
131			"foo":    1,
132			"qux": map[string]interface{}{
133				"foo": 1,
134				"bar": "baz2",
135			},
136		})
137	}
138}
139
140func testMaps(t *testing.T, actual, expected map[string]interface{}) {
141	if !reflect.DeepEqual(actual, expected) {
142		t.Fatal("trees aren't equal.\n", "Expected:\n", expected, "\nActual:\n", actual)
143	}
144}
145
146func TestTreeWriteToMapSimple(t *testing.T) {
147	tree, _ := Load("a = 42\nb = 17")
148
149	expected := map[string]interface{}{
150		"a": int64(42),
151		"b": int64(17),
152	}
153
154	testMaps(t, tree.ToMap(), expected)
155}
156
157func TestTreeWriteToInvalidTreeSimpleValue(t *testing.T) {
158	tree := Tree{values: map[string]interface{}{"foo": int8(1)}}
159	_, err := tree.ToTomlString()
160	assertErrorString(t, "invalid value type at foo: int8", err)
161}
162
163func TestTreeWriteToInvalidTreeTomlValue(t *testing.T) {
164	tree := Tree{values: map[string]interface{}{"foo": &tomlValue{value: int8(1), comment: "", position: Position{}}}}
165	_, err := tree.ToTomlString()
166	assertErrorString(t, "unsupported value type int8: 1", err)
167}
168
169func TestTreeWriteToInvalidTreeTomlValueArray(t *testing.T) {
170	tree := Tree{values: map[string]interface{}{"foo": &tomlValue{value: int8(1), comment: "", position: Position{}}}}
171	_, err := tree.ToTomlString()
172	assertErrorString(t, "unsupported value type int8: 1", err)
173}
174
175func TestTreeWriteToFailingWriterInSimpleValue(t *testing.T) {
176	toml, _ := Load(`a = 2`)
177	writer := failingWriter{failAt: 0, written: 0}
178	_, err := toml.WriteTo(&writer)
179	assertErrorString(t, "failingWriter failed after writing 0 bytes", err)
180}
181
182func TestTreeWriteToFailingWriterInTable(t *testing.T) {
183	toml, _ := Load(`
184[b]
185a = 2`)
186	writer := failingWriter{failAt: 2, written: 0}
187	_, err := toml.WriteTo(&writer)
188	assertErrorString(t, "failingWriter failed after writing 2 bytes", err)
189
190	writer = failingWriter{failAt: 13, written: 0}
191	_, err = toml.WriteTo(&writer)
192	assertErrorString(t, "failingWriter failed after writing 13 bytes", err)
193}
194
195func TestTreeWriteToFailingWriterInArray(t *testing.T) {
196	toml, _ := Load(`
197[[b]]
198a = 2`)
199	writer := failingWriter{failAt: 2, written: 0}
200	_, err := toml.WriteTo(&writer)
201	assertErrorString(t, "failingWriter failed after writing 2 bytes", err)
202
203	writer = failingWriter{failAt: 15, written: 0}
204	_, err = toml.WriteTo(&writer)
205	assertErrorString(t, "failingWriter failed after writing 15 bytes", err)
206}
207
208func TestTreeWriteToMapExampleFile(t *testing.T) {
209	tree, _ := LoadFile("example.toml")
210	expected := map[string]interface{}{
211		"title": "TOML Example",
212		"owner": map[string]interface{}{
213			"name":         "Tom Preston-Werner",
214			"organization": "GitHub",
215			"bio":          "GitHub Cofounder & CEO\nLikes tater tots and beer.",
216			"dob":          time.Date(1979, time.May, 27, 7, 32, 0, 0, time.UTC),
217		},
218		"database": map[string]interface{}{
219			"server":         "192.168.1.1",
220			"ports":          []interface{}{int64(8001), int64(8001), int64(8002)},
221			"connection_max": int64(5000),
222			"enabled":        true,
223		},
224		"servers": map[string]interface{}{
225			"alpha": map[string]interface{}{
226				"ip": "10.0.0.1",
227				"dc": "eqdc10",
228			},
229			"beta": map[string]interface{}{
230				"ip": "10.0.0.2",
231				"dc": "eqdc10",
232			},
233		},
234		"clients": map[string]interface{}{
235			"data": []interface{}{
236				[]interface{}{"gamma", "delta"},
237				[]interface{}{int64(1), int64(2)},
238			},
239		},
240	}
241	testMaps(t, tree.ToMap(), expected)
242}
243
244func TestTreeWriteToMapWithTablesInMultipleChunks(t *testing.T) {
245	tree, _ := Load(`
246	[[menu.main]]
247        a = "menu 1"
248        b = "menu 2"
249        [[menu.main]]
250        c = "menu 3"
251        d = "menu 4"`)
252	expected := map[string]interface{}{
253		"menu": map[string]interface{}{
254			"main": []interface{}{
255				map[string]interface{}{"a": "menu 1", "b": "menu 2"},
256				map[string]interface{}{"c": "menu 3", "d": "menu 4"},
257			},
258		},
259	}
260	treeMap := tree.ToMap()
261
262	testMaps(t, treeMap, expected)
263}
264
265func TestTreeWriteToMapWithArrayOfInlineTables(t *testing.T) {
266	tree, _ := Load(`
267    	[params]
268	language_tabs = [
269    		{ key = "shell", name = "Shell" },
270    		{ key = "ruby", name = "Ruby" },
271    		{ key = "python", name = "Python" }
272	]`)
273
274	expected := map[string]interface{}{
275		"params": map[string]interface{}{
276			"language_tabs": []interface{}{
277				map[string]interface{}{
278					"key":  "shell",
279					"name": "Shell",
280				},
281				map[string]interface{}{
282					"key":  "ruby",
283					"name": "Ruby",
284				},
285				map[string]interface{}{
286					"key":  "python",
287					"name": "Python",
288				},
289			},
290		},
291	}
292
293	treeMap := tree.ToMap()
294	testMaps(t, treeMap, expected)
295}
296
297func TestTreeWriteToFloat(t *testing.T) {
298	tree, err := Load(`a = 3.0`)
299	if err != nil {
300		t.Fatal(err)
301	}
302	str, err := tree.ToTomlString()
303	if err != nil {
304		t.Fatal(err)
305	}
306	expected := `a = 3.0`
307	if strings.TrimSpace(str) != strings.TrimSpace(expected) {
308		t.Fatalf("Expected:\n%s\nGot:\n%s", expected, str)
309	}
310}
311
312func TestTreeWriteToSpecialFloat(t *testing.T) {
313	expected := `a = +inf
314b = -inf
315c = nan`
316
317	tree, err := Load(expected)
318	if err != nil {
319		t.Fatal(err)
320	}
321	str, err := tree.ToTomlString()
322	if err != nil {
323		t.Fatal(err)
324	}
325	if strings.TrimSpace(str) != strings.TrimSpace(expected) {
326		t.Fatalf("Expected:\n%s\nGot:\n%s", expected, str)
327	}
328}
329
330func BenchmarkTreeToTomlString(b *testing.B) {
331	toml, err := Load(sampleHard)
332	if err != nil {
333		b.Fatal("Unexpected error:", err)
334	}
335
336	for i := 0; i < b.N; i++ {
337		_, err := toml.ToTomlString()
338		if err != nil {
339			b.Fatal(err)
340		}
341	}
342}
343
344var sampleHard = `# Test file for TOML
345# Only this one tries to emulate a TOML file written by a user of the kind of parser writers probably hate
346# This part you'll really hate
347
348[the]
349test_string = "You'll hate me after this - #"          # " Annoying, isn't it?
350
351    [the.hard]
352    test_array = [ "] ", " # "]      # ] There you go, parse this!
353    test_array2 = [ "Test #11 ]proved that", "Experiment #9 was a success" ]
354    # You didn't think it'd as easy as chucking out the last #, did you?
355    another_test_string = " Same thing, but with a string #"
356    harder_test_string = " And when \"'s are in the string, along with # \""   # "and comments are there too"
357    # Things will get harder
358
359        [the.hard."bit#"]
360        "what?" = "You don't think some user won't do that?"
361        multi_line_array = [
362            "]",
363            # ] Oh yes I did
364            ]
365
366# Each of the following keygroups/key value pairs should produce an error. Uncomment to them to test
367
368#[error]   if you didn't catch this, your parser is broken
369#string = "Anything other than tabs, spaces and newline after a keygroup or key value pair has ended should produce an error unless it is a comment"   like this
370#array = [
371#         "This might most likely happen in multiline arrays",
372#         Like here,
373#         "or here,
374#         and here"
375#         ]     End of array comment, forgot the #
376#number = 3.14  pi <--again forgot the #         `
377