tests: smoke request count assertion (#12325)

This commit is contained in:
Adam Raine 2021-04-15 14:00:17 -04:00 коммит произвёл GitHub
Родитель bfa5351639
Коммит 35aec5986f
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
8 изменённых файлов: 122 добавлений и 24 удалений

26
lighthouse-cli/test/fixtures/static-server.js поставляемый
Просмотреть файл

@ -28,6 +28,8 @@ class Server {
this._server = http.createServer(this._requestHandler.bind(this)); this._server = http.createServer(this._requestHandler.bind(this));
/** @type {(data: string) => string=} */ /** @type {(data: string) => string=} */
this._dataTransformer = undefined; this._dataTransformer = undefined;
/** @type {string[]} */
this._requestUrls = [];
} }
getPort() { getPort() {
@ -62,8 +64,32 @@ class Server {
this._dataTransformer = fn; this._dataTransformer = fn;
} }
/**
* @return {string[]}
*/
takeRequestUrls() {
const requestUrls = this._requestUrls;
this._requestUrls = [];
return requestUrls;
}
/**
* @param {http.IncomingMessage} request
*/
_updateRequestUrls(request) {
// Favicon is not fetched in headless mode and robots is not fetched by every test.
// Ignoring these makes the assertion much simpler.
if (['/favicon.ico', '/robots.txt'].includes(request.url)) return;
this._requestUrls.push(request.url);
}
/**
* @param {http.IncomingMessage} request
* @param {http.ServerResponse} response
*/
_requestHandler(request, response) { _requestHandler(request, response) {
const requestUrl = parseURL(request.url); const requestUrl = parseURL(request.url);
this._updateRequestUrls(request);
const filePath = requestUrl.pathname; const filePath = requestUrl.pathname;
const queryString = requestUrl.search && parseQueryString(requestUrl.search.slice(1)); const queryString = requestUrl.search && parseQueryString(requestUrl.search.slice(1));
let absoluteFilePath = path.join(this.baseDir, filePath); let absoluteFilePath = path.join(this.baseDir, filePath);

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

@ -132,7 +132,11 @@ async function begin() {
const invertMatch = argv.invertMatch; const invertMatch = argv.invertMatch;
const testDefns = getDefinitionsToRun(allTestDefns, requestedTestIds, {invertMatch}); const testDefns = getDefinitionsToRun(allTestDefns, requestedTestIds, {invertMatch});
const options = {jobs, retries, isDebug: argv.debug, lighthouseRunner}; const takeNetworkRequestUrls = () => {
return server.takeRequestUrls();
};
const options = {jobs, retries, isDebug: argv.debug, lighthouseRunner, takeNetworkRequestUrls};
let isPassing; let isPassing;
try { try {

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

@ -157,13 +157,13 @@ function makeComparison(name, actualResult, expectedResult) {
* @param {LocalConsole} localConsole * @param {LocalConsole} localConsole
* @param {LH.Result} lhr * @param {LH.Result} lhr
* @param {Smokehouse.ExpectedRunnerResult} expected * @param {Smokehouse.ExpectedRunnerResult} expected
* @param {boolean|undefined} isSync
*/ */
function pruneExpectations(localConsole, lhr, expected) { function pruneExpectations(localConsole, lhr, expected, isSync) {
const userAgent = lhr.environment.hostUserAgent; const userAgent = lhr.environment.hostUserAgent;
const userAgentMatch = /Chrome\/(\d+)/.exec(userAgent); // Chrome/85.0.4174.0 const userAgentMatch = /Chrome\/(\d+)/.exec(userAgent); // Chrome/85.0.4174.0
if (!userAgentMatch) throw new Error('Could not get chrome version.'); if (!userAgentMatch) throw new Error('Could not get chrome version.');
const actualChromeVersion = Number(userAgentMatch[1]); const actualChromeVersion = Number(userAgentMatch[1]);
/** /**
* @param {*} obj * @param {*} obj
*/ */
@ -199,6 +199,19 @@ function pruneExpectations(localConsole, lhr, expected) {
} }
const cloned = cloneDeep(expected); const cloned = cloneDeep(expected);
// Tests must be run synchronously so we can clear the request list between tests.
// We do not have a good way to map requests to test definitions if the tests are run in parallel.
if (!isSync && expected.networkRequests) {
const msg = 'Network requests should only be asserted on tests run synchronously';
if (process.env.CI) {
throw new Error(msg);
} else {
localConsole.log(`${msg}, pruning expectation: ${JSON.stringify(expected.networkRequests)}`);
delete cloned.networkRequests;
}
}
pruneNewerChromeExpectations(cloned); pruneNewerChromeExpectations(cloned);
return cloned; return cloned;
} }
@ -206,7 +219,7 @@ function pruneExpectations(localConsole, lhr, expected) {
/** /**
* Collate results into comparisons of actual and expected scores on each audit/artifact. * Collate results into comparisons of actual and expected scores on each audit/artifact.
* @param {LocalConsole} localConsole * @param {LocalConsole} localConsole
* @param {{lhr: LH.Result, artifacts: LH.Artifacts}} actual * @param {{lhr: LH.Result, artifacts: LH.Artifacts, networkRequests: string[]}} actual
* @param {Smokehouse.ExpectedRunnerResult} expected * @param {Smokehouse.ExpectedRunnerResult} expected
* @return {Comparison[]} * @return {Comparison[]}
*/ */
@ -254,6 +267,16 @@ function collateResults(localConsole, actual, expected) {
return makeComparison(auditName + ' audit', actualResult, expectedResult); return makeComparison(auditName + ' audit', actualResult, expectedResult);
}); });
/** @type {Comparison[]} */
const requestCountAssertion = [];
if (expected.networkRequests) {
requestCountAssertion.push(makeComparison(
'Requests',
actual.networkRequests,
expected.networkRequests
));
}
return [ return [
{ {
name: 'final url', name: 'final url',
@ -263,6 +286,7 @@ function collateResults(localConsole, actual, expected) {
}, },
runtimeErrorAssertion, runtimeErrorAssertion,
runWarningsAssertion, runWarningsAssertion,
...requestCountAssertion,
...artifactAssertions, ...artifactAssertions,
...auditAssertions, ...auditAssertions,
]; ];
@ -334,15 +358,15 @@ function assertLogString(count) {
/** /**
* Log all the comparisons between actual and expected test results, then print * Log all the comparisons between actual and expected test results, then print
* summary. Returns count of passed and failed tests. * summary. Returns count of passed and failed tests.
* @param {{lhr: LH.Result, artifacts: LH.Artifacts}} actual * @param {{lhr: LH.Result, artifacts: LH.Artifacts, networkRequests: string[]}} actual
* @param {Smokehouse.ExpectedRunnerResult} expected * @param {Smokehouse.ExpectedRunnerResult} expected
* @param {{isDebug?: boolean}=} reportOptions * @param {{isDebug?: boolean, isSync?: boolean}=} reportOptions
* @return {{passed: number, failed: number, log: string}} * @return {{passed: number, failed: number, log: string}}
*/ */
function report(actual, expected, reportOptions = {}) { function report(actual, expected, reportOptions = {}) {
const localConsole = new LocalConsole(); const localConsole = new LocalConsole();
expected = pruneExpectations(localConsole, actual.lhr, expected); expected = pruneExpectations(localConsole, actual.lhr, expected, reportOptions.isSync);
const comparisons = collateResults(localConsole, actual, expected); const comparisons = collateResults(localConsole, actual, expected);
let correctCount = 0; let correctCount = 0;

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

@ -35,15 +35,16 @@ const DEFAULT_RETRIES = 0;
/** /**
* Runs the selected smoke tests. Returns whether all assertions pass. * Runs the selected smoke tests. Returns whether all assertions pass.
* @param {Array<Smokehouse.TestDfn>} smokeTestDefns * @param {Array<Smokehouse.TestDfn>} smokeTestDefns
* @param {Smokehouse.SmokehouseOptions=} smokehouseOptions * @param {Smokehouse.SmokehouseOptions} smokehouseOptions
* @return {Promise<{success: boolean, testResults: SmokehouseResult[]}>} * @return {Promise<{success: boolean, testResults: SmokehouseResult[]}>}
*/ */
async function runSmokehouse(smokeTestDefns, smokehouseOptions = {}) { async function runSmokehouse(smokeTestDefns, smokehouseOptions) {
const { const {
isDebug, isDebug,
jobs = DEFAULT_CONCURRENT_RUNS, jobs = DEFAULT_CONCURRENT_RUNS,
retries = DEFAULT_RETRIES, retries = DEFAULT_RETRIES,
lighthouseRunner = cliLighthouseRunner, lighthouseRunner = cliLighthouseRunner,
takeNetworkRequestUrls,
} = smokehouseOptions; } = smokehouseOptions;
assertPositiveInteger('jobs', jobs); assertPositiveInteger('jobs', jobs);
assertNonNegativeInteger('retries', retries); assertNonNegativeInteger('retries', retries);
@ -54,7 +55,7 @@ async function runSmokehouse(smokeTestDefns, smokehouseOptions = {}) {
for (const testDefn of smokeTestDefns) { for (const testDefn of smokeTestDefns) {
// If defn is set to `runSerially`, we'll run its tests in succession, not parallel. // If defn is set to `runSerially`, we'll run its tests in succession, not parallel.
const concurrency = testDefn.runSerially ? 1 : jobs; const concurrency = testDefn.runSerially ? 1 : jobs;
const options = {concurrency, lighthouseRunner, retries, isDebug}; const options = {concurrency, lighthouseRunner, retries, isDebug, takeNetworkRequestUrls};
const result = runSmokeTestDefn(concurrentMapper, testDefn, options); const result = runSmokeTestDefn(concurrentMapper, testDefn, options);
smokePromises.push(result); smokePromises.push(result);
} }
@ -110,21 +111,25 @@ function assertNonNegativeInteger(loggableName, value) {
* once all are finished. * once all are finished.
* @param {ConcurrentMapper} concurrentMapper * @param {ConcurrentMapper} concurrentMapper
* @param {Smokehouse.TestDfn} smokeTestDefn * @param {Smokehouse.TestDfn} smokeTestDefn
* @param {{concurrency: number, retries: number, lighthouseRunner: Smokehouse.LighthouseRunner, isDebug?: boolean}} defnOptions * @param {{concurrency: number, retries: number, lighthouseRunner: Smokehouse.LighthouseRunner, isDebug?: boolean, takeNetworkRequestUrls: () => string[]}} defnOptions
* @return {Promise<SmokehouseResult>} * @return {Promise<SmokehouseResult>}
*/ */
async function runSmokeTestDefn(concurrentMapper, smokeTestDefn, defnOptions) { async function runSmokeTestDefn(concurrentMapper, smokeTestDefn, defnOptions) {
const {id, config: configJson, expectations} = smokeTestDefn; const {id, config: configJson, expectations} = smokeTestDefn;
const {concurrency, lighthouseRunner, retries, isDebug} = defnOptions; const {concurrency, lighthouseRunner, retries, isDebug, takeNetworkRequestUrls} = defnOptions;
const individualTests = expectations.map(expectation => ({ const individualTests = expectations.map(expectation => {
requestedUrl: expectation.lhr.requestedUrl, return {
configJson, requestedUrl: expectation.lhr.requestedUrl,
expectation, configJson,
lighthouseRunner, expectation,
retries, lighthouseRunner,
isDebug, retries,
})); isDebug,
isSync: concurrency === 1,
takeNetworkRequestUrls,
};
});
// Loop sequentially over expectations, comparing against Lighthouse run, and // Loop sequentially over expectations, comparing against Lighthouse run, and
// reporting result. // reporting result.
@ -171,14 +176,23 @@ function purpleify(str) {
/** /**
* Run Lighthouse in the selected runner. Returns `log`` for logging once * Run Lighthouse in the selected runner. Returns `log`` for logging once
* all tests in a defn are complete. * all tests in a defn are complete.
* @param {{requestedUrl: string, configJson?: LH.Config.Json, expectation: Smokehouse.ExpectedRunnerResult, lighthouseRunner: Smokehouse.LighthouseRunner, retries: number, isDebug?: boolean}} testOptions * @param {{requestedUrl: string, configJson?: LH.Config.Json, expectation: Smokehouse.ExpectedRunnerResult, lighthouseRunner: Smokehouse.LighthouseRunner, retries: number, isDebug?: boolean, isSync?: boolean, takeNetworkRequestUrls: () => string[]}} testOptions
* @return {Promise<{passed: number, failed: number, log: string}>} * @return {Promise<{passed: number, failed: number, log: string}>}
*/ */
async function runSmokeTest(testOptions) { async function runSmokeTest(testOptions) {
// Use a buffered LocalConsole to keep logged output so it's not interleaved // Use a buffered LocalConsole to keep logged output so it's not interleaved
// with other currently running tests. // with other currently running tests.
const localConsole = new LocalConsole(); const localConsole = new LocalConsole();
const {requestedUrl, configJson, expectation, lighthouseRunner, retries, isDebug} = testOptions; const {
requestedUrl,
configJson,
expectation,
lighthouseRunner,
retries,
isDebug,
isSync,
takeNetworkRequestUrls,
} = testOptions;
// Rerun test until there's a passing result or retries are exhausted to prevent flakes. // Rerun test until there's a passing result or retries are exhausted to prevent flakes.
let result; let result;
@ -192,14 +206,17 @@ async function runSmokeTest(testOptions) {
// Run Lighthouse. // Run Lighthouse.
try { try {
result = await lighthouseRunner(requestedUrl, configJson, {isDebug}); result = {
...await lighthouseRunner(requestedUrl, configJson, {isDebug}),
networkRequests: takeNetworkRequestUrls(),
};
} catch (e) { } catch (e) {
logChildProcessError(localConsole, e); logChildProcessError(localConsole, e);
continue; // Retry, if possible. continue; // Retry, if possible.
} }
// Assert result. // Assert result.
report = getAssertionReport(result, expectation, {isDebug}); report = getAssertionReport(result, expectation, {isDebug, isSync});
if (report.failed) { if (report.failed) {
localConsole.log(`${report.failed} assertion(s) failed.`); localConsole.log(`${report.failed} assertion(s) failed.`);
continue; // Retry, if possible. continue; // Retry, if possible.

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

@ -35,6 +35,7 @@ const smokeTests = [{
id: 'dbw', id: 'dbw',
expectations: require('./dobetterweb/dbw-expectations.js'), expectations: require('./dobetterweb/dbw-expectations.js'),
config: require('./dobetterweb/dbw-config.js'), config: require('./dobetterweb/dbw-config.js'),
runSerially: true, // Need access to network request assertions.
}, { }, {
id: 'redirects', id: 'redirects',
expectations: require('./redirects/expectations.js'), expectations: require('./redirects/expectations.js'),

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

@ -11,6 +11,12 @@
*/ */
const expectations = [ const expectations = [
{ {
networkRequests: {
// 50 requests made for normal page testing.
// 6 extra requests made because stylesheets are evicted from the cache by the time DT opens.
// 3 extra requests made to /dobetterweb/clock.appcache
length: 59,
},
artifacts: { artifacts: {
HostFormFactor: 'desktop', HostFormFactor: 'desktop',
Stacks: [{ Stacks: [{

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

@ -11,6 +11,11 @@
*/ */
module.exports = [ module.exports = [
{ {
networkRequests: {
// 8 requests made for normal page testing.
// 1 extra request made because stylesheets are evicted from the cache by the time DT opens.
length: 9,
},
lhr: { lhr: {
requestedUrl: 'http://localhost:10200/preload.html', requestedUrl: 'http://localhost:10200/preload.html',
finalUrl: 'http://localhost:10200/preload.html', finalUrl: 'http://localhost:10200/preload.html',
@ -62,6 +67,9 @@ module.exports = [
}, },
}, },
{ {
networkRequests: {
length: 8,
},
lhr: { lhr: {
requestedUrl: 'http://localhost:10200/perf/perf-budgets/load-things.html', requestedUrl: 'http://localhost:10200/perf/perf-budgets/load-things.html',
finalUrl: 'http://localhost:10200/perf/perf-budgets/load-things.html', finalUrl: 'http://localhost:10200/perf/perf-budgets/load-things.html',
@ -140,6 +148,9 @@ module.exports = [
}, },
}, },
{ {
networkRequests: {
length: 5,
},
lhr: { lhr: {
requestedUrl: 'http://localhost:10200/perf/fonts.html', requestedUrl: 'http://localhost:10200/perf/fonts.html',
finalUrl: 'http://localhost:10200/perf/fonts.html', finalUrl: 'http://localhost:10200/perf/fonts.html',
@ -168,6 +179,9 @@ module.exports = [
}, },
}, },
{ {
networkRequests: {
length: 3,
},
artifacts: { artifacts: {
TraceElements: [ TraceElements: [
{ {
@ -291,6 +305,9 @@ module.exports = [
}, },
}, },
{ {
networkRequests: {
length: 2,
},
lhr: { lhr: {
requestedUrl: 'http://localhost:10200/perf/frame-metrics.html', requestedUrl: 'http://localhost:10200/perf/frame-metrics.html',
finalUrl: 'http://localhost:10200/perf/frame-metrics.html', finalUrl: 'http://localhost:10200/perf/frame-metrics.html',

3
types/smokehouse.d.ts поставляемый
Просмотреть файл

@ -19,6 +19,7 @@
export type ExpectedRunnerResult = { export type ExpectedRunnerResult = {
lhr: ExpectedLHR, lhr: ExpectedLHR,
artifacts?: Partial<Record<keyof LH.Artifacts, any>> artifacts?: Partial<Record<keyof LH.Artifacts, any>>
networkRequests?: {length: number};
} }
export interface TestDfn { export interface TestDfn {
@ -41,6 +42,8 @@
retries?: number; retries?: number;
/** A function that runs Lighthouse with the given options. Defaults to running Lighthouse via the CLI. */ /** A function that runs Lighthouse with the given options. Defaults to running Lighthouse via the CLI. */
lighthouseRunner?: LighthouseRunner; lighthouseRunner?: LighthouseRunner;
/** A function that gets a list of URLs requested to the server since the last fetch. */
takeNetworkRequestUrls: () => string[];
} }
export interface SmokehouseLibOptions extends SmokehouseOptions { export interface SmokehouseLibOptions extends SmokehouseOptions {