-
Notifications
You must be signed in to change notification settings - Fork 654
Expand file tree
/
Copy patherrorcode_test.go
More file actions
151 lines (131 loc) · 3.86 KB
/
errorcode_test.go
File metadata and controls
151 lines (131 loc) · 3.86 KB
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
package apierrors
import (
"flag"
"fmt"
"go/ast"
"go/parser"
"go/token"
"maps"
"os"
"slices"
"strings"
"sync"
"testing"
"github.com/stretchr/testify/require"
)
var generateFlag = flag.Bool("generate", false, "Run tests that generate code")
func TestErrorCodesMap(t *testing.T) {
cur := helpParseErrorCodes(t)
gen := errorCodesMap
for curCode, curName := range cur {
genName, ok := gen[curCode]
if !ok {
t.Fatalf("error code %q: (%v) missing in errorCodesMap",
curCode, curName)
}
if genName != curName {
t.Fatalf("error code %q: (%v) has different name (%q) in errorCodesMap",
curCode, curName, genName)
}
}
if a, b := len(cur), len(gen); a != b {
const msg = "generated code out of sync:" +
" errorCodeSlice len(%v) != constant declaration len (%v)"
t.Fatalf(msg, a, b)
}
}
func TestGenerate(t *testing.T) {
if !*generateFlag {
t.SkipNow()
}
ecm := helpParseErrorCodes(t)
ecs := slices.Sorted(maps.Keys(ecm))
var sb strings.Builder
sb.WriteString("package apierrors\n\n")
sb.WriteString("//go:generate go test -run TestGenerate -args -generate\n")
sb.WriteString("//go:generate go fmt\n\n")
{
sb.WriteString("var errorCodesMap = map[string]string{\n")
for _, ec := range ecs {
fmt.Fprintf(&sb, "\t%q: %q,\n", ec, ecm[ec])
}
sb.WriteString("}\n\n")
}
os.WriteFile("errorcode_gen.go", []byte(sb.String()), 0644)
}
func helpParseErrorCodes(t *testing.T) map[string]string {
ecm, err := parseErrorCodesOnce()
require.NoError(t, err)
require.NotEmpty(t, ecm)
return maps.Clone(ecm)
}
var parseErrorCodesOnce = sync.OnceValues(func() (map[string]string, error) {
return parseErrorCodes()
})
func parseErrorCodes() (map[string]string, error) {
data, err := os.ReadFile(`errorcode.go`)
if err != nil {
const msg = "parseErrorCodes: os.ReadFile(`errorcode.go`): %w"
return nil, fmt.Errorf(msg, err)
}
src := string(data)
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, "", src, parser.SkipObjectResolution)
if err != nil {
const msg = "parseErrorCodes: parser.ParseFile: %w"
return nil, fmt.Errorf(msg, err)
}
ecm := make(map[string]string)
for declIdx, decl := range f.Decls {
if err := parseErrorCodesDecl(ecm, declIdx, decl); err != nil {
return nil, fmt.Errorf("parseErrorCodes %w", err)
}
}
return ecm, nil
}
func parseErrorCodesDecl(ecm map[string]string, decIdx int, decl ast.Decl) error {
dec, ok := decl.(*ast.GenDecl)
if !ok || dec.Tok != token.CONST {
return nil
}
if n := len(dec.Specs); n == 0 {
return fmt.Errorf("decl[%d]: specs are empty", decIdx)
}
for idx, spec := range dec.Specs {
valSpec, ok := spec.(*ast.ValueSpec)
if !ok {
return fmt.Errorf("const[%d]: unexpected type: %T", idx, spec)
}
if n := len(valSpec.Names); n != 1 {
return fmt.Errorf("const[%d]: unexpected const len: %T", idx, n)
}
constName := valSpec.Names[0].Name
if !strings.HasPrefix(constName, "ErrorCode") {
return fmt.Errorf("const[%d]: missing ErrorCode prefix: %v", idx, constName)
}
if n := len(valSpec.Values); n != 1 {
return fmt.Errorf("const[%d]: unexpected const value len: %v", idx, n)
}
constExpr := valSpec.Values[0]
basicLit, ok := constExpr.(*ast.BasicLit)
if !ok {
return fmt.Errorf("const[%d]: unexpected const value expr type: %T", idx, constExpr)
}
constValue := basicLit.Value
if n := len(constValue); n <= 3 {
return fmt.Errorf("const[%d]: unexpected const value string len: %v (%q)",
idx, n, constValue)
}
if constValue[0] != '"' || constValue[len(constValue)-1] != '"' {
return fmt.Errorf("const[%d]: unexpected const value string quoting (%q)",
idx, constValue)
}
constValue = constValue[1 : len(constValue)-1]
if prev, found := ecm[constValue]; found {
msg := "const[%d]: duplicate error code: %q: already defined by %q"
return fmt.Errorf(msg, idx, constValue, prev)
}
ecm[constValue] = constName
}
return nil
}