Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(378)

Side by Side Diff: third_party/WebKit/LayoutTests/webaudio/tools/layout-test-tidy.js

Issue 2872393002: Add layout-test-tidy to LayoutTests/webaudio/tools (Closed)
Patch Set: Created 3 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 #!/usr/bin/env node
2
3 // Copyright 2017 The Chromium Authors. All rights reserved.
4 // Use of this source code is governed by a BSD-style license that can be
5 // found in the LICENSE file.
6
7 'use strict';
8
9 const os = require('os');
10 const fs = require('fs');
11 const path = require('path');
12 const glob = require("glob")
13 const libtidy = require('libtidy');
14 const cp = require('child_process');
15 const jsdom = require('jsdom');
16 const { JSDOM } = jsdom;
17
18
19 /**
20 * Options for sub modules.
21 */
22 const OPTIONS = {
23
24 HTMLTidy: {
25 'indent': 'yes',
26 'indent-spaces': '2',
27 'wrap': '80',
28 'tidy-mark': 'no'
29 },
30
31 ClangFormat: [
32 '-style=Chromium',
33 '-assume-filename=a.js'
34 ],
35
36 // RegExp text swap collection (ordered key-value pair) for post-processing.
37 RegExpSwapCollection: [
38 {regexp: /var /, replace: 'let '},
Raymond Toy 2017/05/10 21:28:44 I think you need to tighten up the regexp here: l
hongchan 2017/05/11 21:13:02 Please advise if you have a better idea.
39 // Move one line up the dangling closing script tags.
40 {regexp: /\>\n\s{2,}\<\/script\>\n/, replace: '></script>\n'},
41 // Clean up redundant braces at the beginning/end.
Raymond Toy 2017/05/10 21:28:44 Expand on why this is needed. Presumably for clang
hongchan 2017/05/11 21:13:02 This is a hack for clang-format module. The result
42 {regexp: /test\-code\"\>(\n\s*\{){3}/, replace: 'test-code">'},
Raymond Toy 2017/05/10 21:28:44 Why is the replacement "test-code>"?
hongchan 2017/05/11 21:13:01 Fixed with more specific patterns.
hongchan 2017/05/11 21:13:02 Basically |test-code>| is the beginning of the tes
43 // Remove extra braces that are added for indented JS code.
44 {regexp: /(\}\n\s*){3}\<\/script\>/, replace: '</script>'},
45 // Remove all the empty lines in html.
46 {regexp: /\>\n{2,}/, replace: '>\n'}
47 ]
48 };
49
50
51 const Bar80 = {
Raymond Toy 2017/05/10 21:28:44 What is this for?
hongchan 2017/05/11 21:13:01 This prints out the 80 col ruler top and bottom, o
52 Start: '+.........' + '.'.repeat(60) + '.80.COLS.+',
53 End: '+.........' + '.'.repeat(60) + '.....EOF.+'
54 };
55
56
57 /**
58 * Basic utilities.
59 */
60 const Util = {
61
62 logAndExit: (moduleName, messageString) => {
63 console.error('[layout-test-tidy::' + moduleName + '] ' + messageString);
64 process.exit(1);
65 },
66
67 loadFileToStringSync: (filePath) => {
68 return fs.readFileSync(filePath);
69 },
70
71 writeStringToFileSync: (pageString, filePath) => {
72 fs.writeFileSync(filePath, pageString);
73 }
74
75 };
76
77
78 /**
79 * Wrapper for external modules like HTMLTidy and clang format.
80 * @type {Object}
81 */
82 const Module = {
83
84 /**
85 * Perform a batch RegExp string substution.
86 * @param {String} targetString Target string.
87 * @param {Array} swapCollection Array of key-value pairs. {regexp: replace}
Raymond Toy 2017/05/10 21:28:44 "{regexp: replace}" looks weird for what you're tr
hongchan 2017/05/11 21:13:02 Done.
88 * @return {String}
89 */
90 runRegExpSwapSync: (targetString, regExpSwapCollection) => {
91 let tempString = targetString;
92 regExpSwapCollection.forEach((item) => {
93 let re = new RegExp(item.regexp, 'g');
94 tempString = tempString.replace(re, item.replace);
95 });
96
97 return tempString;
98 },
99
100 /**
101 * Run HTMLTidy on input string with options.
102 * @param {String} pageString [description]
103 * @param {Object} options HTMLTidy option as key-value pair.
104 * @param {Task} task Associated Task object.
105 * @return {String}
106 */
107 runHTMLTidySync: (pageString, options, task) => {
108 let tidyDoc = new libtidy.TidyDoc();
109 for (let option in options)
110 tidyDoc.optSet(option, options[option]);
111
112 // This actually process the data inside of |tidyDoc|.
113 let logs = '';
114 logs += tidyDoc.parseBufferSync(Buffer(pageString));
115 logs += tidyDoc.cleanAndRepairSync();
116 logs += tidyDoc.runDiagnosticsSync();
117
118 task.addLog('Module.runHTMLTidySync', logs.split('\n'));
119
120 return tidyDoc.saveBufferSync().toString();
121 },
122
123 /**
124 * Run clang-format and return a promise.
125 * @param {String} codeString JS code to apply clang-format.
126 * @param {Array} clangFormatOption optios array for clang-format.
127 * @param {Task} task Associated Task object.
128 * @return {Promise} Processed code as string.
129 * @resolve {String} clang-formatted JS code as string.
130 * @reject {Error}
131 */
132 runClangFormat: (codeString, clangFormatOption, task) => {
133 let clangFormatBinary = __dirname + '/node_modules/clang-format/bin/';
Raymond Toy 2017/05/10 21:28:44 Does this mean we actually install our own copy of
hongchan 2017/05/11 21:13:02 Until the weird depot_tools issue is resolved, I p
134 clangFormatBinary += (os.platform() === 'win32')
135 ? 'win32/clang-format.exe'
136 : os.platform() + "_" + os.arch() + '/clang-format';
137
138 return new Promise((resolve, reject) => {
139 let echo = cp.spawn('echo', [codeString]);
Raymond Toy 2017/05/10 21:28:44 IIUC, this basically runs a shell with the echo co
hongchan 2017/05/11 21:13:02 Well, that I don't understand and this worked perf
140 let result = '';
141
142 // Be sure to pipe the result, not to stdio.
143 let clangFormat = cp.spawn(clangFormatBinary,
144 clangFormatOption,
145 {stdio: ['pipe', 'pipe', process.stderr]});
146
147 echo.stdout.pipe(clangFormat.stdin);
148
149 // For debug purpose:
150 // clangFormat.stdout.pipe(process.stdout);
151
152 clangFormat.stdout.on('data', (data) => {
153 // Capture the data streamed.
154 result += data;
155 });
156
157 clangFormat.stdout.on('close', (exitCode) => {
158 if (exitCode) {
159 Util.logAndExit('Module.runClangFormat', 'exit code = 1');
160 } else {
161 task.addLog('Module.runClangFormat', 'clang-format was successful.');
162 resolve(result);
163 }
164 });
165 });
166 },
167
168 /**
169 * Detect line overflow and record the line number to the task log.
170 * @param {String} pageOrCodeString HTML page or JS code data in string.
171 * @param {TidyTask} task Associated TidyTask object.
172 */
173 detectLineOverflow: (pageOrCodeString, task) => {
174 let currentLineNumber = 0;
175 let index0 = 0;
176 let index1 = 0;
177 while (index0 < pageOrCodeString.length - 1) {
178 index1 = pageOrCodeString.indexOf('\n', index0);
179 if (index1 - index0 > 80) {
180 task.addLog('Module.detectLineOverflow',
181 'Overflow (> 80 cols.) at line ' + currentLineNumber + '.');
182 }
183 currentLineNumber++;
184 index0 = index1 + 1;
185 }
186 }
187
188 };
189
190
191 /**
192 * DOM utilities. Process DOM processing after parsing the string by JSDOM.
193 */
194 const DOMUtil = {
195
196 /**
197 * Parse string, generate JSDOM object and return |document| element.
198 * @param {String} pageString An HTML page in string.
199 * @return {Document} A |document| object.
200 */
201 getJSDOMFromStringSync: (pageString) => {
202 return new JSDOM(`${pageString}`);
203 // return jsdom_.window.document;
204 },
205
206 /**
207 * In-place tidy up head element.
208 * @param {Document} document A |document| object.
209 * @param {Task} task An associated Task object.
210 * @return {Void}
211 */
212 tidyHeadElementSync: (document, task) => {
213 try {
214 // If the title is missing, add one with the file name.
Raymond Toy 2017/05/10 21:28:43 "with the" -> "from the"
hongchan 2017/05/11 21:13:01 Done.
215 let titleElement = document.querySelector('title');
216 if (!titleElement) {
217 titleElement = document.createElement('title');
218 titleElement.textContent = path.basename(task.targetFilePath_);
219 task.addLog('DOMUtil.tidyHeadElementSync',
220 'Title element was missing thus a new one was added.');
221 }
222
223 // The title element should be the first.
224 let headElement = document.querySelector('head');
225 headElement.insertBefore(titleElement, headElement.firstChild);
226
227 // If a script element in body does not have JS code, move to the head
228 // section.
Raymond Toy 2017/05/10 21:28:44 Do we really want to do this? Can't think of anyth
hongchan 2017/05/11 21:13:01 Yes, we do want to have this. All the scripts in
229 let scriptElementsInBody = document.body.querySelectorAll('script');
230 scriptElementsInBody.forEach((scriptElement) => {
231 if (!scriptElement.textContent)
232 headElement.appendChild(scriptElement);
233 });
234 } catch (error) {
235 task.addLog('DOMUtil.tidyHeadElementSync', error.toString());
236 }
237 },
238
239 /**
240 * Sanitize and extract |script| element with JS test code.
241 * @param {Document} document A |document| object.
242 * @param {Task} task An associated Task object.
243 * @return {ScriptElement}
244 */
245 getElementWithTestCodeSync: (document, task) => {
246 let numberOfElementsWithCode = 0;
247 let elementWithTestCode;
248 let scriptElements = document.querySelectorAll('script');
249
250 scriptElements.forEach(function (element) {
251 // We don't want type attribute.
252 element.removeAttribute('type');
253
254 if (element.textContent.length > 0) {
255 ++numberOfElementsWithCode;
256 elementWithTestCode = element;
257 element.id = 'layout-test-code';
Raymond Toy 2017/05/10 21:28:44 Is this safe? What if another element has the sam
hongchan 2017/05/11 21:13:01 If there are two or more script elements with the
258 // If the element was belong to something else than body, move it.
Raymond Toy 2017/05/10 21:28:44 "was belong" -> "belongs", I think. "else than" ->
hongchan 2017/05/11 21:13:02 Done.
259 if (element.parentElement !== document.body)
260 document.body.appendChild(element);
261 }
262 });
263
264 if (numberOfElementsWithCode !== 1) {
265 task.addLog('DOMUtil.getElementWithTestCodeSync',
Raymond Toy 2017/05/10 21:28:43 Is it really bad to have more than one? What if we
hongchan 2017/05/11 21:13:01 Yes, it's bad. If there are two script tags with a
266 numberOfElementsWithCode + ' <script> element(s) with JS ' +
267 'code were found.');
268 elementWithTestCode = null;
269 }
270
271 return elementWithTestCode;
272 }
273
274 };
275
276
277 /**
278 * @class TidyTask
279 * @description Per-file processing task. This object should be constructed
280 * directly. The task runner creates this when it is necessary.
281 */
282 class TidyTask {
283
284 /**
285 * @param {String} targetFilePath A path to file to be processed.
286 * @param {Boolean} isDryRun Print out the result to |stdout| when true. By
287 * default, this is true.
288 */
289 constructor (targetFilePath, isDryRun) {
290 this.targetFilePath_ = targetFilePath;
291 this.isDryRun_ = isDryRun;
292
293 this.fileType_ = path.extname(this.targetFilePath_);
294 this.pageString_ = Util.loadFileToStringSync(this.targetFilePath_);
295 this.jsdom_ = null;
296 this.logs_ = {};
297 }
298
299 /**
300 * Run processing sequence. Don't call this directly.
301 * @param {Function} taskDone Task runner callback function.
302 */
303 run (taskDone) {
304 switch (this.fileType_) {
305 case '.html':
306 this.processHTML_(taskDone);
307 break;
308 case '.js':
309 this.processJS_(taskDone);
310 break;
311 default:
312 Util.logAndExit('TidyTask.constructor', 'Invalid file type: '
313 + this.fileType_);
314 break;
315 }
316 }
317
318 processHTML_ (taskDone) {
319 // Parse page string into JSDOM.element object.
320 this.jsdom_ = DOMUtil.getJSDOMFromStringSync(this.pageString_);
321
322 // Clean up the head element section.
323 DOMUtil.tidyHeadElementSync(this.jsdom_.window.document, this);
324
325 let scriptElement =
326 DOMUtil.getElementWithTestCodeSync(this.jsdom_.window.document, this);
327
328 if (!scriptElement)
329 Util.logAndExit('TidyTask.processHTML_', 'Invalid <script> element.');
330
331 // Extract JS code string with additional braces for indentation hack.
Raymond Toy 2017/05/10 21:28:44 Add note that the number of braces depends on the
hongchan 2017/05/11 21:13:01 Currently the main script is always two level lowe
332 let codeString = '{\n{\n{' + scriptElement.textContent + '}\n}\n}';
Raymond Toy 2017/05/10 21:28:44 When I did this by hand, I inserted "{{{\n" and ap
333
334 // Start with clang-foramt and text-based operation.
Raymond Toy 2017/05/10 21:28:43 What does "Start" mean here? What will you end wi
hongchan 2017/05/11 21:13:02 Done.
335 Module.runClangFormat(codeString, OPTIONS.ClangFormat, this)
336 .then((formattedCodeString) => {
337 // Replace the original code with clang-formatted code.
338 scriptElement.textContent = formattedCodeString;
339
340 // Then tidy the text data from JSDOM. After this point, DOM
341 // manipulation is not possible anymore.
342 let pageString = this.jsdom_.serialize();
343 pageString = Module.runHTMLTidySync(
344 pageString, OPTIONS.HTMLTidy, this);
345 pageString = Module.runRegExpSwapSync(
346 pageString, OPTIONS.RegExpSwapCollection);
347
348 Module.detectLineOverflow(pageString, this);
349
350 this.finish_(pageString, taskDone);
351 });
352 }
353
354 processJS_ (taskDone) {
355 // The file is a JS code, so run clang-format directly.
356 Module.runClangFormat(this.pageString_, OPTIONS.ClangFormat, this)
357 .then((formattedCodeString) => {
358 formattedCodeString = Module.runRegExpSwapSync(
359 formattedCodeString, [OPTIONS.RegExpSwapCollection[0]]);
360
361 Module.detectLineOverflow(formattedCodeString, this);
362
363 this.finish_(formattedCodeString, taskDone);
364 });
365 }
366
367 finish_ (resultString, taskDone) {
368 // Print out the result to console when 'Dry Run', otherwise save
369 // directly to the target file path.
370 if (this.isDryRun_) {
371 console.log(Bar80.Start + '\n' + resultString + '\n' + Bar80.End);
372 } else {
373 Util.writeStringToFileSync(resultString, this.targetFilePath_);
374 }
375
376 this.printLog();
377 taskDone();
378 }
379
380 /**
381 * Adding log message.
382 * @param {String} location Caller information.
383 * @param {String} message Log message.
384 */
385 addLog (location, message) {
386 if (!this.logs_.hasOwnProperty(location))
387 this.logs_[location] = [];
388 this.logs_[location].push(message);
389 }
390
391 /**
392 * Print log messages at the end of task.
393 */
394 printLog () {
395 console.log('> Logs from: ' + this.targetFilePath_);
396 for (let location in this.logs_) {
397 console.log(' [] ' + location);
398 this.logs_[location].forEach((message) => {
399 if (Array.isArray(message)) {
400 message.forEach((subMessage) => {
401 if (subMessage.length > 0)
402 console.log(' - ' + subMessage);
403 });
404 } else {
405 console.log(' - ' + message);
406 }
407 });
408 }
409 }
410
411 }
412
413
414 /**
415 * @class TidyTaskRunner
416 */
417 class TidyTaskRunner {
418
419 /**
420 * @param {Array} files A list of file paths.
421 * @param {Boolean} isDryRun Dry run flag. When |true| the processed output
422 * will be printed out via stdout instead of actual
423 * target files.
424 * @return {TidyTaskRunner} A task runner object.
425 */
426 constructor (files, isDryRun) {
427 this.targetFiles_ = files;
428 this.isDryRun_ = isDryRun;
429
430 this.tasks_ = [];
431 this.currentTask_ = 0;
432 this.oncomplete = null;
433 }
434
435 startProcessing () {
436 this.targetFiles_.forEach((filePath) => {
437 this.tasks_.push(new TidyTask(filePath, this.isDryRun_));
438 });
439
440 console.log('[layout-test-tidy] Task runner started: '
441 + this.targetFiles_.length + ' file(s).');
442 this.runTask_();
443 }
444
445 runTask_ () {
446 console.log('[layout-test-tidy] Running task #' + (this.currentTask_ + 1)
447 + ': ' + this.targetFiles_[this.currentTask_]
448 + (this.isDryRun_ ? ' (DRYRUN)' : ''));
449 this.tasks_[this.currentTask_].run(this.done_.bind(this));
450 }
451
452 done_ () {
453 console.log('[layout-test-tidy] Task #' + (this.currentTask_ + 1)
454 + ' completed.\n');
455 this.currentTask_++;
456 if (this.currentTask_ < this.tasks_.length) {
457 this.runTask_();
458 } else {
459 console.log('[layout-test-tidy] Task runner completed: '
460 + this.targetFiles_.length + ' file(s) processed.');
461 if (this.oncomplete)
462 this.oncomplete();
463 }
464 }
465
466 }
467
468
469 // Entry point.
470 function main() {
471 let args = process.argv.slice(2);
472 if (!args || args.length == 0 || args.length > 2)
473 Util.logAndExit('main', 'Invalid arguments. (' + args + ')');
474
475 let targetPath = args[0];
476 let isDryRun = args[1] === '--dryrun';
477
478 let files = [];
479
480 if (targetPath) {
481 try {
482 let stat = fs.lstatSync(targetPath);
483 if (stat.isFile()) {
484 files.push(targetPath);
485 } else if (stat.isDirectory()) {
486 files = glob.sync(targetPath + '/**/*.{html,js}');
487 }
488 } catch (error) {
489 let errorMessage = 'Invalid file path. (' + targetPath + ')\n'
490 + ' > ' + error.toString();
491 Util.logAndExit('main', errorMessage);
492 }
493 }
494
495 // Files to be skipped.
496 let filesToBeSkipped =
497 Util.loadFileToStringSync('skip-tidy').toString().split('\n');
498 filesToBeSkipped.forEach((fileSkipped) => {
499 let index = files.indexOf(fileSkipped);
500 if (index > -1) {
501 files.splice(index, 1);
502 }
503 });
504
505 if (files.length > 0) {
506 let taskRunner = new TidyTaskRunner(files, isDryRun);
507 taskRunner.startProcessing();
508 } else {
509 Util.logAndExit('main', 'No files to process.');
510 }
511 }
512
513 main();
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698