core(report-generator): extract scoring into separate module (#4161)

* core(scoring): extract from reportgenerator into separate file

* core(scoring): extract existing tests into separate file

* scoreAllCategories
This commit is contained in:
Paul Irish 2018-01-02 17:28:58 -08:00 коммит произвёл Patrick Hulce
Родитель d7a9f3bc37
Коммит 7b63a66dba
5 изменённых файлов: 159 добавлений и 135 удалений

Просмотреть файл

@ -44,23 +44,6 @@ class ReportGeneratorV2 {
return REPORT_TEMPLATES;
}
/**
* Computes the weighted-average of the score of the list of items.
* @param {!Array<{score: number|undefined, weight: number|undefined}>} items
* @return {number}
*/
static arithmeticMean(items) {
const results = items.reduce((result, item) => {
const score = Number(item.score) || 0;
const weight = Number(item.weight) || 0;
return {
weight: result.weight + weight,
sum: result.sum + score * weight,
};
}, {weight: 0, sum: 0});
return (results.sum / results.weight) || 0;
}
/**
* Replaces all the specified strings in source without serial replacements.
@ -81,35 +64,6 @@ class ReportGeneratorV2 {
.join(firstReplacement.replacement);
}
/**
* Returns the report JSON object with computed scores.
* @param {{categories: !Object<string, {id: string|undefined, weight: number|undefined, audits: !Array<{id: string, weight: number|undefined}>}>}} config
* @param {!Object<{score: ?number|boolean|undefined}>} resultsByAuditId
* @return {{score: number, categories: !Array<{audits: !Array<{score: number, result: !Object}>}>}}
*/
generateReportJson(config, resultsByAuditId) {
const categories = Object.keys(config.categories).map(categoryId => {
const category = config.categories[categoryId];
category.id = categoryId;
const audits = category.audits.map(audit => {
const result = resultsByAuditId[audit.id];
// Cast to number to catch `null` and undefined when audits error
let auditScore = Number(result.score) || 0;
if (typeof result.score === 'boolean') {
auditScore = result.score ? 100 : 0;
}
return Object.assign({}, audit, {result, score: auditScore});
});
const categoryScore = ReportGeneratorV2.arithmeticMean(audits);
return Object.assign({}, category, {audits, score: categoryScore});
});
const overallScore = ReportGeneratorV2.arithmeticMean(categories);
return {score: overallScore, categories};
}
/**
* Returns the report HTML as a string with the report JSON and renderer JS inlined.

Просмотреть файл

@ -8,7 +8,7 @@
const Driver = require('./gather/driver.js');
const GatherRunner = require('./gather/gather-runner');
const ReportGeneratorV2 = require('./report/v2/report-generator');
const ReportScoring = require('./scoring');
const Audit = require('./audits/audit');
const emulation = require('./lib/emulation');
const log = require('lighthouse-logger');
@ -146,8 +146,7 @@ class Runner {
let reportCategories = [];
let score = 0;
if (config.categories) {
const reportGenerator = new ReportGeneratorV2();
const report = reportGenerator.generateReportJson(config, resultsById);
const report = ReportScoring.scoreAllCategories(config, resultsById);
reportCategories = report.categories;
score = report.score;
}

Просмотреть файл

@ -0,0 +1,59 @@
/**
* @license Copyright 2018 Google Inc. All Rights Reserved.
* Licensed 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 CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';
class ReportScoring {
/**
* Computes the weighted-average of the score of the list of items.
* @param {!Array<{score: number|undefined, weight: number|undefined}>} items
* @return {number}
*/
static arithmeticMean(items) {
const results = items.reduce((result, item) => {
const score = Number(item.score) || 0;
const weight = Number(item.weight) || 0;
return {
weight: result.weight + weight,
sum: result.sum + score * weight,
};
}, {weight: 0, sum: 0});
return (results.sum / results.weight) || 0;
}
/**
* Returns the report JSON object with computed scores.
* @param {{categories: !Object<string, {id: string|undefined, weight: number|undefined, audits: !Array<{id: string, weight: number|undefined}>}>}} config
* @param {!Object<{score: ?number|boolean|undefined}>} resultsByAuditId
* @return {{score: number, categories: !Array<{audits: !Array<{score: number, result: !Object}>}>}}
*/
static scoreAllCategories(config, resultsByAuditId) {
const categories = Object.keys(config.categories).map(categoryId => {
const category = config.categories[categoryId];
category.id = categoryId;
const audits = category.audits.map(audit => {
const result = resultsByAuditId[audit.id];
// Cast to number to catch `null` and undefined when audits error
let auditScore = Number(result.score) || 0;
if (typeof result.score === 'boolean') {
auditScore = result.score ? 100 : 0;
}
return Object.assign({}, audit, {result, score: auditScore});
});
const categoryScore = ReportScoring.arithmeticMean(audits);
return Object.assign({}, category, {audits, score: categoryScore});
});
const overallScore = ReportScoring.arithmeticMean(categories);
return {score: overallScore, categories};
}
}
module.exports = ReportScoring;

Просмотреть файл

@ -35,92 +35,6 @@ describe('ReportGeneratorV2', () => {
});
});
describe('#arithmeticMean', () => {
it('should work for empty list', () => {
assert.equal(ReportGeneratorV2.arithmeticMean([]), 0);
});
it('should work for equal weights', () => {
assert.equal(ReportGeneratorV2.arithmeticMean([
{score: 10, weight: 1},
{score: 20, weight: 1},
{score: 3, weight: 1},
]), 11);
});
it('should work for varying weights', () => {
assert.equal(ReportGeneratorV2.arithmeticMean([
{score: 10, weight: 2},
{score: 0, weight: 7},
{score: 20, weight: 1},
]), 4);
});
it('should work for missing values', () => {
assert.equal(ReportGeneratorV2.arithmeticMean([
{weight: 1},
{score: 30, weight: 1},
{weight: 1},
{score: 100},
]), 10);
});
});
describe('#generateReportJson', () => {
it('should return a score', () => {
const result = new ReportGeneratorV2().generateReportJson({
categories: {
'categoryA': {weight: 1, audits: [{id: 'auditA', weight: 1}]},
'categoryB': {weight: 4, audits: [{id: 'auditB', weight: 1}]},
'categoryC': {audits: []},
},
}, {auditA: {score: 50}, auditB: {score: 100}});
assert.equal(result.score, 90);
});
it('should return categories', () => {
const result = new ReportGeneratorV2().generateReportJson({
categories: {
'my-category': {name: 'My Category', audits: []},
'my-other-category': {description: 'It is a nice category', audits: []},
},
}, {});
assert.equal(result.categories.length, 2);
assert.equal(result.categories[0].name, 'My Category');
assert.equal(result.categories[1].description, 'It is a nice category');
});
it('should score the categories', () => {
const auditResults = {
'my-audit': {rawValue: 'you passed'},
'my-boolean-audit': {score: true, extendedInfo: {}},
'my-scored-audit': {score: 100},
'my-failed-audit': {score: 20},
'my-boolean-failed-audit': {score: false},
};
const result = new ReportGeneratorV2().generateReportJson({
categories: {
'my-category': {audits: [{id: 'my-audit'}]},
'my-scored': {
audits: [
{id: 'my-boolean-audit', weight: 1},
{id: 'my-scored-audit', weight: 1},
{id: 'my-failed-audit', weight: 1},
{id: 'my-boolean-failed-audit', weight: 1},
],
},
},
}, auditResults);
assert.equal(result.categories.length, 2);
assert.equal(result.categories[0].score, 0);
assert.equal(result.categories[1].score, 55);
});
});
describe('#generateHtmlReport', () => {
it('should return html', () => {
const result = new ReportGeneratorV2().generateReportHtml({});

Просмотреть файл

@ -0,0 +1,98 @@
/**
* @license Copyright 2018 Google Inc. All Rights Reserved.
* Licensed 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 CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';
const assert = require('assert');
const ReportScoring = require('../scoring');
/* eslint-env mocha */
describe('ReportScoring', () => {
describe('#arithmeticMean', () => {
it('should work for empty list', () => {
assert.equal(ReportScoring.arithmeticMean([]), 0);
});
it('should work for equal weights', () => {
assert.equal(ReportScoring.arithmeticMean([
{score: 10, weight: 1},
{score: 20, weight: 1},
{score: 3, weight: 1},
]), 11);
});
it('should work for varying weights', () => {
assert.equal(ReportScoring.arithmeticMean([
{score: 10, weight: 2},
{score: 0, weight: 7},
{score: 20, weight: 1},
]), 4);
});
it('should work for missing values', () => {
assert.equal(ReportScoring.arithmeticMean([
{weight: 1},
{score: 30, weight: 1},
{weight: 1},
{score: 100},
]), 10);
});
});
describe('#scoreAllCategories', () => {
it('should return a score', () => {
const result = ReportScoring.scoreAllCategories({
categories: {
'categoryA': {weight: 1, audits: [{id: 'auditA', weight: 1}]},
'categoryB': {weight: 4, audits: [{id: 'auditB', weight: 1}]},
'categoryC': {audits: []},
},
}, {auditA: {score: 50}, auditB: {score: 100}});
assert.equal(result.score, 90);
});
it('should return categories', () => {
const result = ReportScoring.scoreAllCategories({
categories: {
'my-category': {name: 'My Category', audits: []},
'my-other-category': {description: 'It is a nice category', audits: []},
},
}, {});
assert.equal(result.categories.length, 2);
assert.equal(result.categories[0].name, 'My Category');
assert.equal(result.categories[1].description, 'It is a nice category');
});
it('should score the categories', () => {
const auditResults = {
'my-audit': {rawValue: 'you passed'},
'my-boolean-audit': {score: true, extendedInfo: {}},
'my-scored-audit': {score: 100},
'my-failed-audit': {score: 20},
'my-boolean-failed-audit': {score: false},
};
const result = ReportScoring.scoreAllCategories({
categories: {
'my-category': {audits: [{id: 'my-audit'}]},
'my-scored': {
audits: [
{id: 'my-boolean-audit', weight: 1},
{id: 'my-scored-audit', weight: 1},
{id: 'my-failed-audit', weight: 1},
{id: 'my-boolean-failed-audit', weight: 1},
],
},
},
}, auditResults);
assert.equal(result.categories.length, 2);
assert.equal(result.categories[0].score, 0);
assert.equal(result.categories[1].score, 55);
});
});
});