/
golden_response_checker.go
171 lines (143 loc) · 4.65 KB
/
golden_response_checker.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
package experimental
import (
"bufio"
"encoding/base64"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"os"
"strings"
"github.com/google/go-cmp/cmp"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
// CheckGoldenFramer calls CheckGoldenDataResponse using a data.Framer instead of a backend.DataResponse.
func CheckGoldenFramer(path string, f data.Framer, updateFile bool) error {
return CheckGoldenDataResponse(path, backend.FrameResponse(f), updateFile)
}
// CheckGoldenFrame calls CheckGoldenDataResponse using a single frame
func CheckGoldenFrame(path string, f *data.Frame, updateFile bool) error {
dr := backend.DataResponse{}
dr.Frames = data.Frames{f}
return CheckGoldenDataResponse(path, &dr, updateFile)
}
// CheckGoldenDataResponse will verify that the stored file matches the given data.DataResponse
// when the updateFile flag is set, this will both add errors to the response and update the saved file
func CheckGoldenDataResponse(path string, dr *backend.DataResponse, updateFile bool) error {
saved, err := readGoldenFile(path)
if err != nil {
return errorAfterUpdate(fmt.Errorf("error reading golden file: %s\n%s", path, err.Error()), path, dr, updateFile)
}
if diff := cmp.Diff(saved.Error, dr.Error); diff != "" {
return errorAfterUpdate(fmt.Errorf("errors mismatch %s (-want +got):\n%s", path, diff), path, dr, updateFile)
}
// When the frame count is different, you can check manually
if diff := cmp.Diff(len(saved.Frames), len(dr.Frames)); diff != "" {
return errorAfterUpdate(fmt.Errorf("frame count mismatch (-want +got):\n%s", diff), path, dr, updateFile)
}
errorString := ""
// Check each frame
for idx, frame := range dr.Frames {
expectedFrame := saved.Frames[idx]
if diff := cmp.Diff(expectedFrame, frame, data.FrameTestCompareOptions()...); diff != "" {
errorString += fmt.Sprintf("frame[%d] mismatch (-want +got):\n%s\n", idx, diff)
}
}
if len(errorString) > 0 {
return errorAfterUpdate(fmt.Errorf(errorString), path, dr, updateFile)
}
return nil // OK
}
func errorAfterUpdate(err error, path string, dr *backend.DataResponse, updateFile bool) error {
if updateFile {
_ = writeGoldenFile(path, dr)
log.Printf("golden file updated: %s\n", path)
}
return err
}
const binaryDataSection = "====== TEST DATA RESPONSE (arrow base64) ======"
func readGoldenFile(path string) (*backend.DataResponse, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
}
defer file.Close()
dr := &backend.DataResponse{}
foundDataSection := false
scanner := bufio.NewScanner(file)
fi, err := file.Stat()
if err != nil {
return nil, err
}
fsize := fi.Size()
buf := make([]byte, 0, bufio.MaxScanTokenSize)
scanner.Buffer(buf, int(fsize))
for scanner.Scan() {
line := scanner.Text()
if foundDataSection {
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue // skip lines without KEY=VALUE
}
key := parts[0]
val := parts[1]
switch key {
case "ERROR":
return nil, fmt.Errorf("error matching not yet supported: %s", line)
case "FRAME":
bytes, err := base64.StdEncoding.DecodeString(val)
if err != nil {
return nil, err
}
frame, err := data.UnmarshalArrowFrame(bytes)
if err != nil {
return nil, err
}
dr.Frames = append(dr.Frames, frame)
default:
return nil, fmt.Errorf("unknown saved key: %s", key)
}
} else if strings.HasPrefix(line, binaryDataSection) {
foundDataSection = true
}
}
if err := scanner.Err(); err != nil {
return nil, err // error reading file
}
if !foundDataSection {
return nil, fmt.Errorf("no saved result found in: %s", path)
}
return dr, nil
}
// The golden file has a text description at the top and a binary response at the bottom
// The text part is not used for testing, but aims to give a legible response format
func writeGoldenFile(path string, dr *backend.DataResponse) error {
str := "🌟 This was machine generated. Do not edit. 🌟\n"
if dr.Error != nil {
str = fmt.Sprintf("\nERROR: %+v", dr.Error)
}
if dr.Frames != nil {
for idx, frame := range dr.Frames {
str += fmt.Sprintf("\nFrame[%d] ", idx)
if frame.Meta != nil {
meta, _ := json.MarshalIndent(frame.Meta, "", " ")
str += string(meta)
}
table, _ := frame.StringTable(100, 10)
str += "\n" + table + "\n\n"
}
}
// Add the binary section flag
str += binaryDataSection
if dr.Error != nil {
str += "\nERROR=" + dr.Error.Error()
}
for _, frame := range dr.Frames {
bytes, _ := frame.MarshalArrow()
encoded := base64.StdEncoding.EncodeToString(bytes)
str += "\nFRAME=" + encoded
}
str += "\n"
return ioutil.WriteFile(path, []byte(str), 0600)
}