From edcfb043a9b323ca57e036b2841b619a73683e83 Mon Sep 17 00:00:00 2001 From: Cristian Cavalli Date: Mon, 15 Aug 2016 11:40:14 -0700 Subject: [PATCH] Add ScopeMirror traversal to state.js (#142) Replace the `FrameMirror.localValue` code path with `ScopeMirror` traversal and rewrite the arguments collector to only serialize arguments which are not matched by name with a more deeply-scoped value. Add closure scope type to filter list because of confusion with node.JS requires. Add tests for nested, confounding variable names and try/catch. 03/08/2016-10:07 PST Update to use locals only becuase of 0.12 behaviour. 12/08/2016-12:40 PST Update with full-range of test-cases gathered from bug-bash. Add more testing around locals for node versions less than 1.6. Add more documentation around new ScopeMirror traversal. Add 'this' context extraction and tests. 15/08/2016-10:54 PST Respond to PR feedback --- lib/state.js | 146 ++++++++++-- lib/v8debugapi.js | 2 +- test/fixtures/fat-arrow.js | 11 + .../test-duplicate-nested-expressions.js | 208 ++++++++++++++++++ test/standalone/test-fat-arrow.js | 115 ++++++++++ test/standalone/test-this-context.js | 133 +++++++++++ test/standalone/test-try-catch.js | 135 ++++++++++++ test/test-v8debugapi.js | 59 +++-- 8 files changed, 771 insertions(+), 38 deletions(-) create mode 100644 test/fixtures/fat-arrow.js create mode 100644 test/standalone/test-duplicate-nested-expressions.js create mode 100644 test/standalone/test-fat-arrow.js create mode 100644 test/standalone/test-this-context.js create mode 100644 test/standalone/test-try-catch.js diff --git a/lib/state.js b/lib/state.js index 5c04272f..7fa29b24 100644 --- a/lib/state.js +++ b/lib/state.js @@ -21,9 +21,16 @@ module.exports = { evaluate: evaluate }; +var ScopeType = require('vm').runInDebugContext('ScopeType'); var assert = require('assert'); var semver = require('semver'); var util = require('util'); +var lodash = require('lodash'); +var find = lodash.find; +var transform = lodash.transform; +var remove = lodash.remove; +var flatten = lodash.flatten; +var isEmpty = lodash.isEmpty; var StatusMessage = require('./apiclasses.js').StatusMessage; @@ -67,8 +74,8 @@ MESSAGE_TABLE[STRING_LIMIT_MESSAGE_INDEX] = * @return an object with stackFrames, variableTable, and * evaluatedExpressions fields */ -function capture(execState, expressions, config) { - return (new StateResolver(execState, expressions, config)).capture_(); +function capture(execState, expressions, config, v8) { + return (new StateResolver(execState, expressions, config, v8)).capture_(); } @@ -112,10 +119,11 @@ function evaluate(expression, frame) { * @param {!Object} config * @constructor */ -function StateResolver(execState, expressions, config) { +function StateResolver(execState, expressions, config, v8) { this.state_ = execState; this.expressions_ = expressions; this.config_ = config; + this.ctx_ = v8; this.evaluatedExpressions_ = []; this.totalSize_ = 0; @@ -277,14 +285,32 @@ StateResolver.prototype.isPathInNodeModulesDirectory_ = function(path) { }; StateResolver.prototype.resolveFrame_ = function(frame, resolveVars) { - var args = resolveVars ? this.resolveArgumentList_(frame) : [{ + var noArgs = [{ name: 'arguments_not_available', varTableIndex: ARG_LOCAL_LIMIT_MESSAGE_INDEX }]; - var locals = resolveVars ? this.resolveLocalsList_(frame, args) : [{ + var noLocals = [{ name: 'locals_not_available', varTableIndex: ARG_LOCAL_LIMIT_MESSAGE_INDEX }]; + var args = this.extractArgumentsList_(frame); + var locals = resolveVars ? this.resolveLocalsList_(frame, args) : noLocals; + if (isEmpty(locals)) { + locals = noLocals; + } + if (semver.satisfies(process.version, '<1.6')) { + // If the node version is over 1.6 we do not read the frame arguments since + // the values produced by the frame for the arguments may contain inaccurate + // values. If the version is lower than 1.6, though, the frame's argument + // list can be relied upon to produce accurate values for arguments. + args = resolveVars && (!isEmpty(args)) ? this.resolveArgumentList_(args) : + noArgs; + } else { + // Otherwise, if the version is 1.6 or higher than we will use the values + // aggregated from the ScopeMirror traversal stored in locals which will + // include any applicable arguments from the invocation. + args = noArgs; + } return { function: this.resolveFunctionName_(frame.func()), location: this.resolveLocation_(frame), @@ -308,36 +334,108 @@ StateResolver.prototype.resolveLocation_ = function(frame) { }; }; -StateResolver.prototype.resolveArgumentList_ = function(frame) { +StateResolver.prototype.extractArgumentsList_ = function (frame) { var args = []; for (var i = 0; i < frame.argumentCount(); i++) { // Don't resolve unnamed arguments. if (!frame.argumentName(i)) { continue; } - args.push(this.resolveVariable_( - frame.argumentName(i), frame.argumentValue(i))); + args.push({name: frame.argumentName(i), value: frame.argumentValue(i)}); } return args; }; -StateResolver.prototype.resolveLocalsList_ = function(frame, - resolvedArguments) { - var locals = []; - // Arguments may have been captured as locals in a nested closure. - // We filter them out here. - var predicate = function(localEntry, argEntry) { - return argEntry.varTableIndex === localEntry.varTableIndex; - }; - for (var i = 0; i < frame.localCount(); i++) { - var localEntry = this.resolveVariable_( - frame.localName(i), frame.localValue(i)); - if (!resolvedArguments.some(predicate.bind(null, localEntry))) { - locals.push(this.resolveVariable_( - frame.localName(i), frame.localValue(i))); +StateResolver.prototype.resolveArgumentList_ = function(args) { + var resolveVariable = this.resolveVariable_.bind(this); + return args.map(function (arg){ + return resolveVariable(arg.name, arg.value); + }); +}; + +/** + * Iterates and returns variable information for all scopes (excluding global) + * in a given frame. FrameMirrors should return their scope object list with + * most deeply nested scope first so variables initially encountered will take + * precedence over subsequent instance with the same name - this is tracked in + * the usedNames map. The argument list given to this function may be + * manipulated if variables with a deeper scope occur which have the same name. + * @function resolveLocalsList_ + * @memberof StateResolver + * @param {FrameMirror} frame - A instance of FrameMirror + * @param {Array} args - An array of objects representing any function + * arguments the frame may list + * @returns {Array} - returns an array containing data about selected + * variables + */ +StateResolver.prototype.resolveLocalsList_ = function (frame, args) { + var self = this; + var usedNames = {}; + var makeMirror = this.ctx_.MakeMirror; + return flatten(frame.allScopes().map( + function (scope) { + switch (scope.scopeType()) { + case ScopeType.Global: + // We do not capture globals in the debug client. + case ScopeType.Closure: + // The closure scope is contaminated by Node.JS's require IIFE pattern + // and, if traversed, will cause local variable pools to include what + // are considered node 'globals'. Therefore, avoid traversal. + return []; + } + return transform( + scope.details().object(), + function (locals, value, name) { + var trg = makeMirror(value); + var argMatch = find(args, {name: name}); + if (argMatch && (semver.satisfies(process.version, '<1.6'))) { + // If the version is lower than 1.6 we will use the frame's argument + // list to source argument values, yet the ScopeMirror traversal for + // these Node versions will also return the arguments. Therefore, on + // these versions, compare the value sourced as the argument from + // the FrameMirror to the argument found in the ScopeMirror locals + // list with the same name and attempt to determine whether or not + // they have the same value. If each of these items has the same + // name and value we may assume that the ScopeMirror variable + // representation is merely a duplicate of the FrameMirror's + // variable representation. Otherwise, the variable may have been + // redeclared or reassigned in the function and is therefore a local + // triggering removal from the arguments list and insertion into the + // locals list. + if (argMatch.value.value() === trg.value()) { + // Argument ref is the same ref as the local ref - this is an + // argument do not push this into the locals list + return locals; + } + // There is another local/scope var with the same name and it is not + // the argument so this will take precedence. Remove the same-named + // entry from the arguments list and push the local value onto the + // locals list. + remove(args, {name: name}); + usedNames[name] = true; + locals.push(self.resolveVariable_(name, trg)); + } else if (!usedNames[name]) { + // It's a valid variable that belongs in the locals list and wasn't + // discovered at a lower-scope + usedNames[name] = true; + locals.push(self.resolveVariable_(name, trg)); + } // otherwise another same-named variable occured at a lower scope + return locals; + }, + [] + ); } - } - return locals; + )).concat((function () { + // The frame receiver is the 'this' context that is present during + // invocation. Check to see whether a receiver context is substantive, + // (invocations may be bound to null) if so: store in the locals list + // under the name 'context' which is used by the Chrome DevTools. + var ctx = frame.details().receiver(); + if (ctx) { + return [self.resolveVariable_('context', makeMirror(ctx))]; + } + return []; + }())); }; /** diff --git a/lib/v8debugapi.js b/lib/v8debugapi.js index 52dd7d6a..43425a8f 100644 --- a/lib/v8debugapi.js +++ b/lib/v8debugapi.js @@ -551,7 +551,7 @@ module.exports.create = function(logger_, config_, fileStats_) { breakpoint.evaluatedExpressions = evaluatedExpressions; } } else { - var captured = state.capture(execState, breakpoint.expressions, config); + var captured = state.capture(execState, breakpoint.expressions, config, v8); breakpoint.stackFrames = captured.stackFrames; breakpoint.variableTable = captured.variableTable; breakpoint.evaluatedExpressions = diff --git a/test/fixtures/fat-arrow.js b/test/fixtures/fat-arrow.js new file mode 100644 index 00000000..c9a16bab --- /dev/null +++ b/test/fixtures/fat-arrow.js @@ -0,0 +1,11 @@ +/*1* KEEP THIS CODE AT THE TOP TO AVOID LINE NUMBER CHANGES */ /* jshint shadow:true */ +/*2*/'use strict'; +/*3*/function foo() { +/*4*/ var a = (b) => { +/*5*/ b += 1; +/*6*/ return b; +/*7*/ }; +/*8*/ return a(1); +/*9*/} +/*10*/module.exports = foo; +/*11*/ \ No newline at end of file diff --git a/test/standalone/test-duplicate-nested-expressions.js b/test/standalone/test-duplicate-nested-expressions.js new file mode 100644 index 00000000..5ddaba93 --- /dev/null +++ b/test/standalone/test-duplicate-nested-expressions.js @@ -0,0 +1,208 @@ +/*1* KEEP THIS CODE AT THE TOP TO AVOID LINE NUMBER CHANGES */ /* jshint shadow:true */ +/*2*/'use strict'; +/*3*/function foo(a) { +/*4*/ var a = 10; +/*5*/ a += 1; +/*6*/ return (function (b) { +/*7*/ var a = true; +/*8*/ return a; +/*9*/ }()); +/*10*/} +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var assert = require('assert'); +var v8debugapi = require('../../lib/v8debugapi.js'); +var logModule = require('@google/cloud-diagnostics-common').logger; +var config = require('../../config.js').debug; +var scanner = require('../../lib/scanner.js'); +var path = require('path'); +var semver = require('semver'); + +function stateIsClean(api) { + assert.equal(api.numBreakpoints_(), 0, + 'there should be no breakpoints active'); + assert.equal(api.numListeners_(), 0, + 'there should be no listeners active'); + return true; +} + +describe('v8debugapi', function() { + config.workingDirectory = path.join(process.cwd(), 'test', 'standalone'); + var logger = logModule.create(config.logLevel); + var api = null; + + beforeEach(function(done) { + if (!api) { + scanner.scan(true, config.workingDirectory, function(err, fileStats, hash) { + assert(!err); + api = v8debugapi.create(logger, config, fileStats); + assert.ok(api, 'should be able to create the api'); + done(); + }); + } else { + assert(stateIsClean(api)); + done(); + } + }); + afterEach(function() { assert(stateIsClean(api)); }); + it('Should read the argument before the name is confounded', function(done) { + var brk = { + id: 'fake-id-123', + location: { path: 'test-duplicate-nested-expressions.js', line: 4 } + }; + api.set(brk, function(err) { + assert.ifError(err); + api.wait(brk, function(err) { + assert.ifError(err); + var frame = brk.stackFrames[0]; + var args = frame.arguments; + var locals = frame.locals; + if (semver.satisfies(process.version, '<1.6')) { + assert.equal(args.length, 1, 'There should be one argument'); + assert.deepEqual( + args[0], + {name: 'a', value: '11'} + ); + assert.equal(locals.length, 1); + assert.equal(locals[0].name, 'locals_not_available'); + } else { + assert.equal(args.length, 1, 'There should be one argument'); + assert.equal(args[0].name, 'arguments_not_available'); + assert.equal(locals.length, 1, 'There should be one locals'); + assert.deepEqual( + locals[0], + {name: 'a', value: 'test'} + ); + } + api.clear(brk); + done(); + }); + process.nextTick(foo.bind(null, 'test')); + }); + }); + + it('Should read an argument after the name is confounded', function(done) { + var brk = { + id: 'fake-id-1234', + location: { path: 'test-duplicate-nested-expressions.js', line: 5 } + }; + api.set(brk, function(err) { + assert.ifError(err); + api.wait(brk, function(err) { + assert.ifError(err); + var frame = brk.stackFrames[0]; + var args = frame.arguments; + var locals = frame.locals; + if (semver.satisfies(process.version, '<1.6')) { + assert.equal(args.length, 1, 'There should be one argument'); + assert.deepEqual( + args[0], + {name: 'a', value: '11'} + ); + assert.equal(locals.length, 1); + assert.equal(locals[0].name, 'locals_not_available'); + } else { + assert.equal(args.length, 1, 'There should be one argument'); + assert.equal(args[0].name, 'arguments_not_available'); + assert.equal(locals.length, 1, 'There should be one local'); + assert.deepEqual( + locals[0], + {name: 'a', value: '10'} + ); + } + api.clear(brk); + done(); + }); + process.nextTick(foo.bind(null, 'test')); + }); + }); + + it('Should read an argument value after its value is modified', function(done) { + var brk = { + id: 'fake-id-1234', + location: { path: 'test-duplicate-nested-expressions.js', line: 6 } + }; + api.set(brk, function(err) { + assert.ifError(err); + api.wait(brk, function(err) { + assert.ifError(err); + var frame = brk.stackFrames[0]; + var args = frame.arguments; + var locals = frame.locals; + if (semver.satisfies(process.version, '<1.6')) { + assert.equal(args.length, 1, 'There should be one argument'); + assert.deepEqual( + args[0], + {name: 'a', value: '11'} + ); + assert.equal(locals.length, 1); + assert.equal(locals[0].name, 'locals_not_available'); + } else { + assert.equal(args.length, 1, 'There should be one argument'); + assert.equal(args[0].name, 'arguments_not_available'); + assert.equal(locals.length, 1, 'There should be one local'); + assert.deepEqual( + locals[0], + {name: 'a', value: '11'} + ); + } + api.clear(brk); + done(); + }); + process.nextTick(foo.bind(null, 'test')); + }); + }); + + it('Should represent a var name at its local-scope when clearly defined', function(done) { + var brk = { + id: 'fake-id-1234', + location: { path: 'test-duplicate-nested-expressions.js', line: 8 } + }; + if (semver.satisfies(process.version, '<1.6')) { + // this IIFE test does not work on 0.12. Specifically the IIFE never + // executes and, therefore, the breakpoint is never hit. This will require + // further investigation as to what is causing the IIFE not to execute.a + // @TODO cristiancavalli - investigate why this IIFE does not execute + console.log('Skipping IIFE test due to Node.JS version requirements'); + this.skip(); + return; + } + api.set(brk, function(err) { + assert.ifError(err); + api.wait(brk, function(err) { + assert.ifError(err); + var frame = brk.stackFrames[0]; + var args = frame.arguments; + var locals = frame.locals; + assert.equal(args.length, 1, 'There should be one argument'); + assert.equal(args[0].name, 'arguments_not_available'); + assert.equal(locals.length, 2, 'There should be two locals'); + assert.deepEqual( + locals[0], + {name: 'b', value: 'undefined'} + ); + assert.deepEqual( + locals[1], + {name: 'a', value: 'true'} + ); + api.clear(brk); + done(); + }); + process.nextTick(foo.bind(null, 'test')); + }); + }); +}); diff --git a/test/standalone/test-fat-arrow.js b/test/standalone/test-fat-arrow.js new file mode 100644 index 00000000..2f5dd52d --- /dev/null +++ b/test/standalone/test-fat-arrow.js @@ -0,0 +1,115 @@ +'use strict'; +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var assert = require('assert'); +var v8debugapi = require('../../lib/v8debugapi.js'); +var logModule = require('@google/cloud-diagnostics-common').logger; +var config = require('../../config.js').debug; +var scanner = require('../../lib/scanner.js'); +var path = require('path'); +var semver = require('semver'); + +process.env.GCLOUD_PROJECT = 0; + +function stateIsClean(api) { + assert.equal(api.numBreakpoints_(), 0, + 'there should be no breakpoints active'); + assert.equal(api.numListeners_(), 0, + 'there should be no listeners active'); + return true; +} + +describe('v8debugapi', function() { + config.workingDirectory = path.join(process.cwd(), 'test'); + var logger = logModule.create(config.logLevel); + var api = null; + var foo; + before(function () { + if (semver.satisfies(process.version, '<4.0')) { + // Fat arrow syntax is not recognized by these node versions - skip tests. + console.log('Skipping fat-arrow syntax due to Node.JS version being ' + + 'lower than requirements'); + this.skip(); + return; + } + foo = require('../fixtures/fat-arrow.js'); + }); + beforeEach(function(done) { + if (!api) { + scanner.scan(true, config.workingDirectory, function(err, fileStats, hash) { + assert(!err); + api = v8debugapi.create(logger, config, fileStats); + assert.ok(api, 'should be able to create the api'); + done(); + }); + } else { + assert(stateIsClean(api)); + done(); + } + }); + afterEach(function() { assert(stateIsClean(api)); }); + it('Should read the argument value of the fat arrow', function(done) { + var brk = { + id: 'fake-id-123', + location: { path: 'fixtures/fat-arrow.js', line: 5 } + }; + api.set(brk, function(err) { + assert.ifError(err); + api.wait(brk, function(err) { + assert.ifError(err); + var frame = brk.stackFrames[0]; + var args = frame.arguments; + var locals = frame.locals; + assert.equal(args.length, 1, 'There should be one argument'); + assert.equal(args[0].name, 'arguments_not_available'); + assert.equal(locals.length, 1, 'There should be one local'); + assert.deepEqual( + locals[0], + {name: 'b', value: '1'} + ); + api.clear(brk); + done(); + }); + process.nextTick(foo.bind(null, 'test')); + }); + }); + it('Should process the argument value change of the fat arrow', function(done) { + var brk = { + id: 'fake-id-123', + location: { path: 'fixtures/fat-arrow.js', line: 6 } + }; + api.set(brk, function(err) { + assert.ifError(err); + api.wait(brk, function(err) { + assert.ifError(err); + var frame = brk.stackFrames[0]; + var args = frame.arguments; + var locals = frame.locals; + assert.equal(args.length, 1, 'There should be one argument'); + assert.equal(args[0].name, 'arguments_not_available'); + assert.equal(locals.length, 1, 'There should be one local'); + assert.deepEqual( + locals[0], + {name: 'b', value: '2'} + ); + api.clear(brk); + done(); + }); + process.nextTick(foo.bind(null, 'test')); + }); + }); +}); diff --git a/test/standalone/test-this-context.js b/test/standalone/test-this-context.js new file mode 100644 index 00000000..8742e500 --- /dev/null +++ b/test/standalone/test-this-context.js @@ -0,0 +1,133 @@ +/*1* KEEP THIS CODE AT THE TOP TO AVOID LINE NUMBER CHANGES */ +/*2*/'use strict'; +/*3*/function foo(b) {/* jshint validthis: true */ +/*4*/ this.a = 10; +/*5*/ this.a += b; +/*6*/ return this; +/*7*/} +/*8*/function bar(j) { +/*9*/ return j; +/*10*/} +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var assert = require('assert'); +var v8debugapi = require('../../lib/v8debugapi.js'); +var logModule = require('@google/cloud-diagnostics-common').logger; +var config = require('../../config.js').debug; +var scanner = require('../../lib/scanner.js'); +var path = require('path'); +var semver = require('semver'); + +function stateIsClean(api) { + assert.equal(api.numBreakpoints_(), 0, + 'there should be no breakpoints active'); + assert.equal(api.numListeners_(), 0, + 'there should be no listeners active'); + return true; +} + +describe('v8debugapi', function() { + config.workingDirectory = path.join(process.cwd(), 'test', 'standalone'); + var logger = logModule.create(config.logLevel); + var api = null; + + beforeEach(function(done) { + if (!api) { + scanner.scan(true, config.workingDirectory, function(err, fileStats, hash) { + assert(!err); + api = v8debugapi.create(logger, config, fileStats); + assert.ok(api, 'should be able to create the api'); + done(); + }); + } else { + assert(stateIsClean(api)); + done(); + } + }); + afterEach(function() { assert(stateIsClean(api)); }); + it('Should be able to read the argument and the context', function(done) { + var brk = { + id: 'fake-id-123', + location: { path: 'test-this-context.js', line: 5 } + }; + var ctxMembers; + api.set(brk, function(err) { + assert.ifError(err); + api.wait(brk, function(err) { + assert.ifError(err); + var frame = brk.stackFrames[0]; + var args = frame.arguments; + var locals = frame.locals; + ctxMembers = brk.variableTable.slice(brk.variableTable.length-1)[0] + .members; + assert.deepEqual(ctxMembers.length, 1, + 'There should be one member in the context variable value'); + assert.deepEqual(ctxMembers[0], {name: 'a', value: '10'}); + if (semver.satisfies(process.version, '<1.6')) { + assert.equal(args.length, 1, 'There should be one argument'); + assert.equal(locals.length, 1, 'There should be one local'); + assert.deepEqual( + args[0], + {name: 'b', value: '1'} + ); + assert.deepEqual(locals[0].name, 'context'); + } else { + assert.equal(args.length, 1, 'There should be one argument'); + assert.equal(args[0].name, 'arguments_not_available'); + assert.equal(locals.length, 2, 'There should be two locals'); + assert.deepEqual(locals[0], {name: 'b', value: '1'}); + assert.deepEqual(locals[1].name, 'context'); + } + api.clear(brk); + done(); + }); + process.nextTick(foo.bind({}, 1)); + }); + }); + it('Should be able to read the argument and deny the context', function(done) { + var brk = { + id: 'fake-id-123', + location: { path: 'test-this-context.js', line: 9 } + }; + api.set(brk, function(err) { + assert.ifError(err); + api.wait(brk, function(err) { + assert.ifError(err); + var frame = brk.stackFrames[0]; + var args = frame.arguments; + var locals = frame.locals; + if (semver.satisfies(process.version, '<1.6')) { + assert.equal(args.length, 1, 'There should be one argument'); + assert.equal(locals.length, 1, 'There should be one local'); + assert.deepEqual( + args[0], + {name: 'j', value: '1'} + ); + assert.equal(locals[0].name, 'locals_not_available'); + } else { + assert.equal(args.length, 1, 'There should be one argument'); + assert.equal(args[0].name, 'arguments_not_available'); + assert.equal(locals.length, 1, 'There should be one local'); + assert.deepEqual(locals[0], {name: 'j', value: '1'}); + } + api.clear(brk); + done(); + }); + process.nextTick(bar.bind(null, 1)); + }); + }); +}); diff --git a/test/standalone/test-try-catch.js b/test/standalone/test-try-catch.js new file mode 100644 index 00000000..38b97777 --- /dev/null +++ b/test/standalone/test-try-catch.js @@ -0,0 +1,135 @@ +/*1* KEEP THIS CODE AT THE TOP TO AVOID LINE NUMBER CHANGES */ /* jshint shadow:true */ +/*2*/'use strict'; +/*3*/function foo() { +/*4*/ try { +/*5*/ throw new Error('A test'); +/*6*/ } catch (e) { +/*7*/ var e = 2; +/*8*/ return e; +/*9*/ } +/*10*/} +/** + * Copyright 2015 Google Inc. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var assert = require('assert'); +var v8debugapi = require('../../lib/v8debugapi.js'); +var logModule = require('@google/cloud-diagnostics-common').logger; +var config = require('../../config.js').debug; +var scanner = require('../../lib/scanner.js'); +var path = require('path'); +var semver = require('semver'); + +function stateIsClean(api) { + assert.equal(api.numBreakpoints_(), 0, + 'there should be no breakpoints active'); + assert.equal(api.numListeners_(), 0, + 'there should be no listeners active'); + return true; +} + +describe('v8debugapi', function() { + config.workingDirectory = path.join(process.cwd(), 'test', 'standalone'); + var logger = logModule.create(config.logLevel); + var api = null; + + beforeEach(function(done) { + if (!api) { + scanner.scan(true, config.workingDirectory, function(err, fileStats, hash) { + assert(!err); + api = v8debugapi.create(logger, config, fileStats); + assert.ok(api, 'should be able to create the api'); + done(); + }); + } else { + assert(stateIsClean(api)); + done(); + } + }); + afterEach(function() { assert(stateIsClean(api)); }); + it('Should read e as the caught error', function(done) { + var brk = { + id: 'fake-id-123', + location: { path: 'test-try-catch.js', line: 7 } + }; + api.set(brk, function(err) { + assert.ifError(err); + api.wait(brk, function(err) { + assert.ifError(err); + var frame = brk.stackFrames[0]; + var args = frame.arguments; + var locals = frame.locals; + assert.equal(args.length, 1, 'There should be one argument'); + assert.equal(locals.length, 1, 'There should be one local'); + if (semver.satisfies(process.version, '<1.6')) { + // Try/Catch scope-walking does not work on 0.12 + assert.deepEqual( + locals[0], + {name: 'e', value: 'undefined'} + ); + } else { + assert.deepEqual( + locals[0], + {name: 'e', varTableIndex: 6} + ); + } + + assert.deepEqual( + args[0], + {name: 'arguments_not_available', varTableIndex: 3} + ); + api.clear(brk); + done(); + }); + process.nextTick(foo.bind(null, 'test')); + }); + }); + it('Should read e as the local error', function(done) { + var brk = { + id: 'fake-id-123', + location: { path: 'test-try-catch.js', line: 8 } + }; + api.set(brk, function(err) { + assert.ifError(err); + api.wait(brk, function(err) { + assert.ifError(err); + var frame = brk.stackFrames[0]; + var args = frame.arguments; + var locals = frame.locals; + if (semver.satisfies(process.version, '<1.6')) { + // Try/Catch scope-walking does not work on 0.12 + assert.deepEqual( + locals[0], + {name: 'e', value: 'undefined'} + ); + } else { + assert.equal(args.length, 1, 'There should be one argument'); + assert.equal(locals.length, 1, 'There should be one local'); + assert.deepEqual( + locals[0], + {name: 'e', value: '2'} + ); + assert.deepEqual( + args[0], + {name: 'arguments_not_available', varTableIndex: 3} + ); + } + api.clear(brk); + done(); + }); + process.nextTick(foo.bind(null, 'test')); + }); + }); +}); diff --git a/test/test-v8debugapi.js b/test/test-v8debugapi.js index 1cc643e0..68b0d434 100644 --- a/test/test-v8debugapi.js +++ b/test/test-v8debugapi.js @@ -506,8 +506,17 @@ describe('v8debugapi', function() { var topFrame = bp.stackFrames[0]; assert.ok(topFrame); assert.equal(topFrame['function'], 'foo'); - assert.equal(topFrame.arguments[0].name, 'n'); - assert.equal(topFrame.arguments[0].value, '2'); + if (semver.satisfies(process.version, '<1.6')) { + assert.equal(topFrame.arguments[0].name, 'n'); + assert.equal(topFrame.arguments[0].value, '2'); + assert.equal(topFrame.locals[0].name, 'A'); + assert.equal(topFrame.locals[1].name, 'B'); + } else { + assert.equal(topFrame.locals[0].name, 'n'); + assert.equal(topFrame.locals[0].value, '2'); + assert.equal(topFrame.locals[1].name, 'A'); + assert.equal(topFrame.locals[2].name, 'B'); + } api.clear(bp); done(); }); @@ -558,8 +567,13 @@ describe('v8debugapi', function() { var topFrame = bp.stackFrames[0]; assert.ok(topFrame); assert.equal(topFrame['function'], 'foo'); - assert.equal(topFrame.arguments[0].name, 'n'); - assert.equal(topFrame.arguments[0].value, '2'); + if (semver.satisfies(process.version, '<1.6')) { + assert.equal(topFrame.arguments[0].name, 'n'); + assert.equal(topFrame.arguments[0].value, '2'); + } else { + assert.equal(topFrame.locals[0].name, 'n'); + assert.equal(topFrame.locals[0].value, '2'); + } api.clear(bp); config.capture.maxFrames = oldMax; done(); @@ -587,8 +601,13 @@ describe('v8debugapi', function() { var topFrame = bp.stackFrames[0]; assert.equal(topFrame['function'], 'foo'); - assert.equal(topFrame.arguments[0].name, 'n'); - assert.equal(topFrame.arguments[0].value, '3'); + if (semver.satisfies(process.version, '<1.6')) { + assert.equal(topFrame.arguments[0].name, 'n'); + assert.equal(topFrame.arguments[0].value, '3'); + } else { + assert.equal(topFrame.locals[0].name, 'n'); + assert.equal(topFrame.locals[0].value, '3'); + } var watch = bp.evaluatedExpressions[0]; assert.equal(watch.name, 'process'); @@ -802,9 +821,13 @@ describe('v8debugapi', function() { assert.ok(bp.stackFrames); var topFrame = bp.stackFrames[0]; - assert.equal(topFrame['function'], 'foo'); - assert.equal(topFrame.arguments[0].name, 'n'); - assert.equal(topFrame.arguments[0].value, '5'); + if (semver.satisfies(process.version, '<1.6')) { + assert.equal(topFrame.arguments[0].name, 'n'); + assert.equal(topFrame.arguments[0].value, '5'); + } else { + assert.equal(topFrame.locals[0].name, 'n'); + assert.equal(topFrame.locals[0].value, '5'); + } api.clear(bp); done(); }); @@ -830,8 +853,13 @@ describe('v8debugapi', function() { var topFrame = bp.stackFrames[0]; assert.equal(topFrame['function'], 'foo'); - assert.equal(topFrame.arguments[0].name, 'n'); - assert.equal(topFrame.arguments[0].value, '3'); + if (semver.satisfies(process.version, '<1.6')) { + assert.equal(topFrame.arguments[0].name, 'n'); + assert.equal(topFrame.arguments[0].value, '3'); + } else { + assert.equal(topFrame.locals[0].name, 'n'); + assert.equal(topFrame.locals[0].value, '3'); + } api.clear(bp); done(); }); @@ -870,9 +898,14 @@ describe('v8debugapi', function() { assert.ok(bp.stackFrames); var topFrame = bp.stackFrames[0]; + if (semver.satisfies(process.version, '<1.6')) { + assert.equal(topFrame.arguments[0].name, 'j'); + assert.equal(topFrame.arguments[0].value, '2'); + } else { + assert.equal(topFrame.locals[0].name, 'j'); + assert.equal(topFrame.locals[0].value, '2'); + } assert.equal(topFrame['function'], 'foo'); - assert.equal(topFrame.arguments[0].name, 'j'); - assert.equal(topFrame.arguments[0].value, '2'); api.clear(bp); done(); });