diff --git a/chk/chk.go b/chk/chk.go new file mode 100644 index 0000000..7d5e67b --- /dev/null +++ b/chk/chk.go @@ -0,0 +1,13 @@ +// Package chk is a convenience shortcut to use shorter names to access the lol.Logger. +package chk + +import ( + "github.com/mleku/lol" +) + +var F, E, W, I, D, T lol.Chk + +func init() { + F, E, W, I, D, T = lol.Main.Check.F, lol.Main.Check.E, lol.Main.Check.W, lol.Main.Check.I, + lol.Main.Check.D, lol.Main.Check.T +} diff --git a/errorf/errorf.go b/errorf/errorf.go new file mode 100644 index 0000000..cf7274e --- /dev/null +++ b/errorf/errorf.go @@ -0,0 +1,12 @@ +// Package errorf is a convenience shortcut to use shorter names to access the lol.Logger. +package errorf + +import ( + "github.com/mleku/lol" +) + +var F, E, W, I, D, T lol.Err + +func init() { + F, E, W, I, D, T = lol.Main.Errorf.F, lol.Main.Errorf.E, lol.Main.Errorf.W, lol.Main.Errorf.I, lol.Main.Errorf.D, lol.Main.Errorf.T +} diff --git a/go.mod b/go.mod index fe012fd..9ddefe6 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,14 @@ module github.com/mleku/lol -go 1.22.4 +go 1.25.0 require ( github.com/davecgh/go-spew v1.1.1 - github.com/gookit/color v1.5.4 - go.uber.org/atomic v1.11.0 + github.com/fatih/color v1.18.0 ) require ( - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/sys v0.21.0 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + golang.org/x/sys v0.35.0 // indirect ) diff --git a/go.sum b/go.sum index 7a32659..954c5d2 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,13 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= -github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561 h1:MDc5xs78ZrZr3HMQugiXOAkSZtfTpbJLDr/lwfgO53E= -golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +orly.dev v0.8.7 h1:81Dn93vvZM0Q3GmGxmd+m44PkFOZpmeZBszS33vgk5I= +orly.dev v0.8.7/go.mod h1:jnbkB9+qxjUdsy8bdiR8Mk0t3Y+UeLnS3pY3dS6Oi/M= diff --git a/log.go b/log.go index c5ddb2d..f01d468 100644 --- a/log.go +++ b/log.go @@ -1,3 +1,7 @@ +// Package lol (log of location) is a simple logging library that prints a high +// precision unix timestamp and the source location of a log print to make +// tracing errors simpler. Includes a set of logging levels and the ability to +// filter out higher log levels for a more quiet output. package lol import ( @@ -5,48 +9,13 @@ import ( "io" "os" "runtime" - "strings" + "sync/atomic" "time" "github.com/davecgh/go-spew/spew" - "github.com/gookit/color" - "go.uber.org/atomic" + "github.com/fatih/color" ) -var l = GetStd() - -func GetStd() (ll *Log) { - ll, _ = New(os.Stdout) - return -} - -func init() { - switch strings.ToUpper(os.Getenv("GODEBUG")) { - case "1", "TRUE", "ON": - SetLogLevel(Debug) - l.D.Ln("printing logs at this level and lower") - case "INFO": - SetLogLevel(Info) - case "DEBUG": - SetLogLevel(Debug) - l.D.Ln("printing logs at this level and lower") - case "TRACE": - SetLogLevel(Trace) - l.T.Ln("printing logs at this level and lower") - case "WARN": - SetLogLevel(Warn) - case "ERROR": - SetLogLevel(Error) - case "FATAL": - SetLogLevel(Fatal) - case "0", "OFF", "FALSE": - SetLogLevel(Off) - default: - SetLogLevel(Info) - } - -} - const ( Off = iota Fatal @@ -57,24 +26,36 @@ const ( Trace ) -type ( - // LevelPrinter defines a set of terminal printing primitives that output with - // extra data, time, log logLevelList, and code location +var LevelNames = []string{ + "off", + "fatal", + "error", + "warn", + "info", + "debug", + "trace", +} - // Ln prints lists of interfaces with spaces in between +type ( + // LevelPrinter defines a set of terminal printing primitives that output + // with extra data, time, log logLevelList, and code location + + // Ln prints lists of server with spaces in between Ln func(a ...interface{}) - // F prints like fmt.Println surrounded by log details + // F prints like fmt.Println surrounded []byte log details F func(format string, a ...interface{}) - // S prints a spew.Sdump for an interface slice + // S prints a spew.Sdump for an enveloper slice S func(a ...interface{}) - // C accepts a function so that the extra computation can be avoided if it is - // not being viewed + // C accepts a function so that the extra computation can be avoided if it is not being + // viewed C func(closure func() string) // Chk is a shortcut for printing if there is an error, or returning true Chk func(e error) bool - // Err is a pass-through function that uses fmt.Errorf to construct an error - // and returns the error after printing it to the log - Err func(format string, a ...interface{}) error + // Err is a pass-through function that uses fmt.Errorf to construct an error and returns the + // error after printing it to the log + Err func(format string, a ...any) error + + // LevelPrinter is the set of log printers on each log level. LevelPrinter struct { Ln F @@ -83,10 +64,12 @@ type ( Chk Err } + + // LevelSpec is the name, ID and Colorizer for a log level. LevelSpec struct { ID int Name string - Colorizer func(a ...interface{}) string + Colorizer func(a ...any) string } // Entry is a log entry to be printed as json to the log file @@ -100,33 +83,98 @@ type ( ) var ( - // sep is just a convenient shortcut for this very longwinded expression - sep = string(os.PathSeparator) - currentLevel = atomic.NewInt32(Info) - // writer can be swapped out for any io.*writer* that you want to use instead of - // stdout. - writer io.Writer = os.Stderr + // Writer can be swapped out for any io.*Writer* that you want to use instead of stdout. + Writer io.Writer = os.Stderr + // LevelSpecs specifies the id, string name and color-printing function LevelSpecs = []LevelSpec{ - {Off, " ", color.Bit24(0, 0, 0, false).Sprint}, - {Fatal, "FTL", color.Bit24(128, 0, 0, false).Sprint}, - {Error, "ERR", color.Bit24(255, 0, 0, false).Sprint}, - {Warn, "WRN", color.Bit24(0, 255, 0, false).Sprint}, - {Info, "INF", color.Bit24(255, 255, 0, false).Sprint}, - {Debug, "DBG", color.Bit24(0, 125, 255, false).Sprint}, - {Trace, "TRC", color.Bit24(125, 0, 255, false).Sprint}, + {Off, "", NoSprint}, + {Fatal, "FTL", color.New(color.BgRed, color.FgHiWhite).Sprint}, + {Error, "ERR", color.New(color.FgHiRed).Sprint}, + {Warn, "WRN", color.New(color.FgHiYellow).Sprint}, + {Info, "INF", color.New(color.FgHiGreen).Sprint}, + {Debug, "DBG", color.New(color.FgHiBlue).Sprint}, + {Trace, "TRC", color.New(color.FgHiMagenta).Sprint}, } ) +// NoSprint is a noop for sprint (it returns nothing no matter what is given to it). +func NoSprint(_ ...any) string { return "" } + // Log is a set of log printers for the various Level items. type Log struct { F, E, W, I, D, T LevelPrinter } +// Check is the set of log levels for a Check operation (prints an error if the error is not +// nil). type Check struct { F, E, W, I, D, T Chk } +// Errorf prints an error that is also returned as an error, so the error is logged at the site. +type Errorf struct { + F, E, W, I, D, T Err +} + +// Logger is a collection of things that creates a logger, including levels. +type Logger struct { + *Log + *Check + *Errorf +} + +// Level is the level that the logger is printing at. +var Level atomic.Int32 + +// Main is the main logger. +var Main = &Logger{} + +func init() { + // Main = &Logger{} + Main.Log, Main.Check, Main.Errorf = New(os.Stderr, 2) + ll := os.Getenv("LOG_LEVEL") + if ll == "" { + SetLogLevel("info") + } else { + for i := range LevelNames { + if ll == LevelNames[i] { + SetLoggers(i) + return + } + } + SetLoggers(Info) + } +} + +// SetLoggers configures a log level. +func SetLoggers(level int) { + Main.Log.T.F("log level %s", LevelSpecs[level].Colorizer(LevelNames[level])) + Level.Store(int32(level)) +} + +// GetLogLevel returns the log level number of a string log level. +func GetLogLevel(level string) (i int) { + for i = range LevelNames { + if level == LevelNames[i] { + return i + } + } + return Info +} + +// SetLogLevel sets the log level of the logger. +func SetLogLevel(level string) { + for i := range LevelNames { + if level == LevelNames[i] { + SetLoggers(i) + return + } + } + SetLoggers(Trace) +} + +// JoinStrings joins together anything into a set of strings with space separating the items. func JoinStrings(a ...any) (s string) { for i := range a { s += fmt.Sprint(a[i]) @@ -137,78 +185,124 @@ func JoinStrings(a ...any) (s string) { return } -func GetPrinter(l int32, writer io.Writer) LevelPrinter { +var msgCol = color.New(color.FgBlue).Sprint + +// GetPrinter returns a full logger that writes to the provided io.Writer. +func GetPrinter(l int32, writer io.Writer, skip int) LevelPrinter { return LevelPrinter{ Ln: func(a ...interface{}) { - fmt.Fprintf(writer, - "%s %s %s %s\n", - UnixNanoAsFloat(), + if Level.Load() < l { + return + } + _, _ = fmt.Fprintf( + writer, + "%s%s %s %s\n", + msgCol(TimeStamper()), LevelSpecs[l].Colorizer(LevelSpecs[l].Name), JoinStrings(a...), - GetLoc(2), + msgCol(GetLoc(skip)), ) }, F: func(format string, a ...interface{}) { - fmt.Fprintf(writer, - "%s %s %s %s\n", - UnixNanoAsFloat(), + if Level.Load() < l { + return + } + _, _ = fmt.Fprintf( + writer, + "%s%s %s %s\n", + msgCol(TimeStamper()), LevelSpecs[l].Colorizer(LevelSpecs[l].Name), fmt.Sprintf(format, a...), - GetLoc(2), + msgCol(GetLoc(skip)), ) }, S: func(a ...interface{}) { - fmt.Fprintf(writer, - "%s %s %s %s\n", - UnixNanoAsFloat(), + if Level.Load() < l { + return + } + _, _ = fmt.Fprintf( + writer, + "%s%s %s %s\n", + msgCol(TimeStamper()), LevelSpecs[l].Colorizer(LevelSpecs[l].Name), spew.Sdump(a...), - GetLoc(2), + msgCol(GetLoc(skip)), ) }, C: func(closure func() string) { - fmt.Fprintf(writer, - "%s %s %s %s\n", - UnixNanoAsFloat(), + if Level.Load() < l { + return + } + _, _ = fmt.Fprintf( + writer, + "%s%s %s %s\n", + msgCol(TimeStamper()), LevelSpecs[l].Colorizer(LevelSpecs[l].Name), closure(), - GetLoc(2), + msgCol(GetLoc(skip)), ) }, Chk: func(e error) bool { + if Level.Load() < l { + return e != nil + } if e != nil { - fmt.Fprintf(writer, - "%s %s %s %s\n", - UnixNanoAsFloat(), + _, _ = fmt.Fprintf( + writer, + "%s%s %s %s\n", + msgCol(TimeStamper()), LevelSpecs[l].Colorizer(LevelSpecs[l].Name), e.Error(), - GetLoc(2), + msgCol(GetLoc(skip)), ) return true } return false }, Err: func(format string, a ...interface{}) error { - fmt.Fprintf(writer, - "%s %s %s %s\n", - UnixNanoAsFloat(), - LevelSpecs[l].Colorizer(LevelSpecs[l].Name, " "), - fmt.Sprintf(format, a...), - GetLoc(2), - ) + if Level.Load() >= l { + _, _ = fmt.Fprintf( + writer, + "%s%s %s %s\n", + msgCol(TimeStamper()), + LevelSpecs[l].Colorizer(LevelSpecs[l].Name), + fmt.Sprintf(format, a...), + msgCol(GetLoc(skip)), + ) + } return fmt.Errorf(format, a...) }, } } -func New(writer io.Writer) (l *Log, c *Check) { +// GetNullPrinter is a logger that doesn't log. +func GetNullPrinter() LevelPrinter { + return LevelPrinter{ + Ln: func(a ...interface{}) {}, + F: func(format string, a ...interface{}) {}, + S: func(a ...interface{}) {}, + C: func(closure func() string) {}, + Chk: func(e error) bool { return e != nil }, + Err: func( + format string, a ...interface{}, + ) error { + return fmt.Errorf(format, a...) + }, + } +} + +// New creates a new logger with all the levels and things. +func New(writer io.Writer, skip int) (l *Log, c *Check, errorf *Errorf) { + if writer == nil { + writer = Writer + } l = &Log{ - F: GetPrinter(Fatal, writer), - E: GetPrinter(Error, writer), - W: GetPrinter(Warn, writer), - I: GetPrinter(Info, writer), - D: GetPrinter(Debug, writer), - T: GetPrinter(Trace, writer), + T: GetPrinter(Trace, writer, skip), + D: GetPrinter(Debug, writer, skip), + I: GetPrinter(Info, writer, skip), + W: GetPrinter(Warn, writer, skip), + E: GetPrinter(Error, writer, skip), + F: GetPrinter(Fatal, writer, skip), } c = &Check{ F: l.F.Chk, @@ -218,38 +312,35 @@ func New(writer io.Writer) (l *Log, c *Check) { D: l.D.Chk, T: l.T.Chk, } + errorf = &Errorf{ + F: l.F.Err, + E: l.E.Err, + W: l.W.Err, + I: l.I.Err, + D: l.D.Err, + T: l.T.Err, + } return } -// SetLogLevel sets the log level via a string, which can be truncated down to -// one character, similar to nmcli's argument processor, as the first letter is -// unique. This could be used with a linter to make larger command sets. -func SetLogLevel(l int) { - currentLevel.Store(int32(l)) -} - -func GetLogLevel() (l int) { - return int(currentLevel.Load()) -} - -// UnixNanoAsFloat e -func UnixNanoAsFloat() (s string) { - timeText := fmt.Sprint(time.Now().UnixNano()) - lt := len(timeText) - lb := lt + 1 - var timeBytes = make([]byte, lb) - copy(timeBytes[lb-9:lb], timeText[lt-9:lt]) - timeBytes[lb-10] = '.' - lb -= 10 - lt -= 9 - copy(timeBytes[:lb], timeText[:lt]) - return color.Bit24(0, 128, 255, false).Sprint(string(timeBytes)) +// TimeStamper generates the timestamp for logs. +func TimeStamper() (s string) { + ts := time.Now().Format("150405.000000") + ds := time.Now().Format("2006-01-02") + s += color.New(color.FgBlue).Sprint(ds[0:4]) + s += color.New(color.FgHiBlue).Sprint(ds[5:7]) + s += color.New(color.FgBlue).Sprint(ds[8:]) + s += color.New(color.FgHiBlue).Sprint(ts[0:2]) + s += color.New(color.FgBlue).Sprint(ts[2:4]) + s += color.New(color.FgHiBlue).Sprint(ts[4:6]) + s += color.New(color.FgBlue).Sprint(ts[7:]) + s += " " + return } +// GetLoc returns the code location of the caller. func GetLoc(skip int) (output string) { _, file, line, _ := runtime.Caller(skip) - output = color.Bit24(0, 128, 255, false).Sprint( - file, ":", line, - ) + output = fmt.Sprintf("%s:%d", file, line) return } diff --git a/log/log.go b/log/log.go new file mode 100644 index 0000000..78abce7 --- /dev/null +++ b/log/log.go @@ -0,0 +1,12 @@ +// Package log is a convenience shortcut to use shorter names to access the lol.Logger. +package log + +import ( + "github.com/mleku/lol" +) + +var F, E, W, I, D, T lol.LevelPrinter + +func init() { + F, E, W, I, D, T = lol.Main.Log.F, lol.Main.Log.E, lol.Main.Log.W, lol.Main.Log.I, lol.Main.Log.D, lol.Main.Log.T +} diff --git a/log_test.go b/log_test.go index 700d952..211aa7f 100644 --- a/log_test.go +++ b/log_test.go @@ -1,34 +1,298 @@ -package lol_test +package lol import ( + "bytes" "errors" - "os" + "fmt" + "strings" "testing" - - "github.com/mleku/lol" ) -var log, chk = lol.New(os.Stdout) +func TestLogLevels(t *testing.T) { + // Test that log levels are correctly ordered + if !(Off < Fatal && Fatal < Error && Error < Warn && Warn < Info && Info < Debug && Debug < Trace) { + t.Error("Log levels are not correctly ordered") + } -func TestGetLogger(t *testing.T) { - for i := 0; i < 100; i++ { - lol.SetLogLevel(lol.Trace) - log.T.Ln("testing log level", lol.LevelSpecs[lol.Trace].Name) - log.D.Ln("testing log level", lol.LevelSpecs[lol.Debug].Name) - log.I.Ln("testing log level", lol.LevelSpecs[lol.Info].Name) - log.W.Ln("testing log level", lol.LevelSpecs[lol.Warn].Name) - log.E.F("testing log level %s", lol.LevelSpecs[lol.Error].Name) - log.F.Ln("testing log level", lol.LevelSpecs[lol.Fatal].Name) - chk.F(errors.New("dummy error as fatal")) - chk.E(errors.New("dummy error as error")) - chk.W(errors.New("dummy error as warning")) - chk.I(errors.New("dummy error as info")) - chk.D(errors.New("dummy error as debug")) - chk.T(errors.New("dummy error as trace")) - log.I.Ln("log.I.Err", - log.I.Err("format string %d '%s'", 5, "testing") != nil) - log.I.Chk(errors.New("dummy information check")) - log.I.Chk(nil) - log.I.S("`backtick wrapped string`", t) + // Test that LevelNames matches the constants + expectedLevelNames := []string{ + "off", "fatal", "error", "warn", "info", "debug", "trace", + } + for i, name := range expectedLevelNames { + if LevelNames[i] != name { + t.Errorf("LevelNames[%d] = %s, want %s", i, LevelNames[i], name) + } + } +} + +func TestGetLogLevel(t *testing.T) { + tests := []struct { + level string + expected int + }{ + {"off", Off}, + {"fatal", Fatal}, + {"error", Error}, + {"warn", Warn}, + {"info", Info}, + {"debug", Debug}, + {"trace", Trace}, + {"unknown", Info}, // Default to Info for unknown levels + } + + for _, test := range tests { + t.Run( + test.level, func(t *testing.T) { + result := GetLogLevel(test.level) + if result != test.expected { + t.Errorf( + "GetLogLevel(%q) = %d, want %d", test.level, result, + test.expected, + ) + } + }, + ) + } +} + +func TestSetLogLevel(t *testing.T) { + // Save original level + originalLevel := Level.Load() + defer SetLoggers(int(originalLevel)) // Restore original level after test + + tests := []struct { + level string + expected int32 + }{ + {"off", Off}, + {"fatal", Fatal}, + {"error", Error}, + {"warn", Warn}, + {"info", Info}, + {"debug", Debug}, + {"trace", Trace}, + {"unknown", Trace}, // Should default to Trace for unknown levels + } + + for _, test := range tests { + t.Run( + test.level, func(t *testing.T) { + SetLogLevel(test.level) + result := Level.Load() + if result != test.expected { + t.Errorf( + "After SetLogLevel(%q), Level = %d, want %d", + test.level, result, test.expected, + ) + } + }, + ) + } +} + +func TestJoinStrings(t *testing.T) { + tests := []struct { + args []any + expected string + }{ + {[]any{}, ""}, + {[]any{"hello"}, "hello"}, + {[]any{"hello", "world"}, "hello world"}, + {[]any{1, 2, 3}, "1 2 3"}, + {[]any{1, "hello", 3.14}, "1 hello 3.14"}, + } + + for i, test := range tests { + t.Run( + fmt.Sprintf("case_%d", i), func(t *testing.T) { + result := JoinStrings(test.args...) + if result != test.expected { + t.Errorf( + "JoinStrings(%v) = %q, want %q", test.args, result, + test.expected, + ) + } + }, + ) + } +} + +func TestGetLoc(t *testing.T) { + loc := GetLoc(1) + if !strings.Contains(loc, "log_test.go") { + t.Errorf("GetLoc(1) = %q, expected to contain 'log_test.go'", loc) + } +} + +func TestGetPrinter(t *testing.T) { + // Create a buffer to capture output + var buf bytes.Buffer + + // Set log level to Info + originalLevel := Level.Load() + Level.Store(int32(Info)) + defer Level.Store(originalLevel) // Restore original level + + // Create a printer for Debug level + printer := GetPrinter(int32(Debug), &buf, 1) + + // Test Ln method - should not print because Debug > Info + buf.Reset() + printer.Ln("test message") + if buf.String() != "" { + t.Errorf( + "printer.Ln() printed when level is too high: %q", buf.String(), + ) + } + + // Set log level to Debug + Level.Store(int32(Debug)) + + // Test Ln method - should print now + buf.Reset() + printer.Ln("test message") + output := buf.String() + if output == "" { + t.Error("printer.Ln() did not print when it should have") + } + if !strings.Contains(output, "test message") { + t.Errorf( + "printer.Ln() output %q does not contain 'test message'", output, + ) + } + + // Test F method + buf.Reset() + printer.F("formatted %s", "message") + output = buf.String() + if !strings.Contains(output, "formatted message") { + t.Errorf( + "printer.F() output %q does not contain 'formatted message'", + output, + ) + } + + // Test S method + buf.Reset() + printer.S("spew message") + output = buf.String() + if !strings.Contains(output, "spew message") { + t.Errorf( + "printer.S() output %q does not contain 'spew message'", output, + ) + } + + // Test C method + buf.Reset() + printer.C(func() string { return "closure message" }) + output = buf.String() + if !strings.Contains(output, "closure message") { + t.Errorf( + "printer.C() output %q does not contain 'closure message'", output, + ) + } + + // Test Chk method with nil error + buf.Reset() + result := printer.Chk(nil) + if result != false { + t.Error("printer.Chk(nil) returned true, expected false") + } + if buf.String() != "" { + t.Errorf("printer.Chk(nil) printed output: %q", buf.String()) + } + + // Test Chk method with error + buf.Reset() + testErr := errors.New("test error") + result = printer.Chk(testErr) + if result != true { + t.Error("printer.Chk(error) returned false, expected true") + } + if !strings.Contains(buf.String(), "test error") { + t.Errorf( + "printer.Chk(error) output %q does not contain 'test error'", + buf.String(), + ) + } + + // Test Err method + buf.Reset() + err := printer.Err("error %s", "message") + if err == nil { + t.Error("printer.Err() returned nil error") + } + if err.Error() != "error message" { + t.Errorf( + "printer.Err() returned error with message %q, expected 'error message'", + err.Error(), + ) + } + // Check if the message was logged + if !strings.Contains(buf.String(), "error message") { + t.Errorf( + "printer.Err() output %q does not contain 'error message'", + buf.String(), + ) + } +} + +func TestGetNullPrinter(t *testing.T) { + printer := GetNullPrinter() + + // Test that Ln, F, S, C methods don't panic + printer.Ln("test") + printer.F("test %s", "format") + printer.S("test") + printer.C(func() string { return "test" }) + + // Test Chk method + if !printer.Chk(errors.New("test")) { + t.Error("GetNullPrinter().Chk(error) returned false, expected true") + } + if printer.Chk(nil) { + t.Error("GetNullPrinter().Chk(nil) returned true, expected false") + } + + // Test Err method + err := printer.Err("test %s", "error") + if err == nil { + t.Error("GetNullPrinter().Err() returned nil error") + } + if err.Error() != "test error" { + t.Errorf( + "GetNullPrinter().Err() returned error with message %q, expected 'test error'", + err.Error(), + ) + } +} + +func TestNew(t *testing.T) { + var buf bytes.Buffer + log, check, errorf := New(&buf, 1) + + // Verify that all components are created + if log == nil { + t.Error("New() returned nil Log") + } + if check == nil { + t.Error("New() returned nil Check") + } + if errorf == nil { + t.Error("New() returned nil Errorf") + } + + // Test that the log functions work + originalLevel := Level.Load() + Level.Store(int32(Debug)) + defer Level.Store(originalLevel) + + buf.Reset() + log.D.Ln("test message") + if !strings.Contains(buf.String(), "test message") { + t.Errorf( + "log.D.Ln() output %q doesn't contain 'test message'", + buf.String(), + ) } }