package nlog import ( "encoding/json" "errors" "fmt" "io" "log" "os" "reflect" "runtime" "strings" "sync" "sync/atomic" "testing" "time" "git.noahlan.cn/noahlan/ntool/ntest/assert" ) var ( s = []byte("Sending #11 notification (id: 1451875113812010473) in #1 connection") pool = make(chan []byte, 1) _ Writer = (*mockWriter)(nil) ) type mockWriter struct { lock sync.Mutex builder strings.Builder } func (mw *mockWriter) Alert(v any) { mw.lock.Lock() defer mw.lock.Unlock() output(&mw.builder, levelAlert, v) } func (mw *mockWriter) Debug(v any, fields ...LogField) { mw.lock.Lock() defer mw.lock.Unlock() output(&mw.builder, levelDebug, v, fields...) } func (mw *mockWriter) Error(v any, fields ...LogField) { mw.lock.Lock() defer mw.lock.Unlock() output(&mw.builder, levelError, v, fields...) } func (mw *mockWriter) Info(v any, fields ...LogField) { mw.lock.Lock() defer mw.lock.Unlock() output(&mw.builder, levelInfo, v, fields...) } func (mw *mockWriter) Severe(v any) { mw.lock.Lock() defer mw.lock.Unlock() output(&mw.builder, levelSevere, v) } func (mw *mockWriter) Slow(v any, fields ...LogField) { mw.lock.Lock() defer mw.lock.Unlock() output(&mw.builder, levelSlow, v, fields...) } func (mw *mockWriter) Stack(v any) { mw.lock.Lock() defer mw.lock.Unlock() output(&mw.builder, levelError, v) } func (mw *mockWriter) Stat(v any, fields ...LogField) { mw.lock.Lock() defer mw.lock.Unlock() output(&mw.builder, levelStat, v, fields...) } func (mw *mockWriter) Close() error { return nil } func (mw *mockWriter) Contains(text string) bool { mw.lock.Lock() defer mw.lock.Unlock() return strings.Contains(mw.builder.String(), text) } func (mw *mockWriter) Reset() { mw.lock.Lock() defer mw.lock.Unlock() mw.builder.Reset() } func (mw *mockWriter) String() string { mw.lock.Lock() defer mw.lock.Unlock() return mw.builder.String() } func TestField(t *testing.T) { tests := []struct { name string f LogField want map[string]any }{ { name: "error", f: Field("foo", errors.New("bar")), want: map[string]any{ "foo": "bar", }, }, { name: "errors", f: Field("foo", []error{errors.New("bar"), errors.New("baz")}), want: map[string]any{ "foo": []any{"bar", "baz"}, }, }, { name: "strings", f: Field("foo", []string{"bar", "baz"}), want: map[string]any{ "foo": []any{"bar", "baz"}, }, }, { name: "duration", f: Field("foo", time.Second), want: map[string]any{ "foo": "1s", }, }, { name: "durations", f: Field("foo", []time.Duration{time.Second, 2 * time.Second}), want: map[string]any{ "foo": []any{"1s", "2s"}, }, }, { name: "times", f: Field("foo", []time.Time{ time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC), time.Date(2020, time.January, 2, 0, 0, 0, 0, time.UTC), }), want: map[string]any{ "foo": []any{"2020-01-01 00:00:00 +0000 UTC", "2020-01-02 00:00:00 +0000 UTC"}, }, }, { name: "stringer", f: Field("foo", ValStringer{val: "bar"}), want: map[string]any{ "foo": "bar", }, }, { name: "stringers", f: Field("foo", []fmt.Stringer{ValStringer{val: "bar"}, ValStringer{val: "baz"}}), want: map[string]any{ "foo": []any{"bar", "baz"}, }, }, } for _, test := range tests { test := test t.Run(test.name, func(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) Infow("foo", test.f) validateFields(t, w.String(), test.want) }) } } func TestFileLineFileMode(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) file, line := getFileLine() Error("anything") assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1))) file, line = getFileLine() Errorf("anything %s", "format") assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1))) } func TestFileLineConsoleMode(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) file, line := getFileLine() Error("anything") assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1))) w.Reset() file, line = getFileLine() Errorf("anything %s", "format") assert.True(t, w.Contains(fmt.Sprintf("%s:%d", file, line+1))) } func TestStructedLogAlert(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelAlert, w, func(v ...any) { Alert(fmt.Sprint(v...)) }) } func TestStructedLogDebug(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelDebug, w, func(v ...any) { Debug(v...) }) } func TestStructedLogDebugf(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelDebug, w, func(v ...any) { Debugf(fmt.Sprint(v...)) }) } func TestStructedLogDebugv(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelDebug, w, func(v ...any) { Debugv(fmt.Sprint(v...)) }) } func TestStructedLogDebugw(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelDebug, w, func(v ...any) { Debugw(fmt.Sprint(v...), Field("foo", time.Second)) }) } func TestStructedLogError(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelError, w, func(v ...any) { Error(v...) }) } func TestStructedLogErrorf(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelError, w, func(v ...any) { Errorf("%s", fmt.Sprint(v...)) }) } func TestStructedLogErrorv(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelError, w, func(v ...any) { Errorv(fmt.Sprint(v...)) }) } func TestStructedLogErrorw(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelError, w, func(v ...any) { Errorw(fmt.Sprint(v...), Field("foo", "bar")) }) } func TestStructedLogInfo(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelInfo, w, func(v ...any) { Info(v...) }) } func TestStructedLogInfof(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelInfo, w, func(v ...any) { Infof("%s", fmt.Sprint(v...)) }) } func TestStructedLogInfov(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelInfo, w, func(v ...any) { Infov(fmt.Sprint(v...)) }) } func TestStructedLogInfow(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelInfo, w, func(v ...any) { Infow(fmt.Sprint(v...), Field("foo", "bar")) }) } func TestStructedLogInfoConsoleAny(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLogConsole(t, w, func(v ...any) { old := atomic.LoadUint32(&encoding) atomic.StoreUint32(&encoding, plainEncodingType) defer func() { atomic.StoreUint32(&encoding, old) }() Infov(v) }) } func TestStructedLogInfoConsoleAnyString(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLogConsole(t, w, func(v ...any) { old := atomic.LoadUint32(&encoding) atomic.StoreUint32(&encoding, plainEncodingType) defer func() { atomic.StoreUint32(&encoding, old) }() Infov(fmt.Sprint(v...)) }) } func TestStructedLogInfoConsoleAnyError(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLogConsole(t, w, func(v ...any) { old := atomic.LoadUint32(&encoding) atomic.StoreUint32(&encoding, plainEncodingType) defer func() { atomic.StoreUint32(&encoding, old) }() Infov(errors.New(fmt.Sprint(v...))) }) } func TestStructedLogInfoConsoleAnyStringer(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLogConsole(t, w, func(v ...any) { old := atomic.LoadUint32(&encoding) atomic.StoreUint32(&encoding, plainEncodingType) defer func() { atomic.StoreUint32(&encoding, old) }() Infov(ValStringer{ val: fmt.Sprint(v...), }) }) } func TestStructedLogInfoConsoleText(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLogConsole(t, w, func(v ...any) { old := atomic.LoadUint32(&encoding) atomic.StoreUint32(&encoding, plainEncodingType) defer func() { atomic.StoreUint32(&encoding, old) }() Info(fmt.Sprint(v...)) }) } func TestStructedLogSlow(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelSlow, w, func(v ...any) { Slow(v...) }) } func TestStructedLogSlowf(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelSlow, w, func(v ...any) { Slowf(fmt.Sprint(v...)) }) } func TestStructedLogSlowv(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelSlow, w, func(v ...any) { Slowv(fmt.Sprint(v...)) }) } func TestStructedLogSloww(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelSlow, w, func(v ...any) { Sloww(fmt.Sprint(v...), Field("foo", time.Second)) }) } func TestStructedLogStat(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelStat, w, func(v ...any) { Stat(v...) }) } func TestStructedLogStatf(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelStat, w, func(v ...any) { Statf(fmt.Sprint(v...)) }) } func TestStructedLogSevere(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelSevere, w, func(v ...any) { Severe(v...) }) } func TestStructedLogSeveref(t *testing.T) { w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) doTestStructedLog(t, levelSevere, w, func(v ...any) { Severef(fmt.Sprint(v...)) }) } func TestStructedLogWithDuration(t *testing.T) { const message = "hello there" w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) WithDuration(time.Second).Info(message) var entry map[string]any if err := json.Unmarshal([]byte(w.String()), &entry); err != nil { t.Error(err) } assert.Equal(t, levelInfo, entry[levelKey]) assert.Equal(t, message, entry[contentKey]) assert.Equal(t, "1000.0ms", entry[durationKey]) } func TestSetLevel(t *testing.T) { SetLevel(ErrorLevel) const message = "hello there" w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) Info(message) assert.Equal(t, 0, w.builder.Len()) } func TestSetLevelTwiceWithMode(t *testing.T) { testModes := []string{ "console", "volumn", "mode", } w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) for _, mode := range testModes { testSetLevelTwiceWithMode(t, mode, w) } } func TestSetLevelWithDuration(t *testing.T) { SetLevel(ErrorLevel) const message = "hello there" w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) WithDuration(time.Second).Info(message) assert.Equal(t, 0, w.builder.Len()) } func TestErrorfWithWrappedError(t *testing.T) { SetLevel(ErrorLevel) const message = "there" w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) Errorf("hello %w", errors.New(message)) assert.True(t, strings.Contains(w.String(), "hello there")) } func TestMustNil(t *testing.T) { Must(nil) } func TestSetup(t *testing.T) { defer func() { SetLevel(InfoLevel) atomic.StoreUint32(&encoding, jsonEncodingType) }() MustSetup(LogConf{ ServiceName: "any", Mode: "console", TimeFormat: timeFormat, }) MustSetup(LogConf{ ServiceName: "any", Mode: "file", Path: os.TempDir(), }) MustSetup(LogConf{ ServiceName: "any", Mode: "volume", Path: os.TempDir(), }) MustSetup(LogConf{ ServiceName: "any", Mode: "console", TimeFormat: timeFormat, }) MustSetup(LogConf{ ServiceName: "any", Mode: "console", Encoding: plainEncoding, }) defer os.RemoveAll("CD01CB7D-2705-4F3F-889E-86219BF56F10") assert.NotNil(t, setupWithVolume(LogConf{})) assert.Nil(t, setupWithVolume(LogConf{ ServiceName: "CD01CB7D-2705-4F3F-889E-86219BF56F10", })) assert.Nil(t, setupWithVolume(LogConf{ ServiceName: "CD01CB7D-2705-4F3F-889E-86219BF56F10", Rotation: sizeRotationRule, })) assert.NotNil(t, setupWithFiles(LogConf{})) assert.Nil(t, setupWithFiles(LogConf{ ServiceName: "any", Path: os.TempDir(), Compress: true, KeepDays: 1, MaxBackups: 3, MaxSize: 1024 * 1024, })) setupLogLevel(LogConf{ Level: levelInfo, }) setupLogLevel(LogConf{ Level: levelError, }) setupLogLevel(LogConf{ Level: levelSevere, }) _, err := createOutput("") assert.NotNil(t, err) Disable() SetLevel(InfoLevel) atomic.StoreUint32(&encoding, jsonEncodingType) } func TestDisable(t *testing.T) { Disable() var opt logOptions WithKeepDays(1)(&opt) WithGzip()(&opt) WithMaxBackups(1)(&opt) WithMaxSize(1024)(&opt) assert.Nil(t, Close()) assert.Nil(t, Close()) } func TestDisableStat(t *testing.T) { DisableStat() const message = "hello there" w := new(mockWriter) old := writer.Swap(w) defer writer.Store(old) Stat(message) assert.Equal(t, 0, w.builder.Len()) } func TestSetWriter(t *testing.T) { atomic.StoreUint32(&disableLog, 0) Reset() SetWriter(nopWriter{}) assert.NotNil(t, writer.Load()) assert.True(t, writer.Load() == nopWriter{}) mocked := new(mockWriter) SetWriter(mocked) assert.Equal(t, mocked, writer.Load()) } func TestWithGzip(t *testing.T) { fn := WithGzip() var opt logOptions fn(&opt) assert.True(t, opt.gzipEnabled) } func TestWithKeepDays(t *testing.T) { fn := WithKeepDays(1) var opt logOptions fn(&opt) assert.Equal(t, 1, opt.keepDays) } func BenchmarkCopyByteSliceAppend(b *testing.B) { for i := 0; i < b.N; i++ { var buf []byte buf = append(buf, getTimestamp()...) buf = append(buf, ' ') buf = append(buf, s...) _ = buf } } func BenchmarkCopyByteSliceAllocExactly(b *testing.B) { for i := 0; i < b.N; i++ { now := []byte(getTimestamp()) buf := make([]byte, len(now)+1+len(s)) n := copy(buf, now) buf[n] = ' ' copy(buf[n+1:], s) } } func BenchmarkCopyByteSlice(b *testing.B) { var buf []byte for i := 0; i < b.N; i++ { buf = make([]byte, len(s)) copy(buf, s) } fmt.Fprint(io.Discard, buf) } func BenchmarkCopyOnWriteByteSlice(b *testing.B) { var buf []byte for i := 0; i < b.N; i++ { size := len(s) buf = s[:size:size] } fmt.Fprint(io.Discard, buf) } func BenchmarkCacheByteSlice(b *testing.B) { for i := 0; i < b.N; i++ { dup := fetch() copy(dup, s) put(dup) } } func BenchmarkLogs(b *testing.B) { b.ReportAllocs() log.SetOutput(io.Discard) for i := 0; i < b.N; i++ { Info(i) } } func fetch() []byte { select { case b := <-pool: return b default: } return make([]byte, 4096) } func getFileLine() (string, int) { _, file, line, _ := runtime.Caller(1) short := file for i := len(file) - 1; i > 0; i-- { if file[i] == '/' { short = file[i+1:] break } } return short, line } func put(b []byte) { select { case pool <- b: default: } } func doTestStructedLog(t *testing.T, level string, w *mockWriter, write func(...any)) { const message = "hello there" write(message) var entry map[string]any if err := json.Unmarshal([]byte(w.String()), &entry); err != nil { t.Error(err) } assert.Equal(t, level, entry[levelKey]) val, ok := entry[contentKey] assert.True(t, ok) assert.True(t, strings.Contains(val.(string), message)) } func doTestStructedLogConsole(t *testing.T, w *mockWriter, write func(...any)) { const message = "hello there" write(message) assert.True(t, strings.Contains(w.String(), message)) } func testSetLevelTwiceWithMode(t *testing.T, mode string, w *mockWriter) { writer.Store(nil) SetUp(LogConf{ Mode: mode, Level: "debug", Path: "/dev/null", Encoding: plainEncoding, Stat: false, TimeFormat: time.RFC3339, }) SetUp(LogConf{ Mode: mode, Level: "info", Path: "/dev/null", }) const message = "hello there" Info(message) assert.Equal(t, 0, w.builder.Len()) Infof(message) assert.Equal(t, 0, w.builder.Len()) ErrorStack(message) assert.Equal(t, 0, w.builder.Len()) ErrorStackf(message) assert.Equal(t, 0, w.builder.Len()) } type ValStringer struct { val string } func (v ValStringer) String() string { return v.val } func validateFields(t *testing.T, content string, fields map[string]any) { var m map[string]any if err := json.Unmarshal([]byte(content), &m); err != nil { t.Error(err) } for k, v := range fields { if reflect.TypeOf(v).Kind() == reflect.Slice { assert.EqualValues(t, v, m[k]) } else { assert.Equal(t, v, m[k], content) } } }