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

[approvaltest] Ignore dynamic fields from sort fields #61

Merged
merged 2 commits into from
Jun 7, 2024
Merged
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
93 changes: 47 additions & 46 deletions pkg/approvaltest/approvals.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"os"
"path/filepath"
"sort"
"strings"
"testing"

"github.com/google/go-cmp/cmp"
Expand Down Expand Up @@ -53,16 +54,16 @@ const (
func ApproveEvents(t testing.TB, name string, hits []espoll.SearchHit, dynamic ...string) {
t.Helper()

// Sort events for repeatable diffs.
sort.Slice(hits, func(i, j int) bool {
return compareDocumentFields(hits[i].RawFields, hits[j].RawFields) < 0
})

sources := make([][]byte, len(hits))
for i, hit := range hits {
sources[i] = hit.RawSource
}
approveEventDocs(t, filepath.Join("approvals", name), sources, dynamic...)
rewriteDynamic(t, sources, false, dynamic...)
// Rewrite dynamic fields and sort them for repeatable diffs.
sort.Slice(sources, func(i, j int) bool {
return compareDocumentFields(sources[i], sources[j]) < 0
})
approveEventDocs(t, filepath.Join("approvals", name), sources)
}

// ApproveFields compares the fields of the search hits with the
Expand All @@ -79,26 +80,23 @@ func ApproveEvents(t testing.TB, name string, hits []espoll.SearchHit, dynamic .
func ApproveFields(t testing.TB, name string, hits []espoll.SearchHit, dynamic ...string) {
t.Helper()

// Sort events for repeatable diffs.
sort.Slice(hits, func(i, j int) bool {
return compareDocumentFields(hits[i].RawFields, hits[j].RawFields) < 0
})

fields := make([][]byte, len(hits))
for i, hit := range hits {
fields[i] = hit.RawFields
}
approveFields(t, filepath.Join("approvals", name), fields, dynamic...)
// Rewrite dynamic fields and sort them for repeatable diffs.
rewriteDynamic(t, fields, true, dynamic...)
sort.Slice(fields, func(i, j int) bool {
return compareDocumentFields(fields[i], fields[j]) < 0
})
approveFields(t, filepath.Join("approvals", name), fields)
}

// approveEventDocs compares the given event documents with
// the contents of the file in "<name>.approved.json".
//
// Any specified dynamic fields (e.g. @timestamp, observer.id)
// will be replaced with a static string for comparison.
//
// If the events differ, then the test will fail.
func approveEventDocs(t testing.TB, name string, eventDocs [][]byte, dynamic ...string) {
// rewriteDynamic rewrites all dynamic fields to have a known value, so dynamic
// fields don't affect diffs. The flattenedKeys parameter defines how the
// field should be queried in the source, if flattenedKeys is passed as true
// then the source will be queried for the dynamic fields as flattened keys.
func rewriteDynamic(t testing.TB, srcs [][]byte, flattenedKeys bool, dynamic ...string) {
t.Helper()

// Fields generated by the server (e.g. observer.*)
Expand All @@ -115,22 +113,44 @@ func approveEventDocs(t testing.TB, name string, eventDocs [][]byte, dynamic ...
"observer.version",
}, dynamic...)

// Rewrite all dynamic fields to have a known value,
// so dynamic fields don't affect diffs.
events := make([]interface{}, len(eventDocs))
for i, doc := range eventDocs {
for i := range srcs {
for _, field := range dynamic {
existing := gjson.GetBytes(doc, field)
if flattenedKeys {
field = strings.ReplaceAll(field, ".", "\\.")
}
existing := gjson.GetBytes(srcs[i], field)
if !existing.Exists() {
continue
}

var v interface{}
if existing.IsArray() {
v = []any{"dynamic"}
} else {
v = "dynamic"
}

var err error
doc, err = sjson.SetBytes(doc, field, "dynamic")
srcs[i], err = sjson.SetBytes(srcs[i], field, v)
if err != nil {
t.Fatal(err)
}
}
}
}

// approveEventDocs compares the given event documents with
// the contents of the file in "<name>.approved.json".
//
// Any specified dynamic fields (e.g. @timestamp, observer.id)
// will be replaced with a static string for comparison.
//
// If the events differ, then the test will fail.
func approveEventDocs(t testing.TB, name string, eventDocs [][]byte) {
t.Helper()

events := make([]interface{}, len(eventDocs))
for i, doc := range eventDocs {

var event map[string]interface{}
if err := json.Unmarshal(doc, &event); err != nil {
Expand All @@ -143,23 +163,9 @@ func approveEventDocs(t testing.TB, name string, eventDocs [][]byte, dynamic ...
approve(t, name, received)
}

func approveFields(t testing.TB, name string, docs [][]byte, dynamic ...string) {
func approveFields(t testing.TB, name string, docs [][]byte) {
t.Helper()

// Fields generated by the server (e.g. observer.*)
// agent which may change between tests.
//
// Ignore their values in comparisons, but compare
// existence: either the field exists in both, or neither.
dynamic = append([]string{
"ecs.version",
"event.ingested",
"observer.ephemeral_id",
"observer.hostname",
"observer.id",
"observer.version",
}, dynamic...)

// Rewrite all dynamic fields to have a known value,
// so dynamic fields don't affect diffs.
decodedDocs := make([]any, len(docs))
Expand All @@ -168,11 +174,6 @@ func approveFields(t testing.TB, name string, docs [][]byte, dynamic ...string)
if err := json.Unmarshal(doc, &fields); err != nil {
t.Fatal(err)
}
for _, field := range dynamic {
if _, ok := fields[field]; ok {
fields[field] = []any{"dynamic"}
}
}
decodedDocs[i] = fields
}

Expand Down