tests: smoke request count assertion (#12325)
This commit is contained in:
Родитель
bfa5351639
Коммит
35aec5986f
|
@ -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',
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Загрузка…
Ссылка в новой задаче