Arlolra has uploaded a new change for review. ( https://gerrit.wikimedia.org/r/336719 )
Change subject: Promisify parserTests main ...................................................................... Promisify parserTests main * Return the exit code rather than exiting. Change-Id: Ib52f8569c9a8e93820772226cde8e956849e4370 --- M bin/parserTests.js M tests/parserTests.utils.js 2 files changed, 285 insertions(+), 285 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/mediawiki/services/parsoid refs/changes/19/336719/1 diff --git a/bin/parserTests.js b/bin/parserTests.js index 40cffea..6c57afe 100755 --- a/bin/parserTests.js +++ b/bin/parserTests.js @@ -165,27 +165,6 @@ /** * @method * - * Process an article test case (i.e. the text of an article we need for a test) - * - * @param {Object} item - * @param {string} item.title - * @param {string} item.text - * @param {Function} cb - */ -ParserTests.prototype.processArticle = function(item, cb) { - var err = null; - var key = this.env.normalizedTitleKey(item.title, false, true); - if (this.articles.hasOwnProperty(key)) { - err = new Error('Duplicate article: ' + item.title); - } else { - this.articles[key] = item.text; - } - setImmediate(cb, err); -}; - -/** - * @method - * * Convert a DOM to Wikitext. * * @param {Object} options @@ -681,7 +660,7 @@ * @param {string} mode * @param {Function} endCb */ -ParserTests.prototype.processTest = function(item, options, mode, endCb) { +ParserTests.prototype.prepareTest = function(item, options, mode, endCb) { if (!('title' in item)) { return endCb(new Error('Missing title from test case.')); } @@ -974,7 +953,7 @@ * @method * @param {Object} options */ -ParserTests.prototype.main = function(options) { +ParserTests.prototype.main = Promise.method(function(options) { this.runDisabled = Util.booleanOption(options['run-disabled']); this.runPHP = Util.booleanOption(options['run-php']); @@ -985,13 +964,7 @@ // shows up as a numeric type rather than a string. // Ex: parserTests.js --filter 53221 var pattern = options.regex || Util.escapeRegExp(options.filter.toString()); - try { - this.testFilter = new RegExp(pattern); - } catch (e) { - console.error('\nERROR> --filter was given an invalid regular expression.'); - console.error('\nERROR> See below for JS engine error:\n' + e + '\n'); - process.exit(1); - } + this.testFilter = new RegExp(pattern); } this.testParserFilePath = path.join(__dirname, '../tests/parserTests.pegjs'); @@ -1006,8 +979,6 @@ this.cases.length = n; } } - - options.expandExtensions = true; var setup = function(parsoidConfig) { // Set tracing and debugging before the env. object is @@ -1058,12 +1029,8 @@ var parsoidConfig = new ParsoidConfig({ setup: setup }, options); // Create a new parser environment - MWParserEnvironment.getParserEnv(parsoidConfig, { prefix: 'enwiki' }, - function(err, env) { - // For posterity: err will never be non-null here, because we expect - // the WikiConfig to be basically empty, since the parserTests - // environment is very bare. - console.assert(!err, (err && err.stack) || err); + return MWParserEnvironment.getParserEnv(parsoidConfig, { prefix: 'enwiki' }) + .then(function(env) { this.env = env; if (Util.booleanOption(options.quiet)) { @@ -1102,9 +1069,9 @@ options.reportStart(); this.env.pageCache = this.articles; this.comments = []; - this.processCase(0, options); + return this.processCase(0, options, false); }.bind(this)); -}; +}); /** * FIXME: clean up this mess! @@ -1147,7 +1114,7 @@ console.assert(newitem.changetree === 'manual' || newitem.changetree === undefined); newitem.changetree = 'manual'; - self.processTest(newitem, options, 'selser', function(err) { + self.prepareTest(newitem, options, 'selser', function(err) { setImmediate(cb, err); }); }); @@ -1163,7 +1130,7 @@ tasks.push(function(cb) { newitem = Util.clone(item); newitem.changetree = 5; - self.processTest(newitem, options, 'selser', function(err) { + self.prepareTest(newitem, options, 'selser', function(err) { setImmediate(cb, err); }); }); @@ -1181,7 +1148,7 @@ // Make sure we aren't reusing the one from manual changes console.assert(newitem.changetree === undefined); newitem.seed = changesIndex + ''; - this.processTest(newitem, options, targetModes[modeIndex], function(err) { + this.prepareTest(newitem, options, targetModes[modeIndex], function(err) { if (this.isDuplicateChangeTree(item.selserChangeTrees, newitem.changes)) { // Once we get a duplicate change tree, we can no longer // generate and run new tests. So, be done now! @@ -1201,14 +1168,14 @@ if (item.options.parsoid && item.options.parsoid.changes) { // If it does, we need to clone the item so that previous // results don't clobber this one. - tasks.push(this.processTest.bind(this, Util.clone(item), options, targetModes[i])); + tasks.push(this.prepareTest.bind(this, Util.clone(item), options, targetModes[i])); } else { // If it doesn't have manual changes, just skip it. continue; } } else { // A non-selser task, we can reuse the item. - tasks.push(this.processTest.bind(this, item, options, targetModes[i])); + tasks.push(this.prepareTest.bind(this, item, options, targetModes[i])); } } } @@ -1218,245 +1185,27 @@ /** * @method */ -ParserTests.prototype.processCase = function(i, options, err) { - var item; - var targetModes = options.modes; - var nextCallback = this.processCase.bind(this, i + 1, options); - - // There are two types of errors that reach here. The first is just - // a notification that a test failed. We use the error propagation - // mechanism to get back to this point to print the summary. The - // second type is an actual exception that we should hard fail on. - // exitUnexpected is a sentinel for the first type. - if (err && err !== exitUnexpected) { - this.env.log('fatal', err); - process.exit(1); // Should not reach here. - } - var earlyExit = options['exit-unexpected'] && (err === exitUnexpected); - +ParserTests.prototype.processCase = function(i, options, earlyExit) { if (i < this.cases.length && !earlyExit) { - item = this.cases[i]; - if (typeof item === 'string') { - // this is a comment line in the file, ignore it. - return setImmediate(nextCallback); - } - - if (!item.options) { item.options = {}; } - - // backwards-compatibility aliases for section names. - if ('input' in item) { item.wikitext = item.input; delete item.input; } - if ('result' in item) { item.html = item.result; delete item.result; } - - // html/* and html/parsoid should be treated as html. - [ 'html/*', 'html/*+tidy', 'html+tidy', 'html/parsoid' ].forEach(function(alt) { - if (alt in item) { - item.html = item[alt]; - } + var self = this; + return new Promise(function(resolve, reject) { + var item = self.cases[i]; + self.processItem(item, options, function(err) { + // There are two types of errors that reach here. The first is just + // a notification that a test failed. We use the error propagation + // mechanism to get back to this point to print the summary. The + // second type is an actual exception that we should hard fail on. + // exitUnexpected is a sentinel for the first type. + if (err && err !== exitUnexpected) { + reject(err); + } else { + resolve(options['exit-unexpected'] && (err === exitUnexpected)); + } + }); + }) + .then(function(ee) { + return self.processCase(i + 1, options, ee); }); - // ensure that test is not skipped if it has a wikitext/edited section - if ('wikitext/edited' in item) { item.html = true; } - - // Reset the cached results for the new case. - // All test modes happen in a single run of processCase. - item.cachedBODYstr = null; - item.cachedNormalizedHTML = null; - - // Also reset the logger, since we might have changed it to support - // the `suppressErrors` option. - this.env.setLogger(this.defaultLogger); - // Similarly for parsing resource limits. - this.env.setResourceLimits(); - - // console.log( 'processCase ' + i + JSON.stringify( item ) ); - if (typeof item === 'object') { - switch (item.type) { - case 'article': - this.comments = []; - this.processArticle(item, nextCallback); - break; - case 'test': - if (this.tests.has(item.title)) { - return setImmediate(nextCallback, - new Error('Duplicate titles: ' + item.title)); - } else { - this.tests.add(item.title); - } - - if (!('wikitext' in item && 'html' in item) || - ('disabled' in item.options && !this.runDisabled) || - ('php' in item.options && - !('html/parsoid' in item || this.runPHP)) || - (this.testFilter && - -1 === item.title.search(this.testFilter))) { - // Skip test whose title does not match --filter - // or which is disabled or php-only - this.comments = []; - setImmediate(nextCallback); - break; - } - // Add comments to following test. - item.comments = item.comments || this.comments; - this.comments = []; - - var suppressErrors = item.options.parsoid && item.options.parsoid.suppressErrors; - if (suppressErrors) { - this.env.setLogger(this.suppressLogger); - } - if (item.options.parsoid && item.options.parsoid.modes) { - // Avoid filtering out the selser test - if (options.selser && - item.options.parsoid.modes.indexOf("selser") < 0 && - item.options.parsoid.modes.indexOf("wt2wt") >= 0 - ) { - item.options.parsoid.modes.push("selser"); - } - - targetModes = targetModes.filter(function(mode) { - return item.options.parsoid.modes.indexOf(mode) >= 0; - }); - } - - if (targetModes.length) { - // Honor language option in parserTests.txt - var prefix = item.options.language || 'enwiki'; - if (!/wiki/.test(prefix)) { - // Convert to our enwiki.. format - prefix = prefix + 'wiki'; - } - this.env.switchToConfig(prefix, function(err2) { - if (err2) { - return nextCallback(err2); - } - - // TODO: set language variant - // adjust config to match that used for PHP tests - // see core/tests/parser/parserTest.inc:setupGlobals() for - // full set of config normalizations done. - var wikiConf = this.env.conf.wiki; - wikiConf.fakeTimestamp = 123; - wikiConf.timezoneOffset = 0; // force utc for parsertests - wikiConf.server = 'http://example.org'; - wikiConf.wgScriptPath = '/'; - wikiConf.script = '/index.php'; - wikiConf.articlePath = '/wiki/$1'; - // Hard-code some interwiki prefixes, as is done - // in parserTest.inc:setupInterwikis() - var iwl = { - local: { - url: 'http://doesnt.matter.org/$1', - localinterwiki: '', - }, - wikipedia: { - url: 'http://en.wikipedia.org/wiki/$1', - }, - meatball: { - // this has been updated in the live wikis, but the parser tests - // expect the old value (as set in parserTest.inc:setupInterwikis()) - url: 'http://www.usemod.com/cgi-bin/mb.pl?$1', - }, - memoryalpha: { - url: 'http://www.memory-alpha.org/en/index.php/$1', - }, - zh: { - url: 'http://zh.wikipedia.org/wiki/$1', - language: '\u4e2d\u6587', - local: '', - }, - es: { - url: 'http://es.wikipedia.org/wiki/$1', - language: 'espa\u00f1ol', - local: '', - }, - fr: { - url: 'http://fr.wikipedia.org/wiki/$1', - language: 'fran\u00e7ais', - local: '', - }, - ru: { - url: 'http://ru.wikipedia.org/wiki/$1', - language: '\u0440\u0443\u0441\u0441\u043a\u0438\u0439', - local: '', - }, - mi: { - url: 'http://mi.wikipedia.org/wiki/$1', - // better for testing if one of the - // localinterwiki prefixes is also a - // language - language: 'Test', - local: '', - localinterwiki: '', - }, - mul: { - url: 'http://wikisource.org/wiki/$1', - extralanglink: '', - linktext: 'Multilingual', - sitename: 'WikiSource', - local: '', - }, - // not in PHP setupInterwikis(), but needed - en: { - url: 'http://en.wikipedia.org/wiki/$1', - language: 'English', - local: '', - protorel: '', - }, - }; - wikiConf.interwikiMap.clear(); - Object.keys(iwl).forEach(function(key) { - iwl[key].prefix = key; - wikiConf.interwikiMap.set(key, {}); - Object.keys(iwl[key]).forEach(function(f) { - wikiConf.interwikiMap.get(key)[f] = iwl[key][f]; - }); - }); - // Add 'MemoryAlpha' namespace (bug 51680) - wikiConf.namespaceNames['100'] = 'MemoryAlpha'; - wikiConf.namespaceIds.memoryalpha = - wikiConf.canonicalNamespaces.memoryalpha = 100; - // Cannot add namespace 100 otherwise since - // baseConfig is deep frozen. - wikiConf.siteInfo.namespaces = Util.clone(wikiConf.siteInfo.namespaces, true); - wikiConf.siteInfo.namespaces['100'] = { - "case": "first-letter", - "*": "MemoryAlpha", - "canonical": "MemoryAlpha", - }; - - // Update $wgInterwikiMagic flag - // default (undefined) setting is true - this.env.conf.wiki.interwikimagic = - item.options.wginterwikimagic === undefined || - /^(1|true|)$/.test(item.options.wginterwikimagic); - - async.series(this.buildTasks(item, targetModes, options), - nextCallback); - }.bind(this)); - - } else { - setImmediate(nextCallback); - } - - break; - case 'comment': - this.comments.push(item.comment); - setImmediate(nextCallback); - break; - case 'hooks': - this.env.log('warn', 'parserTests: Unhandled extension hook', JSON.stringify(item)); - setImmediate(nextCallback); - break; - case 'functionhooks': - this.env.log("warn", "parserTests: Unhandled functionhook", JSON.stringify(item)); - setImmediate(nextCallback); - break; - default: - this.comments = []; - setImmediate(nextCallback); - break; - } - } else { - setImmediate(nextCallback); - } } else { // update the blacklist, if requested if (Util.booleanOption(options['rewrite-blacklist'])) { @@ -1472,7 +1221,7 @@ var contents = shell[0]; contents += '// ### DO NOT REMOVE THIS LINE ### '; contents += '(start of automatically-generated section)\n'; - targetModes.forEach(function(mode) { + options.modes.forEach(function(mode) { contents += '\n// Blacklist for ' + mode + '\n'; this.stats.modes[mode].failList.forEach(function(fail) { contents += 'add(' + JSON.stringify(mode) + ', ' + @@ -1516,8 +1265,193 @@ if (Util.booleanOption(options['exit-zero'])) { failures = false; } - process.exit(failures ? 2 : 0); // exit status 1 == uncaught exception + + return failures ? 2 : 0; // exit status 1 == uncaught exception } +}; + +/** + * @method + */ +ParserTests.prototype.processItem = function(item, options, nextCallback) { + if (typeof item !== 'object') { + // this is a comment line in the file, ignore it. + return setImmediate(nextCallback); + } + + if (!item.options) { item.options = {}; } + + // backwards-compatibility aliases for section names. + if ('input' in item) { item.wikitext = item.input; delete item.input; } + if ('result' in item) { item.html = item.result; delete item.result; } + + // html/* and html/parsoid should be treated as html. + [ 'html/*', 'html/*+tidy', 'html+tidy', 'html/parsoid' ].forEach(function(alt) { + if (alt in item) { + item.html = item[alt]; + } + }); + + // ensure that test is not skipped if it has a wikitext/edited section + if ('wikitext/edited' in item) { item.html = true; } + + // Reset the cached results for the new case. + // All test modes happen in a single run of processCase. + item.cachedBODYstr = null; + item.cachedNormalizedHTML = null; + + // Also reset the logger, since we might have changed it to support + // the `suppressErrors` option. + this.env.setLogger(this.defaultLogger); + // Similarly for parsing resource limits. + this.env.setResourceLimits(); + + switch (item.type) { + case 'article': + this.comments = []; + this.processArticle(item, nextCallback); + break; + case 'test': + this.processTest(item, options, nextCallback); + break; + case 'comment': + this.comments.push(item.comment); + setImmediate(nextCallback); + break; + case 'hooks': + this.comments = []; + this.env.log('warn', 'parserTests: Unhandled extension hook', JSON.stringify(item)); + setImmediate(nextCallback); + break; + case 'functionhooks': + this.comments = []; + this.env.log("warn", "parserTests: Unhandled functionhook", JSON.stringify(item)); + setImmediate(nextCallback); + break; + default: + this.comments = []; + setImmediate(nextCallback); + break; + } +}; + +/** + * @method + * + * Process an article test case (i.e. the text of an article we need for a test) + * + * @param {Object} item + * @param {string} item.title + * @param {string} item.text + * @param {Function} cb + */ +ParserTests.prototype.processArticle = function(item, cb) { + var err = null; + var key = this.env.normalizedTitleKey(item.title, false, true); + if (this.articles.hasOwnProperty(key)) { + err = new Error('Duplicate article: ' + item.title); + } else { + this.articles[key] = item.text; + } + setImmediate(cb, err); +}; + +/** + * @method + */ +ParserTests.prototype.processTest = function(item, options, nextCallback) { + var targetModes = options.modes; + if (this.tests.has(item.title)) { + return setImmediate(nextCallback, + new Error('Duplicate titles: ' + item.title)); + } else { + this.tests.add(item.title); + } + if (!('wikitext' in item && 'html' in item) || + ('disabled' in item.options && !this.runDisabled) || + ('php' in item.options && + !('html/parsoid' in item || this.runPHP)) || + (this.testFilter && + -1 === item.title.search(this.testFilter))) { + // Skip test whose title does not match --filter + // or which is disabled or php-only + this.comments = []; + setImmediate(nextCallback); + return; + } + // Add comments to following test. + item.comments = item.comments || this.comments; + this.comments = []; + var suppressErrors = item.options.parsoid && item.options.parsoid.suppressErrors; + if (suppressErrors) { + this.env.setLogger(this.suppressLogger); + } + if (item.options.parsoid && item.options.parsoid.modes) { + // Avoid filtering out the selser test + if (options.selser && + item.options.parsoid.modes.indexOf("selser") < 0 && + item.options.parsoid.modes.indexOf("wt2wt") >= 0 + ) { + item.options.parsoid.modes.push("selser"); + } + + targetModes = targetModes.filter(function(mode) { + return item.options.parsoid.modes.indexOf(mode) >= 0; + }); + } + if (!targetModes.length) { + setImmediate(nextCallback); + return; + } + // Honor language option in parserTests.txt + var prefix = item.options.language || 'enwiki'; + if (!/wiki/.test(prefix)) { + // Convert to our enwiki.. format + prefix = prefix + 'wiki'; + } + this.env.switchToConfig(prefix, function(err2) { + if (err2) { return nextCallback(err2); } + // TODO: set language variant + // adjust config to match that used for PHP tests + // see core/tests/parser/parserTest.inc:setupGlobals() for + // full set of config normalizations done. + var wikiConf = this.env.conf.wiki; + wikiConf.fakeTimestamp = 123; + wikiConf.timezoneOffset = 0; // force utc for parsertests + wikiConf.server = 'http://example.org'; + wikiConf.wgScriptPath = '/'; + wikiConf.script = '/index.php'; + wikiConf.articlePath = '/wiki/$1'; + wikiConf.interwikiMap.clear(); + var iwl = PTUtils.iwl; + Object.keys(iwl).forEach(function(key) { + iwl[key].prefix = key; + wikiConf.interwikiMap.set(key, {}); + Object.keys(iwl[key]).forEach(function(f) { + wikiConf.interwikiMap.get(key)[f] = iwl[key][f]; + }); + }); + // Add 'MemoryAlpha' namespace (bug 51680) + wikiConf.namespaceNames['100'] = 'MemoryAlpha'; + wikiConf.namespaceIds.memoryalpha = + wikiConf.canonicalNamespaces.memoryalpha = 100; + // Cannot add namespace 100 otherwise since + // baseConfig is deep frozen. + wikiConf.siteInfo.namespaces = Util.clone(wikiConf.siteInfo.namespaces, true); + wikiConf.siteInfo.namespaces['100'] = { + "case": "first-letter", + "*": "MemoryAlpha", + "canonical": "MemoryAlpha", + }; + // Update $wgInterwikiMagic flag + // default (undefined) setting is true + this.env.conf.wiki.interwikimagic = + item.options.wginterwikimagic === undefined || + /^(1|true|)$/.test(item.options.wginterwikimagic); + + async.series(this.buildTasks(item, targetModes, options), + nextCallback); + }.bind(this)); }; // Start the mock api server and kick off parser tests @@ -1532,5 +1466,8 @@ testFilePath = path.join(__dirname, '../tests/parserTests.txt'); } var ptests = new ParserTests(testFilePath, options.modes); - return ptests.main(options); + return ptests.main(options) + .then(function(status) { + process.exit(status); + }); }).done(); diff --git a/tests/parserTests.utils.js b/tests/parserTests.utils.js index 6157d74..642d8b0 100644 --- a/tests/parserTests.utils.js +++ b/tests/parserTests.utils.js @@ -767,3 +767,66 @@ return options; }; + +// Hard-code some interwiki prefixes, as is done +// in parserTest.inc:setupInterwikis() +PTUtils.iwl = { + local: { + url: 'http://doesnt.matter.org/$1', + localinterwiki: '', + }, + wikipedia: { + url: 'http://en.wikipedia.org/wiki/$1', + }, + meatball: { + // this has been updated in the live wikis, but the parser tests + // expect the old value (as set in parserTest.inc:setupInterwikis()) + url: 'http://www.usemod.com/cgi-bin/mb.pl?$1', + }, + memoryalpha: { + url: 'http://www.memory-alpha.org/en/index.php/$1', + }, + zh: { + url: 'http://zh.wikipedia.org/wiki/$1', + language: '\u4e2d\u6587', + local: '', + }, + es: { + url: 'http://es.wikipedia.org/wiki/$1', + language: 'espa\u00f1ol', + local: '', + }, + fr: { + url: 'http://fr.wikipedia.org/wiki/$1', + language: 'fran\u00e7ais', + local: '', + }, + ru: { + url: 'http://ru.wikipedia.org/wiki/$1', + language: '\u0440\u0443\u0441\u0441\u043a\u0438\u0439', + local: '', + }, + mi: { + url: 'http://mi.wikipedia.org/wiki/$1', + // better for testing if one of the + // localinterwiki prefixes is also a + // language + language: 'Test', + local: '', + localinterwiki: '', + }, + mul: { + url: 'http://wikisource.org/wiki/$1', + extralanglink: '', + linktext: 'Multilingual', + sitename: 'WikiSource', + local: '', + }, + // not in PHP setupInterwikis(), but needed + en: { + url: 'http://en.wikipedia.org/wiki/$1', + language: 'English', + local: '', + protorel: '', + }, +}; -- To view, visit https://gerrit.wikimedia.org/r/336719 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: Ib52f8569c9a8e93820772226cde8e956849e4370 Gerrit-PatchSet: 1 Gerrit-Project: mediawiki/services/parsoid Gerrit-Branch: master Gerrit-Owner: Arlolra <abrea...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits