|  | // Copyright 2025 The Chromium Authors | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | /** | 
|  | * @file This script provides automatic bisecting between | 
|  | * test file in cases where file A makes B fail due to improper | 
|  | * clean up. | 
|  | */ | 
|  | import * as child_process from 'child_process'; | 
|  | import * as fs from 'fs'; | 
|  | import * as path from 'path'; | 
|  | import yargs from 'yargs'; | 
|  | import {hideBin} from 'yargs/helpers'; | 
|  |  | 
|  | const options = | 
|  | yargs(hideBin(process.argv)) | 
|  | .option('verbose', { | 
|  | type: 'boolean', | 
|  | default: false, | 
|  | alias: 'v', | 
|  | }) | 
|  | .option('dumpOutput', { | 
|  | type: 'boolean', | 
|  | default: false, | 
|  | description: 'Dumps the test command output to std', | 
|  | }) | 
|  | .option('test', { | 
|  | type: 'string', | 
|  | demandOption: true, | 
|  | desc: 'The failing test that we want to find the culprit for.', | 
|  | alias: 't', | 
|  | }) | 
|  | .option('folder', { | 
|  | type: 'string', | 
|  | description: 'Folder to start the bisect from. Default to `front_end/` for unit test and `test/` for e2e', | 
|  | alias: 'f', | 
|  | }) | 
|  | .option('fileExtension', { | 
|  | type: 'string', | 
|  | description: 'The extension for testing. Defaults to `.test.ts` for front_end and `_test.ts` for E2E.', | 
|  | alias: 'e' | 
|  | }) | 
|  | .check(args => { | 
|  | if (!args.folder) { | 
|  | if (args.test.includes('front_end')) { | 
|  | args.folder = 'front_end/'; | 
|  | } else { | 
|  | args.folder = 'test/'; | 
|  | } | 
|  | } | 
|  | if (args.folder.includes('front_end')) { | 
|  | args.fileExtension = '.test.ts'; | 
|  | } else { | 
|  | args.fileExtension = '_test.ts'; | 
|  | } | 
|  |  | 
|  | return true; | 
|  | }) | 
|  | .parseSync(); | 
|  |  | 
|  | function debugLog(...args: any[]) { | 
|  | if (options.verbose) { | 
|  | console.log(...args); | 
|  | } | 
|  | } | 
|  |  | 
|  | function findAndSortTests(dir: string): string[] { | 
|  | const files: string[] = []; | 
|  | const entries = fs.readdirSync(dir, { | 
|  | withFileTypes: true, | 
|  | }); | 
|  |  | 
|  | for (const entry of entries) { | 
|  | const fullPath = path.join(dir, entry.name); | 
|  | if (entry.isDirectory()) { | 
|  | files.push(...findAndSortTests(fullPath)); | 
|  | } else if (entry.isFile() && entry.name.endsWith(options.fileExtension ?? '<unknown>')) { | 
|  | files.push(fullPath); | 
|  | } | 
|  | } | 
|  |  | 
|  | // Sort files alphabetically | 
|  | return files.sort(); | 
|  | } | 
|  |  | 
|  | function runTestCommand(files: string[]): boolean { | 
|  | const command = `npm run test -- ${files.join(' ')}`; | 
|  | if (options.dumpOutput) { | 
|  | debugLog('Executing command:\n', command); | 
|  | } | 
|  | try { | 
|  | child_process.execSync(command, { | 
|  | stdio: options.dumpOutput ? 'inherit' : 'ignore', | 
|  | }); | 
|  | return true; | 
|  | } catch { | 
|  | return false; | 
|  | } | 
|  | } | 
|  |  | 
|  | const testFiles = options.folder ? findAndSortTests(options.folder) : []; | 
|  | if (testFiles.length <= 1) { | 
|  | throw new Error('No test files found to bisect against.'); | 
|  | } else if (testFiles.length === 2) { | 
|  | throw new Error( | 
|  | `Only two files found. Error should be in:\n${testFiles.filter(t => !t.endsWith(options.test)).join('\n')}`); | 
|  | } | 
|  |  | 
|  | // Ensure we skip all the test after the failing test | 
|  | const failingTestIndex = testFiles.findIndex(p => p.endsWith(options.test)); | 
|  | if (failingTestIndex === -1) { | 
|  | throw new Error(`Failing test not found in the current selected folder.`); | 
|  | } | 
|  |  | 
|  | let low = 0; | 
|  | let high = failingTestIndex - 1; | 
|  | let culpritIndex = -1; | 
|  |  | 
|  | while (low < high) { | 
|  | const mid = Math.floor((low + high) / 2); | 
|  |  | 
|  | const testToRun = [ | 
|  | // Slice is excluding the element in end so we need to add +1 | 
|  | ...testFiles.slice(low, mid + 1), | 
|  | // We need to explicitly add the failing test to ensure it runs. | 
|  | testFiles[failingTestIndex], | 
|  | ]; | 
|  |  | 
|  | console.log(''); | 
|  | console.log(`Testing from ${testFiles[low]} to ${testFiles[mid]} (average step remaining ${ | 
|  | Math.ceil(Math.log2(high - low + 1))})`); | 
|  |  | 
|  | const success = runTestCommand(testToRun); | 
|  |  | 
|  | if (success) { | 
|  | debugLog(`Culprit is *after* ${testFiles[mid]}.`); | 
|  | low = mid + 1; | 
|  | } else { | 
|  | debugLog(`Culprit is at or *before* ${testFiles[mid]}.`); | 
|  | culpritIndex = mid; | 
|  | high = mid; | 
|  | } | 
|  | } | 
|  |  | 
|  | if (culpritIndex === -1) { | 
|  | console.log('No culprit found for bisection'); | 
|  | process.exit(); | 
|  | } | 
|  |  | 
|  | console.log(); | 
|  | console.log('--- Suspected culprit found ---'); | 
|  | console.log(testFiles[culpritIndex]); |