Skip to content

Commit db2b469

Browse files
Merge pull request #456 from nextmv-io/merschformann/golden-rounding
Enhance golden file testing with rounding functionality
2 parents 6484c84 + c1317be commit db2b469

File tree

4 files changed

+130
-1
lines changed

4 files changed

+130
-1
lines changed

golden/config.go

+11
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,9 @@ type OutputProcessConfig struct {
162162
// VolatileRegexReplacements defines regex replacements to be applied to the
163163
// golden file before comparison.
164164
VolatileRegexReplacements []VolatileRegexReplacement
165+
// RoundingConfig defines how to round fields in the output before
166+
// comparison.
167+
RoundingConfig []RoundingConfig
165168
// VolatileDataFiles are files that contain volatile data and should get
166169
// post-processed to be more stable. This is only supported in directory
167170
// mode ([BashTest]) of golden bash testing, i.e., this will be ignored in
@@ -173,6 +176,14 @@ type OutputProcessConfig struct {
173176
RelativeDestination string
174177
}
175178

179+
// RoundingConfig defines how to round a field in the output before comparison.
180+
type RoundingConfig struct {
181+
// Key is the JSONPath-like key to the field that should be rounded.
182+
Key string
183+
// Precision is the number of decimal places to round to.
184+
Precision int
185+
}
186+
176187
// ExecutionConfig defines the configuration for non-SDK golden file tests.
177188
type ExecutionConfig struct {
178189
// Command is the command of the entrypoint of the app to be executed. E.g.,

golden/file.go

+6
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,8 @@ func comparison(
153153
inputPath string,
154154
config Config,
155155
) {
156+
var err error
157+
156158
goldenPath := inputPath + goldenExtension
157159
if config.OutputProcessConfig.RelativeDestination != "" {
158160
goldenPath = filepath.Join(
@@ -171,6 +173,10 @@ func comparison(
171173

172174
flattenedOutput = flatten(output)
173175
flattenedOutput = replaceTransient(flattenedOutput, config.TransientFields...)
176+
flattenedOutput, err = roundFields(flattenedOutput, config.OutputProcessConfig.RoundingConfig...)
177+
if err != nil {
178+
t.Fatal(err)
179+
}
174180
nestedOutput, err := nest(flattenedOutput)
175181
if err != nil {
176182
t.Fatal(err)

golden/map.go

+81-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package golden
22

33
import (
44
"fmt"
5+
"math"
56
"reflect"
7+
"regexp"
68
"strconv"
79
"strings"
810
"time"
@@ -22,17 +24,32 @@ func replaceTransient(
2224

2325
replaced := map[string]any{}
2426
for key, value := range original {
27+
// Keep the original value.
2528
replaced[key] = value
29+
30+
// Check if the field is meant to be replaced. If not, continue.
31+
// We also check for wildcard replacements that are meant to replace all
32+
// fields in a slice.
2633
replacement, isTransient := transientLookup[key]
27-
if !isTransient {
34+
cleanedKey := replaceIndicesInKeys(key)
35+
replacementCleaned, isTransientCleaned := transientLookup[cleanedKey]
36+
if !isTransient && !isTransientCleaned {
37+
// No replacement defined, continue and keep the original value.
2838
continue
2939
}
40+
if isTransientCleaned {
41+
replacement = replacementCleaned
42+
}
3043

44+
// Replace the value with the replacement value.
3145
if replacement != nil {
3246
replaced[key] = replacement
3347
continue
3448
}
3549

50+
// No replacement defined, we fall back to default stable values here
51+
// (based on type).
52+
3653
if stringValue, isString := value.(string); isString {
3754
if _, err := time.Parse(time.RFC3339, stringValue); err == nil {
3855
replaced[key] = StableTime
@@ -67,6 +84,69 @@ func replaceTransient(
6784
return replaced
6885
}
6986

87+
// roundFields rounds all the values in a map whose key is contained in the
88+
// variadic list of roundingConfigs. The rounded value has a stable value
89+
// according to the data type.
90+
func roundFields(
91+
original map[string]any,
92+
roundedFields ...RoundingConfig,
93+
) (map[string]any, error) {
94+
roundingLookup := map[string]int{}
95+
for _, field := range roundedFields {
96+
roundingLookup[field.Key] = field.Precision
97+
}
98+
99+
replaced := map[string]any{}
100+
for key, value := range original {
101+
// Keep the original value.
102+
replaced[key] = value
103+
104+
// Check if the field is meant to be rounded. If not, continue.
105+
// We also check for wildcard replacements that are meant to replace all
106+
// fields in a slice.
107+
cleanedKey := replaceIndicesInKeys(key)
108+
replacement, isRounded := roundingLookup[key]
109+
replacementCleaned, isRoundedCleaned := roundingLookup[cleanedKey]
110+
if !isRounded && !isRoundedCleaned {
111+
// No rounding defined, continue and keep the original value.
112+
continue
113+
}
114+
if isRoundedCleaned {
115+
replacement = replacementCleaned
116+
}
117+
118+
// We don't deal with negative precision values.
119+
if replacement < 0 {
120+
continue
121+
}
122+
123+
// Replace the value with the rounded value.
124+
if _, isFloat := value.(float64); isFloat {
125+
replaced[key] = round(value.(float64), replacement)
126+
continue
127+
}
128+
129+
// If the value was not a float, return an error.
130+
return nil, fmt.Errorf("field %s is not a float", key)
131+
}
132+
133+
return replaced, nil
134+
}
135+
136+
// round rounds a float64 value to a given precision.
137+
func round(value float64, precision int) float64 {
138+
shift := math.Pow(10, float64(precision))
139+
return math.Round(value*shift) / shift
140+
}
141+
142+
var keyIndexMatcher = regexp.MustCompile(`\[\d+\]`)
143+
144+
// replaceIndicesInKeys replaces all the indices in a key with "[]" to make it
145+
// easier to match them with configuration defined in jq-style.
146+
func replaceIndicesInKeys(key string) string {
147+
return keyIndexMatcher.ReplaceAllString(key, "[]")
148+
}
149+
70150
/*
71151
flatten takes a nested map and flattens it into a single level map. The
72152
flattening roughly follows the [JSONPath] standard. Please see test function to

golden/map_test.go

+32
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package golden
22

33
import (
4+
"fmt"
45
"reflect"
56
"testing"
67
)
@@ -464,3 +465,34 @@ func Test_nest(t *testing.T) {
464465
})
465466
}
466467
}
468+
469+
func Test_round(t *testing.T) {
470+
tests := []struct {
471+
num float64
472+
want float64
473+
precision int
474+
}{
475+
{
476+
num: 1.23456789,
477+
want: 1.235,
478+
precision: 3,
479+
},
480+
{
481+
num: 1.234567891234567891234,
482+
want: 1.23456789123456789123,
483+
precision: 20,
484+
},
485+
{
486+
num: 1.234567891234567891234,
487+
want: 1,
488+
precision: 0,
489+
},
490+
}
491+
for _, tt := range tests {
492+
t.Run(fmt.Sprintf("round %d", tt.precision), func(t *testing.T) {
493+
if got := round(tt.num, tt.precision); got != tt.want {
494+
t.Errorf("round() = %v, want %v", got, tt.want)
495+
}
496+
})
497+
}
498+
}

0 commit comments

Comments
 (0)