diff --git a/tools/converter/convert.js b/tools/converter/convert.js new file mode 100644 index 0000000000..8a40fdc520 --- /dev/null +++ b/tools/converter/convert.js @@ -0,0 +1,429 @@ + +(function(global) { + "use strict"; + + var t262 = global.t262; + var platform = t262.platform; + var regExp = platform.regExp; + + var headerPattern = /(?:(?:\/\/.*)?\s*\n)*/; + var captureCommentPattern = /\/\*\*?((?:\s|\S)*?)\*\/\s*\n/; + var anyPattern = /(?:\s|\S)*/; + var blanksPattern = /(?:\s|\n)*/; + + // Should match anything + var testEnvelopePattern = + regExp('^(', headerPattern, + ')(?:', captureCommentPattern, + ')?(', anyPattern, + ')$'); + + var registerPattern = + regExp('^(', anyPattern, '?)(', + /ES5Harness\.registerTest\s*\(\s*\{/, anyPattern, + /\}\s*\)/, ')', + /\s*;?(?:\s|\n)*$/); + + var captureFuncBodyPattern = + regExp(/^function(?:\s+\w*)?\(\s*\)\s*\{/, + '(', anyPattern, ')', + /;?/, blanksPattern, + /\}$/); + + var captureExprBodyPattern = + regExp(/^return\s+/, + '(', anyPattern, '?)', + /;$/); + + var capturePredicatePattern = + regExp(/^if\s+\((.*?)\)\s*\{/, blanksPattern, + /return\s+true;?/, blanksPattern, + /\}$/); + + /** + * Strip the left margin "*"s that are found in the body of a + * multiline doc-comment like this one. + */ + function stripStars(text) { + return text.replace(/\s*\n\s*\*\s?/g, '\n').trim(); + } + + /** + * Parses the source of a test262 test case file into a JSON + * envelope record. + * + *

The input can be in old sputnik or ietestcenter style, or in + * the canonical test262 style. In all cases, we have an optional + * header, an optional "/*" comment possibly containing properties + * of the form

+    *   @propName: propValue;
+    * 
which populate the test record. This is followed by the + * rest of the text, which is the test itself. In the case of an + * ietestcenter style test, this is followed by a call to + * ES5Harness\.registerTest to register a test record. + */ + function parseTestEnvelope(src, name) { + var envelope = { testRecord: {} }; + var envelopeMatch = testEnvelopePattern.exec(src); + if (!envelopeMatch) { + // Can't happen? + throw new Error('unrecognized: ' + name); + } + envelope.header = envelopeMatch[1].trim(); + + if (envelopeMatch[2]) { + var propTexts = envelopeMatch[2].split(/\s*\n\s*\*\s*@/); + envelope.comment = stripStars(propTexts.shift()), // notice side effect + propTexts.forEach(function(propText) { + var propName = propText.match(/^\w+/)[0]; + var propVal = propText.substring(propName.length); + var propMatch = /^:?([^;]*);?\s*$/.exec(propVal); + if (propMatch) { propVal = propMatch[1]; } + propVal = stripStars(propVal); + if (propName in envelope.testRecord) { + throw new Error('duplicate: ' + propName); + } + envelope.testRecord[propName] = propVal; + }); + } + envelope.rest = envelopeMatch[3]; // Do not trim + + var registerMatch = registerPattern.exec(envelope.rest); + if (registerMatch) { + envelope.rest = registerMatch[1].trim(); + envelope.registerExpr = registerMatch[2].trim(); + } else if (envelope.rest.indexOf('ES5Harness.registerTest') >= 0) { + print(' \n--header---\n|' + envelope.header + + '|\n--rest-----\n|' + envelope.rest + + '|\n--harness--\n|' + envelope.registerExpr + + '|\n-----------\n'); + throw new Error('Malformed harness? ' + name); + } + return envelope; + } + + /** + * Given a function, return the source for an expression that, when + * evaluated in the environment the function assumes, will behave + * the same as calling that function in that environment. + */ + function expressionize(func) { + var funcSrc = '' + func; + var cfbMatch = captureFuncBodyPattern.exec(funcSrc); + if (cfbMatch) { + // Look for special cases + var body = cfbMatch[1].trim(); + + var cebMatch = captureExprBodyPattern.exec(body); + if (cebMatch) { return '(' + cebMatch[1].trim() + ')'; } + + var cpMatch = capturePredicatePattern.exec(body); + if (cpMatch) { return '(' + cpMatch[1].trim() + ')'; } + + } else { + // signal an error? + } + return '(' + funcSrc + ').call(this)'; + } + + /** + * Given an ietestcenter style test, this evaluates the + * registration expression in order to gather the test record. + */ + function gatherOne(envelope, name) { + if (envelope.testRecord) { + var propNames = Object.keys(envelope.testRecord); + if (propNames.length >= 1) { + // This need not be an error. It's just here so we notice the + // first time it happens. This would happen if an + // ietestcenter style test also had a comment with "@" + // property definitions. + throw new Error('unexpected in ' + name + ': ' + propNames); + } + } + var testRecords = []; + + // Evaluating!!!! + platform.evalExprIn(envelope.registerExpr, + { + ES5Harness: { + registerTest: function(testRecord) { + testRecords.push(testRecord); + } + } + }, + 'forceNonStrict'); + + if (testRecords.length !== 1) { + // We plan to lift this restriction in order to support test + // generators. + throw new Error('not singleton: ' + name); + } + var testRecord = testRecords[0]; + + if (typeof testRecord.test === 'function') { + testRecord.test = envelope.rest + + 'assertTrue(' + expressionize(testRecord.test) + ');\n'; + } + if (typeof testRecord.precondition === 'function') { + var precondition = expressionize(testRecord.precondition); + if (precondition === '(true)') { + delete testRecord.precondition; + } else { + testRecord.precondition = precondition; + } + } + + return testRecord; + }; + + /** + * Normalizes the properties of testRecord to be the canonical + * test262 style properties, that will be assumed by the new test + * runners. + */ + function normalizeProps(testRecord) { + if (!testRecord.id && testRecord.name) { + testRecord.id = testRecord.name; + delete testRecord.name; + } + + if (!('strict_only' in testRecord) && testRecord.strict === 1) { + testRecord.strict_only = ''; + delete testRecord.strict; + } + + if ('strict_mode_negative' in testRecord) { + if (!('strict_only' in testRecord)) { + testRecord.strict_only = ''; + } + if (!'negative' in testRecord) { + testRecord.negative = testRecord.strict_mode_negative; + delete testRecord.strict_mode_negative; + } + } + + if (!testRecord.negative && 'errortype' in testRecord) { + testRecord.negative = testRecord.errortype; + delete testRecord.errortype; + } + + if (!testRecord.description && testRecord.assertion) { + testRecord.description = testRecord.assertion; + delete testRecord.assertion; + } + if (!testRecord.comment && testRecord.assertion) { + testRecord.comment = testRecord.assertion; + delete testRecord.assertion; + } + }; + t262.normalizeProps = normalizeProps; + + /** + * Parses the source of a test262 test case file into a normalized + * JSON test record. + */ + function parseTestRecord(path, name) { + var nextPath = path.concat([name]); + + var src = platform.read(nextPath); + var testRecord; + if (!src) { throw new Error('no src: ' + nextPath.join('/')); } + var envelope = parseTestEnvelope(src, name); + + if (envelope.registerExpr) { + testRecord = gatherOne(envelope, name); + } else { + testRecord = envelope.testRecord; + if (!testRecord.test) { + testRecord.test = envelope.rest; + } + } + testRecord.header = envelope.header; + testRecord.comment = envelope.comment; + + normalizeProps(testRecord); + return testRecord; + }; + t262.parseTestRecord = parseTestRecord; + + // The known ones will be rendered first, and in this order. + var KNOWN_PROPS = ['id', 'section', 'path', 'description', + 'strict_only', 'negative']; + + /** + * Turns the (assumed) normalized test record into its string form + * in canonical test262 style. + * + * NOTE: This is currently destructive of testRecord. Easy to fix + *if it becomes a problem. + */ + function formatTestRecord(testRecord) { + var test = testRecord.test; + delete testRecord.test; + + function addProp(pname) { + if (pname in testRecord) { + result += ' * @' + pname; + if (testRecord[pname]) { + result += ': ' + testRecord[pname].replace(/\n/g, '\n * '); + } + result += ';\n'; + delete testRecord[pname]; + } + } + + var result = testRecord.header + '\n\n'; + delete testRecord.header; + result += '/**\n'; + if (testRecord.comment) { + result += ' * ' + testRecord.comment.replace(/\n/g, '\n * ') + '\n *\n'; + } + delete testRecord.comment; + KNOWN_PROPS.concat(['precondition']).forEach(addProp); + Object.keys(testRecord).forEach(addProp); + result += ' */\n\n' + test; + return result; + }; + t262.formatTestRecord = formatTestRecord; + + /** + * Reads the test case at pathStr and returns the source of that + * test case converted to canonical test262 style. + */ + function convertTest(pathStr) { + var path = platform.toPath(pathStr); + var name = path.pop(); + var testRecord = parseTestRecord(path, name); + var result = formatTestRecord(testRecord); + return result; + }; + t262.convertTest = convertTest; + + var SRC_DIRS = [ + ['test', 'suite', 'other'], + ['test', 'suite', 'sputnik', 'Conformance'], + ['test', 'suite', 'ietestcenter'] + ]; + + var CONV_DIR = ['test', 'suite', 'converted']; + + var OUT_DIR = ['website', 'resources', 'scripts', 'testcases2']; + + var ME_PATH = platform.CONVERTER_PATH.concat('convert.js'); + + /** + * Convert all the testcases found at inBase+relDir to test cases + * in canonical test262 style, to be stored at corresponding + * positions in outBase+relPath. + */ + function convertAll(inBase, outBase, relPath) { + var inPath = inBase.concat(relPath); + var outPath = outBase.concat(relPath); + platform.mkdir(outPath); + platform.ls(inPath).forEach(function(name) { + var nextRelPath = relPath.concat([name]); + if (platform.isDirectory(inBase.concat(nextRelPath))) { + convertAll(inBase, outBase, nextRelPath); + } else if (/\.js$/.test(name)) { + var inFilePath = inPath.concat([name]); + var outFilePath = outPath.concat([name]); + platform.writeSpawn( + [ME_PATH], + 't262.convertTest("' + platform.toPathStr(inFilePath) + '")', + void 0, + outFilePath); + } + }); + }; + t262.convertAll = convertAll; + + /** + * Do all the conversions (from sputnik style, ietestcenter style, + * or other to canonical test262 style) matching relPath. + */ + function convert(opt_relPath) { + SRC_DIRS.forEach(function(srcDir) { + convertAll(srcDir, CONV_DIR, opt_relPath || []); + }); + }; + t262.convert = convert; + + /** + * Reads all the test case records for the section corresponding to + * the directory at pathStr, and return a JSON record for a test + * case section, as would be uploaded to a browser-based test + * runner. + */ + function buildSection(pathStr) { + var path = platform.toPath(pathStr); + if (!platform.isDirectory(path)) { throw new Error('not dir: ' + path); } + + var jsFiles = platform.ls(path).filter(function(name) { + return /\.js$/.test(name); + }); + var testRecords = jsFiles.map(function(name) { + var testRecord = parseTestRecord(path, name); + + delete testRecord.header; + delete testRecord.comment; + return testRecord; + }); + testRecords = testRecords.filter(function(testRecord) { + return testRecord !== null; + }); + return { + testCollection: { + name: path[path.length -1], + numTests: testRecords.length, + tests: testRecords + } + }; + }; + t262.buildSection = buildSection; + + /** + * Use the test cases at inBase+relPath to build the test + * collection portion of the website, at outBase. + */ + function buildAll(inBase, outBase, relPath) { + var inPath = inBase.concat(relPath); + var hasJS = false; + platform.ls(inPath).forEach(function(name) { + var nextRelPath = relPath.concat([name]); + if (platform.isDirectory(inBase.concat(nextRelPath))) { + buildAll(inBase, outBase, nextRelPath); + } else if (/\.js$/.test(name)) { + hasJS = true; + } + }); + if (hasJS) { + var name = relPath[relPath.length -1] + '.json'; + var outFilePath = outBase.concat([name]); + platform.writeSpawn( + [ME_PATH], + 't262.asJSONTxt(t262.buildSection("' + + platform.toPathStr(inPath) + '"))', + void 0, + outFilePath); + } + }; + t262.buildAll = buildAll; + + /** + * Build those test case files for the website corresponding to the + * test cases matching relPath. + * + *

Right now it's building from the pre-converted test + * files. Once we switch over to converted as the maintained + * sources, we should change this. + */ + function buildWebSite(opt_relPath) { + SRC_DIRS.forEach(function(srcDir) { + buildAll(srcDir, OUT_DIR, opt_relPath || []); + }); +// buildAll(CONV_DIR, OUT_DIR, opt_relPath || []); + }; + t262.buildWebSite = buildWebSite; + + })(this); diff --git a/tools/converter/v8PosixPlatform.js b/tools/converter/v8PosixPlatform.js new file mode 100644 index 0000000000..cfd849b07e --- /dev/null +++ b/tools/converter/v8PosixPlatform.js @@ -0,0 +1,275 @@ + + +/** + * Each implementation of *Platform.js abstracts the underlying OS and JS + * engine peculiarities. + * + *

The implementation here is specific to the v8 shell running on a + * Posix platform. + */ +(function (global) { + "use strict"; + + /////////////////// Development Switches ///////////////// + + var VERBOSE = true; + + // Affects side effecting os operations, + // currently only platform.writeSpawn and platform.mkdir. + var DRY_RUN = false; + + // When converting paths to path strings, should the pathstring be + // relative to the TEST262_ROOT, or should it be relative to the + // current working directory? + var ABSOLUTE_PATHSTR = false; + + //////////////////////////////////////////////////////// + + global.t262 = global.t262 || {}; + + var platform = global.t262.platform = {}; + + /** + * Appends a bunch of RegExps together into a single RegExp, + * solving both the RegExp-one-liner problem and the doubled + * backslash problem when composing literal string. + * + *

The arguments can be any mixture of RegExps and strings. The + * strings are added as is without escaping -- BEWARE. If + * arguments[0] is a RegExp, we use its flag on the resuting RegExp. + * + *

Not platform dependent, so does not really belong in this + * file. + */ + function regExp(var_args) { + var args = [].slice.call(arguments, 0); + var reSrc = args.map(function(arg) { + return (typeof arg === 'string') ? arg : arg.source; + }).join(''); + var flags = ''; + if (typeof args[0] === 'object') { + var parts = (''+args[0]).split('/'); + flags = parts[parts.length -1]; + } + return new RegExp(reSrc, flags); + } + platform.regExp = regExp; + + ////////////////// Needed for building and running ////////////// + + try { + read('tools/converter/v8PosixPlatform.js'); + } catch (err) { + throw new Error('Must run in a test262 source root'); + } + + var ABS_ROOT = os.system('pwd', ['-P']).trim().split('/'); + + var TEST262_ROOT = ABSOLUTE_PATHSTR ? ABS_ROOT : []; + + var TEST262_ROOT_STR = TEST262_ROOT.join('/'); + + var CONVERTER_PATH = ['tools', 'converter']; + platform.CONVERTER_PATH = CONVERTER_PATH; + + var ME_PATH = CONVERTER_PATH.concat('v8PosixPlatform.js'); + + /** + * + */ + function validatePath(path) { + var pathStr = path.join('/'); + path.forEach(function(segment) { + if (segment === '') { + throw new Error('A path cannot have empty segment: ' + pathStr); + } + if (segment === '/') { + throw new Error('Path insufficiently parsed: ' + pathStr); + } + if (segment === '..') { + throw new Error('Cannot use "..": ' + pathStr); + } + }); + return path; + } + + /** + * Converts a path to a pathStr. + * + * A path is an array of filenames relative to TEST262_ROOT. A + * pathStr is a (possibly fully qualified string) for referring to + * that string on the current platform, according to the operations + * in this *Platform.js file. + */ + function toPathStr(path) { + validatePath(path); + return TEST262_ROOT.concat(path).join('/'); + }; + platform.toPathStr = toPathStr; + + /** + * Returns the text found at path, with newlines normalized and + * any initial BOM (Unicode Byte Order Mark) removed. + */ + platform.read = function(path) { + var text = read(toPathStr(path)). + replace(/\r\n/g, '\n'). + replace(/\r/g, '\n'); + if (text.charCodeAt(0) === 0xfeff) { return text.substring(1); } + return text; + }; + + /** + * How one JavaScript script possibly spawns another and possibly + * redirects its printed form to a chosen file (or resource). + * + *

For example, if !DRY_RUN, then

+    *   writeSpawn([], '+arguments[0] + +arguments[1]', ['3', '5'])
+    * 
+ * should return the string "8", whether or not writeSpawn decides + * to spawn. + * + * @param scriptPaths An array of path arrays of JavaScript source + * files to be loaded into the spawned JS engine (in addition to + * the spawning platform file) if we are indeed spawning. + * @param opt_exprSrc An expression to be evaluated in an + * environment in which "arguments" is bound to the list of strings + * provided by opt_args. The result is the value of the expression + * coerced to a string, unfortunately, as prepended by whatever + * these scripts (if spawned) have already written to their + * stdout. On platforms (like SES) where this can be a safely + * confining evaluation, it should be. The implementation here is + * not safe. + * @param opt_args A list of strings to be bound to 'arguments' + * both in opt_expr and in the possibly spawed scripts. + * @param opt_targetPath A path array naming a file where the + * result of opt_exprSrc should be written. On v8 currently, if + * this is provided, then writeSpawn will spawn, since we have no + * other way to implement this functionality. In the browser + * context, the result is PUT (using XHR) to the target resource. + * @param opt_spawn_required If truthy, forces spawning. + * @returns If there is a target, then the null string. Otherwise, + * the string result of evaluating opt_exprSrc. + */ + platform.writeSpawn = function(scriptPaths, + opt_exprSrc, + opt_args, + opt_targetPath, + opt_spawn_required, + opt_forceNonStrict) { + if (opt_exprSrc && !opt_targetPath && !opt_spawn_required) { + var str = '(function(/*var_args*/) {'; + if (opt_forceNonStrict !== 'forceNonStrict') { + str += '"use strict";'; + } + str += ' return (' + opt_exprSrc + '); })'; + return ''+(1,eval)(str).apply(void 0, opt_args || []); + } + + var cmd = 'v8 ' + toPathStr(ME_PATH) + ' '; + cmd += scriptPaths.map(toPathStr).join(' '); + + if (opt_exprSrc) { + cmd += ' -e ' + JSON.stringify('print(' + opt_exprSrc + ')'); + } + if (opt_args) { + cmd += ' -- ' + opt_args.map(JSON.stringify).join(' '); + } + if (opt_targetPath) { + cmd += ' > ' + toPathStr(opt_targetPath); + } + if (VERBOSE || DRY_RUN) { print(cmd); } + if (DRY_RUN) { return ''; } + return os.system('bash', ['-c', cmd]); + }; + + ////////////////// Only needed for building ///////////////////// + + /** + * Calls a non-strict indirect eval function on exprSrc. + * + * On platforms (like SES) where this can be a safely confining + * evaluation, it should be. The implementation here is not safe. + */ + platform.evalExprIn = function(exprSrc, env, opt_forceNonStrict) { + var varNames = Object.getOwnPropertyNames(env); + var str = '(function(' + varNames.join(',') + ') {'; + if (opt_forceNonStrict !== 'forceNonStrict') { + str += '"use strict";'; + } + str += ' return (' + exprSrc + '); })'; + return (1,eval)(str).apply(void 0, varNames.map(function(varName) { + return env[varName]; + })); + }; + + /** + * Converts a pathStr to a path. + * + * See toPathStr. + */ + function toPath(pathStr) { + if (pathStr[0] === '/') { + if (pathStr.indexOf(TEST262_ROOT_STR + '/') !== 0) { + throw new Error('"' + pathStr + '" must start with "' + + TEST262_ROOT_STR + '/"'); + } + pathStr = pathStr.substring(TEST262_ROOT_STR.length + 1); + } + return validatePath(pathStr.split('/')); + } + platform.toPath = toPath; + + /** + * Does path name a directory? + */ + platform.isDirectory = function(path) { + var fileOut = os.system('file', [toPathStr(path)]); + var fileMatch = fileOut.match(/:\s*([^:]*)\s*$/); + if (!fileMatch) { return null; } + var fileType = fileMatch[1].trim(); + return fileType === 'directory'; + }; + + /** + * A list of the filenames found in path, which must name a + * directory. + */ + platform.ls = function(path) { + var pathStr = toPathStr(path); + var lines = os.system('ls', [pathStr]).trim(); + if (lines === '') { return []; } + return lines.split('\n'); + }; + + /** + * Emits the jsonRecord serialized as JSON, either compactly or + * readably according to VERBOSE. + */ + function asJSONTxt(jsonRecord) { + if (VERBOSE) { + return JSON.stringify(jsonRecord, void 0, ' '); + } else { + return JSON.stringify(jsonRecord); + } + } + global.t262.asJSONTxt = platform.asJSONTxt = asJSONTxt; + + platform.mkdir = function(path) { + var pathStr = toPathStr(path); + if (DRY_RUN) { + print('mkdir ' + pathStr); + return; + } + try { + os.mkdirp(pathStr); + } catch (err) { + print('***could not mkdir: ' + pathStr); + throw err; + } + }; + + ////////////////// Only needed for running ////////////////////// + + + })(this);