Bring the magic of Pulumi to Node v8.11.1, v9.11.1, v6.10.3 (#1167)

* Introduce a simple repetition operator to match expected error messages against actual ones

* Convert required and optional objects to use a Map (node v9 compat), improve the error formatting for failed tests

* Test node v6, v8, and v9 in CI

* Get rid of PULUMI_API env in .travis.yml, it's set from the Travis console now
This commit is contained in:
Sean Gillespie 2018-04-14 11:50:01 -07:00 committed by GitHub
parent fe45eb875e
commit 1f5be5f8cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 112 additions and 48 deletions

View file

@ -1,14 +1,16 @@
# It may be tempting to add parens around each individual clause in this expression, but Travis then builds pushes anyway
if: branch = master OR branch =~ ^release/ OR tag IS present
jobs:
matrix:
include:
- os: linux
env: NODE_VERSION=v6.10.3
- os: linux
env: NODE_VERSION=v8.11.1
- os: osx
env: NODE_VERSION=v9.11.1
language: go
go: 1.9
sudo: true # give us 7.5GB and >2 bursted cores.
env:
- PULUMI_API=https://api.pulumi-staging.io
git:
depth: false
before_install:

View file

@ -1,4 +1,4 @@
nvm install v6.10.2
nvm install ${NODE_VERSION-v8.11.1}
# Travis sources this script, so we can export variables into the
# outer shell, so we don't want to set options like nounset because

View file

@ -440,7 +440,7 @@ function createFunctionInfo(
function processCapturedVariables(
capturedVariables: CapturedVariableMap, throwOnFailure: boolean): void {
for (const name of Object.keys(capturedVariables)) {
for (const name of capturedVariables.keys()) {
let value: any;
try {
value = v8.lookupCapturedVariableValue(func, name, throwOnFailure);
@ -481,7 +481,7 @@ function createFunctionInfo(
function processCapturedVariable(
capturedVariables: CapturedVariableMap, name: string, value: any) {
const properties = capturedVariables[name];
const properties = capturedVariables.get(name);
const serializedName = getOrCreateEntry(name, undefined, context, serialize);
// try to only serialize out the properties that were used by the user's code.

View file

@ -38,8 +38,7 @@ export interface CapturedPropertyInfo {
invoked: boolean;
}
export interface CapturedVariableMap extends Record<string, CapturedPropertyInfo[]> {
}
export type CapturedVariableMap = Map<string, CapturedPropertyInfo[]>;
// The set of variables the function attempts to capture. There is a required set an an optional
// set. The optional set will not block closure-serialization if we cannot find them, while the
@ -80,7 +79,7 @@ export function parseFunction(funcString: string): [string, ParsedFunction] {
result.capturedVariables = capturedVariables;
result.usesNonLexicalThis = usesNonLexicalThis;
if (result.capturedVariables.required.this) {
if (result.capturedVariables.required.has("this")) {
return [
"arrow function captured 'this'. Assign 'this' to another name outside function and capture that.",
result,
@ -329,8 +328,8 @@ function computeUsesNonLexicalThis(file: ts.SourceFile): boolean {
function computeCapturedVariableNames(file: ts.SourceFile): CapturedVariables {
// Now that we've parsed the file, compute the free variables, and return them.
let required: CapturedVariableMap = {};
let optional: CapturedVariableMap = {};
let required: CapturedVariableMap = new Map();
let optional: CapturedVariableMap = new Map();
const scopes: Set<string>[] = [];
let functionVars: Set<string> = new Set();
@ -346,18 +345,18 @@ function computeCapturedVariableNames(file: ts.SourceFile): CapturedVariables {
// 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.
const result: CapturedVariables = { required: {}, optional: {} };
const result: CapturedVariables = { required: new Map(), optional: new Map() };
for (const key of Object.keys(required)) {
if (required[key] && !isBuiltIn(key)) {
result.required[key] = required[key].concat(
optional.hasOwnProperty(key) ? optional[key] : []);
for (const key of required.keys()) {
if (required.has(key) && !isBuiltIn(key)) {
result.required.set(key, required.get(key)!.concat(
optional.has(key) ? optional.get(key)! : []));
}
}
for (const key of Object.keys(optional)) {
if (optional[key] && !isBuiltIn(key) && !required[key]) {
result.optional[key] = optional[key];
for (const key of optional.keys()) {
if (optional.has(key) && !isBuiltIn(key) && !required.has(key)) {
result.optional.set(key, optional.get(key)!);
}
}
@ -393,9 +392,9 @@ function computeCapturedVariableNames(file: ts.SourceFile): CapturedVariables {
// "typeof undeclared_id" is legal in JS (and is actually used in libraries). So keep
// track that we would like to capture this variable, but mark that capture as optional
// so we will not throw if we aren't able to find it in scope.
optional[name] = combineProperties(optional[name], capturedProperty);
optional.set(name, combineProperties(optional.get(name), capturedProperty));
} else {
required[name] = combineProperties(required[name], capturedProperty);
required.set(name, combineProperties(required.get(name), capturedProperty));
}
}
@ -440,7 +439,7 @@ function computeCapturedVariableNames(file: ts.SourceFile): CapturedVariables {
}
function visitThisExpression(node: ts.ThisExpression): void {
required["this"] = combineProperties(required["this"], determineCapturedPropertyInfo(node));
required.set("this", combineProperties(required.get("this"), determineCapturedPropertyInfo(node)));
}
function combineProperties(existing: CapturedPropertyInfo[] | undefined,
@ -519,8 +518,8 @@ function computeCapturedVariableNames(file: ts.SourceFile): CapturedVariables {
const savedOptional = optional;
const savedFunctionVars = functionVars;
required = {};
optional = {};
required = new Map();
optional = new Map();
functionVars = new Set();
scopes.push(new Set());
@ -545,8 +544,8 @@ function computeCapturedVariableNames(file: ts.SourceFile): CapturedVariables {
// Remove any function-scoped variables that we encountered during the walk.
for (const v of functionVars) {
delete required[v];
delete optional[v];
required.delete(v);
optional.delete(v);
}
// Restore the prior context and merge our free list with the previous one.
@ -560,11 +559,10 @@ function computeCapturedVariableNames(file: ts.SourceFile): CapturedVariables {
optional = savedOptional;
}
// Record<string, CapturedPropertyInfo[]>
function mergeMaps(target: CapturedVariableMap, source: CapturedVariableMap) {
for (const key of Object.keys(source)) {
const sourcePropInfos = source[key];
let targetPropInfos = target[key];
for (const key of source.keys()) {
const sourcePropInfos = source.get(key)!;
let targetPropInfos = target.get(key)!;
if (sourcePropInfos.length === 0) {
// we want to capture everything. Make sure that's reflected in the target.
@ -578,7 +576,7 @@ function computeCapturedVariableNames(file: ts.SourceFile): CapturedVariables {
}
}
target[key] = targetPropInfos;
target.set(key, targetPropInfos);
}
}

View file

@ -3,6 +3,7 @@
// tslint:disable:max-line-length
import * as assert from "assert";
import { EOL } from "os";
import { runtime } from "../../index";
import * as resource from "../../resource";
import { Output, output } from "../../resource";
@ -845,11 +846,11 @@ return () => { let x = eval("undefined + null + NaN + Infinity + __filename"); r
'() => os': closure.spec.js(0,0): captured
module 'os' which indirectly referenced
function 'getHostname': which could not be serialized because
(...)
it was a native code function.
Function code:
function getHostname() { [native code] }
function (...)() { [native code] }
Capturing modules can sometimes cause problems.
Consider using import('os') or require('os') inside '() => os': closure.spec.js(0,0)`,
@ -873,11 +874,11 @@ Consider using import('os') or require('os') inside '() => os': closure.spec.js(
'(a, b, c) => { const v = os; ...': closure.spec.js(0,0): captured
module 'os' which indirectly referenced
function 'getHostname': which could not be serialized because
(...)
it was a native code function.
Function code:
function getHostname() { [native code] }
function (...)() { [native code] }
Capturing modules can sometimes cause problems.
Consider using import('os') or require('os') inside '(a, b, c) => { const v = os; ...': closure.spec.js(0,0)`,
@ -903,11 +904,11 @@ Consider using import('os') or require('os') inside '(a, b, c) => { const v = os
'handler', a function defined at
'() => os': closure.spec.js(0,0): which captured
module 'os' which indirectly referenced
function 'getHostname': which could not be serialized because
(...)
it was a native code function.
Function code:
function getHostname() { [native code] }
function (...)() { [native code] }
Capturing modules can sometimes cause problems.
Consider using import('os') or require('os') inside '() => os': closure.spec.js(0,0)`,
@ -927,18 +928,11 @@ Consider using import('os') or require('os') inside '() => os': closure.spec.js(
module './bin/tests/util.js' which indirectly referenced
function 'assertAsyncThrows': util.js(0,0): which captured
module 'assert' which indirectly referenced
function 'ok': assert.js(0,0): which referenced
function 'AssertionError': assert.js(0,0): which referenced
function 'getMessage': assert.js(0,0): which captured
module 'util' which indirectly referenced
function 'inspect': util.js(0,0): which referenced
function 'formatValue': util.js(0,0): which captured
variable 'binding' which indirectly referenced
function 'isArrayBuffer': which could not be serialized because
it was a native code function.
(...)
it was a native code function.
Function code:
function isArrayBuffer() { [native code] }
function (...)() { [native code] }
Capturing modules can sometimes cause problems.
Consider using import('./bin/tests/util.js') or require('./bin/tests/util.js') inside '() => util': closure.spec.js(0,0)`,
@ -3818,7 +3812,9 @@ return function /*f3*/() {
// updated any time this file changes.
const regex = /\([0-9]+,[0-9]+\)/g;
const withoutLocations = message.replace(regex, "(0,0)");
assert.equal(withoutLocations, test.error);
if (test.error) {
compareErrorText(test.error, withoutLocations);
}
}
}));
@ -3828,3 +3824,71 @@ return function /*f3*/() {
}
}
});
/**
* compareErrorText compares an "expected" error string and an "actual" error string
* and issues an error if they do not match.
*
* This function accepts two repetition operators to make writing tests easier against
* error messages that are dependent on the environment:
*
* * (...) alone on a single line causes the matcher to accept zero or more lines
* between the repetition and the next line.
* * (...) within in the context of a line causes the matcher to accept zero or more characters
* between the repetition and the next character.
*
* This is useful when testing error messages that you get when capturing bulit-in modules,
* because the specific error message differs between Node versions.
* @param expected The expected error message string, potentially containing repetitions
* @param actual The actual error message string
*/
function compareErrorText(expected: string, actual: string) {
const wildcard = "(...)";
if (!expected.includes(wildcard)) {
// We get a nice diff view if we diff the entire string, so do that
// if we didn't get a wildcard.
assert.equal(actual, expected);
return;
}
const expectedLines = expected.split(EOL);
const actualLines = actual.split(EOL);
let actualIndex = 0;
for (let expectedIndex = 0; expectedIndex < expectedLines.length; expectedIndex++) {
const expectedLine = expectedLines[expectedIndex].trim();
if (expectedLine === wildcard) {
if (expectedIndex + 1 === expectedLines.length) {
return;
}
const nextLine = expectedLines[++expectedIndex].trim();
while (true) {
const actualLine = actualLines[actualIndex++].trim();
if (actualLine === nextLine) {
break;
}
if (actualIndex === actualLines.length) {
assert.fail(`repetition failed to find match: expected terminator ${nextLine}, received ${actual}`);
}
}
} else if (expectedLine.includes(wildcard)) {
const line = actualLines[actualIndex++].trim();
const index = expectedLine.indexOf(wildcard);
const indexAfter = index + wildcard.length;
assert.equal(line.substring(0, index), expectedLine.substring(0, index));
let repetitionIndex = index;
for (; repetitionIndex < line.length; repetitionIndex++) {
if (line[repetitionIndex] === expectedLine[indexAfter]) {
break;
}
}
assert.equal(line.substring(repetitionIndex), expectedLine.substring(indexAfter));
} else {
assert.equal(actualLines[actualIndex++].trim(), expectedLine);
}
}
}