| OLD | NEW |
| (Empty) | |
| 1 // Copyright 2017 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 'use strict'; |
| 6 |
| 7 const fs = require('fs'); |
| 8 const path = require('path'); |
| 9 |
| 10 const cheerio = require('cheerio'); |
| 11 const mkdirp = require('mkdirp'); |
| 12 const recast = require('recast'); |
| 13 const types = recast.types; |
| 14 const b = recast.types.builders; |
| 15 const args = require('yargs') |
| 16 .wrap(Math.min(process.stdout.columns, 120)) |
| 17 .example('node $0 existing-test.html') |
| 18 .boolean('dry-run') |
| 19 .help('help') |
| 20 .demand(1) |
| 21 .argv; |
| 22 |
| 23 const utils = require('../utils'); |
| 24 |
| 25 const FRONT_END_PATH = path.resolve(__dirname, '..', '..', 'front_end'); |
| 26 const LINE_BREAK = '$$SECRET_IDENTIFIER_FOR_LINE_BREAK$$();'; |
| 27 |
| 28 function main() { |
| 29 const inputPaths = args._; |
| 30 const identifierMap = generateTestHelperMap(); |
| 31 for (const inputPath of inputPaths) { |
| 32 console.log('Starting to migrate: ', inputPath); |
| 33 migrateTest(inputPath, identifierMap); |
| 34 console.log('Migrated: ', inputPath); |
| 35 } |
| 36 console.log(`Finished migrating ${inputPaths.length} tests`); |
| 37 } |
| 38 |
| 39 main(); |
| 40 |
| 41 function migrateTest(inputPath, identifierMap) { |
| 42 const htmlTestFile = fs.readFileSync(inputPath, 'utf-8'); |
| 43 const $ = cheerio.load(htmlTestFile); |
| 44 const inputCode = $('script:not([src])')[0].children[0].data; |
| 45 const bodyText = $('body').text().trim(); |
| 46 const helperScripts = $('script[src]').toArray().map((n) => n.attribs.src).map
(src => { |
| 47 const components = src.split('/'); |
| 48 return components[components.length - 1].split('.')[0]; |
| 49 }); |
| 50 const testHelpers = mapTestHelpers(helperScripts); |
| 51 const domFixture = $('body') |
| 52 .html() |
| 53 .trim() |
| 54 // Tries to remove it if it has it's own line |
| 55 .replace(bodyText + '\n', '') |
| 56 // Tries to remove it if it's inline |
| 57 .replace(bodyText, ''); |
| 58 const outputCode = |
| 59 transformTestScript(inputCode, bodyText, identifierMap, testHelpers, getPa
nel(inputPath), domFixture); |
| 60 if (args.dryRun) { |
| 61 console.log(outputCode); |
| 62 } else { |
| 63 const outPath = getOutPath(inputPath); |
| 64 mkdirp.sync(path.dirname(outPath)); |
| 65 fs.writeFileSync(outPath, outputCode); |
| 66 } |
| 67 } |
| 68 |
| 69 function transformTestScript(inputCode, bodyText, identifierMap, testHelpers, pa
nel, domFixture) { |
| 70 const ast = recast.parse(inputCode); |
| 71 unwrapTestFunctionExpressionIfNecessary(ast); |
| 72 unwrapTestFunctionDeclarationIfNecessary(ast); |
| 73 |
| 74 /** |
| 75 * Create test header based on extracted data |
| 76 */ |
| 77 const headerLines = []; |
| 78 headerLines.push(createExpressionNode(`TestRunner.addResult('${bodyText}\\n');
`)); |
| 79 headerLines.push(createExpressionNode(LINE_BREAK)); |
| 80 for (const helper of testHelpers) { |
| 81 headerLines.push(createAwaitExpressionNode(`await TestRunner.loadModule('${h
elper}');`)); |
| 82 } |
| 83 headerLines.push(createAwaitExpressionNode(`await TestRunner.loadPanel('${pane
l}');`)); |
| 84 headerLines.push(createAwaitExpressionNode(`await TestRunner.loadHTML(\` |
| 85 ${domFixture.split('\n').map(line => ' ' + line).join('\n')} |
| 86 \`)`)); |
| 87 ast.program.body = headerLines.concat(ast.program.body); |
| 88 |
| 89 /** |
| 90 * Wrap entire body in an async IIFE |
| 91 */ |
| 92 const iife = b.functionExpression(null, [], b.blockStatement(ast.program.body)
); |
| 93 iife.async = true; |
| 94 ast.program.body = [b.expressionStatement(b.callExpression(iife, []))]; |
| 95 |
| 96 /** |
| 97 * Migrate all the call sites from InspectorTest to .*TestRunner |
| 98 */ |
| 99 recast.visit(ast, { |
| 100 visitIdentifier: function(path) { |
| 101 if (path.parentPath && path.parentPath.value && path.parentPath.value.obje
ct && |
| 102 path.parentPath.value.object.name === 'InspectorTest' && path.value.na
me !== 'InspectorTest') { |
| 103 const newParentIdentifier = identifierMap.get(path.value.name); |
| 104 if (!newParentIdentifier) { |
| 105 throw new Error('Could not find identifier for', path.value.name); |
| 106 } |
| 107 path.parentPath.value.object.name = newParentIdentifier; |
| 108 } |
| 109 return false; |
| 110 } |
| 111 }); |
| 112 |
| 113 return print(ast); |
| 114 } |
| 115 |
| 116 /** |
| 117 * Unwrap test if it's a function expression |
| 118 * var test = function () {...} |
| 119 */ |
| 120 function unwrapTestFunctionExpressionIfNecessary(ast) { |
| 121 const index = |
| 122 ast.program.body.findIndex(n => n.type === 'VariableDeclaration' && n.decl
arations[0].id.name === 'test'); |
| 123 if (index > -1) { |
| 124 const testFunctionNode = |
| 125 ast.program.body[index]; |
| 126 ast.program.body.splice(index, 1); |
| 127 ast.program.body = ast.program.body.concat(testFunctionNode.declarations[0].
init.body.body); |
| 128 } |
| 129 } |
| 130 |
| 131 |
| 132 /** |
| 133 * Unwrap test if it's a function declaration |
| 134 * function test () {...} |
| 135 */ |
| 136 function unwrapTestFunctionDeclarationIfNecessary(ast) { |
| 137 const index = |
| 138 ast.program.body.findIndex(n => n.type === 'FunctionDeclaration' && n.id.n
ame === 'test'); |
| 139 if (index > -1) { |
| 140 const testFunctionNode = |
| 141 ast.program.body[index]; |
| 142 ast.program.body.splice(index, 1); |
| 143 ast.program.body = ast.program.body.concat(testFunctionNode.body.body); |
| 144 } |
| 145 } |
| 146 |
| 147 function print(ast) { |
| 148 const copyrightNotice = `// Copyright 2017 The Chromium Authors. All rights re
served. |
| 149 // Use of this source code is governed by a BSD-style license that can be |
| 150 // found in the LICENSE file. |
| 151 |
| 152 `; |
| 153 |
| 154 /** |
| 155 * Not using clang-format because certain tests look bad when formatted by it. |
| 156 * Recast pretty print is smarter about preserving existing spacing. |
| 157 */ |
| 158 const code = recast.prettyPrint(ast, {tabWidth: 2, wrapColumn: 120, quote: 'si
ngle'}).code; |
| 159 return copyrightNotice + code.split(LINE_BREAK).join('') + '\n'; |
| 160 } |
| 161 |
| 162 |
| 163 function getOutPath(inputPath) { |
| 164 // TODO: Only works for non-http tests |
| 165 const layoutTestPrefix = 'LayoutTests/inspector'; |
| 166 const postfix = |
| 167 inputPath.slice(inputPath.indexOf(layoutTestPrefix) + layoutTestPrefix.len
gth + 1).replace('.html', '.js'); |
| 168 const out = |
| 169 path.resolve('.', '..', '..', '..', '..', 'LayoutTests', 'http', 'tests',
'inspector', 'devtools-js', postfix); |
| 170 return out; |
| 171 } |
| 172 |
| 173 function getPanel(inputPath) { |
| 174 const panelByFolder = { |
| 175 'animation': 'elements', |
| 176 'audits': 'audits', |
| 177 'console': 'console', |
| 178 'elements': 'elements', |
| 179 'editor': 'sources', |
| 180 'layers': 'layers', |
| 181 'network': 'network', |
| 182 'profiler': 'heap_profiler', |
| 183 'resource-tree': 'resources', |
| 184 'search': 'sources', |
| 185 'security': 'security', |
| 186 'service-workers': 'resources', |
| 187 'sources': 'sources', |
| 188 'timeline': 'timeline', |
| 189 'tracing': 'timeline', |
| 190 }; |
| 191 // Only works for non-http tests |
| 192 const folder = inputPath.slice(inputPath.indexOf('LayoutTests/')).split('/')[2
]; |
| 193 const panel = panelByFolder[folder]; |
| 194 if (!panel) { |
| 195 throw new Error('Could not figure out which panel to map folder: ' + folder)
; |
| 196 } |
| 197 return panel; |
| 198 } |
| 199 |
| 200 function mapTestHelpers(testHelpers) { |
| 201 const SKIP = 'SKIP'; |
| 202 const testHelperMap = { |
| 203 'inspector-test': SKIP, |
| 204 'console-test': 'console_test_runner', |
| 205 'elements-test': 'elements_test_runner', |
| 206 }; |
| 207 const mappedHelpers = []; |
| 208 for (const helper of testHelpers) { |
| 209 const mappedHelper = testHelperMap[helper]; |
| 210 if (!mappedHelper) { |
| 211 throw Error('Could not map helper ' + helper); |
| 212 } |
| 213 if (mappedHelper !== SKIP) { |
| 214 mappedHelpers.push(mappedHelper); |
| 215 } |
| 216 } |
| 217 return mappedHelpers; |
| 218 } |
| 219 |
| 220 function generateTestHelperMap() { |
| 221 const map = new Map(); |
| 222 for (const folder of fs.readdirSync(FRONT_END_PATH)) { |
| 223 if (folder.indexOf('test_runner') === -1) { |
| 224 continue; |
| 225 } |
| 226 const testRunnerModulePath = path.resolve(FRONT_END_PATH, folder); |
| 227 if (!utils.isDir(testRunnerModulePath)) { |
| 228 continue; |
| 229 } |
| 230 for (const filename of fs.readdirSync(testRunnerModulePath)) { |
| 231 if (filename === 'module.json') { |
| 232 continue; |
| 233 } |
| 234 scrapeTestHelperIdentifiers(path.resolve(testRunnerModulePath, filename)); |
| 235 } |
| 236 } |
| 237 return map; |
| 238 |
| 239 function scrapeTestHelperIdentifiers(filePath) { |
| 240 var content = fs.readFileSync(filePath).toString(); |
| 241 var lines = content.split('\n'); |
| 242 for (var line of lines) { |
| 243 var line = line.trim(); |
| 244 if (line.indexOf('TestRunner.') === -1) |
| 245 continue; |
| 246 var match = line.match(/^(\b\w*TestRunner.[a-z_A-Z0-9]+)\s*(\=[^,}]|[;])/)
|| |
| 247 line.match(/^(TestRunner.[a-z_A-Z0-9]+)\s*\=$/); |
| 248 if (!match) |
| 249 continue; |
| 250 var name = match[1]; |
| 251 var components = name.split('.'); |
| 252 if (components.length !== 2) |
| 253 continue; |
| 254 map.set(components[1], components[0]); |
| 255 } |
| 256 } |
| 257 } |
| 258 |
| 259 /** |
| 260 * Hack to quickly create an AST node |
| 261 */ |
| 262 function createExpressionNode(code) { |
| 263 return recast.parse(code).program.body[0]; |
| 264 } |
| 265 |
| 266 /** |
| 267 * Hack to quickly create an AST node |
| 268 */ |
| 269 function createAwaitExpressionNode(code) { |
| 270 return recast.parse(`(async function(){${code}})`).program.body[0].expression.
body.body[0]; |
| 271 } |
| OLD | NEW |