summaryrefslogtreecommitdiffstats
path: root/remote/test/puppeteer/utils/bisect.js
diff options
context:
space:
mode:
Diffstat (limited to 'remote/test/puppeteer/utils/bisect.js')
-rwxr-xr-xremote/test/puppeteer/utils/bisect.js314
1 files changed, 314 insertions, 0 deletions
diff --git a/remote/test/puppeteer/utils/bisect.js b/remote/test/puppeteer/utils/bisect.js
new file mode 100755
index 0000000000..bbec3d2431
--- /dev/null
+++ b/remote/test/puppeteer/utils/bisect.js
@@ -0,0 +1,314 @@
+#!/usr/bin/env node
+/**
+ * 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.
+ */
+
+const URL = require('url');
+const debug = require('debug');
+const pptr = require('..');
+const browserFetcher = pptr.createBrowserFetcher();
+const path = require('path');
+const fs = require('fs');
+const {fork, spawn, execSync} = require('child_process');
+
+const COLOR_RESET = '\x1b[0m';
+const COLOR_RED = '\x1b[31m';
+const COLOR_GREEN = '\x1b[32m';
+const COLOR_YELLOW = '\x1b[33m';
+
+const argv = require('minimist')(process.argv.slice(2), {});
+
+const help = `
+Usage:
+ node bisect.js --good <revision> --bad <revision> <script>
+
+Parameters:
+ --good revision that is known to be GOOD, defaults to the chromium revision in src/revision.ts in the main branch
+ --bad revision that is known to be BAD, defaults to the chromium revision in src/revision.ts
+ --no-cache do not keep downloaded Chromium revisions
+ --unit-test pattern that identifies a unit tests that should be checked
+ --script path to a script that returns non-zero code for BAD revisions and 0 for good
+
+Example:
+ node utils/bisect.js --unit-test test
+ node utils/bisect.js --good 577361 --bad 599821 --script simple.js
+ node utils/bisect.js --good 577361 --bad 599821 --unit-test test
+`;
+
+if (argv.h || argv.help) {
+ console.log(help);
+ process.exit(0);
+}
+
+if (typeof argv.good !== 'number') {
+ argv.good = getChromiumRevision('main');
+ if (typeof argv.good !== 'number') {
+ console.log(
+ COLOR_RED +
+ 'ERROR: Could not parse current Chromium revision' +
+ COLOR_RESET
+ );
+ console.log(help);
+ process.exit(1);
+ }
+}
+
+if (typeof argv.bad !== 'number') {
+ argv.bad = getChromiumRevision();
+ if (typeof argv.bad !== 'number') {
+ console.log(
+ COLOR_RED +
+ 'ERROR: Could not parse Chromium revision in the main branch' +
+ COLOR_RESET
+ );
+ console.log(help);
+ process.exit(1);
+ }
+}
+
+if (!argv.script && !argv['unit-test']) {
+ console.log(
+ COLOR_RED +
+ 'ERROR: Expected to be given a script or a unit test to run' +
+ COLOR_RESET
+ );
+ console.log(help);
+ process.exit(1);
+}
+
+const scriptPath = argv.script ? path.resolve(argv.script) : null;
+
+if (argv.script && !fs.existsSync(scriptPath)) {
+ console.log(
+ COLOR_RED +
+ 'ERROR: Expected to be given a path to a script to run' +
+ COLOR_RESET
+ );
+ console.log(help);
+ process.exit(1);
+}
+
+(async (scriptPath, good, bad, pattern, noCache) => {
+ const span = Math.abs(good - bad);
+ console.log(
+ `Bisecting ${COLOR_YELLOW}${span}${COLOR_RESET} revisions in ${COLOR_YELLOW}~${
+ span.toString(2).length
+ }${COLOR_RESET} iterations`
+ );
+
+ while (true) {
+ const middle = Math.round((good + bad) / 2);
+ const revision = await findDownloadableRevision(middle, good, bad);
+ if (!revision || revision === good || revision === bad) {
+ break;
+ }
+ let info = browserFetcher.revisionInfo(revision);
+ const shouldRemove = noCache && !info.local;
+ info = await downloadRevision(revision);
+ const exitCode = await (pattern
+ ? runUnitTest(pattern, info)
+ : runScript(scriptPath, info));
+ if (shouldRemove) {
+ await browserFetcher.remove(revision);
+ }
+ let outcome;
+ if (exitCode) {
+ bad = revision;
+ outcome = COLOR_RED + 'BAD' + COLOR_RESET;
+ } else {
+ good = revision;
+ outcome = COLOR_GREEN + 'GOOD' + COLOR_RESET;
+ }
+ const span = Math.abs(good - bad);
+ let fromText = '';
+ let toText = '';
+ if (good < bad) {
+ fromText = COLOR_GREEN + good + COLOR_RESET;
+ toText = COLOR_RED + bad + COLOR_RESET;
+ } else {
+ fromText = COLOR_RED + bad + COLOR_RESET;
+ toText = COLOR_GREEN + good + COLOR_RESET;
+ }
+ console.log(
+ `- ${COLOR_YELLOW}r${revision}${COLOR_RESET} was ${outcome}. Bisecting [${fromText}, ${toText}] - ${COLOR_YELLOW}${span}${COLOR_RESET} revisions and ${COLOR_YELLOW}~${
+ span.toString(2).length
+ }${COLOR_RESET} iterations`
+ );
+ }
+
+ const [fromSha, toSha] = await Promise.all([
+ revisionToSha(Math.min(good, bad)),
+ revisionToSha(Math.max(good, bad)),
+ ]);
+ console.log(
+ `RANGE: https://chromium.googlesource.com/chromium/src/+log/${fromSha}..${toSha}`
+ );
+})(scriptPath, argv.good, argv.bad, argv['unit-test'], argv['no-cache']);
+
+function runScript(scriptPath, revisionInfo) {
+ const log = debug('bisect:runscript');
+ log('Running script');
+ const child = fork(scriptPath, [], {
+ stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
+ env: {
+ ...process.env,
+ PUPPETEER_EXECUTABLE_PATH: revisionInfo.executablePath,
+ },
+ });
+ return new Promise((resolve, reject) => {
+ child.on('error', err => {
+ return reject(err);
+ });
+ child.on('exit', code => {
+ return resolve(code);
+ });
+ });
+}
+
+function runUnitTest(pattern, revisionInfo) {
+ const log = debug('bisect:rununittest');
+ log('Running unit test');
+ const child = spawn('npm run test:chrome:headless', ['--', '-g', pattern], {
+ stdio: ['inherit', 'inherit', 'inherit', 'ipc'],
+ shell: true,
+ env: {
+ ...process.env,
+ PUPPETEER_EXECUTABLE_PATH: revisionInfo.executablePath,
+ },
+ });
+ return new Promise((resolve, reject) => {
+ child.on('error', err => {
+ return reject(err);
+ });
+ child.on('exit', code => {
+ return resolve(code);
+ });
+ });
+}
+
+async function downloadRevision(revision) {
+ const log = debug('bisect:download');
+ log(`Downloading ${revision}`);
+ let progressBar = null;
+ let lastDownloadedBytes = 0;
+ return await browserFetcher.download(
+ revision,
+ (downloadedBytes, totalBytes) => {
+ if (!progressBar) {
+ const ProgressBar = require('progress');
+ progressBar = new ProgressBar(
+ `- downloading Chromium r${revision} - ${toMegabytes(
+ totalBytes
+ )} [:bar] :percent :etas `,
+ {
+ complete: '=',
+ incomplete: ' ',
+ width: 20,
+ total: totalBytes,
+ }
+ );
+ }
+ const delta = downloadedBytes - lastDownloadedBytes;
+ lastDownloadedBytes = downloadedBytes;
+ progressBar.tick(delta);
+ }
+ );
+ function toMegabytes(bytes) {
+ const mb = bytes / 1024 / 1024;
+ return `${Math.round(mb * 10) / 10} Mb`;
+ }
+}
+
+async function findDownloadableRevision(rev, from, to) {
+ const log = debug('bisect:findrev');
+ const min = Math.min(from, to);
+ const max = Math.max(from, to);
+ log(`Looking around ${rev} from [${min}, ${max}]`);
+ if (await browserFetcher.canDownload(rev)) {
+ return rev;
+ }
+ let down = rev;
+ let up = rev;
+ while (min <= down || up <= max) {
+ const [downOk, upOk] = await Promise.all([
+ down > min ? probe(--down) : Promise.resolve(false),
+ up < max ? probe(++up) : Promise.resolve(false),
+ ]);
+ if (downOk) {
+ return down;
+ }
+ if (upOk) {
+ return up;
+ }
+ }
+ return null;
+
+ async function probe(rev) {
+ const result = await browserFetcher.canDownload(rev);
+ log(` ${rev} - ${result ? 'OK' : 'missing'}`);
+ return result;
+ }
+}
+
+async function revisionToSha(revision) {
+ const json = await fetchJSON(
+ 'https://cr-rev.appspot.com/_ah/api/crrev/v1/redirect/' + revision
+ );
+ return json.git_sha;
+}
+
+function fetchJSON(url) {
+ return new Promise((resolve, reject) => {
+ const agent = url.startsWith('https://')
+ ? require('https')
+ : require('http');
+ const options = URL.parse(url);
+ options.method = 'GET';
+ options.headers = {
+ 'Content-Type': 'application/json',
+ };
+ const req = agent.request(options, function (res) {
+ let result = '';
+ res.setEncoding('utf8');
+ res.on('data', chunk => {
+ return (result += chunk);
+ });
+ res.on('end', () => {
+ return resolve(JSON.parse(result));
+ });
+ });
+ req.on('error', err => {
+ return reject(err);
+ });
+ req.end();
+ });
+}
+
+function getChromiumRevision(gitRevision = null) {
+ const fileName = 'src/revisions.ts';
+ const command = gitRevision
+ ? `git show ${gitRevision}:${fileName}`
+ : `cat ${fileName}`;
+ const result = execSync(command, {
+ encoding: 'utf8',
+ shell: true,
+ });
+
+ const m = result.match(/chromium: '(\d+)'/);
+ if (!m) {
+ return null;
+ }
+ return parseInt(m[1], 10);
+}