1// Copyright (c) 2016 Uber Technologies, Inc.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in
11// all copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19// THE SOFTWARE.
20
21package zapcore_test
22
23import (
24	"encoding/json"
25	"strings"
26	"testing"
27	"time"
28
29	"github.com/stretchr/testify/assert"
30	"github.com/stretchr/testify/require"
31	"gopkg.in/yaml.v2"
32
33	. "go.uber.org/zap/zapcore"
34)
35
36var (
37	_epoch     = time.Date(1970, time.January, 1, 0, 0, 0, 0, time.UTC)
38	_testEntry = Entry{
39		LoggerName: "main",
40		Level:      InfoLevel,
41		Message:    `hello`,
42		Time:       _epoch,
43		Stack:      "fake-stack",
44		Caller:     EntryCaller{Defined: true, File: "foo.go", Line: 42, Function: "foo.Foo"},
45	}
46)
47
48func testEncoderConfig() EncoderConfig {
49	return EncoderConfig{
50		MessageKey:     "msg",
51		LevelKey:       "level",
52		NameKey:        "name",
53		TimeKey:        "ts",
54		CallerKey:      "caller",
55		FunctionKey:    "func",
56		StacktraceKey:  "stacktrace",
57		LineEnding:     "\n",
58		EncodeTime:     EpochTimeEncoder,
59		EncodeLevel:    LowercaseLevelEncoder,
60		EncodeDuration: SecondsDurationEncoder,
61		EncodeCaller:   ShortCallerEncoder,
62	}
63}
64
65func humanEncoderConfig() EncoderConfig {
66	cfg := testEncoderConfig()
67	cfg.EncodeTime = ISO8601TimeEncoder
68	cfg.EncodeLevel = CapitalLevelEncoder
69	cfg.EncodeDuration = StringDurationEncoder
70	return cfg
71}
72
73func capitalNameEncoder(loggerName string, enc PrimitiveArrayEncoder) {
74	enc.AppendString(strings.ToUpper(loggerName))
75}
76
77func TestEncoderConfiguration(t *testing.T) {
78	base := testEncoderConfig()
79
80	tests := []struct {
81		desc            string
82		cfg             EncoderConfig
83		amendEntry      func(Entry) Entry
84		extra           func(Encoder)
85		expectedJSON    string
86		expectedConsole string
87	}{
88		{
89			desc: "messages to be escaped",
90			cfg:  base,
91			amendEntry: func(ent Entry) Entry {
92				ent.Message = `hello\`
93				return ent
94			},
95			expectedJSON:    `{"level":"info","ts":0,"name":"main","caller":"foo.go:42","func":"foo.Foo","msg":"hello\\","stacktrace":"fake-stack"}` + "\n",
96			expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\\\nfake-stack\n",
97		},
98		{
99			desc: "use custom entry keys in JSON output and ignore them in console output",
100			cfg: EncoderConfig{
101				LevelKey:       "L",
102				TimeKey:        "T",
103				MessageKey:     "M",
104				NameKey:        "N",
105				CallerKey:      "C",
106				FunctionKey:    "F",
107				StacktraceKey:  "S",
108				LineEnding:     base.LineEnding,
109				EncodeTime:     base.EncodeTime,
110				EncodeDuration: base.EncodeDuration,
111				EncodeLevel:    base.EncodeLevel,
112				EncodeCaller:   base.EncodeCaller,
113			},
114			expectedJSON:    `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n",
115			expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n",
116		},
117		{
118			desc: "skip level if LevelKey is omitted",
119			cfg: EncoderConfig{
120				LevelKey:       OmitKey,
121				TimeKey:        "T",
122				MessageKey:     "M",
123				NameKey:        "N",
124				CallerKey:      "C",
125				FunctionKey:    "F",
126				StacktraceKey:  "S",
127				LineEnding:     base.LineEnding,
128				EncodeTime:     base.EncodeTime,
129				EncodeDuration: base.EncodeDuration,
130				EncodeLevel:    base.EncodeLevel,
131				EncodeCaller:   base.EncodeCaller,
132			},
133			expectedJSON:    `{"T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n",
134			expectedConsole: "0\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n",
135		},
136		{
137			desc: "skip timestamp if TimeKey is omitted",
138			cfg: EncoderConfig{
139				LevelKey:       "L",
140				TimeKey:        OmitKey,
141				MessageKey:     "M",
142				NameKey:        "N",
143				CallerKey:      "C",
144				FunctionKey:    "F",
145				StacktraceKey:  "S",
146				LineEnding:     base.LineEnding,
147				EncodeTime:     base.EncodeTime,
148				EncodeDuration: base.EncodeDuration,
149				EncodeLevel:    base.EncodeLevel,
150				EncodeCaller:   base.EncodeCaller,
151			},
152			expectedJSON:    `{"L":"info","N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n",
153			expectedConsole: "info\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n",
154		},
155		{
156			desc: "skip message if MessageKey is omitted",
157			cfg: EncoderConfig{
158				LevelKey:       "L",
159				TimeKey:        "T",
160				MessageKey:     OmitKey,
161				NameKey:        "N",
162				CallerKey:      "C",
163				FunctionKey:    "F",
164				StacktraceKey:  "S",
165				LineEnding:     base.LineEnding,
166				EncodeTime:     base.EncodeTime,
167				EncodeDuration: base.EncodeDuration,
168				EncodeLevel:    base.EncodeLevel,
169				EncodeCaller:   base.EncodeCaller,
170			},
171			expectedJSON:    `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","S":"fake-stack"}` + "\n",
172			expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\nfake-stack\n",
173		},
174		{
175			desc: "skip name if NameKey is omitted",
176			cfg: EncoderConfig{
177				LevelKey:       "L",
178				TimeKey:        "T",
179				MessageKey:     "M",
180				NameKey:        OmitKey,
181				CallerKey:      "C",
182				FunctionKey:    "F",
183				StacktraceKey:  "S",
184				LineEnding:     base.LineEnding,
185				EncodeTime:     base.EncodeTime,
186				EncodeDuration: base.EncodeDuration,
187				EncodeLevel:    base.EncodeLevel,
188				EncodeCaller:   base.EncodeCaller,
189			},
190			expectedJSON:    `{"L":"info","T":0,"C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n",
191			expectedConsole: "0\tinfo\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n",
192		},
193		{
194			desc: "skip caller if CallerKey is omitted",
195			cfg: EncoderConfig{
196				LevelKey:       "L",
197				TimeKey:        "T",
198				MessageKey:     "M",
199				NameKey:        "N",
200				CallerKey:      OmitKey,
201				FunctionKey:    "F",
202				StacktraceKey:  "S",
203				LineEnding:     base.LineEnding,
204				EncodeTime:     base.EncodeTime,
205				EncodeDuration: base.EncodeDuration,
206				EncodeLevel:    base.EncodeLevel,
207				EncodeCaller:   base.EncodeCaller,
208			},
209			expectedJSON:    `{"L":"info","T":0,"N":"main","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n",
210			expectedConsole: "0\tinfo\tmain\tfoo.Foo\thello\nfake-stack\n",
211		},
212		{
213			desc: "skip function if FunctionKey is omitted",
214			cfg: EncoderConfig{
215				LevelKey:       "L",
216				TimeKey:        "T",
217				MessageKey:     "M",
218				NameKey:        "N",
219				CallerKey:      "C",
220				FunctionKey:    OmitKey,
221				StacktraceKey:  "S",
222				LineEnding:     base.LineEnding,
223				EncodeTime:     base.EncodeTime,
224				EncodeDuration: base.EncodeDuration,
225				EncodeLevel:    base.EncodeLevel,
226				EncodeCaller:   base.EncodeCaller,
227			},
228			expectedJSON:    `{"L":"info","T":0,"N":"main","C":"foo.go:42","M":"hello","S":"fake-stack"}` + "\n",
229			expectedConsole: "0\tinfo\tmain\tfoo.go:42\thello\nfake-stack\n",
230		},
231		{
232			desc: "skip stacktrace if StacktraceKey is omitted",
233			cfg: EncoderConfig{
234				LevelKey:       "L",
235				TimeKey:        "T",
236				MessageKey:     "M",
237				NameKey:        "N",
238				CallerKey:      "C",
239				FunctionKey:    "F",
240				StacktraceKey:  OmitKey,
241				LineEnding:     base.LineEnding,
242				EncodeTime:     base.EncodeTime,
243				EncodeDuration: base.EncodeDuration,
244				EncodeLevel:    base.EncodeLevel,
245				EncodeCaller:   base.EncodeCaller,
246			},
247			expectedJSON:    `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello"}` + "\n",
248			expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\n",
249		},
250		{
251			desc: "use the supplied EncodeTime, for both the entry and any times added",
252			cfg: EncoderConfig{
253				LevelKey:       "L",
254				TimeKey:        "T",
255				MessageKey:     "M",
256				NameKey:        "N",
257				CallerKey:      "C",
258				FunctionKey:    "F",
259				StacktraceKey:  "S",
260				LineEnding:     base.LineEnding,
261				EncodeTime:     func(t time.Time, enc PrimitiveArrayEncoder) { enc.AppendString(t.String()) },
262				EncodeDuration: base.EncodeDuration,
263				EncodeLevel:    base.EncodeLevel,
264				EncodeCaller:   base.EncodeCaller,
265			},
266			extra: func(enc Encoder) {
267				enc.AddTime("extra", _epoch)
268				enc.AddArray("extras", ArrayMarshalerFunc(func(enc ArrayEncoder) error {
269					enc.AppendTime(_epoch)
270					return nil
271				}))
272			},
273			expectedJSON: `{"L":"info","T":"1970-01-01 00:00:00 +0000 UTC","N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","extra":"1970-01-01 00:00:00 +0000 UTC","extras":["1970-01-01 00:00:00 +0000 UTC"],"S":"fake-stack"}` + "\n",
274			expectedConsole: "1970-01-01 00:00:00 +0000 UTC\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\t" + // plain-text preamble
275				`{"extra": "1970-01-01 00:00:00 +0000 UTC", "extras": ["1970-01-01 00:00:00 +0000 UTC"]}` + // JSON context
276				"\nfake-stack\n", // stacktrace after newline
277		},
278		{
279			desc: "use the supplied EncodeDuration for any durations added",
280			cfg: EncoderConfig{
281				LevelKey:       "L",
282				TimeKey:        "T",
283				MessageKey:     "M",
284				NameKey:        "N",
285				CallerKey:      "C",
286				FunctionKey:    "F",
287				StacktraceKey:  "S",
288				LineEnding:     base.LineEnding,
289				EncodeTime:     base.EncodeTime,
290				EncodeDuration: StringDurationEncoder,
291				EncodeLevel:    base.EncodeLevel,
292				EncodeCaller:   base.EncodeCaller,
293			},
294			extra: func(enc Encoder) {
295				enc.AddDuration("extra", time.Second)
296				enc.AddArray("extras", ArrayMarshalerFunc(func(enc ArrayEncoder) error {
297					enc.AppendDuration(time.Minute)
298					return nil
299				}))
300			},
301			expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","extra":"1s","extras":["1m0s"],"S":"fake-stack"}` + "\n",
302			expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\t" + // preamble
303				`{"extra": "1s", "extras": ["1m0s"]}` + // context
304				"\nfake-stack\n", // stacktrace
305		},
306		{
307			desc: "use the supplied EncodeLevel",
308			cfg: EncoderConfig{
309				LevelKey:       "L",
310				TimeKey:        "T",
311				MessageKey:     "M",
312				NameKey:        "N",
313				CallerKey:      "C",
314				FunctionKey:    "F",
315				StacktraceKey:  "S",
316				LineEnding:     base.LineEnding,
317				EncodeTime:     base.EncodeTime,
318				EncodeDuration: base.EncodeDuration,
319				EncodeLevel:    CapitalLevelEncoder,
320				EncodeCaller:   base.EncodeCaller,
321			},
322			expectedJSON:    `{"L":"INFO","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n",
323			expectedConsole: "0\tINFO\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n",
324		},
325		{
326			desc: "use the supplied EncodeName",
327			cfg: EncoderConfig{
328				LevelKey:       "L",
329				TimeKey:        "T",
330				MessageKey:     "M",
331				NameKey:        "N",
332				CallerKey:      "C",
333				FunctionKey:    "F",
334				StacktraceKey:  "S",
335				LineEnding:     base.LineEnding,
336				EncodeTime:     base.EncodeTime,
337				EncodeDuration: base.EncodeDuration,
338				EncodeLevel:    base.EncodeLevel,
339				EncodeCaller:   base.EncodeCaller,
340				EncodeName:     capitalNameEncoder,
341			},
342			expectedJSON:    `{"L":"info","T":0,"N":"MAIN","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n",
343			expectedConsole: "0\tinfo\tMAIN\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n",
344		},
345		{
346			desc: "close all open namespaces",
347			cfg: EncoderConfig{
348				LevelKey:       "L",
349				TimeKey:        "T",
350				MessageKey:     "M",
351				NameKey:        "N",
352				CallerKey:      "C",
353				FunctionKey:    "F",
354				StacktraceKey:  "S",
355				LineEnding:     base.LineEnding,
356				EncodeTime:     base.EncodeTime,
357				EncodeDuration: base.EncodeDuration,
358				EncodeLevel:    base.EncodeLevel,
359				EncodeCaller:   base.EncodeCaller,
360			},
361			extra: func(enc Encoder) {
362				enc.OpenNamespace("outer")
363				enc.OpenNamespace("inner")
364				enc.AddString("foo", "bar")
365				enc.OpenNamespace("innermost")
366			},
367			expectedJSON: `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","outer":{"inner":{"foo":"bar","innermost":{}}},"S":"fake-stack"}` + "\n",
368			expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\t" +
369				`{"outer": {"inner": {"foo": "bar", "innermost": {}}}}` +
370				"\nfake-stack\n",
371		},
372		{
373			desc: "handle no-op EncodeTime",
374			cfg: EncoderConfig{
375				LevelKey:       "L",
376				TimeKey:        "T",
377				MessageKey:     "M",
378				NameKey:        "N",
379				CallerKey:      "C",
380				FunctionKey:    "F",
381				StacktraceKey:  "S",
382				LineEnding:     base.LineEnding,
383				EncodeTime:     func(time.Time, PrimitiveArrayEncoder) {},
384				EncodeDuration: base.EncodeDuration,
385				EncodeLevel:    base.EncodeLevel,
386				EncodeCaller:   base.EncodeCaller,
387			},
388			extra:           func(enc Encoder) { enc.AddTime("sometime", time.Unix(0, 100)) },
389			expectedJSON:    `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","sometime":100,"S":"fake-stack"}` + "\n",
390			expectedConsole: "info\tmain\tfoo.go:42\tfoo.Foo\thello\t" + `{"sometime": 100}` + "\nfake-stack\n",
391		},
392		{
393			desc: "handle no-op EncodeDuration",
394			cfg: EncoderConfig{
395				LevelKey:       "L",
396				TimeKey:        "T",
397				MessageKey:     "M",
398				NameKey:        "N",
399				CallerKey:      "C",
400				FunctionKey:    "F",
401				StacktraceKey:  "S",
402				LineEnding:     base.LineEnding,
403				EncodeTime:     base.EncodeTime,
404				EncodeDuration: func(time.Duration, PrimitiveArrayEncoder) {},
405				EncodeLevel:    base.EncodeLevel,
406				EncodeCaller:   base.EncodeCaller,
407			},
408			extra:           func(enc Encoder) { enc.AddDuration("someduration", time.Microsecond) },
409			expectedJSON:    `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","someduration":1000,"S":"fake-stack"}` + "\n",
410			expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\t" + `{"someduration": 1000}` + "\nfake-stack\n",
411		},
412		{
413			desc: "handle no-op EncodeLevel",
414			cfg: EncoderConfig{
415				LevelKey:       "L",
416				TimeKey:        "T",
417				MessageKey:     "M",
418				NameKey:        "N",
419				CallerKey:      "C",
420				FunctionKey:    "F",
421				StacktraceKey:  "S",
422				LineEnding:     base.LineEnding,
423				EncodeTime:     base.EncodeTime,
424				EncodeDuration: base.EncodeDuration,
425				EncodeLevel:    func(Level, PrimitiveArrayEncoder) {},
426				EncodeCaller:   base.EncodeCaller,
427			},
428			expectedJSON:    `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n",
429			expectedConsole: "0\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n",
430		},
431		{
432			desc: "handle no-op EncodeCaller",
433			cfg: EncoderConfig{
434				LevelKey:       "L",
435				TimeKey:        "T",
436				MessageKey:     "M",
437				NameKey:        "N",
438				CallerKey:      "C",
439				FunctionKey:    "F",
440				StacktraceKey:  "S",
441				LineEnding:     base.LineEnding,
442				EncodeTime:     base.EncodeTime,
443				EncodeDuration: base.EncodeDuration,
444				EncodeLevel:    base.EncodeLevel,
445				EncodeCaller:   func(EntryCaller, PrimitiveArrayEncoder) {},
446			},
447			expectedJSON:    `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n",
448			expectedConsole: "0\tinfo\tmain\tfoo.Foo\thello\nfake-stack\n",
449		},
450		{
451			desc: "handle no-op EncodeName",
452			cfg: EncoderConfig{
453				LevelKey:       "L",
454				TimeKey:        "T",
455				MessageKey:     "M",
456				NameKey:        "N",
457				CallerKey:      "C",
458				FunctionKey:    "F",
459				StacktraceKey:  "S",
460				LineEnding:     base.LineEnding,
461				EncodeTime:     base.EncodeTime,
462				EncodeDuration: base.EncodeDuration,
463				EncodeLevel:    base.EncodeLevel,
464				EncodeCaller:   base.EncodeCaller,
465				EncodeName:     func(string, PrimitiveArrayEncoder) {},
466			},
467			expectedJSON:    `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\n",
468			expectedConsole: "0\tinfo\tfoo.go:42\tfoo.Foo\thello\nfake-stack\n",
469		},
470		{
471			desc: "use custom line separator",
472			cfg: EncoderConfig{
473				LevelKey:       "L",
474				TimeKey:        "T",
475				MessageKey:     "M",
476				NameKey:        "N",
477				CallerKey:      "C",
478				FunctionKey:    "F",
479				StacktraceKey:  "S",
480				LineEnding:     "\r\n",
481				EncodeTime:     base.EncodeTime,
482				EncodeDuration: base.EncodeDuration,
483				EncodeLevel:    base.EncodeLevel,
484				EncodeCaller:   base.EncodeCaller,
485			},
486			expectedJSON:    `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + "\r\n",
487			expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack\r\n",
488		},
489		{
490			desc: "omit line separator definition - fall back to default",
491			cfg: EncoderConfig{
492				LevelKey:       "L",
493				TimeKey:        "T",
494				MessageKey:     "M",
495				NameKey:        "N",
496				CallerKey:      "C",
497				FunctionKey:    "F",
498				StacktraceKey:  "S",
499				EncodeTime:     base.EncodeTime,
500				EncodeDuration: base.EncodeDuration,
501				EncodeLevel:    base.EncodeLevel,
502				EncodeCaller:   base.EncodeCaller,
503			},
504			expectedJSON:    `{"L":"info","T":0,"N":"main","C":"foo.go:42","F":"foo.Foo","M":"hello","S":"fake-stack"}` + DefaultLineEnding,
505			expectedConsole: "0\tinfo\tmain\tfoo.go:42\tfoo.Foo\thello\nfake-stack" + DefaultLineEnding,
506		},
507	}
508
509	for i, tt := range tests {
510		json := NewJSONEncoder(tt.cfg)
511		console := NewConsoleEncoder(tt.cfg)
512		if tt.extra != nil {
513			tt.extra(json)
514			tt.extra(console)
515		}
516		entry := _testEntry
517		if tt.amendEntry != nil {
518			entry = tt.amendEntry(_testEntry)
519		}
520		jsonOut, jsonErr := json.EncodeEntry(entry, nil)
521		if assert.NoError(t, jsonErr, "Unexpected error JSON-encoding entry in case #%d.", i) {
522			assert.Equal(
523				t,
524				tt.expectedJSON,
525				jsonOut.String(),
526				"Unexpected JSON output: expected to %v.", tt.desc,
527			)
528		}
529		consoleOut, consoleErr := console.EncodeEntry(entry, nil)
530		if assert.NoError(t, consoleErr, "Unexpected error console-encoding entry in case #%d.", i) {
531			assert.Equal(
532				t,
533				tt.expectedConsole,
534				consoleOut.String(),
535				"Unexpected console output: expected to %v.", tt.desc,
536			)
537		}
538	}
539}
540
541func TestLevelEncoders(t *testing.T) {
542	tests := []struct {
543		name     string
544		expected interface{} // output of encoding InfoLevel
545	}{
546		{"capital", "INFO"},
547		{"lower", "info"},
548		{"", "info"},
549		{"something-random", "info"},
550	}
551
552	for _, tt := range tests {
553		var le LevelEncoder
554		require.NoError(t, le.UnmarshalText([]byte(tt.name)), "Unexpected error unmarshaling %q.", tt.name)
555		assertAppended(
556			t,
557			tt.expected,
558			func(arr ArrayEncoder) { le(InfoLevel, arr) },
559			"Unexpected output serializing InfoLevel with %q.", tt.name,
560		)
561	}
562}
563
564func TestTimeEncoders(t *testing.T) {
565	moment := time.Unix(100, 50005000).UTC()
566	tests := []struct {
567		yamlDoc  string
568		expected interface{} // output of serializing moment
569	}{
570		{"timeEncoder: iso8601", "1970-01-01T00:01:40.050Z"},
571		{"timeEncoder: ISO8601", "1970-01-01T00:01:40.050Z"},
572		{"timeEncoder: millis", 100050.005},
573		{"timeEncoder: nanos", int64(100050005000)},
574		{"timeEncoder: {layout: 06/01/02 03:04pm}", "70/01/01 12:01am"},
575		{"timeEncoder: ''", 100.050005},
576		{"timeEncoder: something-random", 100.050005},
577		{"timeEncoder: rfc3339", "1970-01-01T00:01:40Z"},
578		{"timeEncoder: RFC3339", "1970-01-01T00:01:40Z"},
579		{"timeEncoder: rfc3339nano", "1970-01-01T00:01:40.050005Z"},
580		{"timeEncoder: RFC3339Nano", "1970-01-01T00:01:40.050005Z"},
581	}
582
583	for _, tt := range tests {
584		cfg := EncoderConfig{}
585		require.NoError(t, yaml.Unmarshal([]byte(tt.yamlDoc), &cfg), "Unexpected error unmarshaling %q.", tt.yamlDoc)
586		require.NotNil(t, cfg.EncodeTime, "Unmashalled timeEncoder is nil for %q.", tt.yamlDoc)
587		assertAppended(
588			t,
589			tt.expected,
590			func(arr ArrayEncoder) { cfg.EncodeTime(moment, arr) },
591			"Unexpected output serializing %v with %q.", moment, tt.yamlDoc,
592		)
593	}
594}
595
596func TestTimeEncodersWrongYAML(t *testing.T) {
597	tests := []string{
598		"timeEncoder: [1, 2, 3]", // wrong type
599		"timeEncoder: {foo:bar",  // broken yaml
600	}
601	for _, tt := range tests {
602		cfg := EncoderConfig{}
603		assert.Error(t, yaml.Unmarshal([]byte(tt), &cfg), "Expected unmarshaling %q to become error, but not.", tt)
604	}
605}
606
607func TestTimeEncodersParseFromJSON(t *testing.T) {
608	moment := time.Unix(100, 50005000).UTC()
609	tests := []struct {
610		jsonDoc  string
611		expected interface{} // output of serializing moment
612	}{
613		{`{"timeEncoder": "iso8601"}`, "1970-01-01T00:01:40.050Z"},
614		{`{"timeEncoder": {"layout": "06/01/02 03:04pm"}}`, "70/01/01 12:01am"},
615	}
616
617	for _, tt := range tests {
618		cfg := EncoderConfig{}
619		require.NoError(t, json.Unmarshal([]byte(tt.jsonDoc), &cfg), "Unexpected error unmarshaling %q.", tt.jsonDoc)
620		require.NotNil(t, cfg.EncodeTime, "Unmashalled timeEncoder is nil for %q.", tt.jsonDoc)
621		assertAppended(
622			t,
623			tt.expected,
624			func(arr ArrayEncoder) { cfg.EncodeTime(moment, arr) },
625			"Unexpected output serializing %v with %q.", moment, tt.jsonDoc,
626		)
627	}
628}
629
630func TestDurationEncoders(t *testing.T) {
631	elapsed := time.Second + 500*time.Nanosecond
632	tests := []struct {
633		name     string
634		expected interface{} // output of serializing elapsed
635	}{
636		{"string", "1.0000005s"},
637		{"nanos", int64(1000000500)},
638		{"ms", int64(1000)},
639		{"", 1.0000005},
640		{"something-random", 1.0000005},
641	}
642
643	for _, tt := range tests {
644		var de DurationEncoder
645		require.NoError(t, de.UnmarshalText([]byte(tt.name)), "Unexpected error unmarshaling %q.", tt.name)
646		assertAppended(
647			t,
648			tt.expected,
649			func(arr ArrayEncoder) { de(elapsed, arr) },
650			"Unexpected output serializing %v with %q.", elapsed, tt.name,
651		)
652	}
653}
654
655func TestCallerEncoders(t *testing.T) {
656	caller := EntryCaller{Defined: true, File: "/home/jack/src/github.com/foo/foo.go", Line: 42}
657	tests := []struct {
658		name     string
659		expected interface{} // output of serializing caller
660	}{
661		{"", "foo/foo.go:42"},
662		{"something-random", "foo/foo.go:42"},
663		{"short", "foo/foo.go:42"},
664		{"full", "/home/jack/src/github.com/foo/foo.go:42"},
665	}
666
667	for _, tt := range tests {
668		var ce CallerEncoder
669		require.NoError(t, ce.UnmarshalText([]byte(tt.name)), "Unexpected error unmarshaling %q.", tt.name)
670		assertAppended(
671			t,
672			tt.expected,
673			func(arr ArrayEncoder) { ce(caller, arr) },
674			"Unexpected output serializing file name as %v with %q.", tt.expected, tt.name,
675		)
676	}
677}
678
679func TestNameEncoders(t *testing.T) {
680	tests := []struct {
681		name     string
682		expected interface{} // output of encoding InfoLevel
683	}{
684		{"", "main"},
685		{"full", "main"},
686		{"something-random", "main"},
687	}
688
689	for _, tt := range tests {
690		var ne NameEncoder
691		require.NoError(t, ne.UnmarshalText([]byte(tt.name)), "Unexpected error unmarshaling %q.", tt.name)
692		assertAppended(
693			t,
694			tt.expected,
695			func(arr ArrayEncoder) { ne("main", arr) },
696			"Unexpected output serializing logger name with %q.", tt.name,
697		)
698	}
699}
700
701func assertAppended(t testing.TB, expected interface{}, f func(ArrayEncoder), msgAndArgs ...interface{}) {
702	mem := NewMapObjectEncoder()
703	mem.AddArray("k", ArrayMarshalerFunc(func(arr ArrayEncoder) error {
704		f(arr)
705		return nil
706	}))
707	arr := mem.Fields["k"].([]interface{})
708	require.Equal(t, 1, len(arr), "Expected to append exactly one element to array.")
709	assert.Equal(t, expected, arr[0], msgAndArgs...)
710}
711