Skip to content

Commit e13ef56

Browse files
authored
chore: add UX tests around @jsii/kernel serializer errors (#3632)
Bring attention to error messages as part of the contract of the `@jsii/kernel` serialization component, as such messages are currently a known pain point (not specific enough, confusing, overloaded, ...). Introducing snapshot tests, and quick win improvements to the existing error messages where possible. Fixes #3636 --- By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache 2.0 license]. [Apache 2.0 license]: https://www.apache.org/licenses/LICENSE-2.0
1 parent 5857ea8 commit e13ef56

File tree

5 files changed

+2195
-216
lines changed

5 files changed

+2195
-216
lines changed

packages/@jsii/kernel/src/kernel.test.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ defineTest('fails for invalid enum member name', (sandbox) => {
344344
'$jsii.enum': '@scope/jsii-calc-lib.EnumFromScopedModule/ValueX',
345345
},
346346
});
347-
}).toThrow(/No enum member named ValueX/);
347+
}).toThrow(/No such enum member: 'ValueX'/);
348348
});
349349

350350
defineTest('set for a non existing property', (sandbox) => {
@@ -1688,7 +1688,7 @@ defineTest(
16881688
args: [123, { incomplete: true }],
16891689
});
16901690
}).toThrow(
1691-
/Missing required properties for jsii-calc.TopLevelStruct: required, secondLevel/,
1691+
/Missing required properties for jsii-calc\.TopLevelStruct: 'required', 'secondLevel'/,
16921692
);
16931693
},
16941694
);
@@ -1709,7 +1709,7 @@ defineTest(
17091709
],
17101710
});
17111711
}).toThrow(
1712-
/Missing required properties for jsii-calc.SecondLevelStruct: deeperRequiredProp/,
1712+
/\[as jsii-calc.SecondLevelStruct\] Missing required properties for jsii-calc\.SecondLevelStruct: 'deeperRequiredProp'/,
17131713
);
17141714
},
17151715
);
@@ -1721,7 +1721,9 @@ defineTest('notice when an array is passed instead of varargs', (sandbox) => {
17211721
method: 'howManyVarArgsDidIPass',
17221722
args: [123, [{ required: 'abc', secondLevel: 6 }]],
17231723
});
1724-
}).toThrow(/Got an array where a jsii-calc.TopLevelStruct was expected/);
1724+
}).toThrow(
1725+
/Value is an array \(varargs may have been incorrectly supplied\)/,
1726+
);
17251727
});
17261728

17271729
defineTest(
@@ -2020,7 +2022,7 @@ defineTest('ANY serializer: null/undefined', (sandbox) => {
20202022

20212023
defineTest('ANY serializer: function (fails)', (sandbox) => {
20222024
expect(() => serializeAny(sandbox, 'anyFunction')).toThrow(
2023-
/JSII Kernel is unable to serialize `function`. An instance with methods might have been returned by an `any` method\?/,
2025+
/Functions cannot be passed across language boundaries/,
20242026
);
20252027
});
20262028

packages/@jsii/kernel/src/kernel.ts

Lines changed: 83 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ export class Kernel {
210210
);
211211

212212
this._debug('value:', value);
213-
const ret = this._fromSandbox(value, ti);
213+
const ret = this._fromSandbox(value, ti, `of static property ${symbol}`);
214214
this._debug('ret', ret);
215215
return { value: ret };
216216
}
@@ -233,7 +233,12 @@ export class Kernel {
233233

234234
this._ensureSync(`property ${property}`, () =>
235235
this._wrapSandboxCode(
236-
() => (prototype[property] = this._toSandbox(value, ti)),
236+
() =>
237+
(prototype[property] = this._toSandbox(
238+
value,
239+
ti,
240+
`assigned to static property ${symbol}`,
241+
)),
237242
),
238243
);
239244

@@ -260,7 +265,7 @@ export class Kernel {
260265
() => this._wrapSandboxCode(() => instance[propertyToGet]),
261266
);
262267
this._debug('value:', value);
263-
const ret = this._fromSandbox(value, ti);
268+
const ret = this._fromSandbox(value, ti, `of property ${fqn}.${property}`);
264269
this._debug('ret:', ret);
265270
return { value: ret };
266271
}
@@ -282,7 +287,12 @@ export class Kernel {
282287

283288
this._ensureSync(`property '${objref[TOKEN_REF]}.${propertyToSet}'`, () =>
284289
this._wrapSandboxCode(
285-
() => (instance[propertyToSet] = this._toSandbox(value, propInfo)),
290+
() =>
291+
(instance[propertyToSet] = this._toSandbox(
292+
value,
293+
propInfo,
294+
`assigned to property ${fqn}.${property}`,
295+
)),
286296
),
287297
);
288298

@@ -310,7 +320,11 @@ export class Kernel {
310320
},
311321
);
312322

313-
const result = this._fromSandbox(ret, ti.returns ?? 'void');
323+
const result = this._fromSandbox(
324+
ret,
325+
ti.returns ?? 'void',
326+
`returned by method ${method}`,
327+
);
314328
this._debug('invoke result', result);
315329

316330
return { result };
@@ -343,7 +357,13 @@ export class Kernel {
343357
});
344358

345359
this._debug('method returned:', ret);
346-
return { result: this._fromSandbox(ret, ti.returns ?? 'void') };
360+
return {
361+
result: this._fromSandbox(
362+
ret,
363+
ti.returns ?? 'void',
364+
`returned by static method ${fqn}.${method}`,
365+
),
366+
};
347367
}
348368

349369
public begin(req: api.BeginRequest): api.BeginResponse {
@@ -402,7 +422,13 @@ export class Kernel {
402422
throw e;
403423
}
404424

405-
return { result: this._fromSandbox(result, method.returns ?? 'void') };
425+
return {
426+
result: this._fromSandbox(
427+
result,
428+
method.returns ?? 'void',
429+
`returned by async method ${method.name}`,
430+
),
431+
};
406432
}
407433

408434
public callbacks(_req?: api.CallbacksRequest): api.CallbacksResponse {
@@ -444,6 +470,7 @@ export class Kernel {
444470
const sandoxResult = this._toSandbox(
445471
result,
446472
cb.expectedReturnType ?? 'void',
473+
`returned by callback ${cbid}`,
447474
);
448475
this._debug('completed with result:', sandoxResult);
449476
cb.succeed(sandoxResult);
@@ -682,7 +709,11 @@ export class Kernel {
682709
get: { objref, property: propertyName },
683710
});
684711
this._debug('callback returned', result);
685-
return this._toSandbox(result, propInfo);
712+
return this._toSandbox(
713+
result,
714+
propInfo,
715+
`returned by callback property ${propertyName}`,
716+
);
686717
},
687718
set: (value: any) => {
688719
this._debug('virtual set', objref, propertyName, {
@@ -694,7 +725,11 @@ export class Kernel {
694725
set: {
695726
objref,
696727
property: propertyName,
697-
value: this._fromSandbox(value, propInfo),
728+
value: this._fromSandbox(
729+
value,
730+
propInfo,
731+
`assigned to callback property ${propertyName}`,
732+
),
698733
},
699734
});
700735
},
@@ -822,7 +857,11 @@ export class Kernel {
822857
},
823858
});
824859
this._debug('Result', result);
825-
return this._toSandbox(result, methodInfo.returns ?? 'void');
860+
return this._toSandbox(
861+
result,
862+
methodInfo.returns ?? 'void',
863+
`returned by callback method ${methodName}`,
864+
);
826865
},
827866
});
828867
}
@@ -1063,73 +1102,41 @@ export class Kernel {
10631102
return typeInfo;
10641103
}
10651104

1066-
private _toSandbox(v: any, expectedType: wire.OptionalValueOrVoid): any {
1067-
const serTypes = wire.serializationType(
1105+
private _toSandbox(
1106+
v: any,
1107+
expectedType: wire.OptionalValueOrVoid,
1108+
context: string,
1109+
): any {
1110+
return wire.process(
1111+
{
1112+
objects: this.objects,
1113+
debug: this._debug.bind(this),
1114+
findSymbol: this._findSymbol.bind(this),
1115+
lookupType: this._typeInfoForFqn.bind(this),
1116+
},
1117+
'deserialize',
1118+
v,
10681119
expectedType,
1069-
this._typeInfoForFqn.bind(this),
1070-
);
1071-
this._debug('toSandbox', v, JSON.stringify(serTypes));
1072-
1073-
const host: wire.SerializerHost = {
1074-
objects: this.objects,
1075-
debug: this._debug.bind(this),
1076-
findSymbol: this._findSymbol.bind(this),
1077-
lookupType: this._typeInfoForFqn.bind(this),
1078-
recurse: this._toSandbox.bind(this),
1079-
};
1080-
1081-
const errors = new Array<string>();
1082-
for (const { serializationClass, typeRef } of serTypes) {
1083-
try {
1084-
return wire.SERIALIZERS[serializationClass].deserialize(
1085-
v,
1086-
typeRef,
1087-
host,
1088-
);
1089-
} catch (e: any) {
1090-
// If no union (99% case), rethrow immediately to preserve stack trace
1091-
if (serTypes.length === 1) {
1092-
throw e;
1093-
}
1094-
errors.push(e.message);
1095-
}
1096-
}
1097-
1098-
throw new Error(
1099-
`Value did not match any type in union: ${errors.join(', ')}`,
1120+
context,
11001121
);
11011122
}
11021123

1103-
private _fromSandbox(v: any, targetType: wire.OptionalValueOrVoid): any {
1104-
const serTypes = wire.serializationType(
1124+
private _fromSandbox(
1125+
v: any,
1126+
targetType: wire.OptionalValueOrVoid,
1127+
context: string,
1128+
): any {
1129+
return wire.process(
1130+
{
1131+
objects: this.objects,
1132+
debug: this._debug.bind(this),
1133+
findSymbol: this._findSymbol.bind(this),
1134+
lookupType: this._typeInfoForFqn.bind(this),
1135+
},
1136+
'serialize',
1137+
v,
11051138
targetType,
1106-
this._typeInfoForFqn.bind(this),
1107-
);
1108-
this._debug('fromSandbox', v, JSON.stringify(serTypes));
1109-
1110-
const host: wire.SerializerHost = {
1111-
objects: this.objects,
1112-
debug: this._debug.bind(this),
1113-
findSymbol: this._findSymbol.bind(this),
1114-
lookupType: this._typeInfoForFqn.bind(this),
1115-
recurse: this._fromSandbox.bind(this),
1116-
};
1117-
1118-
const errors = new Array<string>();
1119-
for (const { serializationClass, typeRef } of serTypes) {
1120-
try {
1121-
return wire.SERIALIZERS[serializationClass].serialize(v, typeRef, host);
1122-
} catch (e: any) {
1123-
// If no union (99% case), rethrow immediately to preserve stack trace
1124-
if (serTypes.length === 1) {
1125-
throw e;
1126-
}
1127-
errors.push(e.message);
1128-
}
1129-
}
1130-
1131-
throw new Error(
1132-
`Value did not match any type in union: ${errors.join(', ')}`,
1139+
context,
11331140
);
11341141
}
11351142

@@ -1148,7 +1155,7 @@ export class Kernel {
11481155
private _boxUnboxParameters(
11491156
xs: any[],
11501157
parameters: spec.Parameter[] | undefined,
1151-
boxUnbox: (x: any, t: wire.OptionalValueOrVoid) => any,
1158+
boxUnbox: (x: any, t: wire.OptionalValueOrVoid, context: string) => any,
11521159
) {
11531160
parameters = [...(parameters ?? [])];
11541161
const variadic =
@@ -1166,7 +1173,9 @@ export class Kernel {
11661173
})`,
11671174
);
11681175
}
1169-
return xs.map((x, i) => boxUnbox(x, parameters![i]));
1176+
return xs.map((x, i) =>
1177+
boxUnbox(x, parameters![i], `of parameter ${parameters![i].name}`),
1178+
);
11701179
}
11711180

11721181
private _debug(...args: any[]) {

packages/@jsii/kernel/src/serialization.test.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { TOKEN_REF } from './api';
44
import { ObjectTable } from './objects';
55
import {
66
SerializationClass,
7+
SerializationError,
78
SerializerHost,
89
SERIALIZERS,
910
} from './serialization';
@@ -28,7 +29,6 @@ const host: SerializerHost = {
2829
findSymbol: jest.fn().mockName('host.findSymbol'),
2930
lookupType,
3031
objects: new ObjectTable(lookupType),
31-
recurse: jest.fn().mockName('host.recurse'),
3232
};
3333

3434
describe(SerializationClass.Any, () => {
@@ -41,16 +41,6 @@ describe(SerializationClass.Any, () => {
4141
}
4242
}
4343

44-
beforeEach((done) => {
45-
(host.recurse as jest.Mock<any, any>).mockImplementation(
46-
(x: any, type: OptionalValue) => {
47-
expect(type).toEqual(TYPE_ANY);
48-
return anySerializer.serialize(x, type, host);
49-
},
50-
);
51-
done();
52-
});
53-
5444
describe(anySerializer.serialize, () => {
5545
test('by-value object literal', () => {
5646
expect(
@@ -94,7 +84,7 @@ describe(SerializationClass.Scalar, () => {
9484
test('rejects any value', () =>
9585
expect(() =>
9686
scalarSerializer.deserialize('nothing!', TYPE_VOID, host),
97-
).toThrow(/Encountered unexpected `void` type/));
87+
).toThrow(/Encountered unexpected void type!/));
9888
});
9989

10090
const scalarTypes = [
@@ -115,7 +105,7 @@ describe(SerializationClass.Scalar, () => {
115105
example.type,
116106
host,
117107
),
118-
).toThrow(`Expected a ${example.name}, got {`);
108+
).toThrow(SerializationError);
119109
});
120110

121111
for (const validValue of example.validValues) {
@@ -133,11 +123,7 @@ describe(SerializationClass.Scalar, () => {
133123
test(`rejects: ${invalidValue}`, () =>
134124
expect(() =>
135125
scalarSerializer.deserialize(invalidValue, example.type, host),
136-
).toThrow(
137-
`Expected a ${example.name}, got ${JSON.stringify(
138-
invalidValue,
139-
)} (${typeof invalidValue})`,
140-
));
126+
).toThrow(SerializationError));
141127
}
142128
});
143129
}

0 commit comments

Comments
 (0)