pulumi/sdk/nodejs/tests/runtime/closure.spec.ts

789 lines
25 KiB
TypeScript

// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
import * as assert from "assert";
import { runtime } from "../../index";
import { assertAsyncThrows, asyncTest } from "../util";
interface ClosureCase {
title: string; // a title banner for the test case.
func: Function; // the function whose body and closure to serialize.
expect?: runtime.Closure; // if undefined, error expected; otherwise, the serialized shape.
expectText?: string; // optionally also validate the serialization to JavaScript text.
closureHash?: string; // hash of the closure.
}
// This group of tests ensure that we serialize closures properly.
describe("closure", () => {
describe("hash", () => {
it("is affected by code.", () => {
const closure1: runtime.Closure = {
code: "",
runtime: "",
environment: { },
};
const closure2: runtime.Closure = {
code: "1",
runtime: "",
environment: { },
};
const hash1 = runtime.getClosureHash_forTestingPurposes(closure1);
const hash2 = runtime.getClosureHash_forTestingPurposes(closure2);
assert.notEqual(hash1, hash2);
});
it("is affected by runtime.", () => {
const closure1: runtime.Closure = {
code: "",
runtime: "",
environment: { },
};
const closure2: runtime.Closure = {
code: "",
runtime: "1",
environment: { },
};
const hash1 = runtime.getClosureHash_forTestingPurposes(closure1);
const hash2 = runtime.getClosureHash_forTestingPurposes(closure2);
assert.notEqual(hash1, hash2);
});
it("is affected by module.", () => {
const closure1: runtime.Closure = {
code: "",
runtime: "",
environment: { cap1: { module: "m1" } },
};
const closure2: runtime.Closure = {
code: "",
runtime: "",
environment: { cap1: { module: "m2" } },
};
const hash1 = runtime.getClosureHash_forTestingPurposes(closure1);
const hash2 = runtime.getClosureHash_forTestingPurposes(closure2);
assert.notEqual(hash1, hash2);
});
it("is affected by environment values.", () => {
const closure1: runtime.Closure = {
code: "",
runtime: "",
environment: { },
};
const closure2: runtime.Closure = {
code: "",
runtime: "",
environment: { cap1: { json: 100 } },
};
const hash1 = runtime.getClosureHash_forTestingPurposes(closure1);
const hash2 = runtime.getClosureHash_forTestingPurposes(closure2);
assert.notEqual(hash1, hash2);
});
it("is affected by environment names.", () => {
const closure1: runtime.Closure = {
code: "",
runtime: "",
environment: { cap1: { json: 100 } },
};
const closure2: runtime.Closure = {
code: "",
runtime: "",
environment: { cap2: { json: 100 } },
};
const hash1 = runtime.getClosureHash_forTestingPurposes(closure1);
const hash2 = runtime.getClosureHash_forTestingPurposes(closure2);
assert.notEqual(hash1, hash2);
});
it("is not affected by environment order.", () => {
const closure1: runtime.Closure = {
code: "",
runtime: "",
environment: { cap1: { json: 100 }, cap2: { json: 200 } },
};
const closure2: runtime.Closure = {
code: "",
runtime: "",
environment: { cap2: { json: 200 }, cap1: { json: 100 } },
};
const hash1 = runtime.getClosureHash_forTestingPurposes(closure1);
const hash2 = runtime.getClosureHash_forTestingPurposes(closure2);
assert.equal(hash1, hash2);
});
it("is different with cyclic and non-cyclic environments.", () => {
const closure1: runtime.Closure = {
code: "",
runtime: "",
environment: { cap1: { json: 100 } },
};
closure1.environment.cap1.closure = closure1;
const closure2: runtime.Closure = {
code: "",
runtime: "",
environment: { cap1: { json: 100 } },
};
const hash1 = runtime.getClosureHash_forTestingPurposes(closure1);
const hash2 = runtime.getClosureHash_forTestingPurposes(closure2);
assert.notEqual(hash1, hash2);
});
});
const cases: ClosureCase[] = [];
{
// Ensure we reject function declarations.
class C {
// tslint:disable-next-line
public m(): void { }
}
cases.push({
title: "Reject non-expression function objects",
func: new C().m,
closureHash: "",
});
}
// A few simple positive cases for functions/arrows (no captures).
cases.push({
title: "Empty function closure",
// tslint:disable-next-line
func: function () { },
expect: {
code: "(function () { })",
environment: {},
runtime: "nodejs",
},
closureHash: "__2b3ba3b4fb55b6fb500f9e8d7a4e132cec103fe6",
expectText: `exports.handler = __2b3ba3b4fb55b6fb500f9e8d7a4e132cec103fe6;
function __2b3ba3b4fb55b6fb500f9e8d7a4e132cec103fe6() {
return (function() {
with({ }) {
return (function () { })
}
}).apply(undefined, undefined).apply(this, arguments);
}
`,
});
cases.push({
title: "Function closure with this capture",
// tslint:disable-next-line
func: function () { console.log(this); },
expect: {
code: "(function () { console.log(this); })",
environment: {},
runtime: "nodejs",
},
closureHash: "__cd737a7b5f0ddfaee797a6ff6c8b266051f1c30e",
expectText: `exports.handler = __cd737a7b5f0ddfaee797a6ff6c8b266051f1c30e;
function __cd737a7b5f0ddfaee797a6ff6c8b266051f1c30e() {
return (function() {
with({ }) {
return (function () { console.log(this); })
}
}).apply(undefined, undefined).apply(this, arguments);
}
`,
});
cases.push({
title: "Function closure with this and arguments capture",
// tslint:disable-next-line
func: function () { console.log(this + arguments); },
expect: {
code: "(function () { console.log(this + arguments); })",
environment: {},
runtime: "nodejs",
},
closureHash: "__05437ec790248221e1167f1da8e9a9ffbfe11ebf",
expectText: `exports.handler = __05437ec790248221e1167f1da8e9a9ffbfe11ebf;
function __05437ec790248221e1167f1da8e9a9ffbfe11ebf() {
return (function() {
with({ }) {
return (function () { console.log(this + arguments); })
}
}).apply(undefined, undefined).apply(this, arguments);
}
`,
});
cases.push({
title: "Empty arrow closure",
// tslint:disable-next-line
func: () => { },
expect: {
code: "(() => { })",
environment: {},
runtime: "nodejs",
},
closureHash: "__b135b11756da3f7aecaaa23a36898c0d6d2845ab",
expectText: `exports.handler = __b135b11756da3f7aecaaa23a36898c0d6d2845ab;
function __b135b11756da3f7aecaaa23a36898c0d6d2845ab() {
return (function() {
with({ }) {
return (() => { })
}
}).apply(undefined, undefined).apply(this, arguments);
}
`,
});
cases.push({
title: "Arrow closure with this capture",
// tslint:disable-next-line
func: () => { console.log(this); },
expect: {
code: "(() => { console.log(this); })",
environment: { "this": { "module": "./bin/tests/runtime/closure.spec.js" } },
runtime: "nodejs",
},
closureHash: "__7909a569cc754ce6ee42e2eaf967c6a4a86d1dd8",
expectText: `exports.handler = __7909a569cc754ce6ee42e2eaf967c6a4a86d1dd8;
function __7909a569cc754ce6ee42e2eaf967c6a4a86d1dd8() {
return (function() {
with({ }) {
return (() => { console.log(this); })
}
}).apply(require("./bin/tests/runtime/closure.spec.js"), undefined).apply(this, arguments);
}
`,
});
cases.push({
title: "Arrow closure with this and arguments capture",
// tslint:disable-next-line
func: (function() { return () => { console.log(this + arguments); } }).apply(this, [0, 1]),
expect: {
code: "(() => { console.log(this + arguments); })",
environment: {
this: { module: "./bin/tests/runtime/closure.spec.js" },
arguments: { arr: [{ json: 0 }, { json: 1 }] },
},
runtime: "nodejs",
},
closureHash: "__20d3571e4247f51f0a3abf93f4a7e4cfb8b2f26a",
expectText: `exports.handler = __20d3571e4247f51f0a3abf93f4a7e4cfb8b2f26a;
function __20d3571e4247f51f0a3abf93f4a7e4cfb8b2f26a() {
return (function() {
with({ }) {
return (() => { console.log(this + arguments); })
}
}).apply(require("./bin/tests/runtime/closure.spec.js"), [ 0, 1 ]).apply(this, arguments);
}
`,
});
cases.push({
title: "Arrow closure with this capture inside function closure",
// tslint:disable-next-line
func: function () { () => { console.log(this); } },
expect: {
code: "(function () { () => { console.log(this); }; })",
environment: {},
runtime: "nodejs",
},
closureHash: "__6668edd6db8c98baacaf1a227150aa18ce2ae872",
expectText: `exports.handler = __6668edd6db8c98baacaf1a227150aa18ce2ae872;
function __6668edd6db8c98baacaf1a227150aa18ce2ae872() {
return (function() {
with({ }) {
return (function () { () => { console.log(this); }; })
}
}).apply(undefined, undefined).apply(this, arguments);
}
`,
});
cases.push({
title: "Arrow closure with this and arguments capture inside function closure",
// tslint:disable-next-line
func: function () { () => { console.log(this + arguments); } },
expect: {
code: "(function () { () => { console.log(this + arguments); }; })",
environment: {},
runtime: "nodejs",
},
closureHash: "__de8ce937834140441c7413a7e97b67bda12d7205",
expectText: `exports.handler = __de8ce937834140441c7413a7e97b67bda12d7205;
function __de8ce937834140441c7413a7e97b67bda12d7205() {
return (function() {
with({ }) {
return (function () { () => { console.log(this + arguments); }; })
}
}).apply(undefined, undefined).apply(this, arguments);
}
`,
});
cases.push({
title: "Empty function closure w/ args",
// tslint:disable-next-line
func: function (x: any, y: any, z: any) { },
expect: {
code: "(function (x, y, z) { })",
environment: {},
runtime: "nodejs",
},
closureHash: "__e680605f156fcaa89016e23c51d3e2328602ebad",
});
cases.push({
title: "Empty arrow closure w/ args",
// tslint:disable-next-line
func: (x: any, y: any, z: any) => { },
expect: {
code: "((x, y, z) => { })",
environment: {},
runtime: "nodejs",
},
closureHash: "__dd08d1034bd5f0e06f1269cb79974a636ef9cb13",
});
// Serialize captures.
cases.push({
title: "Doesn't serialize global captures",
func: () => { console.log("Just a global object reference"); },
expect: {
code: `(() => { console.log("Just a global object reference"); })`,
environment: {},
runtime: "nodejs",
},
closureHash: "__47ac0033692c3101b014a1a3c17a4318cf7d4330",
});
{
const wcap = "foo";
const xcap = 97;
const ycap = [ true, -1, "yup" ];
const zcap = {
a: "a",
b: false,
c: [ 0 ],
};
cases.push({
title: "Serializes basic captures",
// tslint:disable-next-line
func: () => { console.log(wcap + `${xcap}` + ycap.length + eval(zcap.a)); },
expect: {
code: "(() => { console.log(wcap + `${xcap}` + ycap.length + eval(zcap.a)); })",
environment: {
wcap: {
json: "foo",
},
xcap: {
json: 97,
},
ycap: {
arr: [
{ json: true },
{ json: -1 },
{ json: "yup" },
],
},
zcap: {
obj: {
a: { json: "a" },
b: { json: false },
c: { arr: [ { json: 0 } ] },
},
},
},
runtime: "nodejs",
},
closureHash: "__a07cae0afeaeddbb97b9f7a372b75aafd3b29d0e",
});
}
{
// tslint:disable-next-line
let nocap1 = 1, nocap2 = 2, nocap3 = 3, nocap4 = 4, nocap5 = 5, nocap6 = 6, nocap7 = 7;
// tslint:disable-next-line
let nocap8 = 8, nocap9 = 9, nocap10 = 10;
// tslint:disable-next-line
let cap1 = 100, cap2 = 200, cap3 = 300, cap4 = 400, cap5 = 500, cap6 = 600, cap7 = 700;
// tslint:disable-next-line
let cap8 = 800;
const functext = `((nocap1, nocap2) => {
let zz = nocap1 + nocap2; // not a capture: args
let yy = nocap3; // not a capture: var later on
if (zz) {
zz += cap1; // true capture
let cap1 = 9; // because let is properly scoped
zz += nocap4; // not a capture
var nocap4 = 7; // because var is function scoped
zz += cap2; // true capture
const cap2 = 33;
var nocap3 = 8; // block the above capture
}
let f1 = (nocap5) => {
yy += nocap5; // not a capture: args
cap3++; // capture
};
let f2 = (function (nocap6) {
zz += nocap6; // not a capture: args
if (cap4) { // capture
yy = 0;
}
});
let www = nocap7(); // not a capture; it is defined below
if (true) {
function nocap7() {
}
}
let [{t: [nocap8]},,nocap9 = "hello",...nocap10] = [{t: [true]},null,undefined,1,2];
let vvv = [nocap8, nocap9, nocap10]; // not a capture; declarations from destructuring
let aaa = { // captures in property and method declarations
[cap5]: cap6,
[cap7]() {
cap8
}
}
})`;
cases.push({
title: "Doesn't serialize non-free variables (but retains frees)",
// tslint:disable-next-line
func: eval(functext),
expect: {
code: functext,
environment: {
cap1: { json: 100 },
cap2: { json: 200 },
cap3: { json: 300 },
cap4: { json: 400 },
cap5: { json: 500 },
cap6: { json: 600 },
cap7: { json: 700 },
cap8: { json: 800 },
},
runtime: "nodejs",
},
closureHash: "__f919744848c6471a841a1de62afe7d3d7f7f208a",
});
}
{
// tslint:disable-next-line
let nocap1 = 1;
// tslint:disable-next-line
let cap1 = 100;
cases.push({
title: "Complex capturing cases #1",
func: () => {
// cap1 is captured here.
// nocap1 introduces a new variable that shadows the outer one.
// tslint:disable-next-line
let [nocap1 = cap1] = [];
console.log(nocap1);
},
expect: {
code: `(() => {
// cap1 is captured here.
// nocap1 introduces a new variable that shadows the outer one.
// tslint:disable-next-line
let [nocap1 = cap1] = [];
console.log(nocap1);
})`,
environment: {
cap1: { json: 100 },
},
runtime: "nodejs",
},
closureHash: "__ef48a2e2962bd53acef1b2cda244ae8c72972c05",
});
}
{
// tslint:disable-next-line
let nocap1 = 1;
// tslint:disable-next-line
let cap1 = 100;
cases.push({
title: "Complex capturing cases #2",
func: () => {
// cap1 is captured here.
// nocap1 introduces a new variable that shadows the outer one.
// tslint:disable-next-line
let {nocap1 = cap1} = {};
console.log(nocap1);
},
expect: {
code: `(() => {
// cap1 is captured here.
// nocap1 introduces a new variable that shadows the outer one.
// tslint:disable-next-line
let { nocap1 = cap1 } = {};
console.log(nocap1);
})`,
environment: {
cap1: { json: 100 },
},
runtime: "nodejs",
},
closureHash: "__b409f3bd837d513df07525bef43e57597154625e",
});
}
{
// tslint:disable-next-line
let nocap1 = 1;
// tslint:disable-next-line
let cap1 = 100;
cases.push({
title: "Complex capturing cases #3",
func: () => {
// cap1 is captured here.
// nocap1 introduces a new variable that shadows the outer one.
// tslint:disable-next-line
let {x: nocap1 = cap1} = {};
console.log(nocap1);
},
expect: {
code: `(() => {
// cap1 is captured here.
// nocap1 introduces a new variable that shadows the outer one.
// tslint:disable-next-line
let { x: nocap1 = cap1 } = {};
console.log(nocap1);
})`,
environment: {
cap1: { json: 100 },
},
runtime: "nodejs",
},
closureHash: "__5fa215795194604118a7543ce20b8e273837ae79",
});
}
cases.push({
title: "Don't capture built-ins",
// tslint:disable-next-line
func: () => { let x: any = eval("undefined + null + NaN + Infinity + __filename"); require("os"); },
expect: {
code: `(() => { let x = eval("undefined + null + NaN + Infinity + __filename"); require("os"); })`,
environment: {},
runtime: "nodejs",
},
closureHash: "__fa1c10acee8dd79b39d0f8109d2bc3252b19619a",
});
{
const os = require("os");
cases.push({
title: "Capture built-in modules as stable references, not serialized values",
func: () => os,
expect: {
code: `(() => os)`,
environment: {
os: {
module: "os",
},
},
runtime: "nodejs",
},
closureHash: "__3fa97b166e39ae989158bb37acfa12c7abc25b53",
});
}
{
const util = require("../util");
cases.push({
title: "Capture user-defined modules as stable references, not serialized values",
func: () => util,
expect: {
code: `(() => util)`,
environment: {
util: {
module: "./bin/tests/util.js",
},
},
runtime: "nodejs",
},
closureHash: "__cd171f28483c78d2a63bdda674a8f577dd4b41db",
});
}
cases.push({
title: "Don't capture catch variables",
// tslint:disable-next-line
func: () => { try { } catch (err) { console.log(err); } },
expect: {
code:
`(() => { try { }
catch (err) {
console.log(err);
} })`,
environment: {},
runtime: "nodejs",
},
closureHash: "__040426f0dc90fa8f115c1a7ed52793515564fa98",
});
// Recursive function serialization.
{
const fff = "fff!";
const ggg = "ggg!";
const xcap = {
fff: function () { console.log(fff); },
ggg: () => { console.log(ggg); },
zzz: {
a: [ (a1: any, a2: any) => { console.log(a1 + a2); } ],
},
};
const func = () => {
xcap.fff();
xcap.ggg();
xcap.zzz.a[0]("x", "y");
};
cases.push({
title: "Serializes recursive function captures",
// tslint:disable-next-line
func: func,
expect: {
code: `(() => {
xcap.fff();
xcap.ggg();
xcap.zzz.a[0]("x", "y");
})`,
environment: {
xcap: {
obj: {
fff: {
closure: {
code: "(function () { console.log(fff); })",
environment: { fff: { json: "fff!" } },
runtime: "nodejs",
},
},
ggg: {
closure: {
code: "(() => { console.log(ggg); })",
environment: { ggg: { json: "ggg!" } },
runtime: "nodejs",
},
},
zzz: {
obj: {
a: {
arr: [
{
closure: {
code: "((a1, a2) => { console.log(a1 + a2); })",
environment: {},
runtime: "nodejs",
},
},
],
},
},
},
},
},
},
runtime: "nodejs",
},
closureHash: "__1b6ae6abb4d8b2676bccb50507a0276f5be90371",
});
}
{
class CapCap {
constructor() {
(<any>this).x = 42;
(<any>this).f = () => { console.log((<any>this).x); };
}
}
// Closing over 'this'. This yields a circular closure.
const cap: any = new CapCap();
const env: runtime.Environment = { "this": {} };
env["this"].obj = {
f: {
closure: {
code: "(() => { console.log(this.x); })",
environment: {
"this": env["this"],
},
runtime: "nodejs",
},
},
x: {
json: 42,
},
};
cases.push({
title: "Serializes `this` capturing arrow functions",
func: cap.f,
expect: {
code: "(() => { console.log(this.x); })",
environment: env,
runtime: "nodejs",
},
closureHash: "__8d564176f3cd517bfe3c6e9d6b4da488a1198c0d",
});
}
cases.push({
title: "Don't serialize `this` in function expressions",
func: function() { return this; },
expect: {
code: `(function () { return this; })`,
environment: {},
runtime: "nodejs",
},
closureHash: "__05dabc231611ca558334d59d661ebfb242b31b5d",
});
// Now go ahead and run the test cases, each as its own case.
for (const test of cases) {
it(test.title, asyncTest(async () => {
if (test.expect) {
const closure: runtime.Closure = await runtime.serializeClosure(test.func);
assert.deepEqual(closure, test.expect);
if (test.expectText) {
const text = runtime.serializeJavaScriptText(closure);
assert.equal(text, test.expectText);
}
const closureHash = runtime.getClosureHash_forTestingPurposes(closure);
assert.equal(closureHash, test.closureHash);
} else {
await assertAsyncThrows(async () => {
await runtime.serializeClosure(test.func);
});
}
}));
}
});