Implement free variable calculations

This change implements free variable calculations and wires it up
to closure serialization.  This is recursive, in the sense that
the serializer may need to call back to fetch free variables for
nested functions encountered during serialization.

The free variable calculation works by parsing the serialized
function text and walking the AST, applying the usual scoping rules
to determine what is free.  In particular, it respects nested
function boundaries, and rules around var, let, and const scoping.

We are using Acorn to perform the parsing.  I'd originally gone
down the path of using V8, so that we have one consistent parser
in the game, however unfortunately neither V8's parser nor its AST
is a stable API meant for 3rd parties.  Unlike the exising internal
V8 dependencies, this one got very deep very quickly, and I became
nervous about maintaining all those dependencies.  Furthermore,
by doing it this way, we can write the free variable logic in
JavaScript, which means one fewer C++ component to maintain.

This also includes a fairly significant amount of testing, all
of which passes! 🎉
This commit is contained in:
joeduffy 2017-09-03 09:38:16 -07:00
parent 97c5f0a568
commit 3427647f93
19 changed files with 545 additions and 62 deletions

View file

@ -7,12 +7,13 @@
"typings": "bin/index.d.ts",
"scripts": {
"clean": "rm -rf bin/ && rm -rf runtime/native/build",
"build": "npm run copyprotos && tsc && npm run binplace && npm run lint",
"build": "npm run copyprotos && npm run buildnative && tsc && npm run binplace && npm run lint",
"buildnative": "pushd runtime/native/ && node-gyp build && popd",
"lint": "tslint '*.ts' 'cmd/**/*.ts' 'asset/**/*.ts' 'runtime/**/*.ts' 'test/**/*.ts'",
"copyprotos": "cp -r ../proto/nodejs proto/",
"binplaceprotos": "cp -r proto/ bin/proto/",
"binplacenative": "mkdir -p bin/runtime/native/build/ && cp -r runtime/native/build/ bin/runtime/native/build/",
"binplacetestdata": "mkdir -p bin/tests/langhost/cases/ && find tests/langhost/cases/* -type d -exec cp -R {} bin/tests/langhost/cases/ \\;",
"binplacetestdata": "mkdir -p bin/tests/runtime/langhost/cases/ && find tests/runtime/langhost/cases/* -type d -exec cp -R {} bin/tests/runtime/langhost/cases/ \\;",
"binplace": "npm run binplaceprotos && npm run binplacenative && npm run binplacetestdata",
"test": "npm run cov && npm run covreport",
"cov": "istanbul cover --print none node_modules/.bin/_mocha -- --timeout 15000 'bin/tests/**/*.spec.js'",
@ -28,9 +29,12 @@
"typescript": "^2.1.4"
},
"dependencies": {
"@types/acorn": "^4.0.2",
"acorn": "^5.1.1",
"google-protobuf": "^3.4.0",
"grpc": "^1.4.1",
"minimist": "^1.2.0",
"node-gyp": "^3.6.2",
"source-map-support": "^0.4.16"
}
}

View file

@ -1,5 +1,9 @@
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
import * as acorn from "acorn";
import * as estree from "estree";
const acornwalk = require("acorn/dist/walk");
const nativeruntime = require("./native/build/Release/nativeruntime.node");
// Closure represents the serialized form of a JavaScript serverless function.
@ -24,13 +28,150 @@ export interface EnvEntry {
// as simple JSON. Like toString, it includes the full text of the function's source code, suitable for execution.
// Unlike toString, it actually includes information about the captured environment.
export function serializeClosure(func: Function): Closure {
// Ensure this is an expression function.
let funcstr: string = func.toString();
if (funcstr.indexOf("[Function:") === 0) {
throw new Error("Cannot serialize non-expression functions (such as definitions and generators)");
}
// Produce the free variables and then pass them into the native runtime.
return <Closure><any>nativeruntime.serializeClosure(func);
// Invoke the native runtime. Note that we pass a callback to our function below to compute free variables.
// This must be a callback and not the result of this function alone, since we may recursively compute them.
//
// N.B. We use the Acorn parser to compute them. This has the downside that we now have two parsers in the game,
// V8 and Acorn (three if you include optional TypeScript), but has the significant advantage that V8's parser
// isn't designed to be stable for 3rd party consumtpion. Hence it would be brittle and a maintenance challenge.
// This approach also avoids needing to write a big hunk of complex code in C++, which is nice.
return <Closure>nativeruntime.serializeClosure(func, computeFreeVariables);
}
// computeFreeVariables computes the set of free variables in a given function string. Note that this string is
// expected to be the usual V8-serialized function expression text.
function computeFreeVariables(funcstr: string): string[] {
let opts: acorn.Options = {
ecmaVersion: 8,
sourceType: "script",
};
let parser = new acorn.Parser(opts, funcstr);
let program: estree.Program = parser.parse();
// Now that we've parsed the program, compute the free variables, and return them.
let freecomp = new FreeVariableComputer();
return freecomp.compute(program);
}
type walkCallback = (node: estree.BaseNode, state: any) => void;
class FreeVariableComputer {
private frees: {[key: string]: boolean}; // the in-progress list of free variables.
private scope: {[key: string]: boolean}[]; // a chain of current scopes and variables.
private functionVars: string[]; // list of function-scoped variables (vars).
public compute(program: estree.Program): string[] {
// Reset the state.
this.frees = {};
this.scope = [];
this.functionVars = [];
// Recurse through the tree.
acornwalk.recursive(program, {}, {
Identifier: this.visitIdentifier.bind(this),
BlockStatement: this.visitBlockStatement.bind(this),
FunctionDeclaration: this.visitBaseFunction.bind(this),
FunctionExpression: this.visitBaseFunction.bind(this),
ArrowFunctionExpression: this.visitBaseFunction.bind(this),
VariableDeclaration: this.visitVariableDeclaration.bind(this),
});
// Now just return all variables whose value is true. Filter out any that are part of the built-in
// Node.js global object, however, since those are implicitly availble on the other side of serialization.
let freeVars: string[] = [];
for (let key of Object.keys(this.frees)) {
if (this.frees[key] && (<any>global)[key] === undefined) {
freeVars.push(key);
}
}
return freeVars;
}
private visitIdentifier(node: estree.Identifier, state: any, cb: walkCallback): void {
// Remember undeclared identifiers during the walk, as they are possibly free.
let v: string = node.name;
for (let i = this.scope.length - 1; i >= 0; i--) {
if (this.scope[i][v]) {
// This is currently known in the scope chain, so do not add it as free.
break;
} else if (i === 0) {
// We reached the top of the scope chain and this wasn't found; it's free.
this.frees[v] = true;
}
}
}
private visitBlockStatement(node: estree.BlockStatement, state: any, cb: walkCallback): void {
// Push new scope, visit all block statements, and then restore the scope.
this.scope.push({});
for (let stmt of node.body) {
cb(stmt, state);
}
this.scope.pop();
}
private visitBaseFunction(node: estree.BaseFunction, state: any, cb: walkCallback): void {
// First, push new free vars list, scope, and function vars
let oldFrees: {[key: string]: boolean} = this.frees;
let oldFunctionVars: string[] = this.functionVars;
this.frees = {};
this.functionVars = [];
this.scope.push({});
// Add all parameters to the scope. By visiting the parameters, they end up being seen as
// identifiers, and therefore added to the free variables list. We then migrate them to the scope.
for (let param of node.params) {
cb(param, state);
}
for (let param of Object.keys(this.frees)) {
if (this.frees[param]) {
this.scope[this.scope.length-1][param] = true;
}
}
this.frees = {};
// Next, visit the body underneath this new context.
cb(node.body, state);
// Remove any function-scoped variables that we encountered during the walk.
for (let v of this.functionVars) {
this.frees[v] = false;
}
// Restore the prior context and merge our free list with the previous one.
this.scope.pop();
this.functionVars = oldFunctionVars;
for (let free of Object.keys(this.frees)) {
if (this.frees[free]) {
oldFrees[free] = true;
}
}
this.frees = oldFrees;
}
private visitVariableDeclaration(node: estree.VariableDeclaration, state: any, cb: walkCallback): void {
for (let decl of node.declarations) {
// If the declaration is an identifier, it isn't a free variable, for whatever scope it
// pertains to (function-wide for var and scope-wide for let/const). Track it so we can
// remove any subseqeunt references to that variable, so we know it isn't free.
if (decl.id.type === "Identifier") {
let name = (<estree.Identifier>decl.id).name;
if (node.kind === "var") {
this.functionVars.push(name);
} else {
this.scope[this.scope.length-1][name] = true;
}
// Make sure to walk the initializer.
if (decl.init) {
cb(decl.init, state);
}
} else {
// If the declaration is something else (say a destructuring pattern), recurse into
// it so that we can find any other identifiers held within.
cb(decl, state);
}
}
}
}

View file

@ -1,5 +1,6 @@
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
export * from "./closure";
export * from "./config";
export * from "./langhost";
export * from "./resource";

View file

@ -1,5 +1,7 @@
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
#include <cstring>
#include <vector>
#include <node.h>
#include <src/api.h> // v8 internal APIs
#include <src/objects.h> // v8 internal APIs
@ -46,28 +48,37 @@ Local<Value> Lookup(Isolate* isolate, v8::internal::Handle<v8::internal::Context
v8::internal::ContextLookupFlags::FOLLOW_CHAINS,
&index, &attributes, &bflags);
// Now check the result. There are several possibilities.
if (lookup->IsContext()) {
// The result was found in a context; index contains the slot number within that context.
v8::internal::Isolate* hackiso =
reinterpret_cast<v8::internal::Isolate*>(isolate);
return v8::Utils::Convert<v8::internal::Object, Object>(
v8::internal::FixedArray::get(v8::internal::Context::cast(*lookup), index, hackiso));
} else if (lookup->IsJSObject()) {
// The result was a named property inside of a context extension (such as eval); we can return it as-is.
return v8::Utils::Convert<v8::internal::Object, Object>(lookup);
} else {
// Otherwise, there was no binding found. Return undefined instead.
return Undefined(isolate);
// Now check the result. There are several legal possibilities.
if (!lookup.is_null()) {
if (lookup->IsContext()) {
// The result was found in a context; index contains the slot number within that context.
v8::internal::Isolate* hackiso =
reinterpret_cast<v8::internal::Isolate*>(isolate);
return v8::Utils::Convert<v8::internal::Object, Object>(
v8::internal::FixedArray::get(v8::internal::Context::cast(*lookup), index, hackiso));
} else if (lookup->IsJSObject()) {
// The result was a named property inside of a context extension (such as eval); we can return it as-is.
return v8::Utils::Convert<v8::internal::Object, Object>(lookup);
}
}
// If we fell through, either the lookup is null, or the object wasn't of the expected type. In either case,
// this is an error (possibly a bug), and we will throw and return undefined so we can keep going.
char namestr[255];
name->WriteUtf8(namestr, 255);
Local<String> errormsg = String::Concat(
String::NewFromUtf8(isolate, "Unexpected missing variable in closure environment: "),
String::NewFromUtf8(isolate, namestr));
isolate->ThrowException(Exception::Error(errormsg));
return Undefined(isolate);
}
Local<Value> SerializeFunction(Isolate *isolate, Local<Function> func);
Local<Value> SerializeFunction(Isolate *isolate, Local<Function> func, Local<Function> freevarsFunc);
// SerializeClosureEnvEntry serializes a JavaScript object as JSON so that it can be serialized and uploaded.
Local<Value> SerializeClosureEnvEntry(Isolate* isolate, Local<Value> v) {
Local<Value> SerializeClosureEnvEntry(Isolate* isolate, Local<Value> v, Local<Function> freevarsFunc) {
if (v.IsEmpty()) {
// If the slot is empty, just return unefined.
// If the slot is empty, just return undefined.
return Undefined(isolate);
}
@ -82,13 +93,13 @@ Local<Value> SerializeClosureEnvEntry(Isolate* isolate, Local<Value> v) {
Local<Array> newarr = Array::New(isolate, arr->Length());
for (uint32_t i = 0; i < arr->Length(); i++) {
Local<Integer> index = Integer::New(isolate, i);
newarr->Set(index, SerializeClosureEnvEntry(isolate, arr->Get(index)));
newarr->Set(index, SerializeClosureEnvEntry(isolate, arr->Get(index), freevarsFunc));
}
entry->Set(String::NewFromUtf8(isolate, "arr"), newarr);
} else if (v->IsFunction()) {
// Serialize functions recursively, and store them in a closure property.
entry->Set(String::NewFromUtf8(isolate, "closure"),
SerializeFunction(isolate, Local<Function>::Cast(v)));
SerializeFunction(isolate, Local<Function>::Cast(v), freevarsFunc));
} else if (v->IsObject()) {
// For all other objects, recursively serialize all of its properties.
Local<Object> obj = Local<Object>::Cast(v);
@ -96,7 +107,7 @@ Local<Value> SerializeClosureEnvEntry(Isolate* isolate, Local<Value> v) {
Local<Array> props = obj->GetPropertyNames(isolate->GetCurrentContext()).ToLocalChecked();
for (uint32_t i = 0; i < props->Length(); i++) {
Local<Value> propname = props->Get(Integer::New(isolate, i));
newobj->Set(propname, SerializeClosureEnvEntry(isolate, obj->Get(propname)));
newobj->Set(propname, SerializeClosureEnvEntry(isolate, obj->Get(propname), freevarsFunc));
}
entry->Set(String::NewFromUtf8(isolate, "obj"), newobj);
} else {
@ -106,8 +117,33 @@ Local<Value> SerializeClosureEnvEntry(Isolate* isolate, Local<Value> v) {
return entry;
}
Local<String> SerializeFunctionObjectText(Isolate *isolate, Local<Function> func) {
// Serialize the code simply by calling toString on the Function.
Local<Function> toString = Local<Function>::Cast(
func->Get(String::NewFromUtf8(isolate, "toString")));
Local<String> code = Local<String>::Cast(toString->Call(func, 0, nullptr));
// Ensure that the code is a function expression (including arrows), and not a definition, etc.
const char* badprefix = "[Function:";
size_t badprefixLength = strlen(badprefix);
if (code->Length() >= (int)badprefixLength) {
char buf[badprefixLength];
code->WriteUtf8(buf, badprefixLength);
if (!strncmp(badprefix, buf, badprefixLength)) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate,
"Cannot serialize non-expression functions (such as definitions and generators)")));
}
}
// Wrap the serialized function text in ()s so that it's a legal top-level script/module element.
return String::Concat(
String::Concat(String::NewFromUtf8(isolate, "("), code),
String::NewFromUtf8(isolate, ")"));
}
// SerializeFunction serializes a JavaScript function expression and its associated closure environment.
Local<Value> SerializeFunction(Isolate *isolate, Local<Function> func) {
Local<Value> SerializeFunction(Isolate *isolate, Local<Function> func, Local<Function> freevarsFunc) {
// Get at the innards of the function. Unfortunately, we need to use internal V8 APIs to do this,
// as the closest public function, CreationContext, intentionally returns the non-closure Context for
// Function objects (it returns the constructor context, which is not what we want).
@ -115,19 +151,38 @@ Local<Value> SerializeFunction(Isolate *isolate, Local<Function> func) {
reinterpret_cast<v8::internal::JSFunction**>(const_cast<Function*>(*func)));
v8::internal::Handle<v8::internal::Context> lexical(hackfunc->context());
// Serialize the code simply by calling toString on the Function.
Local<Function> toString = Local<Function>::Cast(
func->Get(String::NewFromUtf8(isolate, "toString")));
Local<String> code = Local<String>::Cast(toString->Call(func, 0, nullptr));
// Get the code as a string.
Local<String> code = SerializeFunctionObjectText(isolate, func);
// Compute the free variables by invoking the callback.
std::vector<Local<String>> freevars;
const unsigned freevarsArgc = 1;
Local<Value> freevarsArgv[freevarsArgc] = { code };
Local<Value> freevarsRet = freevarsFunc->Call(Null(isolate), freevarsArgc, freevarsArgv);
if (freevarsRet.IsEmpty() || !freevarsRet->IsArray()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate, "Free variables return expected to be an Array")));
} else {
Local<Array> freevarsArray = Local<Array>::Cast(freevarsRet);
for (uint32_t i = 0; i < freevarsArray->Length(); i++) {
Local<Integer> index = Integer::New(isolate, i);
Local<Value> elem = freevarsArray->Get(index);
if (elem.IsEmpty() || !elem->IsString()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate, "Free variable Array must contain only String elements")));
} else {
freevars.push_back(Local<String>::Cast(elem));
}
}
}
// Next, serialize all free variables as they exist in the function's original lexical environment.
Local<Object> environment = Object::New(isolate);
Local<String> freevars[1] = { String::NewFromUtf8(isolate, "x") };
for (int i = 0; i < 1; i++) {
for (std::vector<Local<String>>::iterator it = freevars.begin(); it != freevars.end(); ++it) {
// Look up the variable in the lexical closure of the function and then serialize it.
Local<String> freevar = freevars[0];
Local<String> freevar = *it;
Local<Value> v = Lookup(isolate, lexical, freevar);
environment->Set(freevar, SerializeClosureEnvEntry(isolate, v));
environment->Set(freevar, SerializeClosureEnvEntry(isolate, v, freevarsFunc));
}
// Finally, produce a closure object with all the appropriate records, and return it.
@ -140,17 +195,32 @@ Local<Value> SerializeFunction(Isolate *isolate, Local<Function> func) {
void SerializeClosure(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
// First ensure the first argument is a proper function expression object.
if (args.Length() == 0 || args[0]->IsUndefined() || args[0]->IsNull()) {
// Ensure the first argument is a proper function expression object.
if (args.Length() < 1 || args[0]->IsUndefined()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate, "Missing required function argument")));
return;
} else if (!args[0]->IsFunction()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate, "Function argument must be a Function object")));
return;
}
Local<Function> func = Local<Function>::Cast(args[0]);
// And that the second is a callback we can use to compute the free variables list.
if (args.Length() < 2 || args[1]->IsUndefined()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate, "Missing required free variables calculator")));
return;
} else if (!args[1]->IsFunction()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate, "Free variables argument must be a Function object")));
return;
}
Local<Function> freevarsFunc = Local<Function>::Cast(args[1]);
// Now go ahead and serialize it, and return the result.
Local<Value> closure = SerializeFunction(isolate, Local<Function>::Cast(args[0]));
Local<Value> closure = SerializeFunction(isolate, func, freevarsFunc);
args.GetReturnValue().Set(closure);
}

View file

@ -0,0 +1,228 @@
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
import * as assert from "assert";
import { runtime } from "../../index";
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.
}
// This group of tests ensure that we serialize closures properly.
describe("closure", () => {
let cases: ClosureCase[] = [];
// A few simple positive cases for functions/arrows (no captures).
cases.push({
title: "Empty function closure",
func: function () { },
expect: {
code: "(function () { })",
environment: {},
runtime: "nodejs",
},
});
cases.push({
title: "Empty arrow closure",
func: () => { },
expect: {
code: "(() => { })",
environment: {},
runtime: "nodejs",
},
});
cases.push({
title: "Empty function closure w/ args",
func: function (x: any, y: any, z: any) { },
expect: {
code: "(function (x, y, z) { })",
environment: {},
runtime: "nodejs",
},
});
cases.push({
title: "Empty arrow closure w/ args",
func: (x: any, y: any, z: any) => { },
expect: {
code: "((x, y, z) => { })",
environment: {},
runtime: "nodejs",
},
});
// Ensure we reject function declarations.
class C {
public m(): void {}
}
cases.push({
title: "Reject non-expression function objects",
func: new C().m,
});
// 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",
},
});
{
let wcap = "foo";
let xcap = 97;
let ycap = [ true, -1, "yup" ];
let zcap = {
a: "a",
b: false,
c: [ 0 ],
};
cases.push({
title: "Serializes basic captures",
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",
},
});
}
{
let nocap1 = 1, nocap2 = 2, nocap3 = 3, nocap4 = 4, nocap5 = 5;
let cap1 = 100, cap2 = 200, cap3 = 300, cap4 = 400;
let 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;
}
});
})`;
cases.push({
title: "Doesn't serialize non-free variables (but retains frees)",
func: eval(functext),
expect: {
code: functext,
environment: {
cap1: { json: 100 },
cap2: { json: 200 },
cap3: { json: 300 },
cap4: { json: 400 },
},
runtime: "nodejs",
},
});
}
// Recursive function serialization.
{
let fff = "fff!";
let ggg = "ggg!";
let xcap = {
fff: function () { console.log(fff); },
ggg: () => { console.log(ggg); },
zzz: {
a: [ (a1: any, a2: any) => { console.log(a1 + a2); } ],
},
};
let functext = `(() => {
xcap.fff();
xcap.ggg();
xcap.zzz.a[0]("x", "y");
})`;
cases.push({
title: "Serializes recursive function captures",
func: eval(functext),
expect: {
code: functext,
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",
},
});
}
// Now go ahead and run the test cases, each as its own case.
for (let test of cases) {
it(test.title, () => {
if (test.expect) {
assert.deepEqual(runtime.serializeClosure(test.func), test.expect);
} else {
assert.throws(() => runtime.serializeClosure(test.func));
}
});
};
});

View file

@ -1,6 +1,6 @@
// This tests the basic creation of a single propertyless resource.
let fabric = require("../../../../");
let fabric = require("../../../../../../");
class MyResource extends fabric.Resource {
constructor(name) {

View file

@ -1,6 +1,6 @@
// This tests the creation of ten propertyless resources.
let fabric = require("../../../../");
let fabric = require("../../../../../");
class MyResource extends fabric.Resource {
constructor(name) {

View file

@ -2,7 +2,7 @@
// In particular, there aren't any fancy dataflow linked properties.
let assert = require("assert");
let fabric = require("../../../../");
let fabric = require("../../../../../");
class MyResource extends fabric.Resource {
constructor(name) {

View file

@ -1,4 +1,6 @@
let fabric = require("../../../../");
// Define and export a resource class that can be used by index.js.
let fabric = require("../../../../../");
exports.MyResource = class MyResource extends fabric.Resource {
constructor(name, seq) {

View file

@ -1,7 +1,7 @@
// This test case links one resource's property to another.
let assert = require("assert");
let fabric = require("../../../../");
let fabric = require("../../../../../");
class ResourceA extends fabric.Resource {
constructor(name) {

View file

@ -1,6 +1,6 @@
// This tests simple creation of assets.
let fabric = require("../../../../");
let fabric = require("../../../../../");
class FileResource extends fabric.Resource {
constructor(name, data) {

View file

@ -1,6 +1,6 @@
// This tests the ability to use promises for resource properties.
let fabric = require("../../../../");
let fabric = require("../../../../../");
let fs = require("fs");
class FileResource extends fabric.Resource {

View file

@ -1,7 +1,7 @@
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
import { ID, runtime, URN } from "../../index";
import { asyncTest } from "../util";
import { ID, runtime, URN } from "../../../index";
import { asyncTest } from "../../util";
import * as assert from "assert";
import * as childProcess from "child_process";
import * as path from "path";
@ -9,8 +9,8 @@ import * as os from "os";
let gstruct = require("google-protobuf/google/protobuf/struct_pb.js");
let grpc = require("grpc");
let langrpc = require("../../proto/nodejs/languages_grpc_pb");
let langproto = require("../../proto/nodejs/languages_pb");
let langrpc = require("../../../proto/nodejs/languages_grpc_pb");
let langproto = require("../../../proto/nodejs/languages_pb");
interface RunCase {
pwd?: string;
@ -331,7 +331,7 @@ function createMockResourceMonitor(
function serveLanguageHostProcess(monitorAddr: string): { proc: childProcess.ChildProcess, addr: Promise<string> } {
// Spawn the language host in a separate process so that each test case gets an isolated heap, globals, etc.
let proc = childProcess.spawn(process.argv[0], [
path.join(__filename, "..", "..", "..", "cmd", "langhost", "index.js"),
path.join(__filename, "..", "..", "..", "..", "cmd", "langhost", "index.js"),
monitorAddr,
]);
// Hook the first line so we can parse the address. Then we hook the rest to print for debugging purposes, and

View file

@ -36,7 +36,8 @@
"tests/config.spec.ts",
"tests/init.spec.ts",
"tests/util.ts",
"tests/langhost/run.spec.ts"
"tests/runtime/closure.spec.ts",
"tests/runtime/langhost/run.spec.ts"
]
}

View file

@ -2,6 +2,16 @@
# yarn lockfile v1
"@types/acorn@^4.0.2":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-4.0.2.tgz#5c37dade857a53c14258c914d43e634076aec3fd"
dependencies:
"@types/estree" "*"
"@types/estree@*":
version "0.0.37"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.37.tgz#48949c1516d46139c1e521195f9e12993b69d751"
"@types/minimist@^1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.0.tgz#69a23a3ad29caf0097f06eda59b361ee2f0639f6"
@ -18,6 +28,10 @@ abbrev@1, abbrev@1.0.x:
version "1.0.9"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
acorn@^5.1.1:
version "5.1.1"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.1.1.tgz#53fe161111f912ab999ee887a90a0bc52822fd75"
ajv@^4.9.1:
version "4.11.8"
resolved "https://registry.yarnpkg.com/ajv/-/ajv-4.11.8.tgz#82ffb02b29e662ae53bdc20af15947706739c536"
@ -376,7 +390,7 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
glob@7.1.1, glob@^7.0.5, glob@^7.1.1:
glob@7.1.1, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
dependencies:
@ -695,7 +709,7 @@ minimist@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.1:
mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
dependencies:
@ -725,6 +739,24 @@ nan@^2.0.0:
version "2.6.2"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45"
node-gyp@^3.6.2:
version "3.6.2"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60"
dependencies:
fstream "^1.0.0"
glob "^7.0.3"
graceful-fs "^4.1.2"
minimatch "^3.0.2"
mkdirp "^0.5.0"
nopt "2 || 3"
npmlog "0 || 1 || 2 || 3 || 4"
osenv "0"
request "2"
rimraf "2"
semver "~5.3.0"
tar "^2.0.0"
which "1"
node-pre-gyp@^0.6.35:
version "0.6.36"
resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.6.36.tgz#db604112cb74e0d477554e9b505b17abddfab786"
@ -739,7 +771,7 @@ node-pre-gyp@^0.6.35:
tar "^2.2.1"
tar-pack "^3.4.0"
nopt@3.x:
"nopt@2 || 3", nopt@3.x:
version "3.0.6"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
dependencies:
@ -752,7 +784,7 @@ nopt@^4.0.1:
abbrev "1"
osenv "^0.1.4"
npmlog@^4.0.2:
"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b"
dependencies:
@ -815,7 +847,7 @@ os-tmpdir@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274"
osenv@^0.1.4:
osenv@0, osenv@^0.1.4:
version "0.1.4"
resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644"
dependencies:
@ -884,7 +916,7 @@ repeat-string@^1.5.2:
version "1.6.1"
resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637"
request@^2.81.0:
request@2, request@^2.81.0:
version "2.81.0"
resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
dependencies:
@ -941,6 +973,10 @@ semver@^5.3.0:
version "5.4.1"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e"
semver@~5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
set-blocking@~2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
@ -1046,7 +1082,7 @@ tar-pack@^3.4.0:
tar "^2.2.1"
uid-number "^0.0.6"
tar@^2.2.1:
tar@^2.0.0, tar@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.1.tgz#8e4d2a256c0e2185c6b18ad694aec968b83cb1d1"
dependencies:
@ -1138,7 +1174,7 @@ verror@1.10.0:
core-util-is "1.0.2"
extsprintf "^1.2.0"
which@^1.1.1:
which@1, which@^1.1.1:
version "1.3.0"
resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a"
dependencies: