internal/testrunner/runners/pipeline/testresult.go (199 lines of code) (raw):
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.
package pipeline
import (
"bytes"
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/Masterminds/semver/v3"
"github.com/google/go-cmp/cmp"
"github.com/pmezard/go-difflib/difflib"
"github.com/elastic/elastic-package/internal/common"
"github.com/elastic/elastic-package/internal/formatter"
"github.com/elastic/elastic-package/internal/testrunner"
)
const expectedTestResultSuffix = "-expected.json"
type testResult struct {
events []json.RawMessage
}
type testResultDefinition struct {
Expected []json.RawMessage `json:"expected"`
}
func writeTestResult(testCasePath string, result *testResult, specVersion semver.Version) error {
testCaseDir := filepath.Dir(testCasePath)
testCaseFile := filepath.Base(testCasePath)
data, err := marshalTestResultDefinition(result, specVersion)
if err != nil {
return fmt.Errorf("marshalling test result failed: %w", err)
}
err = os.WriteFile(filepath.Join(testCaseDir, expectedTestResultFile(testCaseFile)), append(data, '\n'), 0644)
if err != nil {
return fmt.Errorf("writing test result failed: %w", err)
}
return nil
}
func compareResults(testCasePath string, config *testConfig, result *testResult, specVersion semver.Version) error {
resultsWithoutDynamicFields, err := adjustTestResult(result, config)
if err != nil {
return fmt.Errorf("can't adjust test results: %w", err)
}
actual, err := marshalTestResultDefinition(resultsWithoutDynamicFields, specVersion)
if err != nil {
return fmt.Errorf("marshalling actual test results failed: %w", err)
}
expectedResults, err := readExpectedTestResult(testCasePath, config)
if err != nil {
return fmt.Errorf("reading expected test result failed: %w", err)
}
expected, err := marshalTestResultDefinition(expectedResults, specVersion)
if err != nil {
return fmt.Errorf("marshalling expected test results failed: %w", err)
}
report, err := diffJson(expected, actual, specVersion)
if err != nil {
return fmt.Errorf("comparing expected test result: %w", err)
}
if report != "" {
return testrunner.ErrTestCaseFailed{
Reason: "Expected results are different from actual ones",
Details: report,
}
}
return nil
}
func compareJsonNumbers(a, b json.Number) bool {
if a == b {
// Equal literals, so they are the same.
return true
}
if inta, err := a.Int64(); err == nil {
if intb, err := b.Int64(); err == nil {
return inta == intb
}
if floatb, err := b.Float64(); err == nil {
return float64(inta) == floatb
}
} else if floata, err := a.Float64(); err == nil {
if intb, err := b.Int64(); err == nil {
return floata == float64(intb)
}
if floatb, err := b.Float64(); err == nil {
return floata == floatb
}
}
return false
}
func diffJson(want, got []byte, specVersion semver.Version) (string, error) {
var gotVal, wantVal interface{}
err := formatter.JSONUnmarshalUsingNumber(want, &wantVal)
if err != nil {
return "", fmt.Errorf("invalid want value: %w", err)
}
err = formatter.JSONUnmarshalUsingNumber(got, &gotVal)
if err != nil {
return "", fmt.Errorf("invalid got value: %w", err)
}
if cmp.Equal(gotVal, wantVal, cmp.Comparer(compareJsonNumbers)) {
return "", nil
}
got, err = marshalNormalizedJSON(gotVal, specVersion)
if err != nil {
return "", err
}
want, err = marshalNormalizedJSON(wantVal, specVersion)
if err != nil {
return "", err
}
var buf bytes.Buffer
err = difflib.WriteUnifiedDiff(&buf, difflib.UnifiedDiff{
A: difflib.SplitLines(string(want)),
B: difflib.SplitLines(string(got)),
FromFile: "want",
ToFile: "got",
Context: 3,
})
return buf.String(), err
}
func readExpectedTestResult(testCasePath string, config *testConfig) (*testResult, error) {
testCaseDir := filepath.Dir(testCasePath)
testCaseFile := filepath.Base(testCasePath)
path := filepath.Join(testCaseDir, expectedTestResultFile(testCaseFile))
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading test result file failed: %w", err)
}
u, err := unmarshalTestResult(data)
if err != nil {
return nil, fmt.Errorf("unmarshalling expected test result failed: %w", err)
}
adjusted, err := adjustTestResult(u, config)
if err != nil {
return nil, fmt.Errorf("adjusting test result failed: %w", err)
}
return adjusted, nil
}
func adjustTestResult(result *testResult, config *testConfig) (*testResult, error) {
if config == nil || config.DynamicFields == nil {
return result, nil
}
var stripped testResult
for _, event := range result.events {
if event == nil {
stripped.events = append(stripped.events, nil)
continue
}
var m common.MapStr
err := formatter.JSONUnmarshalUsingNumber(event, &m)
if err != nil {
return nil, fmt.Errorf("can't unmarshal event: %s: %w", string(event), err)
}
if config != nil && config.DynamicFields != nil {
// Strip dynamic fields from test result
for key := range config.DynamicFields {
err := m.Delete(key)
if err != nil && err != common.ErrKeyNotFound {
return nil, fmt.Errorf("can't remove dynamic field: %w", err)
}
}
}
b, err := json.Marshal(&m)
if err != nil {
return nil, fmt.Errorf("can't marshal event: %w", err)
}
stripped.events = append(stripped.events, b)
}
return &stripped, nil
}
func unmarshalTestResult(body []byte) (*testResult, error) {
var trd testResultDefinition
err := formatter.JSONUnmarshalUsingNumber(body, &trd)
if err != nil {
return nil, fmt.Errorf("unmarshalling test result failed: %w", err)
}
var tr testResult
tr.events = append(tr.events, trd.Expected...)
return &tr, nil
}
func marshalTestResultDefinition(result *testResult, specVersion semver.Version) ([]byte, error) {
var trd testResultDefinition
trd.Expected = result.events
body, err := marshalNormalizedJSON(trd, specVersion)
if err != nil {
return nil, fmt.Errorf("marshalling test result definition failed: %w", err)
}
return body, nil
}
// marshalNormalizedJSON marshals test results ensuring that field
// order remains consistent independent of field order returned by
// ES to minimize diff noise during changes.
func marshalNormalizedJSON(v interface{}, specVersion semver.Version) ([]byte, error) {
jsonFormatter := formatter.JSONFormatterBuilder(specVersion)
msg, err := jsonFormatter.Encode(v)
if err != nil {
return msg, err
}
var obj interface{}
err = formatter.JSONUnmarshalUsingNumber(msg, &obj)
if err != nil {
return msg, err
}
return jsonFormatter.Encode(obj)
}
func expectedTestResultFile(testFile string) string {
return fmt.Sprintf("%s%s", testFile, expectedTestResultSuffix)
}