This commit introduces the ability to handle nested fields within log entries. The file has been updated with a function that transforms flat keys with a : delimiter (e.g., obj:foo) into nested JSON objects (e.g., ). The file includes new test cases to verify that: - Nested fields are correctly unmarshalled from string representations. - Nested fields are correctly marshalled into JSON objects. This enhancement allows for more structured and organized metadata within log entries.
348 lines
11 KiB
Go
348 lines
11 KiB
Go
package models
|
|
|
|
import (
|
|
"bufio"
|
|
"encoding"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
// Type assertions
|
|
var _ encoding.TextMarshaler = Entry{}
|
|
var _ encoding.TextUnmarshaler = new(Entry)
|
|
var _ json.Marshaler = Entry{}
|
|
var _ json.Unmarshaler = new(Entry)
|
|
|
|
func TestEntryMarshal(t *testing.T) {
|
|
when := time.Now()
|
|
whens := when.Format(DateFormat)
|
|
simple := []Meta{}
|
|
nolines := []string{}
|
|
tests := []struct {
|
|
name string
|
|
title string
|
|
date time.Time
|
|
fields []Meta
|
|
first string
|
|
lines []string
|
|
err error
|
|
}{
|
|
{"no-title", "", when, simple, "", nolines, ErrorMissingTitle},
|
|
{"zero-date", "Empty title", time.Time{}, simple, "", nolines, ErrorMissingDate},
|
|
{"one-line", "A Title", when, simple, "@begin " + whens + " - A Title", []string{"@id .+ @end"}, nil},
|
|
{
|
|
"one-field",
|
|
"Title 2",
|
|
when,
|
|
[]Meta{{"age", 41}},
|
|
"@begin " + whens + " - Title 2",
|
|
[]string{"@age 41", "@id .*"},
|
|
nil,
|
|
},
|
|
{
|
|
"three-fields",
|
|
"Title 3",
|
|
when,
|
|
[]Meta{{"age", 41}, {"cool", true}, {"name", "Jim"}},
|
|
"@begin " + whens + " - Title 3",
|
|
[]string{"@age 41", "@cool true", "@name Jim"},
|
|
nil,
|
|
},
|
|
{
|
|
"json-field",
|
|
"Title J",
|
|
when,
|
|
[]Meta{{"json", json.RawMessage(`{"age": 41, "cool": true, "name": "Jim"}`)}},
|
|
"@begin " + whens + " - Title J",
|
|
[]string{"@age 41", "@cool true", "@name Jim"},
|
|
nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, getEntryMarshalTestRunner(tt.title, tt.date, tt.fields, tt.first, tt.lines, tt.err))
|
|
}
|
|
}
|
|
|
|
func getEntryMarshalTestRunner(title string, date time.Time, fields []Meta, first string, lines []string, err error) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
en := Entry{title, date, fields}
|
|
o, er := en.MarshalText()
|
|
assert.Equal(t, err, er)
|
|
if first == "" {
|
|
return
|
|
}
|
|
os := string(o)
|
|
if len(lines) == 0 {
|
|
assert.Equal(t, first, os)
|
|
return
|
|
}
|
|
|
|
assert.Regexp(t, first, os)
|
|
for _, line := range lines {
|
|
assert.Regexp(t, "(?m)^"+line, os)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEntryUnmarshal(t *testing.T) {
|
|
when := time.Now()
|
|
whens := when.Format(DateFormat)
|
|
simple := []Meta{}
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
title string
|
|
date time.Time
|
|
fields []Meta
|
|
err error
|
|
}{
|
|
{"one-line", "@begin " + whens + " - A Title @end", "A Title", when, simple, nil},
|
|
{"rfc3999-date", "@begin " + when.Format(time.RFC3339) + " - A Title @end", "A Title", when, simple, nil},
|
|
{"multi-title", "@begin " + whens + " - A Title\nwith break @end", "A Title\nwith break", when, simple, nil},
|
|
{"no-title", "@begin " + whens + " - @end", "", when, simple, ErrorMissingTitle},
|
|
{"parse-error", "this is no good", "", when, simple, ErrorParsing},
|
|
{"no-date", "@begin - A Title @end", "A Title", when, simple, ErrorMissingDate},
|
|
{"bad-date", "@begin not-a-real date - A Title @end", "A Title", when, simple, ErrorParsing},
|
|
{"one-field", "@begin " + whens + " - A Title\n@age 41 @end", "A Title", when, []Meta{{"age", 41}}, nil},
|
|
{
|
|
"two-fields",
|
|
"@begin " + whens + " - A Title\n@age 41\n@cool true @end",
|
|
"A Title",
|
|
when,
|
|
[]Meta{{"age", 41}, {"cool", true}},
|
|
nil,
|
|
},
|
|
{
|
|
"obj-field",
|
|
"@begin " + whens + " - A Title\n" + `@me {"name":"Dan","coder":true} @end`,
|
|
"A Title",
|
|
when,
|
|
[]Meta{{"me", json.RawMessage(`{"name":"Dan","coder":true}`)}},
|
|
nil,
|
|
},
|
|
{
|
|
"nested-field",
|
|
"@begin " + whens + " - A Title\n@me:name Dan\n@me:coder true @end",
|
|
"A Title",
|
|
when,
|
|
[]Meta{{"me:name", "Dan"}, {"me:coder", true}},
|
|
nil,
|
|
},
|
|
{
|
|
"json-field",
|
|
"@begin " + whens + " - Some Guy\n" + `@json {"name":"Dan","coder":true} @end`,
|
|
"Some Guy",
|
|
when,
|
|
[]Meta{{"name", "Dan"}, {"coder", true}},
|
|
nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, getEntryUnmarshalTestRunner(tt.in, tt.title, tt.date, tt.fields, tt.err))
|
|
}
|
|
}
|
|
|
|
func getEntryUnmarshalTestRunner(in string, title string, date time.Time, fields []Meta, err error) func(*testing.T) {
|
|
return func(t *testing.T) {
|
|
e := &Entry{}
|
|
er := e.UnmarshalText([]byte(in))
|
|
if err != nil {
|
|
assert.ErrorIs(t, er, err)
|
|
return
|
|
}
|
|
|
|
assert.Equal(t, title, e.Title)
|
|
assert.WithinRange(t, e.Date, date.Add(-time.Second), date.Add(time.Second))
|
|
for _, f := range fields {
|
|
got := false
|
|
for _, m := range e.Fields {
|
|
var mVal any = m.Value
|
|
var fVal any = f.Value
|
|
if mJ, ok := m.Value.(json.RawMessage); ok {
|
|
mVal = string(mJ)
|
|
}
|
|
if fJ, ok := f.Value.(json.RawMessage); ok {
|
|
fVal = string(fJ)
|
|
}
|
|
if m.Key == f.Key && mVal == fVal {
|
|
got = true
|
|
break
|
|
}
|
|
}
|
|
assert.Truef(t, got, "Couldn't find field %+v. We have %+v", f, e.Fields)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestScan(t *testing.T) {
|
|
in := "@begin date - Title\nlong\n@foo john\njones\n@bar 42@nobody @end"
|
|
read := strings.NewReader(in)
|
|
scan := bufio.NewScanner(read)
|
|
scan.Split(scanEntry)
|
|
assert.True(t, scan.Scan())
|
|
assert.Equal(t, "@begin date - Title\nlong", scan.Text())
|
|
assert.True(t, scan.Scan())
|
|
assert.Equal(t, "@foo john\njones", scan.Text())
|
|
assert.True(t, scan.Scan())
|
|
assert.Equal(t, "@bar 42@nobody", scan.Text())
|
|
assert.False(t, scan.Scan())
|
|
}
|
|
|
|
func TestEntryJsonMarshal(t *testing.T) {
|
|
when := time.Now()
|
|
whens := when.Format(time.RFC3339)
|
|
simple := []Meta{}
|
|
tests := []struct {
|
|
name string
|
|
title string
|
|
date time.Time
|
|
fields []Meta
|
|
out string
|
|
err error
|
|
}{
|
|
{"simple", "A Title", when, simple, `{"title":"A Title","date":"` + whens + `"}`, nil},
|
|
{"one-field", "A Title 2", when, []Meta{{"age", 41}}, `{"title":"A Title 2","date":"` + whens + `","age":41}`, nil},
|
|
{"skip-title-field", "A Title", when, []Meta{{"title", "Different title"}}, `{"title":"A Title","date":"` + whens + `"}`, nil},
|
|
{"skip-date-field", "A Title", when, []Meta{{"date", when.Add(time.Hour)}}, `{"title":"A Title","date":"` + whens + `"}`, nil},
|
|
{"skip-dupe-field", "A Title", when, []Meta{{"foo", "bar"}, {"foo", "baz"}}, `{"title":"A Title","date":"` + whens + `","foo": "bar"}`, nil},
|
|
{"two-fields", "A Title", when, []Meta{{"foo", "bar"}, {"baz", 42}}, `{"title":"A Title","date":"` + whens + `","foo": "bar","baz":42}`, nil},
|
|
{"empty-title", "", when, simple, "", ErrorMissingTitle},
|
|
{"empty-date", "A Title", time.Time{}, simple, "", ErrorMissingDate},
|
|
{"obj-field", "A Title", when, []Meta{{"obj", json.RawMessage(`{"foo":"bar","title":"Sub-title"}`)}}, `{"title":"A Title","date":"` + whens + `","obj":{"foo":"bar","title":"Sub-title"}}`, nil},
|
|
{"date-field", "A Title", when, []Meta{{"when", when.Add(time.Hour)}}, `{"title":"A Title","date":"` + whens + `","when":"` + when.Add(time.Hour).Format(time.RFC3339) + `"}`, nil},
|
|
{"json-field", "A Title", when, []Meta{{"json", json.RawMessage(`{"age": 41, "cool": true, "name": "Jim"}`)}}, `{"title":"A Title","date":"` + whens + `","age":41,"cool": true, "name": "Jim"}`, nil},
|
|
{"nested-field", "A Title", when, []Meta{{"obj:foo", "bar"}, {"obj:title", "Sub-title"}}, `{"title":"A Title","date":"` + whens + `","obj":{"foo":"bar","title":"Sub-title"}}`, nil},
|
|
{"double-nested-field", "A Title", when, []Meta{{"obj:foo", "bar"}, {"obj:me:name", "Dan"}, {"obj:me:age", 27}}, `{"title":"A Title","date":"` + whens + `","obj":{"foo":"bar","me":{"name":"Dan","age":27}}}`, nil},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, getEntryJsonMarshalTestRunner(tt.title, tt.date, tt.fields, tt.out, tt.err))
|
|
}
|
|
}
|
|
|
|
func getEntryJsonMarshalTestRunner(title string, date time.Time, fields []Meta, out string, err error) func(t *testing.T) {
|
|
return func(t *testing.T) {
|
|
e := Entry{title, date, fields}
|
|
o, er := json.Marshal(e)
|
|
if err == nil {
|
|
assert.JSONEq(t, out, string(o))
|
|
|
|
} else {
|
|
assert.ErrorIs(t, er, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestEntryJsonUnmarshal(t *testing.T) {
|
|
when := time.Now().Truncate(time.Second)
|
|
whens := when.Format(time.RFC3339)
|
|
simple := []Meta{}
|
|
tests := []struct {
|
|
name string
|
|
in string
|
|
title string
|
|
date time.Time
|
|
fields []Meta
|
|
err error
|
|
}{
|
|
{"simple", `{"title":"A Title","date":"` + whens + `"}`, "A Title", when, simple, nil},
|
|
{"missing-title", `{"date":"` + whens + `"}`, "", when, simple, ErrorMissingTitle},
|
|
{"missing-date", `{"title":"A Title"}`, "", when, simple, ErrorMissingDate},
|
|
{"empty-title", `{"title":"","date":"` + whens + `"}`, "", when, simple, ErrorMissingTitle},
|
|
{"empty-date", `{"title":"A Title","date":""}`, "", when, simple, ErrorMissingDate},
|
|
{"bad-date", `{"title":"A Title","date":"bad"}`, "", when, simple, ErrorParsing},
|
|
{"bad-json", `{"title":"A Title","date":"`, "", when, simple, ErrorParsing},
|
|
{
|
|
"single-field",
|
|
`{"title":"A Title","date":"` + whens + `","hello":"Hi"}`,
|
|
"A Title",
|
|
when,
|
|
[]Meta{{"hello", "Hi"}},
|
|
nil,
|
|
},
|
|
{
|
|
"many-fields",
|
|
`{"title":"A Title","date":"` + whens + `","hello":"Hi","bye":42,"b":true,"fl":42.13}`,
|
|
"A Title",
|
|
when,
|
|
[]Meta{{"hello", "Hi"}, {"bye", int64(42)}, {"b", true}, {"fl", float64(42.13)}},
|
|
nil,
|
|
},
|
|
{
|
|
"date-field",
|
|
`{"title":"A Title","date":"` + whens + `","posted":"` + when.Add(-time.Hour).In(time.UTC).Format(time.RFC3339) + `"}`,
|
|
"A Title",
|
|
when,
|
|
[]Meta{{"posted", when.Add(-time.Hour)}},
|
|
nil,
|
|
},
|
|
{
|
|
"json-field",
|
|
`{"title":"A Title","date":"` + whens + `","json":{"age": 41, "cool": true, "name": "Jim"}}`,
|
|
"A Title",
|
|
when,
|
|
[]Meta{{"age", int64(41)}, {"cool", true}, {"name", "Jim"}},
|
|
nil,
|
|
},
|
|
{
|
|
"json-field-embed",
|
|
`{"title":"A Title","date":"` + whens + `","json":"{\"age\": 41, \"cool\": true, \"name\": \"Jim\"}"}`,
|
|
"A Title",
|
|
when,
|
|
[]Meta{{"age", int64(41)}, {"cool", true}, {"name", "Jim"}},
|
|
nil,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, getEntryJsonUnmarshalTestRunner(tt.in, tt.title, tt.date, tt.fields, tt.err))
|
|
}
|
|
}
|
|
|
|
func getEntryJsonUnmarshalTestRunner(in, title string, date time.Time, fields []Meta, err error) func(t *testing.T) {
|
|
return func(t *testing.T) {
|
|
e := new(Entry)
|
|
er := e.UnmarshalJSON([]byte(in))
|
|
if err != nil {
|
|
assert.ErrorIs(t, er, err)
|
|
return
|
|
}
|
|
|
|
assert.Nil(t, er)
|
|
assert.Equal(t, title, e.Title)
|
|
assert.WithinRange(t, e.Date, date.Add(-time.Second), date.Add(time.Second))
|
|
assert.Len(t, e.Fields, len(fields))
|
|
for _, f := range fields {
|
|
got := false
|
|
fTime, isTime := f.Value.(time.Time)
|
|
for _, m := range e.Fields {
|
|
var mVal any = m.Value
|
|
var fVal any = f.Value
|
|
if mJ, ok := m.Value.(json.RawMessage); ok {
|
|
mVal = string(mJ)
|
|
}
|
|
if fJ, ok := f.Value.(json.RawMessage); ok {
|
|
fVal = string(fJ)
|
|
}
|
|
if m.Key == f.Key && mVal == fVal {
|
|
got = true
|
|
break
|
|
}
|
|
if isTime && m.Key == f.Key {
|
|
mTime, _ := mVal.(time.Time)
|
|
if assert.WithinRange(t, mTime, fTime.Add(-2*time.Second), fTime.Add(2*time.Second)) {
|
|
got = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
assert.Truef(t, got, "Couldn't find field %+v. We have %+v", f, e.Fields)
|
|
}
|
|
}
|
|
}
|