@adobe/helix-cli
Version:
Project Helix CLI
210 lines (188 loc) • 6.71 kB
JavaScript
/*
* Copyright 2018 Adobe. All rights reserved.
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. You may obtain a copy
* of the License at http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
* OF ANY KIND, either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
'use strict';
const chalk = require('chalk');
const path = require('path');
const _ = require('lodash/fp');
const JunitPerformanceReport = require('./junit-utils');
const AbstractCommand = require('./abstract.cmd.js');
class PerfCommand extends AbstractCommand {
constructor(logger) {
super(logger);
this._strains = null;
this._auth = null;
this._calibre = null;
this._location = 'London';
this._device = 'MotorolaMotoG4';
this._connection = 'regular3G';
this._junit = null;
}
withJunit(value) {
if (value && value !== '') {
this._junit = new JunitPerformanceReport().withOutfile(path.resolve(process.cwd(), value));
}
return this;
}
withCalibreAuth(value) {
this._auth = value;
process.env.CALIBRE_API_TOKEN = this._auth;
// eslint-disable-next-line global-require
this._calibre = require('calibre'); // defer loading of calibre
// client until env is set
return this;
}
withLocation(value) {
this._location = value;
return this;
}
withDevice(value) {
this._device = value;
return this;
}
withConnection(value) {
this._connection = value;
return this;
}
loadStrains() {
if (this._strains) {
return this._strains;
}
this._strains = Array.from(this.config.strains.values());
return this._strains;
}
getDefaultParams() {
const defaultparams = {
device: this._device,
location: this._location,
connection: this._connection,
};
const defaults = this.loadStrains().filter(
({ name }) => name === 'default',
);
if (defaults.length === 1 && defaults[0].perf) {
return Object.assign(defaultparams, defaults[0].perf.toJSON({ minimal: true }));
}
return defaultparams;
}
getStrainParams(strain) {
if (strain.perf) {
return Object.assign(this.getDefaultParams(), strain.perf);
}
return this.getDefaultParams();
}
static getURLs(strain) {
if (strain.url && strain.urls) {
return [strain.url, ...strain.urls];
} if (strain.url) {
return [strain.url];
} if (strain.urls) {
return strain.urls;
}
return [];
}
static formatScore(score, limit) {
if (score >= limit) {
return chalk.green.bold(score);
}
return chalk.red.bold(score) + chalk.red(' (failed)');
}
static formatMeasure(measure, limit) {
if (measure <= limit) {
return chalk.green.bold(measure);
}
return chalk.red.bold(measure) + chalk.red(' (failed)');
}
/**
*
* @param {*} metrics
* @param {*} name name of the test to run
* @param {*} limit
* @returns true if successful, false if unsuccessful and undefined if the name isn't valid
*/
format(metrics, name, limit) {
const metric = metrics.filter(m => m.name === name).length === 1
? metrics.filter(m => m.name === name)[0] : null;
if (metric && metric.name.endsWith('-score')) {
this.log.info(` ${chalk.gray(`${metric.label}: `)}${PerfCommand.formatScore(metric.value, limit)}`);
return PerfCommand.formatScore(metric.value, limit).indexOf('(failed)') === -1;
} if (metric) {
this.log.info(` ${chalk.gray(`${metric.label}: `)}${PerfCommand.formatMeasure(metric.value, limit)}`);
return PerfCommand.formatMeasure(metric.value, limit).indexOf('(failed)') === -1;
}
return undefined;
}
formatResponse(response, params = {}, strainname = 'default') {
this.log.info(`\nTesting ${response.url} on ${response.device.title} (${response.connection.title}) from ${response.location.emoji} ${response.location.name} using ${strainname} strain.\n`);
const strainresults = Object.keys(params).map((key) => {
const value = params[key];
if (Number.isInteger(value)) {
return this.format(response.metrics, key, value);
}
return undefined;
});
if (strainresults.length === 0 || strainresults.every(val => val === undefined)) {
const perf = this.format(response.metrics, 'lighthouse-performance-score', 80);
const access = this.format(response.metrics, 'lighthouse-accessibility-score', 80);
// use the default metrics
return perf && access;
}
// make sure all tests have been passed
return strainresults.every(result => result === true || result === undefined);
}
async run() {
await this.init();
this.log.info(chalk.green('Testing performance…'));
const tests = this.loadStrains()
.filter(strain => PerfCommand.getURLs(strain).length)
.map((strain) => {
const params = this.getStrainParams(strain);
return PerfCommand.getURLs(strain).map(url => this._calibre.Test.create({
url,
location: params.location,
device: params.device,
connection: params.connection,
cookies: [{
name: 'X-Strain', value: strain.name, secure: true, httpOnly: true,
}],
})
.then(({ uuid }) => this._calibre.Test.waitForTest(uuid))
.then((result) => {
if (this._junit) {
this._junit.appendResults(result, params, strain.name);
}
return this.formatResponse(result, params, strain.name);
})
.catch((err) => {
this.log.error(err);
return null;
}));
});
const flattests = _.flatten(tests);
Promise.all(flattests).then((results) => {
if (this._junit) {
this._junit.writeResults();
}
this.log.info('');
const fail = results.filter(result => result === false).length;
const succeed = results.filter(result => result === true).length;
if (fail && succeed) {
this.log.error(chalk.yellow(`all tests completed with ${fail} failures and ${succeed} successes.`));
} else if (fail) {
this.log.error(chalk.red(`all ${fail} tests failed.`));
} else if (succeed) {
this.log.log(chalk.green(`all ${succeed} tests succeeded.`));
}
process.exit(fail);
});
}
}
module.exports = PerfCommand;