Skip to content

Commit bab838b

Browse files
committed
Adds support for custom audits and gatherers
1 parent 3f3e5c2 commit bab838b

File tree

16 files changed

+615
-24
lines changed

16 files changed

+615
-24
lines changed

custom/audit.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
*
3+
* Copyright 2016 Google Inc. All rights reserved.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
// Pass-through file for developer convenience.
19+
module.exports = require('../lighthouse-core/audits/audit');

custom/gatherer.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
*
3+
* Copyright 2016 Google Inc. All rights reserved.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
// Pass-through file for developer convenience.
19+
module.exports = require('../lighthouse-core/gather/gatherers/gatherer');

lighthouse-core/audits/audit.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,13 @@ class Audit {
3333
throw new Error('Audit meta information must be overridden.');
3434
}
3535

36+
/**
37+
* @throws
38+
*/
39+
static audit() {
40+
throw new Error('Audit audit() function must be overridden.');
41+
}
42+
3643
/**
3744
* @param {!AuditResultInput} result
3845
* @return {!AuditResult}

lighthouse-core/config/index.js

Lines changed: 112 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const SpeedlineGatherer = require('../gather/gatherers/speedline');
2323

2424
const GatherRunner = require('../gather/gather-runner');
2525
const log = require('../lib/log');
26+
const path = require('path');
2627

2728
// cleanTrace is run to remove duplicate TracingStartedInPage events,
2829
// and to change TracingStartedInBrowser events into TracingStartedInPage.
@@ -107,7 +108,7 @@ function cleanTrace(trace) {
107108
return trace;
108109
}
109110

110-
function filterPasses(passes, audits) {
111+
function filterPasses(passes, audits, paths) {
111112
const requiredGatherers = getGatherersNeededByAudits(audits);
112113

113114
// Make sure we only have the gatherers that are needed by the audits
@@ -117,7 +118,7 @@ function filterPasses(passes, audits) {
117118

118119
freshPass.gatherers = freshPass.gatherers.filter(gatherer => {
119120
try {
120-
const GathererClass = GatherRunner.getGathererClass(gatherer);
121+
const GathererClass = GatherRunner.getGathererClass(gatherer, paths);
121122
return requiredGatherers.has(GathererClass.name);
122123
} catch (requireError) {
123124
throw new Error(`Unable to locate gatherer: ${gatherer}`);
@@ -171,16 +172,79 @@ function filterAudits(audits, auditWhitelist) {
171172
return filteredAudits;
172173
}
173174

174-
function expandAudits(audits) {
175+
function expandAudits(audits, paths) {
176+
const rootPath = path.join(__dirname, '../../');
177+
175178
return audits.map(audit => {
176-
try {
177-
return require(`../audits/${audit}`);
178-
} catch (requireError) {
179+
// Check each path to see if the audit can be located. First match wins.
180+
const AuditClass = paths.reduce((definition, auditPath) => {
181+
// If the definition has already been found, just propagate it. Otherwise try a search
182+
// on the path in this iteration of the loop.
183+
if (definition !== null) {
184+
return definition;
185+
}
186+
187+
const requirePath = auditPath.startsWith('/') ? auditPath : path.join(rootPath, auditPath);
188+
try {
189+
return require(`${requirePath}/${audit}`);
190+
} catch (requireError) {
191+
return null;
192+
}
193+
}, null);
194+
195+
if (!AuditClass) {
179196
throw new Error(`Unable to locate audit: ${audit}`);
180197
}
198+
199+
// Confirm that the audit appears valid.
200+
const auditValidation = validateAudit(AuditClass);
201+
if (!auditValidation.valid) {
202+
const errors = Object.keys(auditValidation)
203+
.reduce((errorList, item) => {
204+
// Ignore the valid property as it's generated from the other items in the object.
205+
if (item === 'valid') {
206+
return errorList;
207+
}
208+
209+
return errorList + (auditValidation[item] ? '' : `\n - ${item} is missing`);
210+
}, '');
211+
212+
throw new Error(`Invalid audit class: ${errors}`);
213+
}
214+
215+
return AuditClass;
181216
});
182217
}
183218

219+
function validateAudit(auditDefinition) {
220+
const hasAuditMethod = typeof auditDefinition.audit === 'function';
221+
const hasMeta = typeof auditDefinition.meta === 'object';
222+
const hasMetaName = hasMeta && typeof auditDefinition.meta.name !== 'undefined';
223+
const hasMetaCategory = hasMeta && typeof auditDefinition.meta.category !== 'undefined';
224+
const hasMetaDescription = hasMeta && typeof auditDefinition.meta.description !== 'undefined';
225+
const hasMetaRequiredArtifacts = hasMeta && Array.isArray(auditDefinition.meta.requiredArtifacts);
226+
const hasGenerateAuditResult = typeof auditDefinition.generateAuditResult === 'function';
227+
228+
return {
229+
'valid': (
230+
hasAuditMethod &&
231+
hasMeta &&
232+
hasMetaName &&
233+
hasMetaCategory &&
234+
hasMetaDescription &&
235+
hasMetaRequiredArtifacts &&
236+
hasGenerateAuditResult
237+
),
238+
'audit()': hasAuditMethod,
239+
'meta property': hasMeta,
240+
'meta.name property': hasMetaName,
241+
'meta.category property': hasMetaCategory,
242+
'meta.description property': hasMetaDescription,
243+
'meta.requiredArtifacts array': hasMetaRequiredArtifacts,
244+
'generateAuditResult()': hasGenerateAuditResult
245+
};
246+
}
247+
184248
function expandArtifacts(artifacts, includeSpeedline) {
185249
const expandedArtifacts = Object.assign({}, artifacts);
186250

@@ -241,19 +305,53 @@ class Config {
241305
configJSON = defaultConfig;
242306
}
243307

244-
this._audits = configJSON.audits ? expandAudits(
245-
filterAudits(configJSON.audits, auditWhitelist)
308+
this._configJSON = this._initRequirePaths(configJSON);
309+
310+
this._audits = this.json.audits ? expandAudits(
311+
filterAudits(this.json.audits, auditWhitelist), this.json.paths.audits
246312
) : null;
247313
// filterPasses expects audits to have been expanded
248-
this._passes = configJSON.passes ? filterPasses(configJSON.passes, this._audits) : null;
249-
this._auditResults = configJSON.auditResults ? Array.from(configJSON.auditResults) : null;
314+
this._passes = this.json.passes ?
315+
filterPasses(this.json.passes, this._audits, this.json.paths.gatherers) :
316+
null;
317+
this._auditResults = this.json.auditResults ? Array.from(this.json.auditResults) : null;
250318
this._artifacts = null;
251-
if (configJSON.artifacts) {
252-
this._artifacts = expandArtifacts(configJSON.artifacts,
319+
if (this.json.artifacts) {
320+
this._artifacts = expandArtifacts(this.json.artifacts,
253321
// If time-to-interactive is present, add the speedline artifact
254-
configJSON.audits && configJSON.audits.find(a => a === 'time-to-interactive'));
322+
this.json.audits && this.json.audits.find(a => a === 'time-to-interactive'));
323+
}
324+
this._aggregations = this.json.aggregations ? Array.from(this.json.aggregations) : null;
325+
}
326+
327+
_initRequirePaths(configJSON) {
328+
if (typeof configJSON.paths !== 'object') {
329+
configJSON.paths = {};
330+
}
331+
332+
if (!Array.isArray(configJSON.paths.audits)) {
333+
configJSON.paths.audits = [];
334+
}
335+
336+
if (!Array.isArray(configJSON.paths.gatherers)) {
337+
configJSON.paths.gatherers = [];
255338
}
256-
this._aggregations = configJSON.aggregations ? Array.from(configJSON.aggregations) : null;
339+
340+
// Make sure the default paths are prepended to the list
341+
if (configJSON.paths.audits.indexOf('lighthouse-core/audits') === -1) {
342+
configJSON.paths.audits.unshift('lighthouse-core/audits');
343+
}
344+
345+
if (configJSON.paths.gatherers.indexOf('lighthouse-core/gather/gatherers') === -1) {
346+
configJSON.paths.gatherers.unshift('lighthouse-core/gather/gatherers');
347+
}
348+
349+
return configJSON;
350+
}
351+
352+
/** @type {!Object} */
353+
get json() {
354+
return this._configJSON;
257355
}
258356

259357
/** @type {Array<!Pass>} */

lighthouse-core/gather/gather-runner.js

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
const log = require('../lib/log.js');
2020
const Audit = require('../audits/audit');
21+
const path = require('path');
2122

2223
/**
2324
* Class that drives browser to load the page and runs gatherer lifecycle hooks.
@@ -189,7 +190,31 @@ class GatherRunner {
189190
const driver = options.driver;
190191
const tracingData = {traces: {}};
191192

192-
passes = GatherRunner.instantiateGatherers(passes);
193+
if (typeof options.url !== 'string' || options.url.length === 0) {
194+
return Promise.reject(new Error('You must provide a url to the driver'));
195+
}
196+
197+
if (typeof options.flags === 'undefined') {
198+
options.flags = {};
199+
}
200+
201+
if (typeof options.config === 'undefined') {
202+
return Promise.reject(new Error('You must provide a config'));
203+
}
204+
205+
const configJSON = options.config.json;
206+
207+
// Default mobile emulation and page loading to true.
208+
// The extension will switch these off initially.
209+
if (typeof options.flags.mobile === 'undefined') {
210+
options.flags.mobile = true;
211+
}
212+
213+
if (typeof options.flags.loadPage === 'undefined') {
214+
options.flags.loadPage = true;
215+
}
216+
217+
passes = this.instantiateGatherers(passes, configJSON.paths.gatherers);
193218

194219
return driver.connect()
195220
.then(_ => GatherRunner.setupDriver(driver, options))
@@ -222,26 +247,55 @@ class GatherRunner {
222247
const artifacts = Object.assign({}, tracingData);
223248
passes.forEach(pass => {
224249
pass.gatherers.forEach(gatherer => {
250+
if (typeof gatherer.artifact === 'undefined') {
251+
throw new Error(`${gatherer.constructor.name} failed to provide an artifact.`);
252+
}
253+
225254
artifacts[gatherer.name] = gatherer.artifact;
226255
});
227256
});
228257
return artifacts;
229258
});
230259
}
231260

232-
static getGathererClass(gatherer) {
233-
return require(`./gatherers/${gatherer}`);
261+
static getGathererClass(gatherer, paths) {
262+
const rootPath = path.join(__dirname, '../../');
263+
264+
// Check each path to see if the gatherer can be located. First match wins.
265+
const gathererDefinition = paths.reduce((definition, gathererPath) => {
266+
// If the definition has already been found, just propagate it. Otherwise try a search
267+
// on the path in this iteration of the loop.
268+
if (definition !== null) {
269+
return definition;
270+
}
271+
272+
const requirePath = gathererPath.startsWith('/') ?
273+
gathererPath :
274+
path.join(rootPath, gathererPath);
275+
276+
try {
277+
return require(`${requirePath}/${gatherer}`);
278+
} catch (requireError) {
279+
return null;
280+
}
281+
}, null);
282+
283+
if (!gathererDefinition) {
284+
throw new Error(`Unable to locate gatherer: ${gatherer}`);
285+
}
286+
287+
return gathererDefinition;
234288
}
235289

236-
static instantiateGatherers(passes) {
290+
static instantiateGatherers(passes, paths) {
237291
return passes.map(pass => {
238292
pass.gatherers = pass.gatherers.map(gatherer => {
239293
// If this is already instantiated, don't do anything else.
240294
if (typeof gatherer !== 'string') {
241295
return gatherer;
242296
}
243297

244-
const GathererClass = GatherRunner.getGathererClass(gatherer);
298+
const GathererClass = GatherRunner.getGathererClass(gatherer, paths);
245299
return new GathererClass();
246300
});
247301

lighthouse-core/test/audits/audit.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ class B extends Audit {
2626
static get meta() {
2727
return {};
2828
}
29+
30+
static audit() {}
2931
}
3032

3133
describe('Audit', () => {
@@ -37,6 +39,14 @@ describe('Audit', () => {
3739
assert.doesNotThrow(_ => B.meta);
3840
});
3941

42+
it('throws if an audit does not override audit()', () => {
43+
assert.throws(_ => A.audit());
44+
});
45+
46+
it('does not throw if an audit overrides audit()', () => {
47+
assert.doesNotThrow(_ => B.audit());
48+
});
49+
4050
it('throws if an audit does generate a result with a value', () => {
4151
assert.throws(_ => A.generateAuditResult({}));
4252
});

lighthouse-core/test/config/index.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,39 @@ describe('Config', () => {
127127
return assert.equal(typeof config.audits[0], 'function');
128128
});
129129

130+
it('tests multiple paths for expanding audits', () => {
131+
const config = new Config({
132+
paths: {
133+
audits: ['/fake-path/']
134+
},
135+
audits: ['user-timings']
136+
});
137+
138+
assert.ok(Array.isArray(config.audits));
139+
assert.equal(config.audits.length, 1);
140+
141+
return assert.throws(_ => new Config({
142+
paths: {
143+
audits: ['/fake-path/']
144+
},
145+
audits: ['non-existent-audit']
146+
}));
147+
});
148+
149+
it('throws when it finds invalid audits', () => {
150+
const paths = {
151+
audits: ['lighthouse-core/test/fixtures/invalid-audits']
152+
};
153+
154+
assert.throws(_ => new Config({paths, audits: ['missing-meta']}));
155+
assert.throws(_ => new Config({paths, audits: ['missing-audit']}));
156+
assert.throws(_ => new Config({paths, audits: ['missing-category']}));
157+
assert.throws(_ => new Config({paths, audits: ['missing-name']}));
158+
assert.throws(_ => new Config({paths, audits: ['missing-description']}));
159+
assert.throws(_ => new Config({paths, audits: ['missing-required-artifact']}));
160+
return assert.throws(_ => new Config({paths, audits: ['missing-generate-audit-result']}));
161+
});
162+
130163
it('expands artifacts', () => {
131164
const config = new Config({
132165
artifacts: {

0 commit comments

Comments
 (0)