diff --git a/packages/composer-cli/lib/cmds/report/lib/report.js b/packages/composer-cli/lib/cmds/report/lib/report.js index 253bf82985..082bb16c4c 100644 --- a/packages/composer-cli/lib/cmds/report/lib/report.js +++ b/packages/composer-cli/lib/cmds/report/lib/report.js @@ -23,7 +23,7 @@ const chalk = require('chalk'); */ class Report { /** - * Command implementation. + * Command process for report command * @param {Object} args argument list from composer command * @return {Promise} promise when command complete */ @@ -33,14 +33,28 @@ class Report { /** * Get the current environment data + * @return {Promise} Resolved when report completed */ static createReport() { - cmdUtil.log(chalk.bold.blue('Creating Composer report')); - let tmpDirectory = report.setupReportDir(); - cmdUtil.log(chalk.blue('Triggering node report...')); - report.createNodeReport(tmpDirectory); - let outputFile = report.archiveReport(tmpDirectory); - cmdUtil.log(chalk.bold.blue('Created archive file: '+outputFile)); + try { + cmdUtil.log(chalk.bold.blue('Creating Composer report')); + const {reportId, reportDir} = report.beginReport(); + + cmdUtil.log(chalk.blue('Collecting diagnostic data...')); + report.collectBasicDiagnostics(reportId, reportDir); + + const archiveName = report.completeReport(reportId, reportDir); + cmdUtil.log(chalk.bold.blue('\nCreated archive file: ') + archiveName); + + } catch (err) { + if (err.name === 'DirectoryAccessError') { + return Promise.reject(err); + } else { + throw err; + } + } + + return Promise.resolve(); } } module.exports = Report; diff --git a/packages/composer-cli/test/report/report.js b/packages/composer-cli/test/report/report.js index 6de3ccf71c..0179702117 100644 --- a/packages/composer-cli/test/report/report.js +++ b/packages/composer-cli/test/report/report.js @@ -21,22 +21,25 @@ const ReportCmd = require('../../lib/cmds/report/reportCommand.js'); const chai = require('chai'); const sinon = require('sinon'); -const assert = sinon.assert; const sinonChai = require('sinon-chai'); +const should = chai.should(); chai.use(sinonChai); describe('composer report CLI', function() { const sandbox = sinon.sandbox.create(); let consoleLogSpy; - let setupStub; - let reportStub; - let archiveStub; + let beginReportStub; + let collectBasicDiagnosticsStub; + let completeReportStub; beforeEach(function() { consoleLogSpy = sandbox.spy(console, 'log'); - setupStub = sandbox.stub(composerReport, 'setupReportDir').returns('DIR'); - reportStub = sandbox.stub(composerReport, 'createNodeReport'); - archiveStub = sandbox.stub(composerReport, 'archiveReport').returns('ARCHIVE'); + beginReportStub = sandbox.stub(composerReport, 'beginReport').returns({ + reportId: 'REPORT', + reportDir: 'DIR' + }); + collectBasicDiagnosticsStub = sandbox.stub(composerReport, 'collectBasicDiagnostics'); + completeReportStub = sandbox.stub(composerReport, 'completeReport').returns('ARCHIVE'); }); afterEach(function() { @@ -46,13 +49,13 @@ describe('composer report CLI', function() { it('should successfully run the composer report command', function() { const args = {}; return ReportCmd.handler(args).then(() => { - assert.calledThrice(consoleLogSpy); - assert.calledWith(consoleLogSpy, sinon.match('Creating Composer report')); - assert.calledWith(consoleLogSpy, sinon.match('Triggering node report...')); - assert.calledWith(consoleLogSpy, sinon.match('Created archive file: ARCHIVE')); - assert.calledOnce(setupStub); - assert.calledWith(reportStub, 'DIR'); - assert.calledWith(archiveStub, 'DIR'); + consoleLogSpy.should.have.been.calledThrice; + consoleLogSpy.should.have.been.calledWith(sinon.match('Creating Composer report')); + consoleLogSpy.should.have.been.calledWith(sinon.match('Collecting diagnostic data...')); + consoleLogSpy.should.have.been.calledWith(sinon.match(/Created archive file: .*ARCHIVE/)); + beginReportStub.should.have.been.calledOnce; + collectBasicDiagnosticsStub.should.have.been.calledWith('REPORT', 'DIR'); + completeReportStub.should.have.been.calledWith('REPORT', 'DIR'); }); }); @@ -61,4 +64,36 @@ describe('composer report CLI', function() { result.should.be.an.instanceOf(Promise); }); + it('should handle errors', function() { + let testErr = new Error('ERROR'); + beginReportStub.throws(testErr); + + let result; + try { + const args = {}; + return ReportCmd.handler(args).then(() => { + should.fail('Should have thrown an error!'); + }); + } catch (err) { + result = err; + } + + should.exist(result); + result.name.should.not.equal('DirectoryAccessError'); + }); + + it('should throw DirectoryAccessError if the current directory is not writeable', function() { + let testErr = new Error('Access denied'); + testErr.name = 'DirectoryAccessError'; + beginReportStub.throws(testErr); + + const args = {}; + return ReportCmd.handler(args).then(() => { + should.fail('Should have been rejected!'); + }).catch((err) => { + should.exist(err); + err.name.should.equal('DirectoryAccessError'); + }); + }); + }); diff --git a/packages/composer-report/.gitignore b/packages/composer-report/.gitignore index 6f7b85d425..dcebc42c3c 100644 --- a/packages/composer-report/.gitignore +++ b/packages/composer-report/.gitignore @@ -30,13 +30,16 @@ bower_components # node-waf configuration .lock-wscript -# Compiled binary addons (http://nodejs.org/api/addons.html) +# Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ +# Tarballs +*.tgz + # Typescript v1 declaration files typings/ @@ -61,11 +64,13 @@ typings/ # JSDoc out -# Mac files. +# next.js build output +.next + +# OSX files **/.DS_Store *.swp # Build generated files should be ignored by git, but not by npm. index.d.ts - diff --git a/packages/composer-report/bin/cmd.js b/packages/composer-report/bin/cmd.js index 487da1cf7a..5c10124059 100755 --- a/packages/composer-report/bin/cmd.js +++ b/packages/composer-report/bin/cmd.js @@ -17,4 +17,24 @@ const report = require('../lib/report.js'); -report.report(); +try { + const {reportId, reportDir} = report.beginReport(); + + // eslint-disable-next-line no-console + console.log('Collecting diagnostic data...'); + report.collectBasicDiagnostics(reportId, reportDir); + + const archiveName = report.completeReport(reportId, reportDir); + + // eslint-disable-next-line no-console + console.log(`Created archive file: ${archiveName}`); + +} catch (err) { + if (err.name === 'DirectoryAccessError') { + // eslint-disable-next-line no-console + console.log(err.message); + return 1; + } else { + throw err; + } +} diff --git a/packages/composer-report/lib/report.js b/packages/composer-report/lib/report.js index caf923bc7f..c6c181ca5a 100644 --- a/packages/composer-report/lib/report.js +++ b/packages/composer-report/lib/report.js @@ -16,56 +16,65 @@ const fs = require('fs'); const os = require('os'); +const path = require('path'); const { sep } = require('path'); const moment = require('moment'); const nodereport = require('node-report'); const tar = require('tar'); /** - * Main API called from cmd and composer report. - * @return {String} the name of the archive file that was created. + * Prepares a report ID and temporary ready to begin collecting + * diagnostic data + * @return {Object} the report ID and temporary directory + * @throws {Error} DirectoryAccessError if the current directory is not writeable */ -function report() { - let tmpDirectory = setupReportDir(); +function beginReport() { + // Make sure the current directory is writeable for when we get to creating + // the report archive + const currentDirectory = process.cwd(); + try { + fs.accessSync(currentDirectory, fs.constants.R_OK | fs.constants.W_OK); + } catch (err) { + if (err.code === 'EACCES') { + let reportError = new Error('Cannot create report in current directory: permission denied'); + reportError.name = 'DirectoryAccessError'; + throw reportError; + } else { + throw err; + } + } - // TODO write readme file inc. version of the composer-report module - // Plus versions of the other composer modules? + const reportId = _createReportId(); + const reportDir = _setupReportDir(); - createNodeReport(tmpDirectory); - return archiveReport(tmpDirectory); + return { + reportId: reportId, + reportDir: reportDir + }; } /** - * Sets up the temp directory for the report - * @return {String} the Path to the temporary directory - */ -function setupReportDir() { - const tmpDir = os.tmpdir(); - return fs.mkdtempSync(`${tmpDir}${sep}`); -} - -/** - * Trigger node-report to write report in the temp directory + * Collects diagnostic data into the temp directory for the report + * @param {String} reportId report identifier * @param {String} tmpDirectory the temporary directory for collecting report output */ -function createNodeReport(tmpDirectory) { - nodereport.setDirectory(tmpDirectory); - nodereport.triggerReport(); +function collectBasicDiagnostics(reportId, tmpDirectory) { + _createComposerReport(reportId, tmpDirectory); + _createNodeReport(tmpDirectory); } /** - * Creates an archive of the temp directory for the report + * Creates an archive of the temp directory for the report in the current directory + * @param {String} reportId report identifier * @param {String} tmpDirectory the temporary directory for collecting report output * @return {String} the name of the archive file that has been created. */ -function archiveReport(tmpDirectory) { - let timestamp = moment().utc().format('YYYYMMDD[T]HHmmss'); - let prefix = 'composer-report-' + timestamp; - let filename = prefix + '.tgz'; +function completeReport(reportId, tmpDirectory) { + let filename = reportId + '.tgz'; tar.c( { cwd: tmpDirectory+'/', - prefix: prefix, + prefix: reportId, gzip: true, file: filename, sync: true @@ -75,4 +84,50 @@ function archiveReport(tmpDirectory) { return filename; } -module.exports = { report, setupReportDir, createNodeReport, archiveReport } ; +module.exports = { beginReport, collectBasicDiagnostics, completeReport } ; + +/** + * Creates a report identifer for use in filenames etc. + * @return {String} the report identifier + * @private + */ +function _createReportId() { + let timestamp = moment().utc().format('YYYYMMDD[T]HHmmss'); + return 'composer-report-' + timestamp; +} + +/** + * Sets up the temp directory for the report + * @return {String} the Path to the temporary directory + * @private + */ +function _setupReportDir() { + const tmpDir = os.tmpdir(); + return fs.mkdtempSync(`${tmpDir}${sep}`); +} + +/** + * Write simple composer report in the temp directory + * @param {String} reportId report identifier + * @param {String} tmpDirectory the temporary directory for collecting report output + * @private + */ +function _createComposerReport(reportId, tmpDirectory) { + const packageJsonPath = path.join(__dirname, '..', 'package.json'); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + + const composerReportVersion = 'composer-report version: ' + packageJson.version; + + const reportPath = path.join(tmpDirectory, reportId + '.txt'); + fs.writeFileSync(reportPath, composerReportVersion); +} + +/** + * Trigger node-report to write report in the temp directory + * @param {String} tmpDirectory the temporary directory for collecting report output + * @private + */ +function _createNodeReport(tmpDirectory) { + nodereport.setDirectory(tmpDirectory); + nodereport.triggerReport(); +} diff --git a/packages/composer-report/package.json b/packages/composer-report/package.json index 38239b6d95..59c2f7b58c 100644 --- a/packages/composer-report/package.json +++ b/packages/composer-report/package.json @@ -64,9 +64,9 @@ ], "all": true, "check-coverage": true, - "statements": 83, - "branches": 64, - "functions": 78, - "lines": 83 + "statements": 100, + "branches": 100, + "functions": 100, + "lines": 100 } } diff --git a/packages/composer-report/test/cmd.js b/packages/composer-report/test/cmd.js index 777769279f..1358439cf7 100644 --- a/packages/composer-report/test/cmd.js +++ b/packages/composer-report/test/cmd.js @@ -14,27 +14,75 @@ 'use strict'; const report = require('../lib/report.js'); +const path = require('path'); const sinon = require('sinon'); const chai = require('chai'); const expect = chai.expect; - describe('composer-report CLI command', function() { - let sandbox; - let reportStub; + let sandbox = sinon.sandbox.create(); + let beginReportStub; + let collectBasicDiagnosticsStub; + let completeReportStub; + let consoleLogSpy; beforeEach(() => { - sandbox = sinon.sandbox.create(); - reportStub = sandbox.stub(report, 'report'); + consoleLogSpy = sandbox.spy(console, 'log'); + beginReportStub = sandbox.stub(report, 'beginReport'); + collectBasicDiagnosticsStub = sandbox.stub(report, 'collectBasicDiagnostics'); + completeReportStub = sandbox.stub(report, 'completeReport'); }); afterEach(() => { sandbox.restore(); + delete require.cache[path.resolve(__dirname, '../bin/cmd.js')]; }); - it('should call the library function when the command is run', function() { + it('should call the library functions when the command is run', function() { + beginReportStub.returns({ + reportId: 'reportId', + reportDir: 'reportDir' + }); + require('../bin/cmd.js'); - expect(reportStub).to.have.been.calledOnce; + + expect(beginReportStub).to.have.been.calledOnce; + expect(collectBasicDiagnosticsStub).to.have.been.calledOnce; + expect(collectBasicDiagnosticsStub).to.have.been.calledWith('reportId', 'reportDir'); + expect(completeReportStub).to.have.been.calledOnce; + expect(completeReportStub).to.have.been.calledWith('reportId', 'reportDir'); + }); + + it('should handle errors', function() { + let testErr = new Error('ERROR'); + testErr.name = 'NAME'; + beginReportStub.throws(testErr); + + let result; + try { + require('../bin/cmd.js'); + + expect(beginReportStub).to.have.been.calledOnce; + expect(collectBasicDiagnosticsStub).not.to.have.been.called; + expect(completeReportStub).not.to.have.been.called; + } catch (err) { + result = err; + } + + expect(result).to.exist; + }); + + it('should show an error if the current directory is not writeable', function() { + let testErr = new Error('Access denied'); + testErr.name = 'DirectoryAccessError'; + beginReportStub.throws(testErr); + + require('../bin/cmd.js'); + + expect(beginReportStub).to.have.been.calledOnce; + expect(collectBasicDiagnosticsStub).not.to.have.been.called; + expect(completeReportStub).not.to.have.been.called; + expect(consoleLogSpy).to.have.been.calledWith('Access denied'); }); -}); \ No newline at end of file +}); diff --git a/packages/composer-report/test/report.js b/packages/composer-report/test/report.js index 7f1fd7d09e..8ca9b4ddee 100644 --- a/packages/composer-report/test/report.js +++ b/packages/composer-report/test/report.js @@ -15,8 +15,7 @@ 'use strict'; const report = require('../lib/report.js'); -const { sep } = require('path'); -const os = require('os'); +const fs = require('fs'); const nodereport = require('node-report'); const tar = require('tar'); @@ -24,55 +23,111 @@ const chai = require('chai'); const sinon = require('sinon'); const sinonChai = require('sinon-chai'); const expect = chai.expect; -const assert = chai.assert; chai.use(sinonChai); describe('composer-report CLI', function() { const sandbox = sinon.sandbox.create(); - let triggerReportStub; - let setDirectoryStub; - let cStub; - - beforeEach(function() { - cStub = sandbox.stub(tar, 'c').returns(Promise.resolve()); - triggerReportStub = sandbox.stub(nodereport, 'triggerReport'); - setDirectoryStub = sandbox.stub(nodereport, 'setDirectory'); - }); - afterEach(function() { - sandbox.restore(); - }); + describe('#beginReport', function() { + let accessSyncStub; + let mkdtempSyncStub; - it('should successfully run the composer-report command with no arguments specified', function() { - let reportSpy = sinon.spy(report, 'report'); - report.report({}, reportSpy); - expect(reportSpy).to.have.been.calledWith({}); - }); + beforeEach(function() { + accessSyncStub = sandbox.stub(fs, 'accessSync'); + mkdtempSyncStub = sandbox.stub(fs, 'mkdtempSync'); + mkdtempSyncStub.returns('tempDir'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should create a temporary directory to store files to create the report archive from', function() { + let result = report.beginReport(); + expect(result.reportId).to.match(/^composer-report-\d{8}T\d{6}/); + expect(result.reportDir).to.equal('tempDir'); + }); + + it('should handle errors', function() { + let testErr = new Error('ERROR'); + accessSyncStub.throws(testErr); + + let result; + try { + report.beginReport(); + } catch (err) { + result = err; + } + + expect(result).to.exist; + expect(result.name).not.to.equal('DirectoryAccessError'); + }); + + it('should throw DirectoryAccessError if the current directory is not writeable', function() { + let testErr = new Error('Access denied'); + testErr.code = 'EACCES'; + accessSyncStub.throws(testErr); - it('should create a temporary directory to store files to create the report archive from', function() { - let setupSpy = sinon.spy(report, 'setupReportDir'); - let result = report.setupReportDir(setupSpy); - expect(setupSpy).to.have.been.calledOnce; - assert.match(result, new RegExp('^'+os.tmpdir()+sep+'.*$')); + let result; + try { + report.beginReport(); + } catch (err) { + result = err; + } + + expect(result).to.exist; + expect(result.name).to.equal('DirectoryAccessError'); + }); }); - it('should successfully write a node-report report to the temporary directory', function() { - let createReportSpy = sinon.spy(report, 'createNodeReport'); - report.createNodeReport('/tmp'); - expect(createReportSpy).to.have.been.calledWith('/tmp'); - expect(setDirectoryStub).to.have.been.calledWith('/tmp'); - expect(triggerReportStub).to.have.been.calledWith(); + describe('#collectBasicDiagnostics', function() { + let triggerReportStub; + let setDirectoryStub; + let writeFileSyncStub; + + beforeEach(function() { + triggerReportStub = sandbox.stub(nodereport, 'triggerReport'); + setDirectoryStub = sandbox.stub(nodereport, 'setDirectory'); + writeFileSyncStub = sandbox.stub(fs, 'writeFileSync'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should successfully write a composer report text file to the temporary directory', function() { + report.collectBasicDiagnostics('reportId', 'reportDir'); + expect(writeFileSyncStub).to.have.been.calledWith('reportDir/reportId.txt', sinon.match(/^composer-report version: \d+\.\d+\.\d+$/)); + }); + + it('should successfully write a node-report report to the temporary directory', function() { + report.collectBasicDiagnostics('reportId', 'reportDir'); + expect(setDirectoryStub).to.have.been.calledWith('reportDir'); + expect(triggerReportStub).to.have.been.called; + }); }); - it('should successfully create a zipped tar archive of the COMPOSER_REPORT_TEMPDIR in the current directory and log the output filename in the console', function() { - report.archiveReport('COMPOSER_REPORT_TEMPDIR'); - sinon.assert.calledOnce(cStub); - sinon.assert.calledWith(cStub, { - cwd: 'COMPOSER_REPORT_TEMPDIR/', - prefix: sinon.match(/^composer-report-\d{8}T\d{6}$/), - gzip: true, - file: sinon.match(/^composer-report-\d{8}T\d{6}\.tgz$/), - sync: true - }, ['.']); + describe('#completeReport', function() { + let cStub; + + beforeEach(function() { + cStub = sandbox.stub(tar, 'c'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should successfully create a zipped tar archive of the temporary directory in the current directory', function() { + report.completeReport('reportId', 'reportDir'); + sinon.assert.calledOnce(cStub); + sinon.assert.calledWith(cStub, { + cwd: 'reportDir/', + prefix: 'reportId', + gzip: true, + file: 'reportId.tgz', + sync: true + }, ['.']); + }); }); }); diff --git a/packages/composer-tests-integration/features/cli.feature b/packages/composer-tests-integration/features/cli.feature index 5278aba28e..bc90ab87b1 100644 --- a/packages/composer-tests-integration/features/cli.feature +++ b/packages/composer-tests-integration/features/cli.feature @@ -477,7 +477,7 @@ Feature: Cli steps composer report """ Then The stdout information should include text matching /Creating Composer report/ - Then The stdout information should include text matching /Triggering node report.../ + Then The stdout information should include text matching /Collecting diagnostic data.../ Then The stdout information should include text matching /Created archive file: composer-report-/ Then The stdout information should include text matching /Command succeeded/ Then A new file matching this regex should be created /composer-report-/