Skip to content

Commit

Permalink
feat: add merge-reports command
Browse files Browse the repository at this point in the history
  • Loading branch information
sipayRT authored and DudaGod committed Sep 4, 2018
1 parent 961fbba commit b8ba30b
Show file tree
Hide file tree
Showing 23 changed files with 1,555 additions and 171 deletions.
2 changes: 1 addition & 1 deletion gemini.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ module.exports = (gemini, opts) => {
}

plugin
.extendCliByGuiCommand()
.addCliCommands()
.init(prepareData, prepareImages);
};

Expand Down
2 changes: 1 addition & 1 deletion hermione.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ module.exports = (hermione, opts) => {
}

plugin
.extendCliByGuiCommand()
.addCliCommands()
.init(prepareData, prepareImages);
};

Expand Down
23 changes: 23 additions & 0 deletions lib/cli-commands/gui.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use strict';

const runGui = require('../gui');
const Api = require('../gui/api');
const {GUI: commandName} = require('./');

module.exports = (program, pluginConfig, tool) => {
// must be executed here because it adds `gui` field in `gemini` and `hermione tool`,
// which is available to other plugins and is an API for interacting with the current plugin
const guiApi = Api.create(tool);

program
.command(`${commandName} [paths...]`)
.allowUnknownOption()
.description('update the changed screenshots or gather them if they does not exist')
.option('-p, --port <port>', 'Port to launch server on', 8000)
.option('--hostname <hostname>', 'Hostname to launch server on', 'localhost')
.option('-a, --auto-run', 'auto run immediately')
.option('-O, --no-open', 'not to open a browser window after starting the server')
.action((paths, options) => {
runGui({paths, tool, guiApi, configs: {options, program, pluginConfig}});
});
};
6 changes: 6 additions & 0 deletions lib/cli-commands/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'use strict';

module.exports = {
GUI: 'gui',
MERGE_REPORTS: 'merge-reports'
};
21 changes: 21 additions & 0 deletions lib/cli-commands/merge-reports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict';

const {MERGE_REPORTS: commandName} = require('./');
const mergeReports = require('../merge-reports');
const {logError} = require('../server-utils');

module.exports = (program, {path}) => {
program
.command(`${commandName} [paths...]`)
.allowUnknownOption()
.description('merge reports')
.option('-d, --destination <destination>', 'path to directory with merged report', path)
.action(async (paths, options) => {
try {
await mergeReports(paths, options);
} catch (err) {
logError(err);
process.exit(1);
}
});
};
21 changes: 2 additions & 19 deletions lib/gui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,26 +3,9 @@
const chalk = require('chalk');
const opener = require('opener');
const server = require('./server');
const Api = require('./api');
const {logger, logError} = require('../server-utils');

module.exports = (program, tool, pluginConfig) => {
const guiApi = Api.create(tool);

program
.command('gui [paths...]')
.allowUnknownOption()
.description('update the changed screenshots or gather them if they does not exist')
.option('-p, --port <port>', 'Port to launch server on', 8000)
.option('--hostname <hostname>', 'Hostname to launch server on', 'localhost')
.option('-a, --auto-run', 'auto run immediately')
.option('-O, --no-open', 'not to open a browser window after starting the server')
.action((paths, options) => {
runGui({paths, tool, guiApi, configs: {options, program, pluginConfig}});
});
};

function runGui(args) {
module.exports = (args) => {
server.start(args)
.then(({url}) => {
logger.log(`GUI is running at ${chalk.cyan(url)}`);
Expand All @@ -32,4 +15,4 @@ function runGui(args) {
logError(err);
process.exit(1);
});
}
};
172 changes: 172 additions & 0 deletions lib/merge-reports/data-tree.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
'use strict';

const path = require('path');
const _ = require('lodash');
const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs-extra'));
const {isSkippedStatus} = require('../common-utils');
const {findNode, setStatusForBranch} = require('../static/modules/utils');
const {getDataFrom, getStatNameForStatus, getImagePaths} = require('./utils');

module.exports = class DataTree {
static create(initialData, destPath) {
return new DataTree(initialData, destPath);
}

constructor(initialData, destPath) {
this._data = initialData;
this._destPath = destPath;
}

async mergeWith(dataCollection) {
// make it serially in order to perform correct merge/permutation of images and datas
await Promise.each(_.toPairs(dataCollection), async ([path, data]) => {
this._srcPath = path;
this._mergeSkips(data.skips);

await this._mergeSuites(data.suites);
});

return this._data;
}

_mergeSkips(srcSkips) {
srcSkips.forEach((skip) => {
if (!_.find(this._data.skips, {suite: skip.suite, browser: skip.browser})) {
this._data.skips.push(skip);
}
});
}

async _mergeSuites(srcSuites) {
await Promise.map(srcSuites, async (suite) => {
await this._mergeSuiteResult(suite);
});
}

async _mergeSuiteResult(suite) {
const existentSuite = findNode(this._data.suites, suite.suitePath);

if (!existentSuite) {
return await this._addSuiteResult(suite);
}

if (suite.children) {
await Promise.map(suite.children, (childSuite) => this._mergeSuiteResult(childSuite));
} else {
await this._mergeBrowserResult(suite);
}
}

async _mergeBrowserResult(suite) {
await Promise.map(suite.browsers, async (bro) => {
const existentBro = this._findBrowserResult(suite.suitePath, bro.name);

if (!existentBro) {
return await this._addBrowserResult(bro, suite.suitePath);
}

this._moveTestResultToRetries(existentBro);
await this._addTestRetries(existentBro, bro.retries);
await this._changeTestResult(existentBro, bro.result, suite.suitePath);
});
}

async _addSuiteResult(suite) {
if (suite.suitePath.length === 1) {
this._data.suites.push(suite);
} else {
const existentParentSuite = findNode(this._data.suites, suite.suitePath.slice(0, -1));
existentParentSuite.children.push(suite);
}

this._mergeStatistics(suite);
await this._moveImages(suite, {fromFields: ['result', 'retries']});
}

async _addBrowserResult(bro, suitePath) {
const existentParentSuite = findNode(this._data.suites, suitePath);
existentParentSuite.browsers.push(bro);

this._mergeStatistics(bro);
await this._moveImages(bro, {fromFields: ['result', 'retries']});
}

_moveTestResultToRetries(existentBro) {
existentBro.retries.push(existentBro.result);

this._data.retries += 1;
const statName = getStatNameForStatus(existentBro.result.status);
this._data[statName] -= 1;
}

async _addTestRetries(existentBro, retries) {
await Promise.mapSeries(retries, (retry) => this._addTestRetry(existentBro, retry));
}

async _addTestRetry(existentBro, retry) {
const newAttempt = existentBro.retries.length;

await this._moveImages(retry, {newAttempt});
retry = this._changeFieldsWithAttempt(retry, {newAttempt});

existentBro.retries.push(retry);
this._data.retries += 1;
}

async _changeTestResult(existentBro, result, suitePath) {
await this._moveImages(result, {newAttempt: existentBro.retries.length});
existentBro.result = this._changeFieldsWithAttempt(result, {newAttempt: existentBro.retries.length});

const statName = getStatNameForStatus(existentBro.result.status);
this._data[statName] += 1;

if (!isSkippedStatus(existentBro.result.status)) {
setStatusForBranch(this._data.suites, suitePath, existentBro.result.status);
}
}

_mergeStatistics(node) {
const testResultStatuses = getDataFrom(node, {fieldName: 'status', fromFields: 'result'});

testResultStatuses.forEach((testStatus) => {
const statName = getStatNameForStatus(testStatus);
if (this._data.hasOwnProperty(statName)) {
this._data.total += 1;
this._data[statName] += 1;
}
});

const testRetryStatuses = getDataFrom(node, {fieldName: 'status', fromFields: 'retries'});
this._data.retries += testRetryStatuses.length;
}

async _moveImages(node, {newAttempt, fromFields}) {
await Promise.map(getImagePaths(node, fromFields), async (imgPath) => {
const srcImgPath = path.resolve(this._srcPath, imgPath);
const destImgPath = path.resolve(
this._destPath,
_.isNumber(newAttempt) ? imgPath.replace(/\d+(?=.png$)/, newAttempt) : imgPath
);

await fs.moveAsync(srcImgPath, destImgPath);
});
}

_changeFieldsWithAttempt(testResult, {newAttempt}) {
const imagesInfo = testResult.imagesInfo.map((imageInfo) => {
return _.mapValues(imageInfo, (val, key) => {
return ['expectedPath', 'actualPath', 'diffPath'].includes(key)
? val.replace(/\d+(?=.png)/, newAttempt)
: val;
});
});

return _.extend({}, testResult, {attempt: newAttempt, imagesInfo});
}

_findBrowserResult(suitePath, browserId) {
const existentNode = findNode(this._data.suites, suitePath);
return _.find(_.get(existentNode, 'browsers'), {name: browserId});
}
};
21 changes: 21 additions & 0 deletions lib/merge-reports/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use strict';

const ReportBuilder = require('./report-builder');

module.exports = async (srcPaths, {destination: destPath}) => {
validateOpts(srcPaths, destPath);

const reportBuilder = ReportBuilder.create(srcPaths, destPath);

await reportBuilder.build();
};

function validateOpts(srcPaths, destPath) {
if (!srcPaths.length) {
throw new Error('Nothing to merge, no source reports are passed');
}

if (srcPaths.includes(destPath)) {
throw new Error(`Destination report path: ${destPath}, exists in source report paths`);
}
}
66 changes: 66 additions & 0 deletions lib/merge-reports/report-builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
'use strict';

const path = require('path');
const _ = require('lodash');
const Promise = require('bluebird');
const fs = Promise.promisifyAll(require('fs-extra'));
const chalk = require('chalk');
const DataTree = require('./data-tree');
const serverUtils = require('../server-utils');

module.exports = class ReportBuilder {
static create(srcPaths, destPath) {
return new this(srcPaths, destPath);
}

constructor(srcPaths, destPath) {
this.srcPaths = srcPaths;
this.destPath = destPath;
}

async build() {
await this._copyToReportDir(
['images', 'index.html', 'report.min.js', 'report.min.css'],
{from: this.srcPaths[0], to: this.destPath}
);

const srcReportsData = this._loadReportsData();
const dataTree = DataTree.create(srcReportsData[0], this.destPath);
const srcDataCollection = _.zipObject(this.srcPaths.slice(1), srcReportsData.slice(1));

const mergedData = await dataTree.mergeWith(srcDataCollection);

await this._saveDataFile(mergedData);
}

_loadReportsData() {
return _(this.srcPaths)
.map((reportPath) => {
const srcDataPath = path.resolve(reportPath, 'data');

try {
return serverUtils.require(srcDataPath);
} catch (err) {
serverUtils.logger.warn(chalk.yellow(`Not found data file in passed source report path: ${reportPath}`));
return {skips: [], suites: []};
}
})
.value();
}

async _copyToReportDir(files, {from, to}) {
await Promise.map(files, async (dataName) => {
const srcDataPath = path.resolve(from, dataName);
const destDataPath = path.resolve(to, dataName);

await fs.moveAsync(srcDataPath, destDataPath);
});
}

async _saveDataFile(data) {
const formattedData = serverUtils.prepareCommonJSData(data);
const destDataPath = path.resolve(this.destPath, 'data.js');

await fs.writeFile(destDataPath, formattedData);
}
};
Loading

0 comments on commit b8ba30b

Please sign in to comment.