| 'use strict'; |
| const common = require('../common'); |
| const assert = require('assert'); |
| const fs = require('fs'); |
| const http = require('http'); |
| const path = require('path'); |
| const spawn = require('child_process').spawn; |
| const url = require('url'); |
| |
| const DEBUG = false; |
| |
| const TIMEOUT = 15 * 1000; |
| |
| const mainScript = path.join(common.fixturesDir, 'loop.js'); |
| |
| function send(socket, message, id, callback) { |
| const msg = JSON.parse(JSON.stringify(message)); // Clone! |
| msg['id'] = id; |
| if (DEBUG) |
| console.log('[sent]', JSON.stringify(msg)); |
| const messageBuf = Buffer.from(JSON.stringify(msg)); |
| |
| const wsHeaderBuf = Buffer.allocUnsafe(16); |
| wsHeaderBuf.writeUInt8(0x81, 0); |
| let byte2 = 0x80; |
| const bodyLen = messageBuf.length; |
| let maskOffset = 2; |
| if (bodyLen < 126) { |
| byte2 = 0x80 + bodyLen; |
| } else if (bodyLen < 65536) { |
| byte2 = 0xFE; |
| wsHeaderBuf.writeUInt16BE(bodyLen, 2); |
| maskOffset = 4; |
| } else { |
| byte2 = 0xFF; |
| wsHeaderBuf.writeUInt32BE(bodyLen, 2); |
| wsHeaderBuf.writeUInt32BE(0, 6); |
| maskOffset = 10; |
| } |
| wsHeaderBuf.writeUInt8(byte2, 1); |
| wsHeaderBuf.writeUInt32BE(0x01020408, maskOffset); |
| |
| for (let i = 0; i < messageBuf.length; i++) |
| messageBuf[i] = messageBuf[i] ^ (1 << (i % 4)); |
| socket.write( |
| Buffer.concat([wsHeaderBuf.slice(0, maskOffset + 4), messageBuf]), |
| callback); |
| } |
| |
| function parseWSFrame(buffer, handler) { |
| if (buffer.length < 2) |
| return 0; |
| assert.strictEqual(0x81, buffer[0]); |
| let dataLen = 0x7F & buffer[1]; |
| let bodyOffset = 2; |
| if (buffer.length < bodyOffset + dataLen) |
| return 0; |
| if (dataLen === 126) { |
| dataLen = buffer.readUInt16BE(2); |
| bodyOffset = 4; |
| } else if (dataLen === 127) { |
| dataLen = buffer.readUInt32BE(2); |
| bodyOffset = 10; |
| } |
| if (buffer.length < bodyOffset + dataLen) |
| return 0; |
| const message = JSON.parse( |
| buffer.slice(bodyOffset, bodyOffset + dataLen).toString('utf8')); |
| if (DEBUG) |
| console.log('[received]', JSON.stringify(message)); |
| handler(message); |
| return bodyOffset + dataLen; |
| } |
| |
| function tearDown(child, err) { |
| child.kill(); |
| if (err instanceof Error) { |
| console.error(err.stack); |
| process.exit(1); |
| } |
| } |
| |
| function checkHttpResponse(port, path, callback) { |
| http.get({port, path}, function(res) { |
| let response = ''; |
| res.setEncoding('utf8'); |
| res |
| .on('data', (data) => response += data.toString()) |
| .on('end', () => { |
| let err = null; |
| let json = undefined; |
| try { |
| json = JSON.parse(response); |
| } catch (e) { |
| err = e; |
| err.response = response; |
| } |
| callback(err, json); |
| }); |
| }); |
| } |
| |
| function makeBufferingDataCallback(dataCallback) { |
| let buffer = Buffer.alloc(0); |
| return (data) => { |
| const newData = Buffer.concat([buffer, data]); |
| const str = newData.toString('utf8'); |
| const lines = str.split('\n'); |
| if (str.endsWith('\n')) |
| buffer = Buffer.alloc(0); |
| else |
| buffer = Buffer.from(lines.pop(), 'utf8'); |
| for (var line of lines) |
| dataCallback(line); |
| }; |
| } |
| |
| function timeout(message, multiplicator) { |
| return setTimeout(() => common.fail(message), TIMEOUT * (multiplicator || 1)); |
| } |
| |
| const TestSession = function(socket, harness) { |
| this.mainScriptPath = harness.mainScriptPath; |
| this.mainScriptId = null; |
| |
| this.harness_ = harness; |
| this.socket_ = socket; |
| this.expectClose_ = false; |
| this.scripts_ = {}; |
| this.messagefilter_ = null; |
| this.responseCheckers_ = {}; |
| this.lastId_ = 0; |
| this.messages_ = {}; |
| this.expectedId_ = 1; |
| this.lastMessageResponseCallback_ = null; |
| |
| let buffer = Buffer.alloc(0); |
| socket.on('data', (data) => { |
| buffer = Buffer.concat([buffer, data]); |
| let consumed; |
| do { |
| consumed = parseWSFrame(buffer, this.processMessage_.bind(this)); |
| if (consumed) |
| buffer = buffer.slice(consumed); |
| } while (consumed); |
| }).on('close', () => assert(this.expectClose_, 'Socket closed prematurely')); |
| }; |
| |
| TestSession.prototype.scriptUrlForId = function(id) { |
| return this.scripts_[id]; |
| }; |
| |
| TestSession.prototype.processMessage_ = function(message) { |
| const method = message['method']; |
| if (method === 'Debugger.scriptParsed') { |
| const script = message['params']; |
| const scriptId = script['scriptId']; |
| const url = script['url']; |
| this.scripts_[scriptId] = url; |
| if (url === mainScript) |
| this.mainScriptId = scriptId; |
| } |
| this.messagefilter_ && this.messagefilter_(message); |
| const id = message['id']; |
| if (id) { |
| assert.strictEqual(id, this.expectedId_); |
| this.expectedId_++; |
| if (this.responseCheckers_[id]) { |
| assert(message['result'], JSON.stringify(message) + ' (response to ' + |
| JSON.stringify(this.messages_[id]) + ')'); |
| this.responseCheckers_[id](message['result']); |
| delete this.responseCheckers_[id]; |
| } |
| assert(!message['error'], JSON.stringify(message) + ' (replying to ' + |
| JSON.stringify(this.messages_[id]) + ')'); |
| delete this.messages_[id]; |
| if (id === this.lastId_) { |
| this.lastMessageResponseCallback_ && this.lastMessageResponseCallback_(); |
| this.lastMessageResponseCallback_ = null; |
| } |
| } |
| }; |
| |
| TestSession.prototype.sendAll_ = function(commands, callback) { |
| if (!commands.length) { |
| callback(); |
| } else { |
| this.lastId_++; |
| let command = commands[0]; |
| if (command instanceof Array) { |
| this.responseCheckers_[this.lastId_] = command[1]; |
| command = command[0]; |
| } |
| if (command instanceof Function) |
| command = command(); |
| this.messages_[this.lastId_] = command; |
| send(this.socket_, command, this.lastId_, |
| () => this.sendAll_(commands.slice(1), callback)); |
| } |
| }; |
| |
| TestSession.prototype.sendInspectorCommands = function(commands) { |
| if (!(commands instanceof Array)) |
| commands = [commands]; |
| return this.enqueue((callback) => { |
| let timeoutId = null; |
| this.lastMessageResponseCallback_ = () => { |
| timeoutId && clearTimeout(timeoutId); |
| callback(); |
| }; |
| this.sendAll_(commands, () => { |
| timeoutId = setTimeout(() => { |
| let s = ''; |
| for (const id in this.messages_) { |
| s += id + ', '; |
| } |
| common.fail('Messages without response: ' + |
| s.substring(0, s.length - 2)); |
| }, TIMEOUT); |
| }); |
| }); |
| }; |
| |
| TestSession.prototype.createCallbackWithTimeout_ = function(message) { |
| var promise = new Promise((resolve) => { |
| this.enqueue((callback) => { |
| const timeoutId = timeout(message); |
| resolve(() => { |
| clearTimeout(timeoutId); |
| callback(); |
| }); |
| }); |
| }); |
| return () => promise.then((callback) => callback()); |
| }; |
| |
| TestSession.prototype.expectMessages = function(expects) { |
| if (!(expects instanceof Array)) expects = [ expects ]; |
| |
| const callback = this.createCallbackWithTimeout_( |
| 'Matching response was not received:\n' + expects[0]); |
| this.messagefilter_ = (message) => { |
| if (expects[0](message)) |
| expects.shift(); |
| if (!expects.length) { |
| this.messagefilter_ = null; |
| callback(); |
| } |
| }; |
| return this; |
| }; |
| |
| TestSession.prototype.expectStderrOutput = function(regexp) { |
| this.harness_.addStderrFilter( |
| regexp, |
| this.createCallbackWithTimeout_('Timed out waiting for ' + regexp)); |
| return this; |
| }; |
| |
| TestSession.prototype.runNext_ = function() { |
| if (this.task_) { |
| setImmediate(() => { |
| this.task_(() => { |
| this.task_ = this.task_.next_; |
| this.runNext_(); |
| }); |
| }); |
| } |
| }; |
| |
| TestSession.prototype.enqueue = function(task) { |
| if (!this.task_) { |
| this.task_ = task; |
| this.runNext_(); |
| } else { |
| let t = this.task_; |
| while (t.next_) |
| t = t.next_; |
| t.next_ = task; |
| } |
| return this; |
| }; |
| |
| TestSession.prototype.disconnect = function(childDone) { |
| return this.enqueue((callback) => { |
| this.expectClose_ = true; |
| this.harness_.childInstanceDone = |
| this.harness_.childInstanceDone || childDone; |
| this.socket_.destroy(); |
| console.log('[test]', 'Connection terminated'); |
| callback(); |
| }); |
| }; |
| |
| TestSession.prototype.testHttpResponse = function(path, check) { |
| return this.enqueue((callback) => |
| checkHttpResponse(this.harness_.port, path, (err, response) => { |
| check.call(this, err, response); |
| callback(); |
| })); |
| }; |
| |
| |
| const Harness = function(port, childProcess) { |
| this.port = port; |
| this.mainScriptPath = mainScript; |
| this.stderrFilters_ = []; |
| this.process_ = childProcess; |
| this.childInstanceDone = false; |
| this.returnCode_ = null; |
| this.running_ = true; |
| |
| childProcess.stdout.on('data', makeBufferingDataCallback( |
| (line) => console.log('[out]', line))); |
| |
| |
| childProcess.stderr.on('data', makeBufferingDataCallback((message) => { |
| const pending = []; |
| console.log('[err]', message); |
| for (const filter of this.stderrFilters_) |
| if (!filter(message)) pending.push(filter); |
| this.stderrFilters_ = pending; |
| })); |
| childProcess.on('exit', (code, signal) => { |
| assert(this.childInstanceDone, 'Child instance died prematurely'); |
| this.returnCode_ = code; |
| this.running_ = false; |
| }); |
| }; |
| |
| Harness.prototype.addStderrFilter = function(regexp, callback) { |
| this.stderrFilters_.push((message) => { |
| if (message.match(regexp)) { |
| callback(); |
| return true; |
| } |
| }); |
| }; |
| |
| Harness.prototype.run_ = function() { |
| setImmediate(() => { |
| this.task_(() => { |
| this.task_ = this.task_.next_; |
| if (this.task_) |
| this.run_(); |
| }); |
| }); |
| }; |
| |
| Harness.prototype.enqueue_ = function(task) { |
| if (!this.task_) { |
| this.task_ = task; |
| this.run_(); |
| } else { |
| let chain = this.task_; |
| while (chain.next_) |
| chain = chain.next_; |
| chain.next_ = task; |
| } |
| return this; |
| }; |
| |
| Harness.prototype.testHttpResponse = function(path, check) { |
| return this.enqueue_((doneCallback) => { |
| checkHttpResponse(this.port, path, (err, response) => { |
| check.call(this, err, response); |
| doneCallback(); |
| }); |
| }); |
| }; |
| |
| Harness.prototype.wsHandshake = function(devtoolsUrl, tests, readyCallback) { |
| http.get({ |
| port: this.port, |
| path: url.parse(devtoolsUrl).path, |
| headers: { |
| 'Connection': 'Upgrade', |
| 'Upgrade': 'websocket', |
| 'Sec-WebSocket-Version': 13, |
| 'Sec-WebSocket-Key': 'key==' |
| } |
| }).on('upgrade', (message, socket) => { |
| const session = new TestSession(socket, this); |
| if (!(tests instanceof Array)) |
| tests = [tests]; |
| function enqueue(tests) { |
| session.enqueue((sessionCb) => { |
| if (tests.length) { |
| tests[0](session); |
| session.enqueue((cb2) => { |
| enqueue(tests.slice(1)); |
| cb2(); |
| }); |
| } else { |
| readyCallback(); |
| } |
| sessionCb(); |
| }); |
| } |
| enqueue(tests); |
| }).on('response', () => common.fail('Upgrade was not received')); |
| }; |
| |
| Harness.prototype.runFrontendSession = function(tests) { |
| return this.enqueue_((callback) => { |
| checkHttpResponse(this.port, '/json/list', (err, response) => { |
| assert.ifError(err); |
| this.wsHandshake(response[0]['webSocketDebuggerUrl'], tests, callback); |
| }); |
| }); |
| }; |
| |
| Harness.prototype.expectShutDown = function(errorCode) { |
| this.enqueue_((callback) => { |
| if (this.running_) { |
| const timeoutId = timeout('Have not terminated'); |
| this.process_.on('exit', (code) => { |
| clearTimeout(timeoutId); |
| assert.strictEqual(errorCode, code); |
| callback(); |
| }); |
| } else { |
| assert.strictEqual(errorCode, this.returnCode_); |
| callback(); |
| } |
| }); |
| }; |
| |
| exports.startNodeForInspectorTest = function(callback) { |
| const child = spawn(process.execPath, |
| [ '--inspect-brk', mainScript ]); |
| |
| const timeoutId = timeout('Child process did not start properly', 4); |
| |
| let found = false; |
| |
| const dataCallback = makeBufferingDataCallback((text) => { |
| clearTimeout(timeoutId); |
| console.log('[err]', text); |
| if (found) return; |
| const match = text.match(/Debugger listening on port (\d+)/); |
| found = true; |
| child.stderr.removeListener('data', dataCallback); |
| assert.ok(match, text); |
| callback(new Harness(match[1], child)); |
| }); |
| |
| child.stderr.on('data', dataCallback); |
| |
| const handler = tearDown.bind(null, child); |
| |
| process.on('exit', handler); |
| process.on('uncaughtException', handler); |
| process.on('SIGINT', handler); |
| }; |
| |
| exports.mainScriptSource = function() { |
| return fs.readFileSync(mainScript, 'utf8'); |
| }; |