...

Source file src/github.com/launchdarkly/go-sdk-common/v3/ldlogtest/mock_log.go

Documentation: github.com/launchdarkly/go-sdk-common/v3/ldlogtest

     1  package ldlogtest
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"os"
     7  	"regexp"
     8  	"strings"
     9  	"sync"
    10  	"testing"
    11  
    12  	"github.com/stretchr/testify/assert"
    13  
    14  	"github.com/launchdarkly/go-sdk-common/v3/ldlog"
    15  )
    16  
    17  // MockLogItem represents a log message captured by MockLog.
    18  type MockLogItem struct {
    19  	Level   ldlog.LogLevel
    20  	Message string
    21  }
    22  
    23  // MockLog provides the ability to capture log output.
    24  //
    25  // It contains an [ldlog.Loggers] instance which can be used like any other Loggers, but all of the output is
    26  // captured by the enclosing MockLog and can be retrieved with the MockLog methods.
    27  //
    28  //	mockLog := ldlogtest.NewMockLog()
    29  //	mockLog.Loggers.Warn("message")
    30  //	mockLog.Loggers.Warnf("also: %t", true)
    31  //	warnMessages := mockLog.GetOutput(ldlog.Warn) // returns {"message", "also: true"}
    32  type MockLog struct {
    33  	// Loggers is the ldlog.Loggers instance to be used for tests.
    34  	Loggers ldlog.Loggers
    35  	// Output is a map containing all of the lines logged for each level. The level prefix is removed from the text.
    36  	output map[ldlog.LogLevel][]string
    37  	// AllOutput is a list of all the log output for any level in order. The level prefix is removed from the text.
    38  	allOutput []MockLogItem
    39  	lock      sync.Mutex
    40  }
    41  
    42  // NewMockLog creates a log-capturing object.
    43  func NewMockLog() *MockLog {
    44  	ret := &MockLog{output: make(map[ldlog.LogLevel][]string)}
    45  	for _, level := range []ldlog.LogLevel{ldlog.Debug, ldlog.Info, ldlog.Warn, ldlog.Error} {
    46  		ret.Loggers.SetBaseLoggerForLevel(level, mockBaseLogger{owner: ret, level: level})
    47  	}
    48  	return ret
    49  }
    50  
    51  // GetOutput returns the captured output for a specific log level.
    52  func (ml *MockLog) GetOutput(level ldlog.LogLevel) []string {
    53  	ml.lock.Lock()
    54  	defer ml.lock.Unlock()
    55  	lines := ml.output[level]
    56  	ret := make([]string, len(lines))
    57  	copy(ret, lines)
    58  	return ret
    59  }
    60  
    61  // GetAllOutput returns the captured output for all log levels.
    62  func (ml *MockLog) GetAllOutput() []MockLogItem {
    63  	ml.lock.Lock()
    64  	defer ml.lock.Unlock()
    65  	ret := make([]MockLogItem, len(ml.allOutput))
    66  	copy(ret, ml.allOutput)
    67  	return ret
    68  }
    69  
    70  // HasMessageMatch tests whether there is a log message of this level that matches this regex.
    71  func (ml *MockLog) HasMessageMatch(level ldlog.LogLevel, pattern string) bool {
    72  	_, found := ml.findMessageMatching(level, pattern)
    73  	return found
    74  }
    75  
    76  // AssertMessageMatch asserts whether there is a log message of this level that matches this regex.
    77  // This is equivalent to using [assert.True] or [assert.False] with [MockLog.HasMessageMatch], except that
    78  // if the test fails, it includes the actual log messages in the failure message.
    79  func (ml *MockLog) AssertMessageMatch(t *testing.T, shouldMatch bool, level ldlog.LogLevel, pattern string) {
    80  	line, hasMatch := ml.findMessageMatching(level, pattern)
    81  	if hasMatch != shouldMatch {
    82  		if shouldMatch {
    83  			assert.Fail(
    84  				t,
    85  				"log did not contain expected message",
    86  				"level: %s, pattern: /%s/, messages: %v",
    87  				level,
    88  				pattern,
    89  				ml.GetOutput(level),
    90  			)
    91  		} else {
    92  			assert.Fail(
    93  				t,
    94  				"log contained unexpected message",
    95  				"level: %s, message: [%s]",
    96  				level,
    97  				line,
    98  			)
    99  		}
   100  	}
   101  }
   102  
   103  // Dump is a shortcut for writing all captured log lines to a Writer.
   104  func (ml *MockLog) Dump(w io.Writer) {
   105  	for _, line := range ml.GetAllOutput() {
   106  		fmt.Fprintln(w, line.Level.Name()+": "+line.Message)
   107  	}
   108  }
   109  
   110  // DumpIfTestFailed is a shortcut for writing all captured log lines to standard output only if
   111  // t.Failed() is true.
   112  //
   113  // This is useful in tests where you normally don't want to see the log output, but you do want to see it
   114  // if there was a failure. The simplest way to do this is to use defer:
   115  //
   116  //	func TestSomething(t *testing.T) {
   117  //	    ml := ldlogtest.NewMockLog()
   118  //	    defer ml.DumpIfTestFailed(t)
   119  //	    // ... do some test things that may generate log output and/or cause a failure
   120  //	}
   121  func (ml *MockLog) DumpIfTestFailed(t *testing.T) {
   122  	if t.Failed() { // COVERAGE: there's no way to test this in unit tests
   123  		ml.Dump(os.Stdout)
   124  	}
   125  }
   126  
   127  func (ml *MockLog) logLine(level ldlog.LogLevel, line string) {
   128  	ml.lock.Lock()
   129  	defer ml.lock.Unlock()
   130  	message := strings.TrimPrefix(line, strings.ToUpper(level.String())+": ")
   131  	ml.output[level] = append(ml.output[level], message)
   132  	ml.allOutput = append(ml.allOutput, MockLogItem{level, message})
   133  }
   134  
   135  func (ml *MockLog) findMessageMatching(level ldlog.LogLevel, pattern string) (string, bool) {
   136  	r := regexp.MustCompile(pattern)
   137  	for _, line := range ml.GetOutput(level) {
   138  		if r.MatchString(line) {
   139  			return line, true
   140  		}
   141  	}
   142  	return "", false
   143  }
   144  
   145  type mockBaseLogger struct {
   146  	owner *MockLog
   147  	level ldlog.LogLevel
   148  }
   149  
   150  func (l mockBaseLogger) Println(values ...interface{}) {
   151  	l.owner.logLine(l.level, strings.TrimSuffix(fmt.Sprintln(values...), "\n"))
   152  }
   153  
   154  func (l mockBaseLogger) Printf(format string, values ...interface{}) {
   155  	l.owner.logLine(l.level, fmt.Sprintf(format, values...))
   156  }
   157  

View as plain text