@voxgig/sdkgen 4.2.8 → 4.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/voxgig-sdkgen +1 -1
- package/dist/helpers/naming.d.ts +2 -1
- package/dist/helpers/naming.js +50 -12
- package/dist/helpers/naming.js.map +1 -1
- package/dist/sdkgen.d.ts +2 -2
- package/dist/sdkgen.js +4 -3
- package/dist/sdkgen.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/project/.sdk/src/cmp/zig/Config_zig.ts +1 -1
- package/project/.sdk/tm/c/tests/primary_corpus_test.c +565 -0
- package/project/.sdk/tm/c/utility/prepare_method.c +3 -1
- package/project/.sdk/tm/c/utility/transform_request.c +4 -3
- package/project/.sdk/tm/clojure/src/sdk/core.clj +55 -5
- package/project/.sdk/tm/clojure/test/sdk/test/primary.clj +171 -1
- package/project/.sdk/tm/clojure/test/sdk/test/struct_corpus.clj +13 -2
- package/project/.sdk/tm/clojure/test/sdk/test_runner.clj +2 -0
- package/project/.sdk/tm/csharp/test/CustomUtilityTest.cs +104 -0
- package/project/.sdk/tm/csharp/utility/MakeOptions.cs +82 -2
- package/project/.sdk/tm/elixir/lib/projectname/utility.ex +50 -2
- package/project/.sdk/tm/elixir/test/primary_utility_test.exs +18 -200
- package/project/.sdk/tm/elixir/test/support/struct_corpus.ex +413 -2
- package/project/.sdk/tm/go/test/custom_utility_test.go +103 -0
- package/project/.sdk/tm/go/test/feature_corpus_test.go +550 -0
- package/project/.sdk/tm/go/utility/make_options.go +7 -1
- package/project/.sdk/tm/go/utility/register.go +194 -0
- package/project/.sdk/tm/java/test/CustomUtilityTest.java +55 -0
- package/project/.sdk/tm/java/test/FeatureCorpusTest.java +463 -0
- package/project/.sdk/tm/java/utility/MakeOptions.java +11 -2
- package/project/.sdk/tm/java/utility/Register.java +91 -0
- package/project/.sdk/tm/js/test/feature/Corpus.test.js +285 -0
- package/project/.sdk/tm/kotlin/utility/MakeOptions.kt +56 -2
- package/project/.sdk/tm/lua/utility/make_options.lua +22 -2
- package/project/.sdk/tm/ocaml/Makefile +15 -4
- package/project/.sdk/tm/ocaml/sdk_runtime.ml +8 -1
- package/project/.sdk/tm/ocaml/test/corpus_runner.ml +185 -0
- package/project/.sdk/tm/ocaml/test/primary_utility_test.ml +238 -0
- package/project/.sdk/tm/ocaml/test/struct_corpus.ml +5 -160
- package/project/.sdk/tm/perl/t/feature_corpus.t +345 -0
- package/project/.sdk/tm/perl/utility/make_options.pm +33 -1
- package/project/.sdk/tm/php/core/Context.php +3 -0
- package/project/.sdk/tm/php/core/Control.php +13 -0
- package/project/.sdk/tm/php/core/Error.php +12 -0
- package/project/.sdk/tm/php/test/FeatureCorpusTest.php +376 -0
- package/project/.sdk/tm/php/utility/MakeOptions.php +30 -1
- package/project/.sdk/tm/py/pkg/utility/make_options.py +42 -1
- package/project/.sdk/tm/py/test/test_feature_corpus.py +309 -0
- package/project/.sdk/tm/rb/test/feature_corpus_test.rb +281 -0
- package/project/.sdk/tm/rb/utility/make_options.rb +32 -1
- package/project/.sdk/tm/scala/Makefile +1 -0
- package/project/.sdk/tm/scala/sdktest/PrimaryCorpus.scala +294 -0
- package/project/.sdk/tm/scala/sdktest/StructCorpus.scala +0 -0
- package/project/.sdk/tm/scala/utility/Make.scala +49 -2
- package/project/.sdk/tm/scala/utility/Prepare.scala +3 -1
- package/project/.sdk/tm/swift/Sources/ProjectNameSDK/utility/MakeOptions.swift +21 -3
- package/project/.sdk/tm/ts/src/feature/test/TestFeature.ts +13 -2
- package/project/.sdk/tm/ts/test/feature/Corpus.test.ts +287 -0
- package/project/.sdk/tm/zig/core/utility.zig +8 -1
- package/project/.sdk/tm/zig/test/primary_utility_test.zig +445 -0
- package/project/.sdk/tm/zig/test/struct_runner.zig +238 -0
- package/project/sdkgen-package.json +1 -1
- package/src/helpers/naming.ts +54 -12
- package/src/sdkgen.ts +2 -1
|
@@ -77,4 +77,107 @@ func TestCustomUtility(t *testing.T) {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
})
|
|
80
|
+
|
|
81
|
+
// The half the subtest above cannot see. Those keys are ALIASES - `auth`,
|
|
82
|
+
// `body`, `spec` - and no utility member has those names, so landing in
|
|
83
|
+
// Custom is the right outcome for them and the assertion passes whether or
|
|
84
|
+
// not overriding works at all.
|
|
85
|
+
//
|
|
86
|
+
// A key that DOES name a member must replace it. That is the documented
|
|
87
|
+
// contract, it is what ts does, and it was silently absent here: every
|
|
88
|
+
// entry went to Custom, which nothing reads, so `utility: {"fetcher": ...}`
|
|
89
|
+
// did nothing while ts honoured it.
|
|
90
|
+
t.Run("a real utility member is replaced, not shelved", func(t *testing.T) {
|
|
91
|
+
reached := 0
|
|
92
|
+
scripted := func(ctx *sdk.Context, fullurl string, fetchdef map[string]any) (any, error) {
|
|
93
|
+
reached++
|
|
94
|
+
return map[string]any{
|
|
95
|
+
"status": 200,
|
|
96
|
+
"statusText": "OK",
|
|
97
|
+
"headers": map[string]any{},
|
|
98
|
+
"body": map[string]any{"ok": true},
|
|
99
|
+
}, nil
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// NewProjectNameSDK, not TestSDK. The `test` feature is
|
|
103
|
+
// transport: 'base' - it REPLACES the transport by design - so a client
|
|
104
|
+
// in test mode would shadow the scripted fetcher and this would assert
|
|
105
|
+
// nothing.
|
|
106
|
+
//
|
|
107
|
+
// `scripted` is passed AS DECLARED, with no conversion to
|
|
108
|
+
// sdk.FetcherFunc. That is how a caller writes it, and it is the case
|
|
109
|
+
// that was broken: a plain function literal in a map[string]any has the
|
|
110
|
+
// unnamed signature, so an override that only accepted the named type
|
|
111
|
+
// shelved it in Custom. Converting here would have hidden that.
|
|
112
|
+
client := sdk.NewProjectNameSDK(map[string]any{
|
|
113
|
+
"utility": map[string]any{"fetcher": scripted},
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
u := client.GetUtility()
|
|
117
|
+
|
|
118
|
+
if u.Fetcher == nil {
|
|
119
|
+
t.Fatal("Fetcher is nil")
|
|
120
|
+
}
|
|
121
|
+
if _, shelved := u.Custom["fetcher"]; shelved {
|
|
122
|
+
t.Error("fetcher was shelved in Custom instead of replacing the member")
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Behaviour, not identity: a func value cannot be compared, so drive it.
|
|
126
|
+
ctx := u.MakeContext(map[string]any{}, client.GetRootCtx())
|
|
127
|
+
if _, err := u.Fetcher(ctx, "http://example.test/probe", map[string]any{}); err != nil {
|
|
128
|
+
t.Fatalf("scripted fetcher returned an error: %v", err)
|
|
129
|
+
}
|
|
130
|
+
if reached != 1 {
|
|
131
|
+
t.Errorf("the scripted fetcher was not installed: reached %d times, want 1", reached)
|
|
132
|
+
}
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
// The other spelling of the same value. A caller who names the exported
|
|
136
|
+
// type gets a value whose dynamic type is core.FetcherFunc, which asserts
|
|
137
|
+
// to the named type and NOT to the unnamed signature - the mirror image of
|
|
138
|
+
// the subtest above, and broken by any fix that swaps one for the other.
|
|
139
|
+
t.Run("a converted fetcher is accepted too", func(t *testing.T) {
|
|
140
|
+
reached := 0
|
|
141
|
+
client := sdk.NewProjectNameSDK(map[string]any{
|
|
142
|
+
"utility": map[string]any{
|
|
143
|
+
"fetcher": sdk.FetcherFunc(func(
|
|
144
|
+
ctx *sdk.Context, fullurl string, fetchdef map[string]any,
|
|
145
|
+
) (any, error) {
|
|
146
|
+
reached++
|
|
147
|
+
return map[string]any{
|
|
148
|
+
"status": 200,
|
|
149
|
+
"statusText": "OK",
|
|
150
|
+
"headers": map[string]any{},
|
|
151
|
+
"body": map[string]any{"ok": true},
|
|
152
|
+
}, nil
|
|
153
|
+
}),
|
|
154
|
+
},
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
u := client.GetUtility()
|
|
158
|
+
if _, shelved := u.Custom["fetcher"]; shelved {
|
|
159
|
+
t.Error("a converted fetcher was shelved in Custom")
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
ctx := u.MakeContext(map[string]any{}, client.GetRootCtx())
|
|
163
|
+
if _, err := u.Fetcher(ctx, "http://example.test/probe", map[string]any{}); err != nil {
|
|
164
|
+
t.Fatalf("scripted fetcher returned an error: %v", err)
|
|
165
|
+
}
|
|
166
|
+
if reached != 1 {
|
|
167
|
+
t.Errorf("the converted fetcher was not installed: reached %d times, want 1", reached)
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
// An unknown key must still be attached rather than dropped, so the two
|
|
172
|
+
// halves cannot be satisfied by a switch that also swallows extras.
|
|
173
|
+
t.Run("an unknown key is still attached", func(t *testing.T) {
|
|
174
|
+
client := sdk.NewProjectNameSDK(map[string]any{
|
|
175
|
+
"utility": map[string]any{
|
|
176
|
+
"notAUtilityMember": func() string { return "EXTRA" },
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
if _, ok := client.GetUtility().Custom["notAUtilityMember"]; !ok {
|
|
180
|
+
t.Error("an unknown utility key was dropped instead of kept in Custom")
|
|
181
|
+
}
|
|
182
|
+
})
|
|
80
183
|
}
|
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
package sdktest
|
|
2
|
+
|
|
3
|
+
// Feature behaviour, driven by the SHARED corpus.
|
|
4
|
+
//
|
|
5
|
+
// The same route primary_utility_test.go takes for the utilities:
|
|
6
|
+
// language-neutral cases in .sdk/test/test.json, executed against THIS
|
|
7
|
+
// generated SDK. The feature is the ordinary compiled type, built by the
|
|
8
|
+
// generated config, installed by the generated constructor, and driven by a
|
|
9
|
+
// real entity operation. Not a miniature of the pipeline - that is what
|
|
10
|
+
// feature_harness_test.go does, and a miniature can only be as right as the
|
|
11
|
+
// miniature.
|
|
12
|
+
//
|
|
13
|
+
// Everything in a case is data. The two pieces go writes for itself are
|
|
14
|
+
// turning scripted responses into a FetcherFunc, and reading the record back
|
|
15
|
+
// off the feature (go keeps the aggregates on the feature value, where ts
|
|
16
|
+
// keeps them on the client).
|
|
17
|
+
|
|
18
|
+
import (
|
|
19
|
+
"encoding/json"
|
|
20
|
+
"errors"
|
|
21
|
+
"fmt"
|
|
22
|
+
"reflect"
|
|
23
|
+
"strings"
|
|
24
|
+
"testing"
|
|
25
|
+
|
|
26
|
+
sdk "GOMODULE"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
// Features with a corpus section. A name here with no section is a skip, not
|
|
30
|
+
// a failure: an SDK generated without the feature has nothing to run.
|
|
31
|
+
var featureCorpusNames = []string{"cost"}
|
|
32
|
+
|
|
33
|
+
// The standard operation names, in the order the runner prefers them. Every
|
|
34
|
+
// entity declares every CRUD method, so an op that the API does not define
|
|
35
|
+
// errors at runtime rather than failing to compile - which is why usable
|
|
36
|
+
// operations are found by DRIVING them, below.
|
|
37
|
+
var featureCorpusOps = []string{"Load", "List", "Create", "Update", "Remove"}
|
|
38
|
+
|
|
39
|
+
// One operation this SDK can actually perform.
|
|
40
|
+
type fcOp struct {
|
|
41
|
+
key string // "<entity>.<op>", how features attribute it
|
|
42
|
+
accessor string // the client method returning the entity
|
|
43
|
+
method string // the op method on that entity
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// fcFetcher builds a scripted transport from a case's `res` list. Responses
|
|
47
|
+
// are consumed in order and the last one repeats, so a case that does not
|
|
48
|
+
// care how many attempts happen need only declare one.
|
|
49
|
+
func fcFetcher(res []any) sdk.FetcherFunc {
|
|
50
|
+
n := -1
|
|
51
|
+
return func(ctx *sdk.Context, fullurl string, fetchdef map[string]any) (any, error) {
|
|
52
|
+
n++
|
|
53
|
+
var spec map[string]any
|
|
54
|
+
if len(res) > 0 {
|
|
55
|
+
i := n
|
|
56
|
+
if i >= len(res) {
|
|
57
|
+
i = len(res) - 1
|
|
58
|
+
}
|
|
59
|
+
spec, _ = res[i].(map[string]any)
|
|
60
|
+
}
|
|
61
|
+
if spec == nil {
|
|
62
|
+
spec = map[string]any{}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if thrown, _ := spec["throw"].(bool); thrown {
|
|
66
|
+
return nil, fmt.Errorf("scripted transport failure")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
status := 200
|
|
70
|
+
if s, ok := fcNum(spec["status"]); ok {
|
|
71
|
+
status = int(s)
|
|
72
|
+
}
|
|
73
|
+
statusText := "OK"
|
|
74
|
+
if status >= 400 {
|
|
75
|
+
statusText = "ERR"
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
headers := map[string]any{}
|
|
79
|
+
if h, ok := spec["headers"].(map[string]any); ok {
|
|
80
|
+
for k, v := range h {
|
|
81
|
+
headers[k] = v
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
body := spec["body"]
|
|
86
|
+
if body == nil {
|
|
87
|
+
body = map[string]any{}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The shape the real fetcher returns: the PARSED body comes back
|
|
91
|
+
// through a `json` thunk, and `body` is the raw string. makeResult
|
|
92
|
+
// reads the thunk, so a scripted response that only set `body` would
|
|
93
|
+
// look like an empty result - which reads as a feature defect rather
|
|
94
|
+
// than a mis-shaped script.
|
|
95
|
+
raw, _ := json.Marshal(body)
|
|
96
|
+
|
|
97
|
+
return map[string]any{
|
|
98
|
+
"status": status,
|
|
99
|
+
"statusText": statusText,
|
|
100
|
+
"headers": headers,
|
|
101
|
+
"json": (func() any)(func() any { return body }),
|
|
102
|
+
"body": string(raw),
|
|
103
|
+
}, nil
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// fcClient builds a client the way a caller would: the generated constructor,
|
|
108
|
+
// the feature list from the case, and the scripted transport through the
|
|
109
|
+
// documented `utility.fetcher` override.
|
|
110
|
+
//
|
|
111
|
+
// NewProjectNameSDK, not TestSDK: the `test` feature is transport: 'base' and
|
|
112
|
+
// REPLACES the transport, so a client in test mode would shadow the script.
|
|
113
|
+
func fcClient(kase map[string]any) *sdk.ProjectNameSDK {
|
|
114
|
+
res, _ := kase["res"].([]any)
|
|
115
|
+
opts := map[string]any{
|
|
116
|
+
"utility": map[string]any{"fetcher": fcFetcher(res)},
|
|
117
|
+
}
|
|
118
|
+
if f, ok := kase["feature"]; ok {
|
|
119
|
+
opts["feature"] = f
|
|
120
|
+
}
|
|
121
|
+
return sdk.NewProjectNameSDK(opts)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// fcCandidates lists the operations this SDK declares, in a stable order.
|
|
125
|
+
//
|
|
126
|
+
// The corpus cannot name an entity - it is shared by SDKs with none in common
|
|
127
|
+
// - so the runner finds them here. An entity accessor is a client method
|
|
128
|
+
// taking one options map and returning something that answers GetName().
|
|
129
|
+
func fcCandidates(client *sdk.ProjectNameSDK) []fcOp {
|
|
130
|
+
out := []fcOp{}
|
|
131
|
+
|
|
132
|
+
cv := reflect.ValueOf(client)
|
|
133
|
+
ct := cv.Type()
|
|
134
|
+
mapType := reflect.TypeOf(map[string]any{})
|
|
135
|
+
|
|
136
|
+
names := []string{}
|
|
137
|
+
byName := map[string]reflect.Value{}
|
|
138
|
+
|
|
139
|
+
for i := 0; i < ct.NumMethod(); i++ {
|
|
140
|
+
m := ct.Method(i)
|
|
141
|
+
mt := m.Type
|
|
142
|
+
// (receiver, entopts) -> entity
|
|
143
|
+
if mt.NumIn() != 2 || mt.NumOut() != 1 || mt.In(1) != mapType {
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
ent := cv.Method(i).Call([]reflect.Value{reflect.ValueOf(map[string]any(nil))})[0]
|
|
147
|
+
if !ent.IsValid() || (ent.Kind() == reflect.Ptr && ent.IsNil()) {
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
gn := ent.MethodByName("GetName")
|
|
151
|
+
if !gn.IsValid() || gn.Type().NumIn() != 0 || gn.Type().NumOut() != 1 ||
|
|
152
|
+
gn.Type().Out(0).Kind() != reflect.String {
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
entname := gn.Call(nil)[0].String()
|
|
156
|
+
if entname == "" {
|
|
157
|
+
continue
|
|
158
|
+
}
|
|
159
|
+
names = append(names, entname)
|
|
160
|
+
byName[entname] = ent
|
|
161
|
+
_ = m
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Sorted, so the choice of operation is stable across runs.
|
|
165
|
+
for i := 0; i < len(names); i++ {
|
|
166
|
+
for j := i + 1; j < len(names); j++ {
|
|
167
|
+
if names[j] < names[i] {
|
|
168
|
+
names[i], names[j] = names[j], names[i]
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for _, entname := range names {
|
|
174
|
+
ent := byName[entname]
|
|
175
|
+
for _, opname := range featureCorpusOps {
|
|
176
|
+
om := ent.MethodByName(opname)
|
|
177
|
+
if !om.IsValid() || om.Type().NumIn() != 2 || om.Type().NumOut() != 2 {
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
out = append(out, fcOp{
|
|
181
|
+
key: entname + "." + strings.ToLower(opname),
|
|
182
|
+
accessor: entname,
|
|
183
|
+
method: opname,
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return out
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// fcInvoke performs one operation on a client, by entity name and method.
|
|
191
|
+
func fcInvoke(client *sdk.ProjectNameSDK, op fcOp, ctrl map[string]any) error {
|
|
192
|
+
cv := reflect.ValueOf(client)
|
|
193
|
+
ct := cv.Type()
|
|
194
|
+
mapType := reflect.TypeOf(map[string]any{})
|
|
195
|
+
|
|
196
|
+
for i := 0; i < ct.NumMethod(); i++ {
|
|
197
|
+
mt := ct.Method(i).Type
|
|
198
|
+
if mt.NumIn() != 2 || mt.NumOut() != 1 || mt.In(1) != mapType {
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
ent := cv.Method(i).Call([]reflect.Value{reflect.ValueOf(map[string]any(nil))})[0]
|
|
202
|
+
gn := ent.MethodByName("GetName")
|
|
203
|
+
if !gn.IsValid() || gn.Type().NumIn() != 0 || gn.Type().NumOut() != 1 ||
|
|
204
|
+
gn.Type().Out(0).Kind() != reflect.String {
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
if gn.Call(nil)[0].String() != op.accessor {
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
om := ent.MethodByName(op.method)
|
|
211
|
+
if !om.IsValid() {
|
|
212
|
+
return fmt.Errorf("no method %s on entity %s", op.method, op.accessor)
|
|
213
|
+
}
|
|
214
|
+
rets := om.Call([]reflect.Value{
|
|
215
|
+
reflect.ValueOf(map[string]any{}),
|
|
216
|
+
reflect.ValueOf(ctrl),
|
|
217
|
+
})
|
|
218
|
+
if err, ok := rets[1].Interface().(error); ok && err != nil {
|
|
219
|
+
return err
|
|
220
|
+
}
|
|
221
|
+
return nil
|
|
222
|
+
}
|
|
223
|
+
return fmt.Errorf("no entity accessor for %s", op.accessor)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// fcUsableOps picks operations by DRIVING them: an op is usable when it
|
|
227
|
+
// completes against a plain 200 with no feature active. Declared operations
|
|
228
|
+
// are not all callable with no arguments (a required path parameter, a body),
|
|
229
|
+
// and a case failing for that reason would read as a feature defect.
|
|
230
|
+
func fcUsableOps(want int) []fcOp {
|
|
231
|
+
picked := []fcOp{}
|
|
232
|
+
probe := fcClient(map[string]any{})
|
|
233
|
+
for _, cand := range fcCandidates(probe) {
|
|
234
|
+
client := fcClient(map[string]any{})
|
|
235
|
+
if err := fcInvoke(client, cand, map[string]any{}); err != nil {
|
|
236
|
+
continue
|
|
237
|
+
}
|
|
238
|
+
picked = append(picked, cand)
|
|
239
|
+
if len(picked) >= want {
|
|
240
|
+
break
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return picked
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// fcResolve replaces #OPn throughout a case, keys included.
|
|
247
|
+
func fcResolve(node any, tokens map[string]string) any {
|
|
248
|
+
switch v := node.(type) {
|
|
249
|
+
case string:
|
|
250
|
+
out := v
|
|
251
|
+
for tok, val := range tokens {
|
|
252
|
+
out = strings.ReplaceAll(out, tok, val)
|
|
253
|
+
}
|
|
254
|
+
return out
|
|
255
|
+
case []any:
|
|
256
|
+
out := make([]any, len(v))
|
|
257
|
+
for i, item := range v {
|
|
258
|
+
out[i] = fcResolve(item, tokens)
|
|
259
|
+
}
|
|
260
|
+
return out
|
|
261
|
+
case map[string]any:
|
|
262
|
+
out := map[string]any{}
|
|
263
|
+
for k, item := range v {
|
|
264
|
+
key, _ := fcResolve(k, tokens).(string)
|
|
265
|
+
out[key] = fcResolve(item, tokens)
|
|
266
|
+
}
|
|
267
|
+
return out
|
|
268
|
+
}
|
|
269
|
+
return node
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// fcTokensUsed reports the highest #OPn a case mentions. A case wanting more
|
|
273
|
+
// operations than this SDK offers is skipped rather than failed.
|
|
274
|
+
func fcTokensUsed(kase map[string]any) int {
|
|
275
|
+
raw, err := json.Marshal(kase)
|
|
276
|
+
if err != nil {
|
|
277
|
+
return 0
|
|
278
|
+
}
|
|
279
|
+
max := 0
|
|
280
|
+
s := string(raw)
|
|
281
|
+
for i := 0; i+4 < len(s); i++ {
|
|
282
|
+
if s[i:i+3] != "#OP" {
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
285
|
+
n := 0
|
|
286
|
+
j := i + 3
|
|
287
|
+
for j < len(s) && s[j] >= '0' && s[j] <= '9' {
|
|
288
|
+
n = n*10 + int(s[j]-'0')
|
|
289
|
+
j++
|
|
290
|
+
}
|
|
291
|
+
if n > max {
|
|
292
|
+
max = n
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return max
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
func fcNum(v any) (float64, bool) {
|
|
299
|
+
switch n := v.(type) {
|
|
300
|
+
case float64:
|
|
301
|
+
return n, true
|
|
302
|
+
case float32:
|
|
303
|
+
return float64(n), true
|
|
304
|
+
case int:
|
|
305
|
+
return float64(n), true
|
|
306
|
+
case int64:
|
|
307
|
+
return float64(n), true
|
|
308
|
+
case json.Number:
|
|
309
|
+
f, err := n.Float64()
|
|
310
|
+
return f, err == nil
|
|
311
|
+
}
|
|
312
|
+
return 0, false
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// fcSubset asserts that `actual` contains `expect`, recursively. Cases assert
|
|
316
|
+
// only the fields they are about, so a full deep-equal would force every case
|
|
317
|
+
// to restate the whole record.
|
|
318
|
+
//
|
|
319
|
+
// `actual` is a Go value, not a map: the aggregates live on the feature as
|
|
320
|
+
// typed structs, so an expected key is matched to an exported field by
|
|
321
|
+
// capitalising it.
|
|
322
|
+
func fcSubset(t *testing.T, actual any, expect any, path string) {
|
|
323
|
+
t.Helper()
|
|
324
|
+
|
|
325
|
+
if em, ok := expect.(map[string]any); ok {
|
|
326
|
+
for k, want := range em {
|
|
327
|
+
got, found := fcMember(actual, k)
|
|
328
|
+
if !found {
|
|
329
|
+
t.Errorf("%s.%s: no such member", path, k)
|
|
330
|
+
continue
|
|
331
|
+
}
|
|
332
|
+
fcSubset(t, got, want, path+"."+k)
|
|
333
|
+
}
|
|
334
|
+
return
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if wn, ok := fcNum(expect); ok {
|
|
338
|
+
gn, ok := fcNum(actual)
|
|
339
|
+
if !ok {
|
|
340
|
+
t.Errorf("%s: expected number %v, got %v", path, expect, actual)
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
// Money is float arithmetic; compare with a tolerance far below any
|
|
344
|
+
// amount a case states.
|
|
345
|
+
if diff := gn - wn; diff > 1e-9 || diff < -1e-9 {
|
|
346
|
+
t.Errorf("%s: got %v, want %v", path, gn, wn)
|
|
347
|
+
}
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if fmt.Sprintf("%v", actual) != fmt.Sprintf("%v", expect) {
|
|
352
|
+
t.Errorf("%s: got %v, want %v", path, actual, expect)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// fcMember reads one member from a struct (by capitalised field name), a
|
|
357
|
+
// pointer to one, or a map (by key).
|
|
358
|
+
func fcMember(actual any, key string) (any, bool) {
|
|
359
|
+
if actual == nil {
|
|
360
|
+
return nil, false
|
|
361
|
+
}
|
|
362
|
+
v := reflect.ValueOf(actual)
|
|
363
|
+
for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
|
|
364
|
+
if v.IsNil() {
|
|
365
|
+
return nil, false
|
|
366
|
+
}
|
|
367
|
+
v = v.Elem()
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
switch v.Kind() {
|
|
371
|
+
case reflect.Map:
|
|
372
|
+
mv := v.MapIndex(reflect.ValueOf(key))
|
|
373
|
+
if !mv.IsValid() {
|
|
374
|
+
return nil, false
|
|
375
|
+
}
|
|
376
|
+
return mv.Interface(), true
|
|
377
|
+
case reflect.Struct:
|
|
378
|
+
f := v.FieldByName(strings.ToUpper(key[:1]) + key[1:])
|
|
379
|
+
if !f.IsValid() {
|
|
380
|
+
return nil, false
|
|
381
|
+
}
|
|
382
|
+
return f.Interface(), true
|
|
383
|
+
}
|
|
384
|
+
return nil, false
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// fcRecord finds the named feature on the client and hands back the value
|
|
388
|
+
// carrying its aggregates. go keeps them on the feature; ts keeps them on the
|
|
389
|
+
// client. Same data, different home.
|
|
390
|
+
//
|
|
391
|
+
// Returned as the feature value itself, NOT type-asserted to a concrete
|
|
392
|
+
// feature type. Naming one here would be a compile-time reference to a
|
|
393
|
+
// feature the project may not have: `target add` trims unselected features,
|
|
394
|
+
// and this template is not trimmed with them, so an SDK generated without
|
|
395
|
+
// that feature would ship a test that does not build. featuresource.test.ts
|
|
396
|
+
// guards exactly that, and caught it - including, on its first pass, the
|
|
397
|
+
// spelling of the type inside this very comment.
|
|
398
|
+
//
|
|
399
|
+
// fcSubset reads the expected keys off the struct by capitalising them, so
|
|
400
|
+
// `total` finds Total, `ops` finds Ops, and a feature added later needs no
|
|
401
|
+
// new go here.
|
|
402
|
+
func fcRecord(client *sdk.ProjectNameSDK, name string) any {
|
|
403
|
+
for _, f := range client.Features {
|
|
404
|
+
if f.GetName() == name {
|
|
405
|
+
return f
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return nil
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
func TestFeatureCorpus(t *testing.T) {
|
|
412
|
+
spec := loadTestSpec(t)
|
|
413
|
+
|
|
414
|
+
featureSection := getSpec(spec, "feature")
|
|
415
|
+
if featureSection == nil {
|
|
416
|
+
// A corpus with no `feature` section is a SKIP, not a failure. Each
|
|
417
|
+
// project carries its OWN materialised copy of .sdk/test/test.json, so a
|
|
418
|
+
// project scaffolded before the section existed legitimately has no cases
|
|
419
|
+
// to run - and a hard assertion here turned that into a red suite in every
|
|
420
|
+
// SDK on the fleet, for a corpus the project had simply not re-pulled yet.
|
|
421
|
+
// The strict check belongs where the corpus is CONTROLLED: sdkgen's own
|
|
422
|
+
// end-to-end lane supplies one and requires the cases to actually run.
|
|
423
|
+
t.Skip("this project's test.json has no `feature` section - recompile the corpus (create-sdkgen .sdk/test/feature/) to run these cases")
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
ops := fcUsableOps(2)
|
|
427
|
+
|
|
428
|
+
// At least one operation, or every case below would skip and this would
|
|
429
|
+
// report green having run nothing.
|
|
430
|
+
if len(ops) == 0 {
|
|
431
|
+
t.Fatal("no declared operation completed against a plain 200 - the " +
|
|
432
|
+
"corpus cannot exercise a feature without one")
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
for _, name := range featureCorpusNames {
|
|
436
|
+
name := name
|
|
437
|
+
t.Run(name, func(t *testing.T) {
|
|
438
|
+
section, _ := featureSection[name].(map[string]any)
|
|
439
|
+
if section == nil {
|
|
440
|
+
t.Skipf("no corpus section for %s", name)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
basic, _ := section["basic"].(map[string]any)
|
|
444
|
+
cases, _ := basic["set"].([]any)
|
|
445
|
+
if len(cases) == 0 {
|
|
446
|
+
t.Fatalf("corpus section feature.%s ran ZERO cases - a renamed "+
|
|
447
|
+
"section or an emptied fixture must fail loudly", name)
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Only run what this SDK actually has, the same rule the rest of
|
|
451
|
+
// the feature tests use. Probed by ACTIVATING it: the feature
|
|
452
|
+
// defaults to inactive, so an idle client never constructs it and
|
|
453
|
+
// its absence from Features says nothing about the SDK.
|
|
454
|
+
probe := fcClient(map[string]any{
|
|
455
|
+
"feature": []any{map[string]any{"name": name, "active": true}},
|
|
456
|
+
})
|
|
457
|
+
if fcRecord(probe, name) == nil {
|
|
458
|
+
t.Skipf("this SDK was generated without the %s feature", name)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
ran := 0
|
|
462
|
+
for _, raw := range cases {
|
|
463
|
+
kase, _ := raw.(map[string]any)
|
|
464
|
+
if kase == nil {
|
|
465
|
+
continue
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
need := fcTokensUsed(kase)
|
|
469
|
+
if need > len(ops) {
|
|
470
|
+
t.Logf("skip %q: needs %d operations, this SDK offers %d",
|
|
471
|
+
kase["name"], need, len(ops))
|
|
472
|
+
continue
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
tokens := map[string]string{}
|
|
476
|
+
for i := 0; i < need; i++ {
|
|
477
|
+
tokens[fmt.Sprintf("#OP%d", i+1)] = ops[i].key
|
|
478
|
+
}
|
|
479
|
+
resolved, _ := fcResolve(kase, tokens).(map[string]any)
|
|
480
|
+
|
|
481
|
+
byKey := map[string]fcOp{}
|
|
482
|
+
for _, o := range ops {
|
|
483
|
+
byKey[o.key] = o
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
client := fcClient(resolved)
|
|
487
|
+
label, _ := resolved["name"].(string)
|
|
488
|
+
|
|
489
|
+
steps, _ := resolved["op"].([]any)
|
|
490
|
+
for _, rawstep := range steps {
|
|
491
|
+
step, _ := rawstep.(map[string]any)
|
|
492
|
+
if step == nil {
|
|
493
|
+
continue
|
|
494
|
+
}
|
|
495
|
+
opkey, _ := step["op"].(string)
|
|
496
|
+
op, ok := byKey[opkey]
|
|
497
|
+
if !ok {
|
|
498
|
+
t.Errorf("%s: no operation %s", label, opkey)
|
|
499
|
+
continue
|
|
500
|
+
}
|
|
501
|
+
ctrl := map[string]any{}
|
|
502
|
+
if c, ok := step["ctrl"].(map[string]any); ok {
|
|
503
|
+
ctrl = c
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
err := fcInvoke(client, op, ctrl)
|
|
507
|
+
wanterr, haserr := step["err"]
|
|
508
|
+
|
|
509
|
+
if !haserr {
|
|
510
|
+
if err != nil {
|
|
511
|
+
t.Errorf("%s: %s failed unexpectedly: %v", label, opkey, err)
|
|
512
|
+
}
|
|
513
|
+
continue
|
|
514
|
+
}
|
|
515
|
+
if err == nil {
|
|
516
|
+
t.Errorf("%s: %s was expected to fail, and did not", label, opkey)
|
|
517
|
+
continue
|
|
518
|
+
}
|
|
519
|
+
if code, ok := wanterr.(string); ok {
|
|
520
|
+
// The CODE, not the message. The message is prefixed
|
|
521
|
+
// and humanised by makeError, so matching its text
|
|
522
|
+
// would pass on any error that happened to mention
|
|
523
|
+
// the word.
|
|
524
|
+
got := ""
|
|
525
|
+
var sdkerr *sdk.ProjectNameError
|
|
526
|
+
if errors.As(err, &sdkerr) {
|
|
527
|
+
got = sdkerr.Code
|
|
528
|
+
}
|
|
529
|
+
if got != code {
|
|
530
|
+
t.Errorf("%s: wrong error code: got %q (%v), want %q",
|
|
531
|
+
label, got, err, code)
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
fcSubset(t, fcRecord(client, name), resolved["out"], label+": _"+name)
|
|
537
|
+
ran++
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if ran == 0 {
|
|
541
|
+
t.Fatalf("every feature.%s case was skipped", name)
|
|
542
|
+
}
|
|
543
|
+
// Say how many ran. A partial run is legitimate (an SDK with one
|
|
544
|
+
// operation skips the cases needing two) but it should be visible
|
|
545
|
+
// rather than inferred from a green tick.
|
|
546
|
+
t.Logf("feature.%s: ran %d of %d case(s) against %d operation(s)",
|
|
547
|
+
name, ran, len(cases), len(ops))
|
|
548
|
+
})
|
|
549
|
+
}
|
|
550
|
+
}
|
|
@@ -21,11 +21,17 @@ func makeOptionsUtil(ctx *core.Context) map[string]any {
|
|
|
21
21
|
|
|
22
22
|
// Merge custom utility overrides onto the utility object.
|
|
23
23
|
// Read from original options before clone, since vs.Clone strips functions.
|
|
24
|
+
//
|
|
25
|
+
// A key naming a real utility member REPLACES it (overrideUtil); anything
|
|
26
|
+
// else is attached as a custom extra. This mirrors ts, where the utility is
|
|
27
|
+
// an open object and `setprop` does both at once.
|
|
24
28
|
if customUtils := core.ToMapAny(options["utility"]); customUtils != nil {
|
|
25
29
|
utility := ctx.Utility
|
|
26
30
|
if utility != nil {
|
|
27
31
|
for key, val := range customUtils {
|
|
28
|
-
utility
|
|
32
|
+
if !overrideUtil(utility, key, val) {
|
|
33
|
+
utility.Custom[key] = val
|
|
34
|
+
}
|
|
29
35
|
}
|
|
30
36
|
}
|
|
31
37
|
}
|