Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add missing tests and docstrings #1

Merged
merged 16 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions internal/testutils/testutils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package testutils

import (
"reflect"
"testing"
)

func AssertEquals[T any](t *testing.T, expected T, actual T) {
t.Helper()
if !reflect.DeepEqual(expected, actual) {
t.Fatalf("\nExpected: %v\nActual: %v", expected, actual)
}
}
52 changes: 38 additions & 14 deletions pkg/logger/formatter/formatter.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Package formatter contains formatter that interpolates template strings and
// formats them.
package formatter

import (
Expand Down Expand Up @@ -26,35 +28,57 @@ var logLevelColors = map[loglevel.LogLevel]string{
// Reset color
const resetColor = "\033[0m"

// Interface represents interface that shall be satisfied by Formatter.
type Interface interface {
Template() string
Format(message string, loggerName string, level loglevel.LogLevel, colored bool) string
}

// Formatter struct that contains necessary for the formatting fields.
type Formatter struct {
format string
template string
}

// New create a new instance of the Formatter.
func New(template string) *Formatter {
return &Formatter{template: template}
}

func New(format string) *Formatter {
return &Formatter{format: format}
// IsEqual checks that two formatters are the same and returns result of the
// comparison.
func (formatter *Formatter) IsEqual(anotherFormatter *Formatter) bool {
return formatter.template == anotherFormatter.template
}

// EvaluatePreset evaluates pre-defined set of formatting options and returns map
// with mapping of the option to interpolated value.
func (formatter *Formatter) EvaluatePreset(message string, loggerName string, level loglevel.LogLevel) map[string]string {
_, functionName, functionLine, _ := runtime.Caller(2)
var presets = map[string]string{
"%(name)": loggerName,
"%(message)": message,
"%(time)": time.Now().Format(time.TimeOnly),
"%(date)": time.Now().Format(time.DateOnly),
"%(isotime)": time.Now().Format(time.RFC3339),
"%(timestamp)": strconv.FormatInt(time.Now().Unix(), 10),
"%(level)": level.String(),
"%(levelnr)": strconv.Itoa(level.DigitRepresentation()),
"%(fname)": functionName,
"%(fline)": strconv.Itoa(functionLine),
"%(name)": loggerName, // Logger name
"%(message)": message, // Logged message
"%(time)": time.Now().Format(time.TimeOnly), // Current time (format: HH:MM:ss)
"%(date)": time.Now().Format(time.DateOnly), // Current date (format: yyyy-mm-dd)
"%(isotime)": time.Now().Format(time.RFC3339), // Current date and time (format: yyyy-mm-ddTHH:MM:ssGMT)
"%(timestamp)": strconv.FormatInt(time.Now().Unix(), 10), // Current timestamp
"%(level)": level.String(), // Logging level name
"%(levelnr)": strconv.Itoa(level.DigitRepresentation()), // Logging level number
"%(fname)": functionName, // Name of the function from which logger has been called
"%(fline)": strconv.Itoa(functionLine), // Line number from which logger has been called
}
return presets
}

// Template returns template string used by formatter.
func (formatter *Formatter) Template() string {
return formatter.template
}

// Format formats provided message template to the interpolated string.
func (formatter *Formatter) Format(message string, loggerName string, level loglevel.LogLevel, colored bool) string {
var presets = formatter.EvaluatePreset(message, loggerName, level)

format := formatter.format
format := formatter.template

for key, value := range presets {
format = strings.ReplaceAll(format, key, value)
Expand Down
114 changes: 114 additions & 0 deletions pkg/logger/formatter/formatter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
// Package formatter_test has tests for formatter package.
package formatter

import (
"fmt"
"github.com/dl1998/go-logging/internal/testutils"
"github.com/dl1998/go-logging/pkg/logger/loglevel"
"testing"
)

var template = "%(level):%(name):%(message)"
var message = "Test message."
var loggerName = "test"
var loggingLevel = loglevel.LogLevel(loglevel.Debug)

// TestNew tests that New create correct Formatter instance.
func TestNew(t *testing.T) {
newFormatter := New(template)

testutils.AssertEquals(t, template, newFormatter.template)
}

// BenchmarkNew performs benchmarking of the New().
func BenchmarkNew(b *testing.B) {
for index := 0; index < b.N; index++ {
New(template)
}
}

// TestFormatter_IsEqual tests that Formatter.IsEqual returns true, if two
// Formatter(s) are the same.
func TestFormatter_IsEqual(t *testing.T) {
newFormatter := New(template)

isEqual := newFormatter.IsEqual(newFormatter)

if !isEqual {
t.Fatalf("expected: %t, actual: %t", true, isEqual)
}
}

// BenchmarkFormatter_IsEqual performs benchmarking of the Formatter.IsEqual().
func BenchmarkFormatter_IsEqual(b *testing.B) {
newFormatter := New(template)

for index := 0; index < b.N; index++ {
newFormatter.IsEqual(newFormatter)
}
}

// TestFormatter_EvaluatePreset tests that Formatter.EvaluatePreset correctly
// evaluates tags.
func TestFormatter_EvaluatePreset(t *testing.T) {
newFormatter := New(template)

preset := newFormatter.EvaluatePreset(message, loggerName, loggingLevel)

testutils.AssertEquals(t, message, preset["%(message)"])
testutils.AssertEquals(t, loggerName, preset["%(name)"])
testutils.AssertEquals(t, loggingLevel.String(), preset["%(level)"])
}

// BenchmarkFormatter_EvaluatePreset performs benchmarking of the Formatter.EvaluatePreset().
func BenchmarkFormatter_EvaluatePreset(b *testing.B) {
newFormatter := New(template)

for index := 0; index < b.N; index++ {
newFormatter.EvaluatePreset(message, loggerName, loggingLevel)
}
}

// TestFormatter_Template tests that Formatter.Template return assigned template.
func TestFormatter_Template(t *testing.T) {
newFormatter := New(template)

testutils.AssertEquals(t, template, newFormatter.Template())
}

// BenchmarkFormatter_Template performs benchmarking of the Formatter.Template().
func BenchmarkFormatter_Template(b *testing.B) {
newFormatter := New(template)

for index := 0; index < b.N; index++ {
newFormatter.Template()
}
}

// TestFormatter_Format tests that Formatter.Format correctly formats string.
func TestFormatter_Format(t *testing.T) {
newFormatter := New(template)

parameters := []struct {
colored bool
expected string
}{
{false, fmt.Sprintf("%s:%s:%s\n", loggingLevel.String(), loggerName, message)},
{true, fmt.Sprintf("\033[36m%s:%s:%s\033[0m\n", loggingLevel.String(), loggerName, message)},
}

for index := range parameters {
actual := newFormatter.Format(message, loggerName, loggingLevel, parameters[index].colored)

testutils.AssertEquals(t, parameters[index].expected, actual)
}
}

// BenchmarkFormatter_Format performs benchmarking of the Formatter.Format().
func BenchmarkFormatter_Format(b *testing.B) {
newFormatter := New(template)

for index := 0; index < b.N; index++ {
newFormatter.Format(message, loggerName, loggingLevel, true)
}
}
60 changes: 44 additions & 16 deletions pkg/logger/handler/handler.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// Package handler provides handlers for the logger, it contains logic for the
// logging messages.
package handler

import (
Expand All @@ -9,14 +11,29 @@ import (
"strings"
)

var osOpenFile = os.OpenFile
var osStdout = os.Stdout
var osStderr = os.Stderr

// Interface represents interface that shall be satisfied by Handler.
type Interface interface {
Level() loglevel.LogLevel
SetLevel(level loglevel.LogLevel)
Formatter() formatter.Interface
Write(logName string, level loglevel.LogLevel, message string, parameters ...any)
}

// Handler struct contains information where it shall write log message, how to
// format them and their log level.
type Handler struct {
level loglevel.LogLevel
formatter formatter.Formatter
formatter formatter.Interface
writer io.Writer
errorWriter io.Writer
}

func New(level loglevel.LogLevel, newFormatter formatter.Formatter, writer io.Writer, errorWriter io.Writer) *Handler {
// New create a new instance of the Handler.
func New(level loglevel.LogLevel, newFormatter formatter.Interface, writer io.Writer, errorWriter io.Writer) *Handler {
return &Handler{
level: level,
formatter: newFormatter,
Expand All @@ -25,15 +42,16 @@ func New(level loglevel.LogLevel, newFormatter formatter.Formatter, writer io.Wr
}
}

func NewConsoleHandler(level loglevel.LogLevel, newFormatter formatter.Formatter) *Handler {
writer := os.Stdout
errorWriter := os.Stderr

return New(level, newFormatter, writer, errorWriter)
// NewConsoleHandler create a new instance of the Handler that writes log
// messages to the os.Stdout and os.Stderr respectively.
func NewConsoleHandler(level loglevel.LogLevel, newFormatter formatter.Interface) *Handler {
return New(level, newFormatter, osStdout, osStderr)
}

func NewFileHandler(level loglevel.LogLevel, newFormatter formatter.Formatter, file string) *Handler {
writer, err := os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
// NewFileHandler creates a new instance of the Handler that writes log message
// to the log file.
func NewFileHandler(level loglevel.LogLevel, newFormatter formatter.Interface, file string) *Handler {
writer, err := osOpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)

if err != nil {
fmt.Println(err)
Expand All @@ -42,36 +60,46 @@ func NewFileHandler(level loglevel.LogLevel, newFormatter formatter.Formatter, f
return New(level, newFormatter, writer, writer)
}

// Level returns log level of the Handler.
func (handler *Handler) Level() loglevel.LogLevel {
return handler.level
}

// SetLevel sets a new log level for the Handler.
func (handler *Handler) SetLevel(level loglevel.LogLevel) {
handler.level = level
}

// Formatter returns formatter.Interface used by the Handler.
func (handler *Handler) Formatter() formatter.Interface {
return handler.formatter
}

// Write writes log message to the defined by the Handler writer.
func (handler *Handler) Write(logName string, level loglevel.LogLevel, message string, parameters ...any) {
formattedMessage := fmt.Sprintf(message, parameters...)

writer := handler.writer

if level >= loglevel.Error {
writer = handler.errorWriter
}

var colored = false

if consoleSupportsANSIColors() && handler.writer == os.Stdout {
if consoleSupportsANSIColors() && (writer == osStdout || writer == osStderr) {
colored = true
}

log := handler.formatter.Format(formattedMessage, logName, level, colored)

writer := handler.writer

if level >= loglevel.Error {
writer = handler.errorWriter
}

if _, err := writer.Write([]byte(log)); err != nil {
fmt.Println(err)
}
}

// consoleSupportsANSIColors returns true, if current terminal supports ANSI
// colors, otherwise returns False.
func consoleSupportsANSIColors() bool {
term := os.Getenv("TERM")
return strings.Contains(term, "xterm") || strings.Contains(term, "color")
Expand Down
Loading
Loading