From 365b6f3af90e05be13c63d9e5ac8223c7e4f344b Mon Sep 17 00:00:00 2001 From: Timo Tijhof Date: Thu, 12 Feb 2015 04:51:58 +0000 Subject: [PATCH] mediawiki.jqueryMsg: Refactor "Match PHP parser" test suite Previously it was doing quite a few things wrong: * Tests could potentially run in parallel. This was somewhat obscured by the asynchronous getMwLanguage() call being inside the synchronous each() loop. As such it basically first fired off the requests, and then handled them as they came back. They pretty much always come back in order (lucky) so the tests pass, but it was fragile. * By default, jQuery.ajax() appends a cache buster ("&_{random}" query string) to requests with dataType 'script'. Disable this by setting cache=true. Also: * Store Promise objects instead of a low-level Callbacks list so that we don't have to wrap it. Instead, we simply chain from the $.ajax() promise, and use then() to feed the value instead of calling resolve() or fire(). * Use hasOwn() as extra sanity check (in case a random browser's non-standard Object.prototype methods match one of our non-standard language codes). * Remove dead code "mw.messages.set( test.message )". This object has no message property (or anything else that it could be mistaken for). It just performs 15 no-op in a loop. Follows-up 261aac63ec0777d6. While the requests happen to (almost) always arrive in order, them running in parallel put a lot of pressure on slow VMs using SQLite databases (which are I/O bound). This causes a lock when queries from different processes overlap. Change-Id: I0c5064780f80e1ddcc58ce32c390b2639b1f1ab6 --- .../mediawiki/mediawiki.jqueryMsg.test.js | 152 +++++++++++------- 1 file changed, 90 insertions(+), 62 deletions(-) diff --git a/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js b/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js index 28d6d92f94..1fb17230cf 100644 --- a/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js +++ b/tests/qunit/suites/resources/mediawiki/mediawiki.jqueryMsg.test.js @@ -1,6 +1,7 @@ ( function ( mw, $ ) { - var mwLanguageCache = {}, formatText, formatParse, formatnumTests, specialCharactersPageName, - expectedListUsers, expectedEntrypoints; + var formatText, formatParse, formatnumTests, specialCharactersPageName, expectedListUsers, expectedEntrypoints, + mwLanguageCache = {}, + hasOwn = Object.hasOwnProperty; // When the expected result is the same in both modes function assertBothModes( assert, parserArguments, expectedResult, assertMessage ) { @@ -60,31 +61,52 @@ } } ) ); - function getMwLanguage( langCode, cb ) { - if ( mwLanguageCache[langCode] !== undefined ) { - mwLanguageCache[langCode].add( cb ); - return; - } - mwLanguageCache[langCode] = $.Callbacks( 'once memory' ); - mwLanguageCache[langCode].add( cb ); - $.ajax( { - url: mw.util.wikiScript( 'load' ), - data: { - skin: mw.config.get( 'skin' ), - lang: langCode, - debug: mw.config.get( 'debug' ), - modules: [ - 'mediawiki.language.data', - 'mediawiki.language' - ].join( '|' ), - only: 'scripts' - }, - dataType: 'script' - } ).done( function () { - mwLanguageCache[langCode].fire( mw.language ); - } ).fail( function () { - mwLanguageCache[langCode].fire( false ); + /** + * Be careful to no run this in parallel as it uses a global identifier (mw.language) + * to transport the module back to the test. It musn't be overwritten concurrentely. + * + * This function caches the mw.language data to avoid having to request the same module + * multiple times. There is more than one test case for any given language. + */ + function getMwLanguage( langCode ) { + if ( !hasOwn.call( mwLanguageCache, langCode ) ) { + mwLanguageCache[langCode] = $.ajax( { + url: mw.util.wikiScript( 'load' ), + data: { + skin: mw.config.get( 'skin' ), + lang: langCode, + debug: mw.config.get( 'debug' ), + modules: [ + 'mediawiki.language.data', + 'mediawiki.language' + ].join( '|' ), + only: 'scripts' + }, + dataType: 'script', + cache: true + } ).then( function () { + return mw.language; } ); + } + return mwLanguageCache[langCode]; + } + + /** + * @param {Function[]} tasks List of functions that perform tasks + * that may be asynchronous. Invoke the callback parameter when done. + * @param {Function} done When all tasks are done. + * @return + */ + function process( tasks, done ) { + function run() { + var task = tasks.shift(); + if ( task ) { + task( run ); + } else { + done(); + } + } + run(); } QUnit.test( 'Replace', 16, function ( assert ) { @@ -283,23 +305,27 @@ QUnit.test( 'Match PHP parser', mw.libs.phpParserData.tests.length, function ( assert ) { mw.messages.set( mw.libs.phpParserData.messages ); - $.each( mw.libs.phpParserData.tests, function ( i, test ) { - QUnit.stop(); - getMwLanguage( test.lang, function ( langClass ) { - QUnit.start(); - if ( !langClass ) { - assert.ok( false, 'Language "' + test.lang + '" failed to load' ); - return; - } - mw.config.set( 'wgUserLanguage', test.lang ); - var parser = new mw.jqueryMsg.parser( { language: langClass } ); - assert.equal( - parser.parse( test.key, test.args ).html(), - test.result, - test.name - ); - } ); + var tasks = $.map( mw.libs.phpParserData.tests, function ( test ) { + return function ( next ) { + getMwLanguage( test.lang ) + .done( function ( langClass ) { + mw.config.set( 'wgUserLanguage', test.lang ); + var parser = new mw.jqueryMsg.parser( { language: langClass } ); + assert.equal( + parser.parse( test.key, test.args ).html(), + test.result, + test.name + ); + } ) + .fail( function () { + assert.ok( false, 'Language "' + test.lang + '" failed to load.' ); + } ) + .always( next ); + }; } ); + + QUnit.stop(); + process( tasks, QUnit.start ); } ); QUnit.test( 'Links', 6, function ( assert ) { @@ -466,8 +492,8 @@ ); } ); -// Tests that getMessageFunction is used for non-plain messages with curly braces or -// square brackets, but not otherwise. + // Tests that getMessageFunction is used for non-plain messages with curly braces or + // square brackets, but not otherwise. QUnit.test( 'mw.Message.prototype.parser monkey-patch', 22, function ( assert ) { var oldGMF, outerCalled, innerCalled; @@ -618,25 +644,27 @@ formatnumTests = [ QUnit.test( 'formatnum', formatnumTests.length, function ( assert ) { mw.messages.set( 'formatnum-msg', '{{formatnum:$1}}' ); mw.messages.set( 'formatnum-msg-int', '{{formatnum:$1|R}}' ); - $.each( formatnumTests, function ( i, test ) { - QUnit.stop(); - getMwLanguage( test.lang, function ( langClass ) { - QUnit.start(); - if ( !langClass ) { - assert.ok( false, 'Language "' + test.lang + '" failed to load' ); - return; - } - mw.messages.set(test.message ); - mw.config.set( 'wgUserLanguage', test.lang ); - var parser = new mw.jqueryMsg.parser( { language: langClass } ); - assert.equal( - parser.parse( test.integer ? 'formatnum-msg-int' : 'formatnum-msg', - [ test.number ] ).html(), - test.result, - test.description - ); - } ); + var queue = $.map( formatnumTests, function ( test ) { + return function ( next ) { + getMwLanguage( test.lang ) + .done( function ( langClass ) { + mw.config.set( 'wgUserLanguage', test.lang ); + var parser = new mw.jqueryMsg.parser( { language: langClass } ); + assert.equal( + parser.parse( test.integer ? 'formatnum-msg-int' : 'formatnum-msg', + [ test.number ] ).html(), + test.result, + test.description + ); + } ) + .fail( function () { + assert.ok( false, 'Language "' + test.lang + '" failed to load' ); + } ) + .always( next ); + }; } ); + QUnit.stop(); + process( queue, QUnit.start ); } ); // HTML in wikitext -- 2.20.1