@voxgig/sdkgen 4.2.8 → 4.3.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/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/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/ts/test/feature/Corpus.test.ts +287 -0
- package/project/sdkgen-package.json +1 -1
- package/src/helpers/naming.ts +54 -12
- package/src/sdkgen.ts +2 -1
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
package JAVAPACKAGE.sdktest;
|
|
2
|
+
|
|
3
|
+
// ProjectName SDK feature corpus test
|
|
4
|
+
//
|
|
5
|
+
// Feature behaviour, driven by the SHARED corpus.
|
|
6
|
+
//
|
|
7
|
+
// The same route PrimaryUtilityTest takes for the utilities: language-neutral
|
|
8
|
+
// cases in .sdk/test/test.json, executed against THIS generated SDK. The
|
|
9
|
+
// feature is the ordinary class, built by the generated config, installed by
|
|
10
|
+
// the generated constructor, and driven by a real entity operation. Not a
|
|
11
|
+
// miniature of the pipeline - that is what FeatureHarness does, and a
|
|
12
|
+
// miniature can only be as right as the miniature.
|
|
13
|
+
//
|
|
14
|
+
// Everything in a case is data. The one piece java writes for itself is
|
|
15
|
+
// turning scripted responses into a fetcher, through the documented
|
|
16
|
+
// `utility.fetcher` override.
|
|
17
|
+
|
|
18
|
+
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
19
|
+
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
20
|
+
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
21
|
+
import static org.junit.jupiter.api.Assertions.fail;
|
|
22
|
+
import static org.junit.jupiter.api.Assumptions.assumeTrue;
|
|
23
|
+
|
|
24
|
+
import java.lang.reflect.Field;
|
|
25
|
+
import java.lang.reflect.Method;
|
|
26
|
+
import java.util.ArrayList;
|
|
27
|
+
import java.util.LinkedHashMap;
|
|
28
|
+
import java.util.List;
|
|
29
|
+
import java.util.Map;
|
|
30
|
+
import java.util.TreeMap;
|
|
31
|
+
import java.util.function.Supplier;
|
|
32
|
+
import java.util.regex.Matcher;
|
|
33
|
+
import java.util.regex.Pattern;
|
|
34
|
+
|
|
35
|
+
import org.junit.jupiter.api.Test;
|
|
36
|
+
|
|
37
|
+
import JAVAPACKAGE.core.Entity;
|
|
38
|
+
import JAVAPACKAGE.core.Feature;
|
|
39
|
+
import JAVAPACKAGE.core.ProjectNameSDK;
|
|
40
|
+
import JAVAPACKAGE.core.SdkError;
|
|
41
|
+
import JAVAPACKAGE.core.Utility;
|
|
42
|
+
|
|
43
|
+
@SuppressWarnings({"unchecked"})
|
|
44
|
+
public class FeatureCorpusTest {
|
|
45
|
+
|
|
46
|
+
// Features with a corpus section. A name here with no section is a skip,
|
|
47
|
+
// not a failure: an SDK generated without the feature has nothing to run.
|
|
48
|
+
private static final List<String> FEATURE_CORPUS_NAMES = List.of("cost");
|
|
49
|
+
|
|
50
|
+
// The standard operation names, in the order the runner prefers them.
|
|
51
|
+
private static final List<String> FEATURE_CORPUS_OPS =
|
|
52
|
+
List.of("load", "list", "create", "update", "remove");
|
|
53
|
+
|
|
54
|
+
/** One discovered operation: the client accessor plus the entity method. */
|
|
55
|
+
private static final class Op {
|
|
56
|
+
final String key;
|
|
57
|
+
final Method accessor;
|
|
58
|
+
final Method call;
|
|
59
|
+
|
|
60
|
+
Op(String key, Method accessor, Method call) {
|
|
61
|
+
this.key = key;
|
|
62
|
+
this.accessor = accessor;
|
|
63
|
+
this.call = call;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
private static Map<String, Object> corpus() {
|
|
68
|
+
return RunnerSupport.loadTestSpec();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A scripted transport built from a case's `res` list. Responses are
|
|
73
|
+
* consumed in order and the last one repeats, so a case that does not care
|
|
74
|
+
* how many attempts happen need only declare one.
|
|
75
|
+
*
|
|
76
|
+
* <p>Returns the shape the real fetcher returns: the parsed body behind a
|
|
77
|
+
* `json` supplier and `body` as the raw string. A script that only set
|
|
78
|
+
* `body` would look like an empty result, which reads as a feature defect
|
|
79
|
+
* rather than a mis-shaped script.
|
|
80
|
+
*/
|
|
81
|
+
private static Utility.FetcherFn scriptedFetcher(Object resRaw) {
|
|
82
|
+
List<Object> res = resRaw instanceof List ? (List<Object>) resRaw : List.of();
|
|
83
|
+
int[] n = {-1};
|
|
84
|
+
return (ctx, fullurl, fetchdef) -> {
|
|
85
|
+
n[0]++;
|
|
86
|
+
Map<String, Object> spec = new LinkedHashMap<>();
|
|
87
|
+
if (!res.isEmpty()) {
|
|
88
|
+
int i = n[0] >= res.size() ? res.size() - 1 : n[0];
|
|
89
|
+
Object entry = res.get(i);
|
|
90
|
+
if (entry instanceof Map) {
|
|
91
|
+
spec = (Map<String, Object>) entry;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (Boolean.TRUE.equals(spec.get("throw"))) {
|
|
96
|
+
throw new RuntimeException("scripted transport failure");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
Object statusRaw = spec.get("status");
|
|
100
|
+
int status = statusRaw instanceof Number ? ((Number) statusRaw).intValue() : 200;
|
|
101
|
+
Object body = spec.get("body");
|
|
102
|
+
final Object jsonBody = body == null ? new LinkedHashMap<String, Object>() : body;
|
|
103
|
+
|
|
104
|
+
Map<String, Object> headers = new LinkedHashMap<>();
|
|
105
|
+
if (spec.get("headers") instanceof Map) {
|
|
106
|
+
headers.putAll((Map<String, Object>) spec.get("headers"));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
Map<String, Object> out = new LinkedHashMap<>();
|
|
110
|
+
out.put("status", status);
|
|
111
|
+
out.put("statusText", status < 400 ? "OK" : "ERR");
|
|
112
|
+
out.put("headers", headers);
|
|
113
|
+
out.put("json", (Supplier<Object>) () -> jsonBody);
|
|
114
|
+
out.put("body", RunnerSupport.jsonStr(jsonBody));
|
|
115
|
+
return out;
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build a client the way a caller would.
|
|
121
|
+
*
|
|
122
|
+
* <p>The plain constructor, not testSDK: the `test` feature is
|
|
123
|
+
* transport: 'base' and REPLACES the transport, so a client in test mode
|
|
124
|
+
* would shadow the script.
|
|
125
|
+
*/
|
|
126
|
+
private static ProjectNameSDK buildClient(Map<String, Object> kase) {
|
|
127
|
+
Map<String, Object> utility = new LinkedHashMap<>();
|
|
128
|
+
utility.put("fetcher", scriptedFetcher(kase.get("res")));
|
|
129
|
+
|
|
130
|
+
Map<String, Object> opts = new LinkedHashMap<>();
|
|
131
|
+
opts.put("utility", utility);
|
|
132
|
+
if (kase.get("feature") != null) {
|
|
133
|
+
opts.put("feature", kase.get("feature"));
|
|
134
|
+
}
|
|
135
|
+
return new ProjectNameSDK(opts);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Every operation this SDK declares, in a stable order.
|
|
140
|
+
*
|
|
141
|
+
* <p>The corpus cannot name an entity - it is shared by SDKs with none in
|
|
142
|
+
* common - so the runner finds them here. An entity accessor is a client
|
|
143
|
+
* method taking one Map and returning something that answers getName().
|
|
144
|
+
*/
|
|
145
|
+
private static List<Op> candidates(ProjectNameSDK client) {
|
|
146
|
+
Map<String, Object[]> found = new TreeMap<>();
|
|
147
|
+
|
|
148
|
+
for (Method m : client.getClass().getMethods()) {
|
|
149
|
+
if (1 != m.getParameterCount() || !Map.class.isAssignableFrom(m.getParameterTypes()[0])) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (!Entity.class.isAssignableFrom(m.getReturnType())) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
Object ent;
|
|
156
|
+
String entname;
|
|
157
|
+
try {
|
|
158
|
+
ent = m.invoke(client, new Object[] {null});
|
|
159
|
+
entname = ((Entity) ent).getName();
|
|
160
|
+
}
|
|
161
|
+
catch (Exception e) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (null == entname || entname.isEmpty()) {
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
found.put(entname, new Object[] {m, ent});
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
List<Op> out = new ArrayList<>();
|
|
171
|
+
for (Map.Entry<String, Object[]> e : found.entrySet()) {
|
|
172
|
+
Method accessor = (Method) e.getValue()[0];
|
|
173
|
+
Object ent = e.getValue()[1];
|
|
174
|
+
for (String opname : FEATURE_CORPUS_OPS) {
|
|
175
|
+
Method call;
|
|
176
|
+
try {
|
|
177
|
+
call = ent.getClass().getMethod(opname, Map.class, Map.class);
|
|
178
|
+
}
|
|
179
|
+
catch (NoSuchMethodException ex) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
out.add(new Op(e.getKey() + "." + opname, accessor, call));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private static Object invoke(ProjectNameSDK client, Op op, Map<String, Object> ctrl)
|
|
189
|
+
throws Exception {
|
|
190
|
+
Object ent = op.accessor.invoke(client, new Object[] {null});
|
|
191
|
+
try {
|
|
192
|
+
return op.call.invoke(ent, new LinkedHashMap<String, Object>(), ctrl);
|
|
193
|
+
}
|
|
194
|
+
catch (java.lang.reflect.InvocationTargetException ite) {
|
|
195
|
+
// Unwrap: a reflective call reports the SDK's error as its cause, and a
|
|
196
|
+
// case asserting an error code needs the error itself.
|
|
197
|
+
Throwable cause = ite.getCause();
|
|
198
|
+
if (cause instanceof Exception) {
|
|
199
|
+
throw (Exception) cause;
|
|
200
|
+
}
|
|
201
|
+
throw ite;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Pick operations by DRIVING them: an op is usable when it completes against
|
|
207
|
+
* a plain 200 with no feature active. Declared operations are not all
|
|
208
|
+
* callable with no arguments (a required path parameter, a body), and a case
|
|
209
|
+
* failing for that reason would read as a feature defect.
|
|
210
|
+
*/
|
|
211
|
+
private static List<Op> usableOps(int want) {
|
|
212
|
+
List<Op> picked = new ArrayList<>();
|
|
213
|
+
for (Op cand : candidates(buildClient(Map.of()))) {
|
|
214
|
+
try {
|
|
215
|
+
invoke(buildClient(Map.of()), cand, new LinkedHashMap<>());
|
|
216
|
+
}
|
|
217
|
+
catch (Exception e) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
picked.add(cand);
|
|
221
|
+
if (picked.size() >= want) {
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return picked;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Replace #OPn throughout a case, keys included. */
|
|
229
|
+
private static Object resolve(Object node, Map<String, String> tokens) {
|
|
230
|
+
if (node instanceof String) {
|
|
231
|
+
String out = (String) node;
|
|
232
|
+
for (Map.Entry<String, String> t : tokens.entrySet()) {
|
|
233
|
+
out = out.replace(t.getKey(), t.getValue());
|
|
234
|
+
}
|
|
235
|
+
return out;
|
|
236
|
+
}
|
|
237
|
+
if (node instanceof List) {
|
|
238
|
+
List<Object> out = new ArrayList<>();
|
|
239
|
+
for (Object n : (List<Object>) node) {
|
|
240
|
+
out.add(resolve(n, tokens));
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
if (node instanceof Map) {
|
|
245
|
+
Map<String, Object> out = new LinkedHashMap<>();
|
|
246
|
+
for (Map.Entry<String, Object> e : ((Map<String, Object>) node).entrySet()) {
|
|
247
|
+
out.put((String) resolve(e.getKey(), tokens), resolve(e.getValue(), tokens));
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
return node;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The highest #OPn a case mentions. */
|
|
255
|
+
private static int tokensUsed(Object kase) {
|
|
256
|
+
Matcher m = Pattern.compile("#OP(\\d+)").matcher(RunnerSupport.jsonStr(kase));
|
|
257
|
+
int max = 0;
|
|
258
|
+
while (m.find()) {
|
|
259
|
+
max = Math.max(max, Integer.parseInt(m.group(1)));
|
|
260
|
+
}
|
|
261
|
+
return max;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* One named member of the record, whether the record is a map or a POJO.
|
|
266
|
+
*
|
|
267
|
+
* <p>The aggregates are typed classes here and plain maps in the dynamic
|
|
268
|
+
* donors, so the corpus - which knows only field names - reaches both
|
|
269
|
+
* through a public field or a map key.
|
|
270
|
+
*/
|
|
271
|
+
private static Object[] member(Object actual, String key) {
|
|
272
|
+
if (null == actual) {
|
|
273
|
+
return new Object[] {null, false};
|
|
274
|
+
}
|
|
275
|
+
if (actual instanceof Map) {
|
|
276
|
+
Map<String, Object> m = (Map<String, Object>) actual;
|
|
277
|
+
return m.containsKey(key)
|
|
278
|
+
? new Object[] {m.get(key), true}
|
|
279
|
+
: new Object[] {null, false};
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
Field f = actual.getClass().getField(key);
|
|
283
|
+
return new Object[] {f.get(actual), true};
|
|
284
|
+
}
|
|
285
|
+
catch (Exception e) {
|
|
286
|
+
return new Object[] {null, false};
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Assert that `actual` contains `expect`, recursively. Cases assert only the
|
|
292
|
+
* fields they are about, so a full equality check would force every case to
|
|
293
|
+
* restate the whole record.
|
|
294
|
+
*/
|
|
295
|
+
private void subset(Object actual, Object expect, String path) {
|
|
296
|
+
if (expect instanceof Map) {
|
|
297
|
+
for (Map.Entry<String, Object> e : ((Map<String, Object>) expect).entrySet()) {
|
|
298
|
+
Object[] got = member(actual, e.getKey());
|
|
299
|
+
assertTrue((Boolean) got[1], path + "." + e.getKey() + ": no such member");
|
|
300
|
+
subset(got[0], e.getValue(), path + "." + e.getKey());
|
|
301
|
+
}
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (expect instanceof Boolean) {
|
|
306
|
+
assertEquals(expect, actual, path);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (expect instanceof Number) {
|
|
311
|
+
assertTrue(actual instanceof Number, path + ": expected a number, got " + actual);
|
|
312
|
+
// Money is float arithmetic; compare with a tolerance far below any
|
|
313
|
+
// amount a case states.
|
|
314
|
+
assertEquals(((Number) expect).doubleValue(), ((Number) actual).doubleValue(), 1e-9, path);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
assertEquals(expect, actual, path);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* The feature's own record, found by NAME.
|
|
323
|
+
*
|
|
324
|
+
* <p>Named, not typed: a project that trimmed the feature has no such class,
|
|
325
|
+
* and a runner that referred to one would not compile there.
|
|
326
|
+
*/
|
|
327
|
+
private static Object record(ProjectNameSDK client, String name) {
|
|
328
|
+
for (Feature f : client.features) {
|
|
329
|
+
if (name.equals(f.getName())) {
|
|
330
|
+
return f;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return null;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
@Test
|
|
337
|
+
public void corpusCarriesAFeatureSection() {
|
|
338
|
+
// A corpus with no `feature` section is a SKIP, not a failure. Each
|
|
339
|
+
// project carries its OWN materialised copy of .sdk/test/test.json, so a
|
|
340
|
+
// project scaffolded before the section existed legitimately has no cases
|
|
341
|
+
// to run - and a hard assertion here turned that into a red suite in every
|
|
342
|
+
// SDK on the fleet, for a corpus the project had simply not re-pulled yet.
|
|
343
|
+
// The strict check belongs where the corpus is CONTROLLED: sdkgen's own
|
|
344
|
+
// end-to-end lane supplies one and requires the cases to actually run.
|
|
345
|
+
assumeTrue(null != corpus().get("feature"),
|
|
346
|
+
"this project's test.json has no `feature` section - recompile the "
|
|
347
|
+
+ "corpus (create-sdkgen .sdk/test/feature/) to run these cases");
|
|
348
|
+
assertNotNull(corpus().get("feature"));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// At least one operation, or every case below would skip and this suite
|
|
352
|
+
// would report green having run nothing.
|
|
353
|
+
@Test
|
|
354
|
+
public void sdkHasAnOperationTheCorpusCanDrive() {
|
|
355
|
+
assertTrue(0 < usableOps(2).size(),
|
|
356
|
+
"no declared operation completed against a plain 200 - the corpus "
|
|
357
|
+
+ "cannot exercise a feature without one");
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
@Test
|
|
361
|
+
public void featureCorpus() throws Exception {
|
|
362
|
+
Map<String, Object> features =
|
|
363
|
+
(Map<String, Object>) corpus().getOrDefault("feature", Map.of());
|
|
364
|
+
|
|
365
|
+
// Skip rather than run vacuously: with no section this asserts nothing,
|
|
366
|
+
// and a test that passes having checked nothing is the false green the
|
|
367
|
+
// corpus exists to prevent.
|
|
368
|
+
assumeTrue(!features.isEmpty(),
|
|
369
|
+
"this project's test.json has no `feature` section - recompile the "
|
|
370
|
+
+ "corpus (create-sdkgen .sdk/test/feature/) to run these cases");
|
|
371
|
+
|
|
372
|
+
for (String name : FEATURE_CORPUS_NAMES) {
|
|
373
|
+
Object sectionRaw = features.get(name);
|
|
374
|
+
if (!(sectionRaw instanceof Map)) {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
Map<String, Object> section = (Map<String, Object>) sectionRaw;
|
|
378
|
+
|
|
379
|
+
Object basic = section.get("basic");
|
|
380
|
+
List<Object> cases = basic instanceof Map
|
|
381
|
+
? (List<Object>) ((Map<String, Object>) basic).getOrDefault("set", List.of())
|
|
382
|
+
: List.of();
|
|
383
|
+
assertTrue(0 < cases.size(),
|
|
384
|
+
"corpus section feature." + name + " ran ZERO cases - a renamed "
|
|
385
|
+
+ "section or an emptied fixture must fail loudly");
|
|
386
|
+
|
|
387
|
+
// Probed by ACTIVATING it: the feature defaults to inactive, so an idle
|
|
388
|
+
// client never builds it and its absence says nothing.
|
|
389
|
+
ProjectNameSDK probe = buildClient(Map.of("feature",
|
|
390
|
+
List.of(Map.of("name", name, "active", true))));
|
|
391
|
+
if (null == record(probe, name)) {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
List<Op> ops = usableOps(2);
|
|
396
|
+
Map<String, Op> byKey = new LinkedHashMap<>();
|
|
397
|
+
for (Op o : ops) {
|
|
398
|
+
byKey.put(o.key, o);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
int ran = 0;
|
|
402
|
+
for (Object rawCase : cases) {
|
|
403
|
+
int need = tokensUsed(rawCase);
|
|
404
|
+
if (need > ops.size()) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
Map<String, String> tokens = new LinkedHashMap<>();
|
|
409
|
+
for (int i = 0; i < need; i++) {
|
|
410
|
+
tokens.put("#OP" + (i + 1), ops.get(i).key);
|
|
411
|
+
}
|
|
412
|
+
Map<String, Object> kase = (Map<String, Object>) resolve(rawCase, tokens);
|
|
413
|
+
|
|
414
|
+
ProjectNameSDK client = buildClient(kase);
|
|
415
|
+
String label = String.valueOf(kase.get("name"));
|
|
416
|
+
|
|
417
|
+
List<Object> steps = kase.get("op") instanceof List
|
|
418
|
+
? (List<Object>) kase.get("op") : List.of();
|
|
419
|
+
for (Object stepRaw : steps) {
|
|
420
|
+
Map<String, Object> step = (Map<String, Object>) stepRaw;
|
|
421
|
+
Op op = byKey.get(step.get("op"));
|
|
422
|
+
assertNotNull(op, label + ": no operation " + step.get("op"));
|
|
423
|
+
Map<String, Object> ctrl = step.get("ctrl") instanceof Map
|
|
424
|
+
? new LinkedHashMap<>((Map<String, Object>) step.get("ctrl"))
|
|
425
|
+
: new LinkedHashMap<>();
|
|
426
|
+
Object wanterr = step.get("err");
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
invoke(client, op, ctrl);
|
|
430
|
+
if (null != wanterr) {
|
|
431
|
+
fail(label + ": " + step.get("op") + " was expected to fail, and did not");
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
catch (org.opentest4j.AssertionFailedError e) {
|
|
435
|
+
throw e;
|
|
436
|
+
}
|
|
437
|
+
catch (Exception err) {
|
|
438
|
+
assertNotNull(wanterr,
|
|
439
|
+
label + ": " + step.get("op") + " failed unexpectedly: " + err);
|
|
440
|
+
if (wanterr instanceof String) {
|
|
441
|
+
// The CODE, not the message: makeError prefixes and humanises
|
|
442
|
+
// the text, so matching it would pass on any error that
|
|
443
|
+
// happened to mention the word.
|
|
444
|
+
String code = err instanceof SdkError ? ((SdkError) err).code : null;
|
|
445
|
+
assertEquals(wanterr, code, label + ": wrong error code (" + err + ")");
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
subset(record(client, name), kase.get("out"), label + ": _" + name);
|
|
451
|
+
ran++;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
assertTrue(0 < ran, "every feature." + name + " case was skipped");
|
|
455
|
+
// Say how many ran. A partial run is legitimate (an SDK with one
|
|
456
|
+
// operation skips the cases needing two) but it should be visible
|
|
457
|
+
// rather than inferred from a green tick.
|
|
458
|
+
System.err.println(String.format(
|
|
459
|
+
"feature.%s: ran %d of %d case(s) against %d operation(s)",
|
|
460
|
+
name, ran, cases.size(), ops.size()));
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
@@ -20,11 +20,20 @@ final class MakeOptions {
|
|
|
20
20
|
options = new LinkedHashMap<>();
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
// Merge
|
|
23
|
+
// Merge utility overrides from options onto the utility object.
|
|
24
24
|
// Read from original options before clone for parity with the donors.
|
|
25
|
+
//
|
|
26
|
+
// A key naming a real utility member REPLACES it (Register.overrideUtil);
|
|
27
|
+
// anything else is attached as a custom extra. Shelving everything in
|
|
28
|
+
// `custom` - a map nothing reads - made `utility: {"fetcher": ...}`, the
|
|
29
|
+
// documented transport seam, a silent no-op here while ts honoured it.
|
|
25
30
|
Map<String, Object> customUtils = Helpers.toMapAny(options.get("utility"));
|
|
26
31
|
if (customUtils != null && ctx.utility != null) {
|
|
27
|
-
|
|
32
|
+
for (Map.Entry<String, Object> cu : customUtils.entrySet()) {
|
|
33
|
+
if (!Register.overrideUtil(ctx.utility, cu.getKey(), cu.getValue())) {
|
|
34
|
+
ctx.utility.custom.put(cu.getKey(), cu.getValue());
|
|
35
|
+
}
|
|
36
|
+
}
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
Map<String, Object> opts = (Map<String, Object>) Struct.clone(options);
|
|
@@ -40,4 +40,95 @@ public final class Register {
|
|
|
40
40
|
u.transformRequest = TransformRequest::transformRequest;
|
|
41
41
|
u.transformResponse = TransformResponse::transformResponse;
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Replaces one utility member from {@code options.utility}, matching the ts
|
|
46
|
+
* reference: a key naming a real utility member REPLACES it, and any other
|
|
47
|
+
* key is attached as a custom extra.
|
|
48
|
+
*
|
|
49
|
+
* <p>Without this the override was a no-op here. {@code makeOptions} put
|
|
50
|
+
* every entry in {@code u.custom}, which nothing reads, so a caller passing
|
|
51
|
+
* {@code utility: {"fetcher": myTransport}} - the documented way to script
|
|
52
|
+
* the transport, and the seam the shared feature corpus runs on - was
|
|
53
|
+
* silently ignored while ts and js honoured it. The custom-utility test did
|
|
54
|
+
* not catch it because it asserted the side map rather than the behaviour.
|
|
55
|
+
*
|
|
56
|
+
* <p>Returns false when the key names no member, or when the value is not
|
|
57
|
+
* that member's interface; the caller then keeps it in {@code custom}. A
|
|
58
|
+
* wrong shape is deliberately NOT an error: ts attaches whatever it is
|
|
59
|
+
* given, so a typed port that rejected it outright would diverge in the
|
|
60
|
+
* other direction.
|
|
61
|
+
*
|
|
62
|
+
* <p>Erasure limits how much of a shape java can check: the {@code CtxFn}
|
|
63
|
+
* members all erase to the same interface, so the key decides which field a
|
|
64
|
+
* {@code CtxFn} lands on and the cast is unchecked. That is the same
|
|
65
|
+
* guarantee the dynamic donors give.
|
|
66
|
+
*
|
|
67
|
+
* <p>Keep this list in step with registerAll above - a utility added to one
|
|
68
|
+
* and not the other is overridable in ts and not here, which is the
|
|
69
|
+
* divergence this exists to remove.
|
|
70
|
+
*/
|
|
71
|
+
@SuppressWarnings({"unchecked"})
|
|
72
|
+
public static boolean overrideUtil(Utility u, String key, Object val) {
|
|
73
|
+
switch (key) {
|
|
74
|
+
case "clean":
|
|
75
|
+
if (val instanceof Utility.CleanFn) { u.clean = (Utility.CleanFn) val; return true; }
|
|
76
|
+
return false;
|
|
77
|
+
case "makeError":
|
|
78
|
+
if (val instanceof Utility.MakeErrorFn) { u.makeError = (Utility.MakeErrorFn) val; return true; }
|
|
79
|
+
return false;
|
|
80
|
+
case "featureAdd":
|
|
81
|
+
if (val instanceof Utility.FeatureFn) { u.featureAdd = (Utility.FeatureFn) val; return true; }
|
|
82
|
+
return false;
|
|
83
|
+
case "featureHook":
|
|
84
|
+
if (val instanceof Utility.HookFn) { u.featureHook = (Utility.HookFn) val; return true; }
|
|
85
|
+
return false;
|
|
86
|
+
case "featureInit":
|
|
87
|
+
if (val instanceof Utility.FeatureFn) { u.featureInit = (Utility.FeatureFn) val; return true; }
|
|
88
|
+
return false;
|
|
89
|
+
case "fetcher":
|
|
90
|
+
if (val instanceof Utility.FetcherFn) { u.fetcher = (Utility.FetcherFn) val; return true; }
|
|
91
|
+
return false;
|
|
92
|
+
case "makeContext":
|
|
93
|
+
if (val instanceof Utility.MakeContextFn) { u.makeContext = (Utility.MakeContextFn) val; return true; }
|
|
94
|
+
return false;
|
|
95
|
+
case "param":
|
|
96
|
+
if (val instanceof Utility.ParamFn) { u.param = (Utility.ParamFn) val; return true; }
|
|
97
|
+
return false;
|
|
98
|
+
default:
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!(val instanceof Utility.CtxFn)) {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
Utility.CtxFn<Object> fn = (Utility.CtxFn<Object>) val;
|
|
106
|
+
|
|
107
|
+
switch (key) {
|
|
108
|
+
case "done": u.done = fn; return true;
|
|
109
|
+
case "makeFetchDef": u.makeFetchDef = (Utility.CtxFn) fn; return true;
|
|
110
|
+
case "makeOptions": u.makeOptions = (Utility.CtxFn) fn; return true;
|
|
111
|
+
case "makeRequest": u.makeRequest = (Utility.CtxFn) fn; return true;
|
|
112
|
+
case "makeResponse": u.makeResponse = (Utility.CtxFn) fn; return true;
|
|
113
|
+
case "makeResult": u.makeResult = (Utility.CtxFn) fn; return true;
|
|
114
|
+
case "makePoint": u.makePoint = (Utility.CtxFn) fn; return true;
|
|
115
|
+
case "makeSpec": u.makeSpec = (Utility.CtxFn) fn; return true;
|
|
116
|
+
case "makeUrl": u.makeUrl = (Utility.CtxFn) fn; return true;
|
|
117
|
+
case "prepareAuth": u.prepareAuth = (Utility.CtxFn) fn; return true;
|
|
118
|
+
case "prepareBody": u.prepareBody = fn; return true;
|
|
119
|
+
case "prepareHeaders": u.prepareHeaders = (Utility.CtxFn) fn; return true;
|
|
120
|
+
case "prepareMethod": u.prepareMethod = (Utility.CtxFn) fn; return true;
|
|
121
|
+
case "prepareParams": u.prepareParams = (Utility.CtxFn) fn; return true;
|
|
122
|
+
case "preparePath": u.preparePath = (Utility.CtxFn) fn; return true;
|
|
123
|
+
case "prepareQuery": u.prepareQuery = (Utility.CtxFn) fn; return true;
|
|
124
|
+
case "graphqlBody": u.graphqlBody = fn; return true;
|
|
125
|
+
case "graphqlErrors": u.graphqlErrors = (Utility.CtxFn) fn; return true;
|
|
126
|
+
case "resultBasic": u.resultBasic = (Utility.CtxFn) fn; return true;
|
|
127
|
+
case "resultBody": u.resultBody = (Utility.CtxFn) fn; return true;
|
|
128
|
+
case "resultHeaders": u.resultHeaders = (Utility.CtxFn) fn; return true;
|
|
129
|
+
case "transformRequest": u.transformRequest = fn; return true;
|
|
130
|
+
case "transformResponse": u.transformResponse = fn; return true;
|
|
131
|
+
default: return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
43
134
|
}
|