diff --git a/index.js b/index.js index 01e5250..28f61d4 100644 --- a/index.js +++ b/index.js @@ -4,26 +4,26 @@ const _ = require('lodash'); const path = require('path'); const fs = require('fs-extra'); const parseConfig = require('./lib/config'); -const StreamWriter = require('./lib/stream-writer'); +const DataFile = require('./lib/data-file'); const wrapCommands = require('./lib/commands-wrapper'); module.exports = (hermione, opts) => { const pluginConfig = parseConfig(opts); - if (!pluginConfig.enabled) { return; } - let writeStream; + if (hermione.isWorker()) { + hermione.on(hermione.events.NEW_BROWSER, wrapCommands); + return; + } + + let dataFile = DataFile.create(pluginConfig.path); const retriesMap = _(hermione.config.getBrowserIds()) .zipObject() .mapValues(() => new Map()) .value(); - hermione.on(hermione.events.RUNNER_START, () => { - writeStream = StreamWriter.create(pluginConfig.path); - }); - hermione.on(hermione.events.RETRY, (test) => { const fullTitle = test.fullTitle(); @@ -49,15 +49,11 @@ module.exports = (hermione, opts) => { } test.timeEnd = Date.now(); - writeStream.write(test); + dataFile.write(test); }); - hermione.on(hermione.events.ERROR, () => writeStream.end()); - - hermione.on(hermione.events.NEW_BROWSER, wrapCommands); - - hermione.on(hermione.events.RUNNER_END, () => { - writeStream.end(); + hermione.on(hermione.events.RUNNER_END, async () => { + await dataFile.end(); copyToReportDir(pluginConfig.path, ['index.html', 'bundle.min.js', 'styles.css']); }); }; diff --git a/lib/stream-writer.js b/lib/data-file.js similarity index 54% rename from lib/stream-writer.js rename to lib/data-file.js index 2b2a9f2..86e4fa1 100644 --- a/lib/stream-writer.js +++ b/lib/data-file.js @@ -4,14 +4,16 @@ const path = require('path'); const fs = require('fs-extra'); const _ = require('lodash'); -module.exports = class StreamWriter { +module.exports = class DataFile { static create(reportPath) { - return new StreamWriter(reportPath); + return new this(reportPath); } constructor(reportPath) { - fs.ensureDirSync(reportPath); - this._stream = fs.createWriteStream(path.join(reportPath, 'data.js')); + this._promise = fs.ensureDir(reportPath) + .then(() => fs.open(path.join(reportPath, 'data.js'))) + .then((fd) => this._fd = fd) + .then(() => fs.appendFile(this._fd, 'const data = [')); } write(data) { @@ -31,16 +33,14 @@ module.exports = class StreamWriter { testInfo.r = retry; } - this._writeDelim(); - this._stream.write(JSON.stringify(testInfo)); + const chunk = `${JSON.stringify(testInfo)},`; + this._promise = this._promise + .then(() => fs.appendFile(this._fd, chunk)); } end() { - this._stream.end(']'); - } - - _writeDelim() { - this._stream.write('const data = ['); - this._writeDelim = () => this._stream.write(','); + return this._promise + .then(() => fs.appendFile(this._fd, ']')) + .then(() => fs.close(this._fd)); } }; diff --git a/package-lock.json b/package-lock.json index 2aae8d6..4f31bae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2384,6 +2384,15 @@ "type-detect": "^4.0.0" } }, + "chai-as-promised": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-7.1.1.tgz", + "integrity": "sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==", + "dev": true, + "requires": { + "check-error": "^1.0.2" + } + }, "chain-function": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/chain-function/-/chain-function-1.0.0.tgz", @@ -3175,9 +3184,9 @@ } }, "eslint-config-gemini-testing": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/eslint-config-gemini-testing/-/eslint-config-gemini-testing-2.4.1.tgz", - "integrity": "sha1-X1QDAYxOtrw2q+Czrbyy/V0r3OM=", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-config-gemini-testing/-/eslint-config-gemini-testing-2.8.0.tgz", + "integrity": "sha512-zp9yStBk+Yplm0QpeyvgLPZA12wzDsWpvzi/YiN+GLSrt9ilq0NbNvFOp5KEFvnJNh57s8SioeIbv+jJX2BBQA==", "dev": true }, "espree": { diff --git a/package.json b/package.json index 063ca59..ba8ec0e 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "Plugin for hermione for commands execution profiling", "scripts": { "lint": "eslint .", - "test": "npm run lint && npm run test-unit", + "test": "npm run test-unit && npm run lint", "test-unit": "mocha test", "build": "webpack --config webpack.prod.js", "prepublish": "npm run build", @@ -41,9 +41,10 @@ "babel-preset-react": "^6.24.1", "babel-preset-stage-2": "^6.24.1", "chai": "^4.0.2", + "chai-as-promised": "^7.1.1", "css-loader": "^0.28.7", "eslint": "^3.19.0", - "eslint-config-gemini-testing": "^2.4.0", + "eslint-config-gemini-testing": "^2.8.0", "material-ui": "^0.19.3", "mocha": "^3.4.2", "proxyquire": "^1.8.0", diff --git a/test/lib/data-file.js b/test/lib/data-file.js new file mode 100644 index 0000000..2f87b49 --- /dev/null +++ b/test/lib/data-file.js @@ -0,0 +1,224 @@ +'use strict'; + +const fs = require('fs-extra'); +const DataFile = require('../../lib/data-file'); + +describe('stream writer', () => { + const sandbox = sinon.sandbox.create(); + // let streamStub; + + const dataStub = (opts = {}) => { + return Object.assign( + {browserId: 'default-browser'}, + opts, + {fullTitle: () => opts.fullTitle || 'defaultFullTitle'} + ); + }; + + const mkDataFile = (reportPath = 'default/path') => { + return DataFile.create(reportPath); + }; + + beforeEach(() => { + // streamStub = { + // write: sinon.stub().named('write'), + // end: sinon.stub().named('end') + // }; + // sandbox.stub(fs, 'ensureDirSync'); + // sandbox.stub(fs, 'createWriteStream').returns(streamStub); + + sandbox.stub(fs, 'appendFile'); + sandbox.stub(fs, 'ensureDir').resolves(); + sandbox.stub(fs, 'open').resolves(12345); + sandbox.stub(fs, 'close').resolves(); + }); + + afterEach(() => sandbox.restore()); + + it('should return promise on end call'); + it('should not do any fs stuff on create'); + + describe('create', () => { + it('should create ensure target dir is exists', async () => { + const dataFile = mkDataFile('report/path'); + + await dataFile.end(); + + assert.calledOnceWith(fs.ensureDir, 'report/path'); + }); + + it('should open data file in target dir', async () => { + const dataFile = mkDataFile('report/path'); + + await dataFile.end(); + + assert.calledOnceWith(fs.open, 'report/path/data.js'); + }); + + it('should write opening bracket into data file', async () => { + fs.open.resolves(100500); + const dataFile = mkDataFile(); + + await dataFile.end(); + + assert.calledWith(fs.appendFile, 100500, 'const data = ['); + }); + }); + + describe('write', () => { + const getWrittenData = () => { + return JSON.parse(fs.appendFile.secondCall.args[1].replace(/,$/, '')); + }; + + const write = async (data) => { + const dataFile = mkDataFile(); + dataFile.write(data); + await dataFile.end(); + }; + + it('should append data to file', async () => { + fs.open.resolves(100500); + const dataFile = mkDataFile(); + + dataFile.write(dataStub()); + await dataFile.end(); + + assert.calledThrice(fs.appendFile); + assert.alwaysCalledWith(fs.appendFile, 100500, sinon.match.string); + }); + + it('should write data with coma at the end', async () => { + await write(dataStub()); + + const writtenData = fs.appendFile.secondCall.args[1]; + assert.match(writtenData, /,$/); + }); + + it('should write test full title', async () => { + await write(dataStub({ + fullTitle: 'test1' + })); + + assert.match(getWrittenData(), { + n: 'test1' + }); + }); + + it('should write command list from hermioneCtx', async () => { + await write(dataStub({ + hermioneCtx: { + commandList: [{cl: [1]}] + } + })); + + assert.match(getWrittenData(), { + cl: [1] + }); + }); + + it('should write empty command list if there is no command list in data') + + it('should write timings', async () => { + await write(dataStub({ + timeStart: 100400, + timeEnd: 100500 + })); + + assert.match(getWrittenData(), { + ts: 100400, + te: 100500, + d: 100 // duration + }); + }); + + it('should write browser data', async () => { + await write(dataStub({ + browserId: 'bro', + sessionId: '100500' + })); + + assert.match(getWrittenData(), { + bid: 'bro', + sid: '100500' + }); + }); + + it('should write retry', async () => { + await write(dataStub({ + retry: 100500 + })); + + assert.match(getWrittenData(), { + r: 100500 + }); + }); + + it('should not write retry there is no retry in data', async () => { + await write(dataStub()); + + assert.notProperty(getWrittenData(), 'r'); + }); + + // it('should write object with command list from data', () => { + // const stream = DataFile.create('report/path'); + // const data = dataStub({ + // fullTitle: 'test1', + // hermioneCtx: { + // commandList: [{cl: [1]}] + // } + // }); + + // stream.write(data); + // const passedData = JSON.parse(streamStub.write.secondCall.args[0]); + + // assert.deepEqual(passedData.cl, [1]); + // }); + + // it('should write object with empty command list if it does not exist in data', () => { + // const stream = DataFile.create('report/path'); + // const data = dataStub({fullTitle: 'test1'}); + + // stream.write(data); + // const passedData = JSON.parse(streamStub.write.secondCall.args[0]); + + // assert.deepEqual(passedData.cl, []); + // }); + + // it('should divide data chains with comma delimiter', () => { + // const stream = DataFile.create('report/path'); + + // stream.write(dataStub()); + // stream.write(dataStub()); + + // // calls: 1 - open bracket, 2 - first data, 3 - delim, 4 - second data + // assert.calledWithExactly(streamStub.write.thirdCall, ','); + // }); + }); + + describe('end', () => { + it('should reject if failed to open file', async () => { + fs.open.rejects(new Error('foo')); + const dataFile = mkDataFile(); + + await assert.isRejected(dataFile.end(), /foo/); + }); + + it('should add closing bracket to data file', async () => { + fs.open.resolves(100500); + const dataFile = mkDataFile(); + + await dataFile.end(); + + assert.calledWith(fs.appendFile, 100500, ']'); + }); + + it('should close file', async () => { + fs.open.resolves(100500); + const dataFile = mkDataFile(); + + await dataFile.end(); + + assert.calledOnceWith(fs.close, 100500); + }); + }); +}); diff --git a/test/lib/index.js b/test/lib/index.js index 693466f..5f7416e 100644 --- a/test/lib/index.js +++ b/test/lib/index.js @@ -4,17 +4,15 @@ const _ = require('lodash'); const fs = require('fs-extra'); const proxyquire = require('proxyquire'); const EventEmitter = require('events').EventEmitter; -const StreamWriter = require('../../lib/stream-writer'); +const DataFile = require('../../lib/data-file'); const mkHermione = () => { const emitter = new EventEmitter(); emitter.events = { - RUNNER_START: 'runner-start', TEST_BEGIN: 'test-begin', TEST_END: 'test-end', RETRY: 'retry', - ERROR: 'critical-error', RUNNER_END: 'runner-end', NEW_BROWSER: 'new-browser' }; @@ -23,6 +21,8 @@ const mkHermione = () => { getBrowserIds: sinon.stub().returns(['default-bro']) }; + emitter.isWorker = sinon.stub().returns(false); + return emitter; }; @@ -37,7 +37,6 @@ describe('plugin', () => { const sandbox = sinon.sandbox.create(); let hermione; let plugin; - let stream; let commandWrapper; const initPlugin_ = (opts = {}) => { @@ -50,34 +49,25 @@ describe('plugin', () => { beforeEach(() => { hermione = mkHermione(); - stream = { - write: sandbox.stub().named('write'), - end: sandbox.stub().named('end') - }; - sandbox.stub(StreamWriter, 'create').returns(stream); + + sandbox.stub(DataFile, 'create').returns(Object.create(DataFile.prototype)); + sandbox.stub(DataFile.prototype); + sandbox.stub(fs, 'copySync'); }); afterEach(() => sandbox.restore()); - it('should be enabled by default', () => { - initPlugin_(); + it('should create data file on plugin load', () => { + initPlugin_({path: 'report/dir'}); - assert.equal(hermione.listeners(hermione.events.RUNNER_START).length, 1); + assert.calledOnceWith(DataFile.create, 'report/dir'); }); it('should do nothing if plugin is disabled', () => { initPlugin_({enabled: false}); - assert.equal(hermione.listeners(hermione.events.RUNNER_START).length, 0); - }); - - it('should create stream on RUNNER_START', () => { - initPlugin_(); - - hermione.emit(hermione.events.RUNNER_START); - - assert.calledOnce(StreamWriter.create); + assert.notCalled(DataFile.create); }); describe('on TEST_BEGIN', () => { @@ -115,7 +105,6 @@ describe('plugin', () => { describe('on TEST_END', () => { beforeEach(() => { initPlugin_(); - hermione.emit(hermione.events.RUNNER_START); }); it('should set timeEnd for test', () => { @@ -127,12 +116,12 @@ describe('plugin', () => { assert.propertyVal(test, 'timeEnd', 100500); }); - it('should write data to stream', () => { + it('should write data to data file', () => { const test = mkTest(); hermione.emit(hermione.events.TEST_END, test); - assert.calledOnceWith(stream.write, test); + assert.calledOnceWith(DataFile.prototype.write, test); }); it('should do nothing for pending tests', () => { @@ -143,46 +132,48 @@ describe('plugin', () => { hermione.emit(hermione.events.TEST_END, test); assert.notProperty(test, 'timeEnd'); - assert.notCalled(stream.write); + assert.notCalled(DataFile.prototype.write); }); }); - describe('should close stream', () => { - it('on error', () => { + describe('on RUNNER_END', () => { + it('should finalize data file', async () => { initPlugin_(); - hermione.emit(hermione.events.RUNNER_START); - hermione.emit(hermione.events.ERROR); + await hermione.emit(hermione.events.RUNNER_END); - assert.calledOnce(stream.end); + assert.calledOnce(DataFile.prototype.end); }); - it('on runner end', () => { - initPlugin_(); + ['index.html', 'bundle.min.js', 'styles.css'].forEach((fileName, i) => { + it(`should copy "${fileName}" service file to the report dir on runner end`, async () => { + initPlugin_({path: 'reportDir'}); - hermione.emit(hermione.events.RUNNER_START); - hermione.emit(hermione.events.RUNNER_END); + await hermione.emit(hermione.events.RUNNER_END); - assert.calledOnce(stream.end); + assert.equal(fs.copySync.args[i][1], `reportDir/${fileName}`); + }); }); + + it('should copy files asynchronously'); }); - ['index.html', 'bundle.min.js', 'styles.css'].forEach((fileName, i) => { - it(`should copy "${fileName}" service file to the report dir on runner end`, () => { - initPlugin_({path: 'reportDir'}); + describe('on NEW_BROWSER', () => { + it('should wrap browser commands in worker', () => { + hermione.isWorker.returns(true); + initPlugin_(); - hermione.emit(hermione.events.RUNNER_START); - hermione.emit(hermione.events.RUNNER_END); + hermione.emit(hermione.events.NEW_BROWSER); - assert.equal(fs.copySync.args[i][1], `reportDir/${fileName}`); + assert.calledOnce(commandWrapper); }); - }); - it('should wrap browser commands on NEW_BROWSER', () => { - initPlugin_(); + it('should not wrap browser commands in master', () => { + initPlugin_(); - hermione.emit(hermione.events.NEW_BROWSER); + hermione.emit(hermione.events.NEW_BROWSER); - assert.calledOnce(commandWrapper); + assert.notCalled(commandWrapper); + }); }); }); diff --git a/test/lib/stream-writer.js b/test/lib/stream-writer.js deleted file mode 100644 index aac522e..0000000 --- a/test/lib/stream-writer.js +++ /dev/null @@ -1,105 +0,0 @@ -'use strict'; - -const fs = require('fs-extra'); -const StreamWriter = require('../../lib/stream-writer'); - -describe('stream writer', () => { - const sandbox = sinon.sandbox.create(); - let streamStub; - - const dataStub = (opts = {}) => { - return Object.assign( - {browserId: 'default-browser'}, - opts, - {fullTitle: () => opts.fullTitle || 'fullTitle'} - ); - }; - - beforeEach(() => { - streamStub = { - write: sinon.stub().named('write'), - end: sinon.stub().named('end') - }; - sandbox.stub(fs, 'ensureDirSync'); - sandbox.stub(fs, 'createWriteStream').returns(streamStub); - }); - - afterEach(() => sandbox.restore()); - - describe('create', () => { - it('should create directory for report', () => { - StreamWriter.create('report/path'); - - assert.calledOnceWith(fs.ensureDirSync, 'report/path'); - }); - - it('should create write stream to the passed path', () => { - StreamWriter.create('report/path'); - - assert.calledOnceWith(fs.createWriteStream, 'report/path/data.js'); - }); - }); - - describe('write', () => { - it('should write opening bracket at first call', () => { - const stream = StreamWriter.create('report/path'); - - stream.write(dataStub()); - - assert.calledWith(streamStub.write.firstCall, 'const data = ['); - }); - - it('should write object with "n" property as "fullTitle"', () => { - const stream = StreamWriter.create('report/path'); - const data = dataStub({fullTitle: 'test1'}); - - stream.write(data); - - assert.propertyVal(JSON.parse(streamStub.write.secondCall.args[0]), 'n', 'test1'); - }); - - it('should write object with command list from data', () => { - const stream = StreamWriter.create('report/path'); - const data = dataStub({ - fullTitle: 'test1', - hermioneCtx: { - commandList: [{cl: [1]}] - } - }); - - stream.write(data); - const passedData = JSON.parse(streamStub.write.secondCall.args[0]); - - assert.deepEqual(passedData.cl, [1]); - }); - - it('should write object with empty command list if it does not exist in data', () => { - const stream = StreamWriter.create('report/path'); - const data = dataStub({fullTitle: 'test1'}); - - stream.write(data); - const passedData = JSON.parse(streamStub.write.secondCall.args[0]); - - assert.deepEqual(passedData.cl, []); - }); - - it('should divide data chains with comma delimiter', () => { - const stream = StreamWriter.create('report/path'); - - stream.write(dataStub()); - stream.write(dataStub()); - - // calls: 1 - open bracket, 2 - first data, 3 - delim, 4 - second data - assert.calledWithExactly(streamStub.write.thirdCall, ','); - }); - }); - - describe('end', () => { - it('should end stream with the closing bracket', () => { - const stream = StreamWriter.create('report/path'); - - stream.end(); - assert.calledOnceWith(streamStub.end, ']'); - }); - }); -}); diff --git a/test/setup.js b/test/setup.js index 3931e95..bdcfadf 100644 --- a/test/setup.js +++ b/test/setup.js @@ -5,4 +5,6 @@ const chai = require('chai'); global.sinon = require('sinon'); global.assert = chai.assert; +chai.use(require('chai-as-promised')); + sinon.assert.expose(chai.assert, {prefix: ''});