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

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

Issue 2872393002: Add layout-test-tidy to LayoutTests/webaudio/tools (Closed)
Patch Set: Remove indentation hack after l-g-t-m 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 side-by-side diff with in-line comments
Download patch
Index: third_party/WebKit/LayoutTests/webaudio/tools/layout-test-tidy.js
diff --git a/third_party/WebKit/LayoutTests/webaudio/tools/layout-test-tidy.js b/third_party/WebKit/LayoutTests/webaudio/tools/layout-test-tidy.js
new file mode 100644
index 0000000000000000000000000000000000000000..bffab4bc075da8410e10cce0e191affa68bd685e
--- /dev/null
+++ b/third_party/WebKit/LayoutTests/webaudio/tools/layout-test-tidy.js
@@ -0,0 +1,562 @@
+#!/usr/bin/env node
+
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+'use strict';
+
+const os = require('os');
+const fs = require('fs');
+const path = require('path');
+const glob = require('glob');
+const libtidy = require('libtidy');
+const cp = require('child_process');
+const jsdom = require('jsdom');
+const {JSDOM} = jsdom;
+
+
+/**
+ * Options for sub modules.
+ */
+const OPTIONS = {
+
+ HTMLTidy:
+ {'indent': 'yes', 'indent-spaces': '2', 'wrap': '80', 'tidy-mark': 'no'},
+
+ ClangFormat: ['-style=Chromium', '-assume-filename=a.js'],
+
+ // RegExp text swap collection (ordered key-value pair) for post-processing.
+ RegExpSwapCollection: [
+ // Replace |var| with |let|.
+ {regexp: /(\n\s{2,}|\()var /, replace: '$1let '},
+
+ // Move one line up the dangling closing script tags.
+ {regexp: /\>\n\s{2,}\<\/script\>\n/, replace: '></script>\n'},
+
+ // Remove all the empty lines in html.
+ {regexp: /\>\n{2,}/, replace: '>\n'}
+ ]
+};
+
+
+/**
+ * Basic utilities.
+ */
+const Util = {
+
+ logAndExit: (moduleName, messageString) => {
+ console.error('[layout-test-tidy::' + moduleName + '] ' + messageString);
+ process.exit(1);
+ },
+
+ loadFileToStringSync: (filePath) => {
+ return fs.readFileSync(filePath, 'utf8').toString();
+ },
+
+ writeStringToFileSync: (pageString, filePath) => {
+ fs.writeFileSync(filePath, pageString);
+ }
+
+};
+
+
+/**
+ * Wrapper for external modules like HTMLTidy and clang format.
+ * @type {Object}
+ */
+const Module = {
+
+ /**
+ * Perform a batch RegExp string substitution.
+ * @param {String} targetString Target string.
+ * @param {Array} swapCollection Array of key-value pairs. Each item is an
+ * object of { regexp_pattern: replace_string }.
+ * @return {String}
+ */
+ runRegExpSwapSync: (targetString, regExpSwapCollection) => {
+ let tempString = targetString;
+ regExpSwapCollection.forEach((item) => {
+ let re = new RegExp(item.regexp, 'g');
+ tempString = tempString.replace(re, item.replace);
+ });
+
+ return tempString;
+ },
+
+ /**
+ * Run HTMLTidy on input string with options.
+ * @param {String} pageString [description]
+ * @param {Object} options HTMLTidy option as key-value pair.
+ * @param {Task} task Associated Task object.
+ * @return {String}
+ */
+ runHTMLTidySync: (pageString, options, task) => {
+ let tidyDoc = new libtidy.TidyDoc();
+ for (let option in options)
+ tidyDoc.optSet(option, options[option]);
+
+ // This actually process the data inside of |tidyDoc|.
+ let logs = '';
+ logs += tidyDoc.parseBufferSync(Buffer(pageString));
+ logs += tidyDoc.cleanAndRepairSync();
+ logs += tidyDoc.runDiagnosticsSync();
+
+ task.addLog('Module.runHTMLTidySync', logs.split('\n'));
+
+ return tidyDoc.saveBufferSync().toString();
+ },
+
+ /**
+ * Run clang-format and return a promise.
+ * @param {String} codeString JS code to apply clang-format.
+ * @param {Array} clangFormatOption options array for clang-format.
+ * @param {Number} indentLevel Code indentation level.
+ * @param {Task} task Associated Task object.
+ * @return {Promise} Processed code as string.
+ * @resolve {String} clang-formatted JS code as string.
+ * @reject {Error}
+ */
+ runClangFormat: (codeString, clangFormatOption, indentLevel, task) => {
+ let clangFormatBinary = __dirname + '/node_modules/clang-format/bin/';
+ clangFormatBinary += (os.platform() === 'win32') ?
+ 'win32/clang-format.exe' :
+ os.platform() + '_' + os.arch() + '/clang-format';
+
+ if (indentLevel > 0) {
+ codeString =
+ '{'.repeat(indentLevel) + codeString + '}'.repeat(indentLevel);
+ }
+
+ return new Promise((resolve, reject) => {
+ // Be sure to pipe the result to the child process, not to this process's
+ // stdout.
+ let result = '';
+ let clangFormat = cp.spawn(
+ clangFormatBinary, clangFormatOption,
+ {stdio: ['pipe', 'pipe', process.stderr]});
+
+ // Capture the data when it's arrived at the pipe.
+ clangFormat.stdout.on('data', (data) => {
+ result += data;
+ });
+
+ // For debug purpose:
+ // clangFormat.stdout.pipe(process.stdout);
+
+ clangFormat.stdout.on('close', (exitCode) => {
+ if (exitCode) {
+ Util.logAndExit('Module.runClangFormat', 'exit code = 1');
+ } else {
+ task.addLog('Module.runClangFormat', 'clang-format was successful.');
+
+ // Remove shim braces for indentation hack.
+ if (indentLevel > 0) {
+ let codeStart = 0;
+ let codeEnd = result.length - 1;
+ for (let i = 0; i < indentLevel; ++i) {
+ codeStart = result.indexOf('\n', codeStart + 1);
+ codeEnd = result.lastIndexOf('\n', codeEnd - 1);
+ }
+ result = result.substring(codeStart + 1, codeEnd);
+ }
+
+ resolve(result);
+ }
+ });
+
+ clangFormat.stdin.setEncoding('utf-8');
+ clangFormat.stdin.write(codeString);
+ clangFormat.stdin.end();
+ });
+ },
+
+ /**
+ * Detect line overflow and record the line number to the task log.
+ * @param {String} pageOrCodeString HTML page or JS code data in string.
+ * @param {TidyTask} task Associated TidyTask object.
+ */
+ detectLineOverflow: (pageOrCodeString, task) => {
+ let currentLineNumber = 0;
+ let index0 = 0;
+ let index1 = 0;
+ while (index0 < pageOrCodeString.length - 1) {
+ index1 = pageOrCodeString.indexOf('\n', index0);
+ if (index1 - index0 > 80) {
+ task.addLog(
+ 'Module.detectLineOverflow',
+ 'Overflow (> 80 cols.) at line ' + currentLineNumber + '.');
+ }
+ currentLineNumber++;
+ index0 = index1 + 1;
+ }
+ }
+
+};
+
+
+/**
+ * DOM utilities. Process DOM processing after parsing the string by JSDOM.
+ */
+const DOMUtil = {
+
+ /**
+ * Parse string, generate JSDOM object and return |document| element.
+ * @param {String} pageString An HTML page in string.
+ * @return {Document} A |document| object.
+ */
+ getJSDOMFromStringSync: (pageString) => {
+ return new JSDOM(`${pageString}`);
+ // return jsdom_.window.document;
+ },
+
+ /**
+ * In-place tidy up head element.
+ * @param {Document} document A |document| object.
+ * @param {Task} task An associated Task object.
+ * @return {Void}
+ */
+ tidyHeadElementSync: (document, task) => {
+ try {
+ // If the title is missing, add one from the file name.
+ let titleElement = document.querySelector('title');
+ if (!titleElement) {
+ titleElement = document.createElement('title');
+ titleElement.textContent = path.basename(task.targetFilePath_);
+ task.addLog(
+ 'DOMUtil.tidyHeadElementSync',
+ 'Title element was missing thus a new one was added.');
+ }
+
+ // The title element should be the first.
+ let headElement = document.querySelector('head');
+ headElement.insertBefore(titleElement, headElement.firstChild);
+
+ // If a script element in body does not have JS code, move to the head
+ // section.
+ let scriptElementsInBody = document.body.querySelectorAll('script');
+ scriptElementsInBody.forEach((scriptElement) => {
+ if (!scriptElement.textContent)
+ headElement.appendChild(scriptElement);
+ });
+ } catch (error) {
+ task.addLog('DOMUtil.tidyHeadElementSync', error.toString());
+ }
+ },
+
+ /**
+ * Sanitize and extract |script| element with JS test code.
+ * @param {Document} document A |document| object.
+ * @param {Task} task An associated Task object.
+ * @return {ScriptElement}
+ */
+ getElementWithTestCodeSync: (document, task) => {
+ let numberOfScriptElementsWithCode = 0;
+ let scriptElementWithTestCode;
+ let scriptElements = document.querySelectorAll('script');
+
+ scriptElements.forEach((scriptElement) => {
+ // We don't want type attribute.
+ scriptElement.removeAttribute('type');
+
+ if (scriptElement.textContent.length > 0) {
+ ++numberOfScriptElementsWithCode;
+ scriptElementWithTestCode = scriptElement;
+ scriptElement.id = 'layout-test-code';
+ // If the element belongs to something else other than body, move it to
+ // the body. This fixes script elements that are located in weird
+ // positions. (e.g outside of body or head)
+ if (scriptElement.parentElement !== document.body)
+ document.body.appendChild(scriptElement);
+ }
+ });
+
+ if (numberOfScriptElementsWithCode !== 1) {
+ task.addLog(
+ 'DOMUtil.getElementWithTestCodeSync',
+ numberOfScriptElementsWithCode + ' <script> element(s) with JS ' +
+ 'code were found.');
+ scriptElementWithTestCode = null;
+ }
+
+ return scriptElementWithTestCode;
+ }
+
+};
+
+
+/**
+ * @class TidyTask
+ * @description Per-file processing task. This object should be constructed
+ * directly. The task runner creates this when it is necessary.
+ */
+class TidyTask {
+ /**
+ * @param {String} targetFilePath A path to file to be processed.
+ * @param {Object} options Task options.
+ * @param {Boolean} options.inplace |true| for in-place processing directly
+ * writing into the target file. By default,
+ * this is |false| and the result is piped
+ * into the stdout.
+ * @param {Boolean} options.verbose Prints out warnings and logs from the
+ * process when |true|. |false| by default.
+ */
+ constructor(targetFilePath, options) {
+ this.targetFilePath_ = targetFilePath;
+ this.options_ = options;
+
+ this.fileType_ = path.extname(this.targetFilePath_);
+ this.pageString_ = Util.loadFileToStringSync(this.targetFilePath_);
+ this.jsdom_ = null;
+ this.logs_ = {};
+ }
+
+ /**
+ * Run processing sequence. Don't call this directly.
+ * @param {Function} taskDone Task runner callback function.
+ */
+ run(taskDone) {
+ switch (this.fileType_) {
+ case '.html':
+ this.processHTML_(taskDone);
+ break;
+ case '.js':
+ this.processJS_(taskDone);
+ break;
+ default:
+ Util.logAndExit(
+ 'TidyTask.constructor', 'Invalid file type: ' + this.fileType_);
+ break;
+ }
+ }
+
+ /**
+ * Process HTML file. The processing performs the following in order:
+ * - DOM parsing to sanitize invalid/incorrect markup structure.
+ * - Extract JS code, apply clang-format and inject the code to element.
+ * - Apply HTMLTidy to the markup.
+ * - RegExp substitution.
+ * - Detect any line overflows 80 columns.
+ * @param {Function} taskDone completion callback.
+ */
+ processHTML_(taskDone) {
+ // Parse page string into JSDOM.element object.
+ this.jsdom_ = DOMUtil.getJSDOMFromStringSync(this.pageString_);
+
+ // Clean up the head element section.
+ DOMUtil.tidyHeadElementSync(this.jsdom_.window.document, this);
+
+ let scriptElement =
+ DOMUtil.getElementWithTestCodeSync(this.jsdom_.window.document, this);
+
+ if (!scriptElement)
+ Util.logAndExit('TidyTask.processHTML_', 'Invalid <script> element.');
+
+ // Start with clang-foramt, then HTMLTidy and RegExp substitution.
+ Module.runClangFormat(
+ scriptElement.textContent, OPTIONS.ClangFormat, 3, this)
+ .then((formattedCodeString) => {
+ // Replace the original code with clang-formatted code.
+ scriptElement.textContent = formattedCodeString;
+
+ // Then tidy the text data from JSDOM. After this point, DOM
+ // manipulation is not possible anymore.
+ let pageString = this.jsdom_.serialize();
+ pageString =
+ Module.runHTMLTidySync(pageString, OPTIONS.HTMLTidy, this);
+ pageString = Module.runRegExpSwapSync(
+ pageString, OPTIONS.RegExpSwapCollection);
+
+ // Detect any line goes over column 80.
+ Module.detectLineOverflow(pageString, this);
+
+ this.finish_(pageString, taskDone);
+ });
+ }
+
+ /**
+ * Process JS file. The processing performs the following in order:
+ * - Extract JS code, apply clang-format and inject the code to element.
+ * - RegExp substitution.
+ * - Detect any line overflows 80 columns.
+ * @param {Function} taskDone completion callback.
+ */
+ processJS_(taskDone) {
+ // The file is a JS code: run clang-format, RegExp substitution and check
+ // for overflowed lines.
+ Module.runClangFormat(this.pageString_, OPTIONS.ClangFormat, 0, this)
+ .then((formattedCodeString) => {
+ formattedCodeString = Module.runRegExpSwapSync(
+ formattedCodeString, [OPTIONS.RegExpSwapCollection[0]]);
+ Module.detectLineOverflow(formattedCodeString, this);
+ this.finish_(formattedCodeString, taskDone);
+ });
+ }
+
+ finish_(resultString, taskDone) {
+ if (this.options_.inplace) {
+ Util.writeStringToFileSync(resultString, this.targetFilePath_);
+ } else {
+ process.stdout.write(resultString);
+ }
+
+ this.printLog();
+ taskDone();
+ }
+
+ /**
+ * Adding log message.
+ * @param {String} location Caller information.
+ * @param {String} message Log message.
+ */
+ addLog(location, message) {
+ if (!this.logs_.hasOwnProperty(location))
+ this.logs_[location] = [];
+ this.logs_[location].push(message);
+ }
+
+ /**
+ * Print log messages at the end of task.
+ */
+ printLog() {
+ if (!this.options_.verbose)
+ return;
+
+ console.warn('> Logs from: ' + this.targetFilePath_);
+ for (let location in this.logs_) {
+ console.warn(' [] ' + location);
+ this.logs_[location].forEach((message) => {
+ if (Array.isArray(message)) {
+ message.forEach((subMessage) => {
+ if (subMessage.length > 0)
+ console.warn(' - ' + subMessage);
+ });
+ } else {
+ console.warn(' - ' + message);
+ }
+ });
+ }
+ }
+}
+
+
+/**
+ * @class TidyTaskRunner
+ */
+class TidyTaskRunner {
+ /**
+ * @param {Array} files A list of file paths.
+ * @param {Object} options Task options.
+ * @param {Boolean} options.inplace |true| for in-place processing directly
+ * writing into the target file. By default,
+ * this is |false| and the result is piped
+ * into the stdout.
+ * @param {Boolean} options.verbose Prints out warnings and logs from the
+ * process when |true|. |false| by default.
+ * @return {TidyTaskRunner} A task runner object.
+ */
+ constructor(files, options) {
+ this.targetFiles_ = files;
+ this.options_ = options;
+ this.tasks_ = [];
+ this.currentTask_ = 0;
+ }
+
+ startProcessing() {
+ this.targetFiles_.forEach((filePath) => {
+ this.tasks_.push(new TidyTask(filePath, this.options_));
+ });
+ this.log_('Task runner started: ' + this.targetFiles_.length + ' file(s).');
+ this.runTask_();
+ }
+
+ runTask_() {
+ this.log_(
+ 'Running task #' + (this.currentTask_ + 1) + ': ' +
+ this.targetFiles_[this.currentTask_] +
+ (this.options_.inplace ? ' (IN-PLACE)' : ''));
+ this.tasks_[this.currentTask_].run(this.done_.bind(this));
+ }
+
+ done_() {
+ this.log_('Task #' + (this.currentTask_ + 1) + ' completed.');
+ this.currentTask_++;
+ if (this.currentTask_ < this.tasks_.length) {
+ this.runTask_();
+ } else {
+ this.log_(
+ 'Task runner completed: ' + this.targetFiles_.length +
+ ' file(s) processed.');
+ }
+ }
+
+ log_(message) {
+ if (this.options_.verbose)
+ console.warn('[layout-test-tidy] ' + message);
+ }
+}
+
+
+// Entry point.
+function main() {
+ let args = process.argv.slice(2);
+
+ // Extract options from the arguments.
+ let optionArgs = args.filter((arg, index) => {
+ if (arg.startsWith('-') || arg.startsWith('--')) {
+ args[index] = null;
+ return true;
+ }
+ });
+
+ args = args.filter(arg => arg);
+
+ // A single target (a file or a directory) is allowed.
+ if (args.length !== 1) {
+ Util.logAndExit('main', 'Please specify a single target. (' + args + ')');
+ }
+
+ // Populate options flags.
+ let options = {
+ inPlace: optionArgs.includes('-i') || optionArgs.includes('--in-place'),
+ verbose: optionArgs.includes('-v') || optionArgs.includes('--verbose')
+ };
+
+ // Collect target file(s) from the file system.
+ let targetPath = args[0];
+ let files = [];
+ if (targetPath) {
+ try {
+ let stat = fs.lstatSync(targetPath);
+ if (stat.isFile()) {
+ files.push(targetPath);
+ } else if (stat.isDirectory()) {
+ files = glob.sync(
+ targetPath + '/**/*.{html,js}', {ignore: ['**/node_modules/**/*']});
+ }
+ } catch (error) {
+ let errorMessage = 'Invalid file path. (' + targetPath + ')\n' +
+ ' > ' + error.toString();
+ Util.logAndExit('main', errorMessage);
+ }
+ }
+
+ // Files to be skipped.
+ let filesToBeSkipped =
+ Util.loadFileToStringSync('skip-tidy').toString().split('\n');
+ filesToBeSkipped.forEach((fileSkipped) => {
+ let index = files.indexOf(fileSkipped);
+ if (index > -1) {
+ files.splice(index, 1);
+ }
+ });
+
+ if (files.length > 0) {
+ let taskRunner = new TidyTaskRunner(files, options);
+ taskRunner.startProcessing();
+ } else {
+ Util.logAndExit('main', 'No files to process.');
+ }
+}
+
+main();
« no previous file with comments | « third_party/WebKit/LayoutTests/webaudio/tools/README.md ('k') | third_party/WebKit/LayoutTests/webaudio/tools/package.json » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698