From dc03c6e0ac90ca2bb62740924059f5474933af02 Mon Sep 17 00:00:00 2001 From: Ivan Tivonenko Date: Wed, 23 Dec 2015 23:49:52 +0200 Subject: [PATCH] fix to work in browser run unit tests and integration tests in PhantomJS add JUnit reporter to unit test so CircleCI can show results --- Gulpfile.js | 59 +++++++++++++++++++++++++- package.json | 3 ++ scripts/ci.sh | 15 +++++++ src/broadcast.js | 3 ++ src/common/connection.js | 6 ++- src/common/errors.js | 4 +- src/common/utils.js | 3 +- src/common/wswrapper.js | 62 +++++++++++++++++++++++++++ test/api-test.js | 9 +++- test/broadcast-api-test.js | 19 ++++++--- test/connection-test.js | 17 ++++++-- test/integration/integration-test.js | 3 +- test/integration/wallet-web.js | 14 +++++++ test/integration/wallet.js | 1 + test/localintegrationrunner.html | 57 +++++++++++++++++++++++++ test/localrunner.html | 63 ++++++++++++++++++++++++++++ test/mock-rippled.js | 15 ++++++- test/mocked-server.js | 15 +++++++ test/setup-api-web.js | 41 ++++++++++++++++++ 19 files changed, 392 insertions(+), 17 deletions(-) create mode 100644 src/common/wswrapper.js create mode 100644 test/integration/wallet-web.js create mode 100644 test/localintegrationrunner.html create mode 100644 test/localrunner.html create mode 100644 test/mocked-server.js create mode 100644 test/setup-api-web.js diff --git a/Gulpfile.js b/Gulpfile.js index f4fc2d68..9de701c0 100644 --- a/Gulpfile.js +++ b/Gulpfile.js @@ -8,6 +8,7 @@ var rename = require('gulp-rename'); var webpack = require('webpack'); var bump = require('gulp-bump'); var argv = require('yargs').argv; +var assert = require('assert'); var pkg = require('./package.json'); @@ -21,10 +22,19 @@ function webpackConfig(extension, overrides) { path: './build/', filename: ['ripple-', extension].join(pkg.version) }, + plugins: [ + new webpack.NormalModuleReplacementPlugin(/^ws$/, './wswrapper'), + new webpack.NormalModuleReplacementPlugin(/^\.\/wallet$/, './wallet-web'), + new webpack.NormalModuleReplacementPlugin(/^.*setup-api$/, + './setup-api-web') + ], module: { loaders: [{ + test: /jayson/, + loader: 'null' + }, { test: /\.js$/, - exclude: /node_modules/, + exclude: [/node_modules/], loader: 'babel-loader?optional=runtime' }, { test: /\.json/, @@ -35,6 +45,53 @@ function webpackConfig(extension, overrides) { return _.assign({}, defaults, overrides); } +function webpackConfigForWebTest(testFileName, path) { + var match = testFileName.match(/\/?([^\/]*)-test.js$/); + if (!match) { + assert(false, 'wrong filename:' + testFileName); + } + var configOverrides = { + externals: [{ + 'ripple-api': 'ripple', + 'net': 'null' + }], + entry: testFileName, + output: { + library: match[1].replace(/-/g, '_'), + path: './test-compiled-for-web/' + (path ? path : ''), + filename: match[1] + '-test.js' + } + }; + return webpackConfig('.js', configOverrides); +} + +gulp.task('build-for-web-tests', function(callback) { + var configOverrides = { + output: { + library: 'ripple', + path: './test-compiled-for-web/', + filename: 'ripple-for-web-tests.js' + } + }; + var config = webpackConfig('-debug.js', configOverrides); + webpack(config, callback); +}); + +gulp.task('build-tests', function(callback) { + var times = 0; + function done() { + if (++times >= 5) { + callback(); + } + } + webpack(webpackConfigForWebTest('./test/rangeset-test.js'), done); + webpack(webpackConfigForWebTest('./test/connection-test.js'), done); + webpack(webpackConfigForWebTest('./test/api-test.js'), done); + webpack(webpackConfigForWebTest('./test/broadcast-api-test.js'), done); + webpack(webpackConfigForWebTest('./test/integration/integration-test.js', + 'integration/'), done); +}); + gulp.task('build', function(callback) { webpack(webpackConfig('.js'), callback); }); diff --git a/package.json b/package.json index 078cb0f0..0ffe3f94 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,9 @@ "json-loader": "^0.5.2", "json-schema-to-markdown-table": "^0.4.0", "mocha": "^2.1.0", + "mocha-junit-reporter": "^1.9.1", + "mocha-phantomjs": "^4.0.1", + "null-loader": "^0.1.1", "webpack": "^1.5.3", "yargs": "^1.3.1" }, diff --git a/scripts/ci.sh b/scripts/ci.sh index a9aebbdb..69196321 100755 --- a/scripts/ci.sh +++ b/scripts/ci.sh @@ -24,6 +24,7 @@ lint() { unittest() { # test "src" + mocha test --reporter mocha-junit-reporter --reporter-options mochaFile=$CIRCLE_TEST_REPORTS/test-results.xml npm test --coverage npm run coveralls @@ -33,12 +34,26 @@ unittest() { mkdir -p test-compiled/node_modules ln -nfs ../../dist/npm test-compiled/node_modules/ripple-api mocha --opts test-compiled/mocha.opts test-compiled + + # compile tests for browser testing + gulp build-tests build-for-web-tests + node --harmony test-compiled/mocked-server.js > /dev/null & + + echo "Running tests in PhantomJS" + mocha-phantomjs test/localrunner.html + + pkill -f mocked-server.js rm -rf test-compiled } integrationtest() { mocha test/integration/integration-test.js mocha test/integration/http-integration-test.js + + # run integration tests in PhantomJS + gulp build-tests build-for-web-tests + echo "Running integragtion tests in PhantomJS" + mocha-phantomjs test/localintegrationrunner.html } doctest() { diff --git a/src/broadcast.js b/src/broadcast.js index 041ab315..4082953d 100644 --- a/src/broadcast.js +++ b/src/broadcast.js @@ -11,6 +11,9 @@ class RippleAPIBroadcast extends RippleAPI { _.assign({}, options, {server}) )); + // exposed for testing + this._apis = apis; + this.getMethodNames().forEach(name => { this[name] = function() { // eslint-disable-line no-loop-func return Promise.race(apis.map(api => api[name].apply(api, arguments))); diff --git a/src/common/connection.js b/src/common/connection.js index f116e741..ae4bd61d 100644 --- a/src/common/connection.js +++ b/src/common/connection.js @@ -148,10 +148,12 @@ class Connection extends EventEmitter { cert: this._certificate }, _.isUndefined); const websocketOptions = _.assign({}, options, optionsOverrides); - const websocket = new WebSocket(this._url, websocketOptions); + const websocket = new WebSocket(this._url, null, websocketOptions); // we will have a listener for each outstanding request, // so we have to raise the limit (the default is 10) - websocket.setMaxListeners(Infinity); + if (typeof websocket.setMaxListeners === 'function') { + websocket.setMaxListeners(Infinity); + } return websocket; } diff --git a/src/common/errors.js b/src/common/errors.js index daf7358f..3d38069c 100644 --- a/src/common/errors.js +++ b/src/common/errors.js @@ -7,7 +7,9 @@ class RippleError extends Error { this.name = this.constructor.name; this.message = message; this.data = data; - Error.captureStackTrace(this, this.constructor.name); + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor.name); + } } toString() { diff --git a/src/common/utils.js b/src/common/utils.js index c73fa56c..488f9a45 100644 --- a/src/common/utils.js +++ b/src/common/utils.js @@ -35,12 +35,13 @@ function toRippledAmount(amount: Amount): RippledAmount { }; } -const FINDSNAKE = /([a-zA-Z]_[a-zA-Z])/g; function convertKeysFromSnakeCaseToCamelCase(obj: any): any { if (typeof obj === 'object') { let newKey; return _.reduce(obj, (result, value, key) => { newKey = key; + // taking this out of function leads to error in PhantomJS + const FINDSNAKE = /([a-zA-Z]_[a-zA-Z])/g; if (FINDSNAKE.test(key)) { newKey = key.replace(FINDSNAKE, r => r[0] + r[2].toUpperCase()); } diff --git a/src/common/wswrapper.js b/src/common/wswrapper.js new file mode 100644 index 00000000..2187dca4 --- /dev/null +++ b/src/common/wswrapper.js @@ -0,0 +1,62 @@ +'use strict'; + +const {EventEmitter} = require('events'); + +function unsused() {} + +/** + * Provides `EventEmitter` interface for native browser `WebSocket`, + * same, as `ws` package provides. + */ +class WSWrapper extends EventEmitter { + constructor(url, protocols = null, websocketOptions = {}) { + super(); + unsused(protocols); + unsused(websocketOptions); + this.setMaxListeners(Infinity); + + this._ws = new WebSocket(url); + + this._ws.onclose = () => { + this.emit('close'); + }; + + this._ws.onopen = () => { + this.emit('open'); + }; + + this._ws.onerror = (error) => { + if (this.listenerCount('error') > 0) { + this.emit('error', error); + } + }; + + this._ws.onmessage = (message) => { + this.emit('message', message.data); + }; + + } + + close() { + if (this.readyState === 1) { + this._ws.close(); + } + } + + send(message) { + this._ws.send(message); + } + + get readyState() { + return this._ws.readyState; + } + +} + +WSWrapper.CONNECTING = 0; +WSWrapper.OPEN = 1; +WSWrapper.CLOSING = 2; +WSWrapper.CLOSED = 3; + +module.exports = WSWrapper; + diff --git a/test/api-test.js b/test/api-test.js index 4f84e84c..712ae8d4 100644 --- a/test/api-test.js +++ b/test/api-test.js @@ -16,6 +16,8 @@ const ledgerClosed = require('./fixtures/rippled/ledger-close-newer'); const schemaValidator = RippleAPI._PRIVATE.schemaValidator; assert.options.strict = true; +const TIMEOUT = 10000; // how long before each test case times out + function unused() { } @@ -37,6 +39,7 @@ function checkResult(expected, schemaName, response) { describe('RippleAPI', function() { + this.timeout(TIMEOUT); const instructions = {maxLedgerVersionOffset: 100}; beforeEach(setupAPI.setup); afterEach(setupAPI.teardown); @@ -944,7 +947,11 @@ describe('RippleAPI', function() { }); it('getServerInfo - error', function() { - this.mockRippled.returnErrorOnServerInfo = true; + this.api.connection._send(JSON.stringify({ + command: 'config', + data: {returnErrorOnServerInfo: true} + })); + return this.api.getServerInfo().then(() => { assert(false, 'Should throw NetworkError'); }).catch(error => { diff --git a/test/broadcast-api-test.js b/test/broadcast-api-test.js index bdf6bbd4..0f9756c4 100644 --- a/test/broadcast-api-test.js +++ b/test/broadcast-api-test.js @@ -26,7 +26,9 @@ describe('RippleAPIBroadcast', function() { it('base', function() { const expected = {request_server_info: 1}; - this.mocks.forEach(mock => mock.expect(_.assign({}, expected))); + if (!process.browser) { + this.mocks.forEach(mock => mock.expect(_.assign({}, expected))); + } assert(this.api.isConnected()); return this.api.getServerInfo().then( _.partial(checkResult, responses.getServerInfo, 'getServerInfo')); @@ -39,13 +41,16 @@ describe('RippleAPIBroadcast', function() { }); const ledgerNext = _.assign({}, ledgerClosed); ledgerNext.ledger_index++; - this.mocks.forEach(mock => mock.socket.send(JSON.stringify(ledgerNext))); + + this.api._apis.forEach(api => api.connection._send(JSON.stringify({ + command: 'echo', + data: ledgerNext + }))); setTimeout(() => { assert.strictEqual(gotLedger, 1); done(); - }, 50); - + }, 250); }); it('error propagation', function(done) { @@ -54,8 +59,10 @@ describe('RippleAPIBroadcast', function() { assert.strictEqual(info, 'info'); done(); }); - this.mocks[1].socket.send( - JSON.stringify({error: 'type', error_message: 'info'})); + this.api._apis[1].connection._send(JSON.stringify({ + command: 'echo', + data: {error: 'type', error_message: 'info'} + })); }); }); diff --git a/test/connection-test.js b/test/connection-test.js index 0e14cbc7..7fcd878b 100644 --- a/test/connection-test.js +++ b/test/connection-test.js @@ -10,6 +10,8 @@ const utils = RippleAPI._PRIVATE.ledgerUtils; const ledgerClose = require('./fixtures/rippled/ledger-close.json'); +const TIMEOUT = 10000; // how long before each test case times out + function unused() { } @@ -27,6 +29,7 @@ function createServer() { } describe('Connection', function() { + this.timeout(TIMEOUT); beforeEach(setupAPI.setup); afterEach(setupAPI.teardown); @@ -47,6 +50,9 @@ describe('Connection', function() { messages.push(message); } }; + connection._ws = { + send: function() {} + }; connection._onMessage(message1); connection._send(message2); @@ -54,6 +60,10 @@ describe('Connection', function() { }); it('with proxy', function(done) { + if (process.browser) { + done(); + return; + } createServer().then((server) => { const port = server.address().port; const expect = 'CONNECT localhost'; @@ -97,9 +107,10 @@ describe('Connection', function() { }); it('DisconnectedError', function() { - this.api.connection._send = function() { - this._ws.close(); - }; + this.api.connection._send(JSON.stringify({ + command: 'config', + data: {disconnectOnServerInfo: true} + })); return this.api.getServerInfo().then(() => { assert(false, 'Should throw DisconnectedError'); }).catch(error => { diff --git a/test/integration/integration-test.js b/test/integration/integration-test.js index 7c880abb..942ed74c 100644 --- a/test/integration/integration-test.js +++ b/test/integration/integration-test.js @@ -12,7 +12,8 @@ const {isValidSecret} = require('../../src/common'); const {payTo, ledgerAccept} = require('./utils'); -const TIMEOUT = 10000; // how long before each test case times out +// how long before each test case times out +const TIMEOUT = process.browser ? 25000 : 10000; const INTERVAL = 1000; // how long to wait between checks for validated ledger const serverUrl = 'ws://127.0.0.1:6006'; diff --git a/test/integration/wallet-web.js b/test/integration/wallet-web.js new file mode 100644 index 00000000..bc37d674 --- /dev/null +++ b/test/integration/wallet-web.js @@ -0,0 +1,14 @@ +'use strict'; + +function getAddress() { + return 'rQDhz2ZNXmhxzCYwxU6qAbdxsHA4HV45Y2'; +} + +function getSecret() { + return 'shK6YXzwYfnFVn3YZSaMh5zuAddKx'; +} + +module.exports = { + getAddress, + getSecret +}; diff --git a/test/integration/wallet.js b/test/integration/wallet.js index 7652f772..56186384 100644 --- a/test/integration/wallet.js +++ b/test/integration/wallet.js @@ -1,4 +1,5 @@ 'use strict'; + const _ = require('lodash'); const fs = require('fs'); const path = require('path'); diff --git a/test/localintegrationrunner.html b/test/localintegrationrunner.html new file mode 100644 index 00000000..e0df9362 --- /dev/null +++ b/test/localintegrationrunner.html @@ -0,0 +1,57 @@ + + + + + + + +
+
+ + + + + + + + + + diff --git a/test/localrunner.html b/test/localrunner.html new file mode 100644 index 00000000..3b3d7e31 --- /dev/null +++ b/test/localrunner.html @@ -0,0 +1,63 @@ + + + + + + + +
+
+ + + + + + + + + + + + + + + + diff --git a/test/mock-rippled.js b/test/mock-rippled.js index 2ab30abe..6ca63497 100644 --- a/test/mock-rippled.js +++ b/test/mock-rippled.js @@ -72,6 +72,7 @@ module.exports = function(port) { mock.on('connection', function(conn) { this.socket = conn; + conn.config = {}; conn.on('message', function(requestJSON) { const request = JSON.parse(requestJSON); mock.emit('request_' + request.command, request, conn); @@ -95,10 +96,22 @@ module.exports = function(port) { mock.expectedRequests[this.event] -= 1; }); + mock.on('request_config', function(request, conn) { + assert.strictEqual(request.command, 'config'); + conn.config = _.assign(conn.config, request.data); + }); + + mock.on('request_echo', function(request, conn) { + assert.strictEqual(request.command, 'echo'); + conn.send(JSON.stringify(request.data)); + }); + mock.on('request_server_info', function(request, conn) { assert.strictEqual(request.command, 'server_info'); - if (mock.returnErrorOnServerInfo) { + if (conn.config.returnErrorOnServerInfo) { conn.send(createResponse(request, fixtures.server_info.error)); + } else if (conn.config.disconnectOnServerInfo) { + conn.terminate(); } else { conn.send(createResponse(request, fixtures.server_info.normal)); } diff --git a/test/mocked-server.js b/test/mocked-server.js new file mode 100644 index 00000000..59c5badc --- /dev/null +++ b/test/mocked-server.js @@ -0,0 +1,15 @@ +'use strict'; + + +const port = 34371; + +const createMockRippled = require('./mock-rippled'); + +function main() { + console.log('starting server on port ' + port); + createMockRippled(port); + console.log('starting server on port ' + String(port + 1)); + createMockRippled(port + 1); +} + +main(); diff --git a/test/setup-api-web.js b/test/setup-api-web.js new file mode 100644 index 00000000..43d3e99d --- /dev/null +++ b/test/setup-api-web.js @@ -0,0 +1,41 @@ +/* eslint-disable max-nested-callbacks */ +'use strict'; + +const {RippleAPI, RippleAPIBroadcast} = require('ripple-api'); +const ledgerClosed = require('./fixtures/rippled/ledger-close'); + +const port = 34371; + +function setup(port_ = port) { + return new Promise((resolve, reject) => { + this.api = new RippleAPI({server: 'ws://localhost:' + port_}); + this.api.connect().then(() => { + this.api.once('ledger', () => resolve()); + this.api.connection._ws.emit('message', JSON.stringify(ledgerClosed)); + }).catch(reject); + }); +} + +function setupBroadcast() { + const servers = [port, port + 1].map(port_ => 'ws://localhost:' + port_); + this.api = new RippleAPIBroadcast(servers); + return new Promise((resolve, reject) => { + this.api.connect().then(() => { + this.api.once('ledger', () => resolve()); + this.api._apis[0].connection._ws.emit('message', + JSON.stringify(ledgerClosed)); + }).catch(reject); + }); +} + +function teardown() { + if (this.api.isConnected()) { + return this.api.disconnect(); + } +} + +module.exports = { + setup: setup, + teardown: teardown, + setupBroadcast: setupBroadcast +};