package trace
import (
"bytes"
"errors"
"io"
"net/http/httptest"
"strings"
"testing"
"blitiri.com.ar/go/chasquid/internal/nettrace"
"blitiri.com.ar/go/log"
)
type nopCloser struct{ io.Writer }
func (nopCloser) Close() error { return nil }
// setupLog replaces the default logger with one that writes to the returned
// buffer, at the given level. It is restored when the test finishes.
func setupLog(t *testing.T, level log.Level) *bytes.Buffer {
t.Helper()
buf := &bytes.Buffer{}
orig := log.Default
log.Default = log.New(nopCloser{buf})
log.Default.Level = level
log.Default.LogTime = false
log.Default.LogCaller = false
t.Cleanup(func() { log.Default = orig })
return buf
}
// traceEvents returns the rendered (HTML) active traces for the family.
func traceEvents(t *testing.T, family string) string {
t.Helper()
req := httptest.NewRequest("GET", "/debug/traces?fam="+family+"&b=-1", nil)
w := httptest.NewRecorder()
nettrace.RenderTraces(w, req)
return w.Body.String()
}
// countingStringer counts how many times it was formatted.
type countingStringer struct{ n *int }
func (c countingStringer) String() string {
*c.n++
return "stringer"
}
func TestFormatOnce(t *testing.T) {
for _, level := range []log.Level{log.Info, log.Debug} {
setupLog(t, level)
tr := New("TestFormatOnce", "title")
n := 0
tr.Debugf("%v", countingStringer{&n})
if n != 1 {
t.Errorf("level %d: Debugf formatted %d times, expected 1",
level, n)
}
n = 0
tr.Printf("%v", countingStringer{&n})
if n != 1 {
t.Errorf("level %d: Printf formatted %d times, expected 1",
level, n)
}
tr.Finish()
}
}
func TestLog(t *testing.T) {
// At info level, debug messages are not logged.
buf := setupLog(t, log.Info)
tr := New("TestLog", "title")
tr.Printf("info %d", 1)
tr.Debugf("debug %d", 2)
tr.Errorf("error %d", 3)
tr.Error(errors.New("error 4"))
tr.Finish()
expected := "_ TestLog title: info 1\n" +
"_ TestLog title: error: error 3\n" +
"_ TestLog title: error: error 4\n"
if got := buf.String(); got != expected {
t.Errorf("unexpected log at info level:\n got: %q\nwant: %q",
got, expected)
}
// At debug level, they are. Messages are quoted.
buf = setupLog(t, log.Debug)
tr = New("TestLog", "title")
tr.Debugf("debug %q\n", "x")
tr.Finish()
expected = `. TestLog title: debug \"x\"\n` + "\n"
if got := buf.String(); got != expected {
t.Errorf("unexpected log at debug level:\n got: %q\nwant: %q",
got, expected)
}
}
func TestTruncate(t *testing.T) {
rep := strings.Repeat
cases := []struct {
in, want string
}{
{"", ""},
{"short", "short"},
{rep("a", maxMsgLen), rep("a", maxMsgLen)},
{rep("a", maxMsgLen+1),
rep("a", maxMsgLen) + "… [truncated, 8193 bytes total]"},
// The limit is in characters, not bytes ("ñ" is 2 bytes).
{rep("ñ", maxMsgLen), rep("ñ", maxMsgLen)},
{rep("ñ", maxMsgLen+1),
rep("ñ", maxMsgLen) + "… [truncated, 16386 bytes total]"},
// Invalid UTF-8 bytes count as one character each, and are kept.
{rep("\xff", maxMsgLen+1),
rep("\xff", maxMsgLen) + "… [truncated, 8193 bytes total]"},
}
for i, c := range cases {
if got := truncate(c.in); got != c.want {
t.Errorf("%d: truncate(%d bytes) = %q (%d bytes), want %q",
i, len(c.in), got[max(0, len(got)-40):], len(got),
c.want[max(0, len(c.want)-40):])
}
}
}
func TestTruncateMessages(t *testing.T) {
buf := setupLog(t, log.Debug)
tr := New("TestTruncateMessages", "title")
long := strings.Repeat("x", 3*maxMsgLen)
tr.Printf("printf %s", long)
tr.Debugf("debugf %s", long)
err := tr.Errorf("errorf %s", long)
tr.Error(errors.New("error " + long))
// The error returned by Errorf must not be truncated.
if len(err.Error()) != len("errorf ")+len(long) {
t.Errorf("Errorf returned a truncated error (%d bytes)",
len(err.Error()))
}
// All messages are truncated in the log...
lines := strings.Split(strings.TrimSpace(buf.String()), "\n")
if len(lines) != 4 {
t.Fatalf("expected 4 log lines, got %d", len(lines))
}
for _, l := range lines {
if len(l) > maxMsgLen+100 ||
!strings.HasSuffix(l, " bytes total]") {
t.Errorf("log line not truncated (%d bytes): ...%s",
len(l), l[max(0, len(l)-60):])
}
}
// ... and in the trace.
body := traceEvents(t, "TestTruncateMessages")
if n := strings.Count(body, " bytes total]"); n != 4 {
t.Errorf("expected 4 truncated events in the trace, got %d", n)
}
if len(body) > 4*maxMsgLen+10*1024 {
t.Errorf("trace too large (%d bytes), events not truncated?",
len(body))
}
tr.Finish()
}
func TestTraceEvents(t *testing.T) {
// Debug messages are added to the trace even if they are not logged.
setupLog(t, log.Info)
tr := New("TestTraceEvents", "title")
tr.Printf("info %d", 1)
tr.Debugf("debug %d", 2)
tr.Errorf("error %d", 3)
body := traceEvents(t, "TestTraceEvents")
for _, s := range []string{"info 1", "debug 2", "error: error 3"} {
if !strings.Contains(body, s) {
t.Errorf("trace does not contain %q:\n%s", s, body)
}
}
tr.Finish()
}